add gradient and random effect.
This commit is contained in:
+11
-20
@@ -55,27 +55,15 @@ func main() {
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
for i, id := range []string{"40e5", "dead", "beef", "cafe", "1337"} {
|
||||
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",
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+51
-9
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+11
-7
@@ -8,17 +8,21 @@ import (
|
||||
|
||||
type Pattern struct {
|
||||
States []device.State `json:"states,omitempty"`
|
||||
ShiftMS int64 `json:"shiftMs,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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
+2
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user