From 1f5df2e034d48c298491307244c0603816baaeec Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Fri, 19 Aug 2022 10:55:29 +0200 Subject: [PATCH] add gradient and random effect. --- cmd/bustest/main.go | 41 ++++++++++---------------- device/state.go | 60 ++++++++++++++++++++++++++++++++------ effects/gradient.go | 60 ++++++++++++++++++++++++++++++++++++++ effects/manual.go | 4 +-- effects/pattern.go | 20 ++++++++----- effects/random.go | 54 ++++++++++++++++++++++++++++++++++ effects/utils.go | 23 +++++++++++++++ interface.go | 3 +- internal/color/color.go | 10 +++++-- internal/color/hs.go | 7 +++-- internal/color/rgb.go | 8 +++++ internal/gentools/ptr.go | 5 ++++ services/effectenforcer.go | 2 +- 13 files changed, 247 insertions(+), 50 deletions(-) create mode 100644 effects/gradient.go create mode 100644 effects/random.go create mode 100644 effects/utils.go diff --git a/cmd/bustest/main.go b/cmd/bustest/main.go index 9934e7b..b841eac 100644 --- a/cmd/bustest/main.go +++ b/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, }, }) diff --git a/device/state.go b/device/state.go index 833871a..16bf575 100644 --- a/device/state.go +++ b/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"` diff --git a/effects/gradient.go b/effects/gradient.go new file mode 100644 index 0000000..6d7d930 --- /dev/null +++ b/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) +} diff --git a/effects/manual.go b/effects/manual.go index 45e09a8..c57a97e 100644 --- a/effects/manual.go +++ b/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, diff --git a/effects/pattern.go b/effects/pattern.go index b82ac6f..4bf25ca 100644 --- a/effects/pattern.go +++ b/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) } diff --git a/effects/random.go b/effects/random.go new file mode 100644 index 0000000..c187881 --- /dev/null +++ b/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) +} diff --git a/effects/utils.go b/effects/utils.go new file mode 100644 index 0000000..1bf2a52 --- /dev/null +++ b/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() +} diff --git a/interface.go b/interface.go index d0a30ce..6745b82 100644 --- a/interface.go +++ b/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 } diff --git a/internal/color/color.go b/internal/color/color.go index 119896f..16fb5e0 100644 --- a/internal/color/color.go +++ b/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} } } diff --git a/internal/color/hs.go b/internal/color/hs.go index be97786..fe2aaaf 100644 --- a/internal/color/hs.go +++ b/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)} } diff --git a/internal/color/rgb.go b/internal/color/rgb.go index 5eb9878..aa4bb72 100644 --- a/internal/color/rgb.go +++ b/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} diff --git a/internal/gentools/ptr.go b/internal/gentools/ptr.go index baa203e..bf95190 100644 --- a/internal/gentools/ptr.go +++ b/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 +} diff --git a/services/effectenforcer.go b/services/effectenforcer.go index 56e416f..4cf1903 100644 --- a/services/effectenforcer.go +++ b/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,