Browse Source

add gradient and random effect.

beelzebub
Gisle Aune 2 years ago
parent
commit
1f5df2e034
  1. 41
      cmd/bustest/main.go
  2. 60
      device/state.go
  3. 60
      effects/gradient.go
  4. 4
      effects/manual.go
  5. 20
      effects/pattern.go
  6. 54
      effects/random.go
  7. 23
      effects/utils.go
  8. 3
      interface.go
  9. 10
      internal/color/color.go
  10. 7
      internal/color/hs.go
  11. 8
      internal/color/rgb.go
  12. 5
      internal/gentools/ptr.go
  13. 2
      services/effectenforcer.go

41
cmd/bustest/main.go

@ -55,27 +55,15 @@ func main() {
time.Sleep(time.Millisecond)
bus.RunEvent(events.HardwareState{
ID: "nanoleaf:10.80.1.11:40e5",
InternalName: "Hexagon 6",
SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity,
ColorFlags: device.CFlagRGB,
State: device.State{},
})
bus.RunEvent(events.HardwareState{
ID: "nanoleaf:10.80.1.11:dead",
InternalName: "Hexagon 7",
SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity,
ColorFlags: device.CFlagRGB,
State: device.State{},
})
bus.RunEvent(events.HardwareState{
ID: "nanoleaf:10.80.1.11:beef",
InternalName: "Hexagon 8",
SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity,
ColorFlags: device.CFlagRGB,
State: device.State{},
})
for i, id := range []string{"40e5", "dead", "beef", "cafe", "1337"} {
bus.RunEvent(events.HardwareState{
ID: "nanoleaf:10.80.1.11:" + id,
InternalName: fmt.Sprintf("Hexagon %d", 6+i),
SupportFlags: device.SFlagPower | device.SFlagColor | device.SFlagIntensity,
ColorFlags: device.CFlagRGB,
State: device.State{},
})
}
time.Sleep(time.Millisecond)
@ -90,22 +78,25 @@ func main() {
time.Sleep(time.Millisecond)
c1 := gentools.Ptr(color.MustParse("rgb:#ff0000"))
c2 := gentools.Ptr(color.MustParse("rgb:#00ff00"))
bus.RunCommand(commands.Assign{
Match: "**:Hexagon *",
Effect: effects.Pattern{
Effect: effects.Gradient{
States: []device.State{
{
Power: gentools.Ptr(true),
Color: gentools.Ptr(color.MustParse("rgb:#ff0000")),
Color: c1,
Intensity: gentools.Ptr(1.0),
},
{
Power: gentools.Ptr(true),
Color: gentools.Ptr(color.MustParse("rgb:#00ff00")),
Color: c2,
Intensity: gentools.Ptr(0.7),
},
},
ShiftMS: 1000,
AnimationMS: 1000,
Interpolate: true,
},
})

60
device/state.go

@ -3,6 +3,7 @@ package device
import (
"fmt"
"git.aiterp.net/lucifer3/server/internal/color"
"git.aiterp.net/lucifer3/server/internal/gentools"
"strings"
)
@ -13,24 +14,65 @@ type State struct {
Color *color.Color `json:"color"`
}
func (d State) String() string {
func (s State) String() string {
parts := make([]string, 0, 4)
if d.Power != nil {
parts = append(parts, fmt.Sprintf("power:%t", *d.Power))
if s.Power != nil {
parts = append(parts, fmt.Sprintf("power:%t", *s.Power))
}
if d.Temperature != nil {
parts = append(parts, fmt.Sprintf("temperature:%f", *d.Temperature))
if s.Temperature != nil {
parts = append(parts, fmt.Sprintf("temperature:%f", *s.Temperature))
}
if d.Intensity != nil {
parts = append(parts, fmt.Sprintf("intensity:%.2f", *d.Intensity))
if s.Intensity != nil {
parts = append(parts, fmt.Sprintf("intensity:%.2f", *s.Intensity))
}
if d.Color != nil {
parts = append(parts, fmt.Sprintf("color:%s", d.Color.String()))
if s.Color != nil {
parts = append(parts, fmt.Sprintf("color:%s", s.Color.String()))
}
return fmt.Sprint("(", strings.Join(parts, ", "), ")")
}
func (s State) Interpolate(s2 State, f float64) State {
newState := State{}
if s.Color != nil && s2.Color != nil {
newState.Color = gentools.Ptr(s.Color.Interpolate(*s2.Color, f))
} else if s.Color != nil {
newState.Color = gentools.ShallowCopy(s.Color)
} else if s2.Color != nil {
newState.Color = gentools.ShallowCopy(s2.Color)
}
if s.Intensity != nil && s2.Intensity != nil {
newState.Intensity = gentools.Ptr((*s.Intensity * f) + (*s2.Intensity * (1.0 - f)))
} else if s.Intensity != nil {
newState.Intensity = gentools.ShallowCopy(s.Intensity)
} else if s2.Intensity != nil {
newState.Intensity = gentools.ShallowCopy(s2.Intensity)
}
if s.Temperature != nil && s2.Temperature != nil {
newState.Temperature = gentools.Ptr((*s.Temperature * f) + (*s2.Temperature * (1.0 - f)))
} else if s.Temperature != nil {
newState.Temperature = gentools.ShallowCopy(s.Temperature)
} else if s2.Temperature != nil {
newState.Temperature = gentools.ShallowCopy(s2.Temperature)
}
if s.Power != nil && s2.Power != nil {
if f < 0.5 {
newState.Power = gentools.ShallowCopy(s.Power)
} else {
newState.Power = gentools.ShallowCopy(s2.Power)
}
} else if s.Power != nil {
newState.Power = gentools.ShallowCopy(s.Power)
} else if s2.Power != nil {
newState.Power = gentools.ShallowCopy(s2.Power)
}
return newState
}
type PresenceState struct {
Present bool `json:"present"`
AbsenceSeconds float64 `json:"absenceSeconds,omitempty"`

60
effects/gradient.go

@ -0,0 +1,60 @@
package effects
import (
"fmt"
"git.aiterp.net/lucifer3/server/device"
"math"
"time"
)
type Gradient struct {
States []device.State `json:"states,omitempty"`
AnimationMS int64 `json:"AnimationMs,omitempty"`
Reverse bool `json:"backward,omitempty"`
Interpolate bool `json:"interpolate,omitempty"`
}
func (e Gradient) State(index, length, round, _ int) device.State {
if len(e.States) == 0 {
return device.State{}
}
if e.Reverse {
index = length - (index + 1)
}
walkedIndex := (index + round) % length
indexFactor := math.Min(float64(walkedIndex)/float64(length-1), 1)
stateIncrement := 1.0 / float64(len(e.States)-1)
for i := range e.States {
a := float64(i) * stateIncrement
if indexFactor >= a {
si := e.States[i]
sj := e.States[(i+1)%len(e.States)]
f := (indexFactor - a) / stateIncrement
if f < 0 || f > 1 {
panic(f)
}
if e.Interpolate {
return si.Interpolate(sj, f)
} else if f < 0.5 {
return si
} else {
return sj
}
}
}
return e.States[len(e.States)-1]
}
func (e Gradient) Frequency() time.Duration {
return time.Duration(e.AnimationMS) * time.Millisecond
}
func (e Gradient) EffectDescription() string {
return fmt.Sprintf("Gradient(states:%s, anim:%dms, int:%t)", statesDescription(e.States), e.AnimationMS, e.Interpolate)
}

4
effects/manual.go

@ -15,14 +15,14 @@ type Manual struct {
}
func (e Manual) EffectDescription() string {
return fmt.Sprintf("Manual%s", e.State(0, 0, 0).String())
return fmt.Sprintf("Manual%s", e.State(0, 0, 0, 0).String())
}
func (e Manual) Frequency() time.Duration {
return 0
}
func (e Manual) State(int, int, int) device.State {
func (e Manual) State(int, int, int, int) device.State {
return device.State{
Power: e.Power,
Temperature: e.Temperature,

20
effects/pattern.go

@ -7,18 +7,22 @@ import (
)
type Pattern struct {
States []device.State `json:"states,omitempty"`
ShiftMS int64 `json:"shiftMs,omitempty"`
States []device.State `json:"states,omitempty"`
AnimationMS int64 `json:"animationMs,omitempty"`
}
func (p Pattern) State(index, round, _ int) device.State {
return p.States[(index+round)%len(p.States)]
func (e Pattern) State(index, _, round, _ int) device.State {
if len(e.States) == 0 {
return device.State{}
}
return e.States[(index+round)%len(e.States)]
}
func (p Pattern) Frequency() time.Duration {
return time.Duration(p.ShiftMS) * time.Millisecond
func (e Pattern) Frequency() time.Duration {
return time.Duration(e.AnimationMS) * time.Millisecond
}
func (p Pattern) EffectDescription() string {
return fmt.Sprintf("Pattern(len:%d, animation:%dms)", len(p.States), p.ShiftMS)
func (e Pattern) EffectDescription() string {
return fmt.Sprintf("Pattern(states:%s, anim:%dms)", statesDescription(e.States), e.AnimationMS)
}

54
effects/random.go

@ -0,0 +1,54 @@
package effects
import (
"fmt"
"git.aiterp.net/lucifer3/server/device"
"math/rand"
"time"
)
type Random struct {
States []device.State `json:"states,omitempty"`
Interpolate bool `json:"interpolate,omitempty"`
AnimationMS int64 `json:"animationMs,omitempty"`
}
func (e Random) State(_, _, _, _ int) device.State {
if len(e.States) == 0 {
return device.State{}
}
indexFactor := rand.Float64()
stateIncrement := 1.0 / float64(len(e.States)-1)
for i := range e.States {
a := float64(i) * stateIncrement
if indexFactor >= a {
si := e.States[i]
sj := e.States[(i+1)%len(e.States)]
f := (indexFactor - a) / stateIncrement
if f < 0 || f > 1 {
panic(f)
}
if e.Interpolate {
return si.Interpolate(sj, f)
} else if f < 0.5 {
return si
} else {
return sj
}
}
}
return e.States[len(e.States)-1]
}
func (e Random) Frequency() time.Duration {
return time.Duration(e.AnimationMS) * time.Millisecond
}
func (e Random) EffectDescription() string {
return fmt.Sprintf("Random(states:%s, anim:%dms, int:%t)", statesDescription(e.States), e.AnimationMS, e.Interpolate)
}

23
effects/utils.go

@ -0,0 +1,23 @@
package effects
import (
"git.aiterp.net/lucifer3/server/device"
"strings"
)
func statesDescription(states []device.State) string {
sb := strings.Builder{}
sb.Grow(128)
sb.WriteRune('[')
for i, state := range states {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(state.String())
}
sb.WriteRune(']')
return sb.String()
}

3
interface.go

@ -8,9 +8,10 @@ import (
type Effect interface {
// State uses three values:
// index: The position of this ID;
// len: The total length of the effect's IDs;
// round: Increases by one for each run of the effect if Frequency > 0;
// random: A random positive int in the full 31-bit range.
State(index, round, random int) device.State
State(index, len, round, random int) device.State
Frequency() time.Duration
EffectDescription() string
}

10
internal/color/color.go

@ -188,11 +188,17 @@ func (col *Color) Interpolate(other Color, fac float64) Color {
return Color{K: &k3}
}
if fac < 0.000001 {
return *col
} else if fac > 0.999999 {
return other
}
// Get the colorful values.
cvCF := col.colorful()
otherCF := other.colorful()
// Blend and normalize
// Blend and normalize (clamping is hax to avoid issues with some colors)
blended := cvCF.BlendLuv(otherCF, fac)
blendedHue, blendedSat, _ := blended.Hsv()
blendedHs := HueSat{Hue: blendedHue, Sat: blendedSat}
@ -253,7 +259,7 @@ func (col *Color) colorful() colorful.Color {
case col.XY != nil:
return colorful.Xyy(col.XY.X, col.XY.Y, 0.5)
default:
return colorful.Color{R: 255, B: 255, G: 255}
return colorful.Color{R: 1, B: 1, G: 1}
}
}

7
internal/color/hs.go

@ -1,6 +1,9 @@
package color
import "github.com/lucasb-eyer/go-colorful"
import (
"github.com/lucasb-eyer/go-colorful"
"math"
)
type HueSat struct {
Hue float64 `json:"hue"`
@ -13,5 +16,5 @@ func (hs HueSat) ToXY() XY {
func (hs HueSat) ToRGB() RGB {
c := colorful.Hsv(hs.Hue, hs.Sat, 1)
return RGB{Red: c.R, Green: c.G, Blue: c.B}
return RGB{Red: math.Max(c.R, 0), Green: math.Max(c.G, 0), Blue: math.Max(c.B, 0)}
}

8
internal/color/rgb.go

@ -14,6 +14,14 @@ func (rgb RGB) AtIntensity(intensity float64) RGB {
return RGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B}
}
func (rgb RGB) Bytes() [3]uint8 {
return [3]uint8{
uint8(rgb.Red * 255.0),
uint8(rgb.Green * 255.0),
uint8(rgb.Blue * 255.0),
}
}
func (rgb RGB) ToHS() HueSat {
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
return HueSat{Hue: hue, Sat: sat}

5
internal/gentools/ptr.go

@ -3,3 +3,8 @@ package gentools
func Ptr[T any](t T) *T {
return &t
}
func ShallowCopy[T any](t *T) *T {
tCopy := *t
return &tCopy
}

2
services/effectenforcer.go

@ -94,7 +94,7 @@ func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) {
continue
}
state := run.effect.State(j, run.round, r)
state := run.effect.State(j, len(run.ids), run.round, r)
commandQueue = append(commandQueue, commands.SetState{
ID: id,
State: state,

Loading…
Cancel
Save