rework color system and make it work with drivers.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
type ColorHS struct {
|
||||
Hue float64 `json:"hue"`
|
||||
Sat float64 `json:"sat"`
|
||||
}
|
||||
|
||||
func (hs ColorHS) ToXY() ColorXY {
|
||||
return hs.ToRGB().ToXY()
|
||||
}
|
||||
|
||||
func (hs ColorHS) ToRGB() ColorRGB {
|
||||
c := colorful.Hsv(hs.Hue, hs.Sat, 1)
|
||||
return ColorRGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package models
|
||||
|
||||
import "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
type ColorRGB struct {
|
||||
Red float64 `json:"red"`
|
||||
Green float64 `json:"green"`
|
||||
Blue float64 `json:"blue"`
|
||||
}
|
||||
|
||||
func (rgb ColorRGB) AtIntensity(intensity float64) ColorRGB {
|
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
hsv2 := colorful.Hsv(hue, sat, intensity)
|
||||
return ColorRGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B}
|
||||
}
|
||||
|
||||
func (rgb ColorRGB) ToHS() ColorHS {
|
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
return ColorHS{Hue: hue, Sat: sat}
|
||||
}
|
||||
|
||||
func (rgb ColorRGB) ToXY() ColorXY {
|
||||
x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz()
|
||||
|
||||
return ColorXY{
|
||||
X: x / (x + y + z),
|
||||
Y: y / (x + y + z),
|
||||
}
|
||||
}
|
||||
+283
-44
@@ -2,62 +2,219 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ColorValue struct {
|
||||
Hue float64 `json:"h,omitempty"` // 0..360
|
||||
Saturation float64 `json:"s,omitempty"` // 0..=1
|
||||
Kelvin int `json:"kelvin,omitempty"`
|
||||
XY *ColorXY `json:"xy,omitempty"`
|
||||
RGB *ColorRGB `json:"rgb,omitempty"`
|
||||
HS *ColorHS `json:"hs,omitempty"`
|
||||
K *int `json:"k,omitempty"`
|
||||
XY *ColorXY `json:"xy,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsEmpty() bool {
|
||||
return c.XY == nil && c.Kelvin == 0 && c.Saturation == 0 && c.Hue == 0
|
||||
func (cv *ColorValue) IsHueSat() bool {
|
||||
return cv.HS != nil
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsHueSat() bool {
|
||||
return !c.IsKelvin()
|
||||
func (cv *ColorValue) IsHueSatKelvin() bool {
|
||||
return cv.HS != nil && cv.K != nil
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsKelvin() bool {
|
||||
return !c.IsXY() && c.Kelvin > 0
|
||||
func (cv *ColorValue) IsKelvin() bool {
|
||||
return cv.K != nil
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsXY() bool {
|
||||
return c.XY != nil
|
||||
func (cv *ColorValue) IsEmpty() bool {
|
||||
return *cv == ColorValue{}
|
||||
}
|
||||
|
||||
// ToXY converts the color to XY if possible. If the color already is XY, it returns
|
||||
// a copy of its held value. There are no guarantees of conforming to a gamut, however.
|
||||
func (c *ColorValue) ToXY() (xy ColorXY, ok bool) {
|
||||
if c.XY != nil {
|
||||
xy = *c.XY
|
||||
func (cv *ColorValue) SetK(k int) {
|
||||
*cv = ColorValue{K: &k}
|
||||
}
|
||||
|
||||
func (cv *ColorValue) SetXY(xy ColorXY) {
|
||||
*cv = ColorValue{XY: &xy}
|
||||
}
|
||||
|
||||
// ToRGB tries to copy the color to an RGB color. If it's already RGB, it will be plainly copied, but HS
|
||||
// will be returned. If ok is false, then no copying has occurred and cv2 will be blank.
|
||||
func (cv *ColorValue) ToRGB() (cv2 ColorValue, ok bool) {
|
||||
if cv.RGB != nil {
|
||||
rgb := *cv.RGB
|
||||
cv2 = ColorValue{RGB: &rgb}
|
||||
ok = true
|
||||
} else if c.Kelvin > 0 && c.Hue < 0.001 && c.Saturation <= 0.001 {
|
||||
ok = false
|
||||
} else {
|
||||
xy = hsToXY(c.Hue, c.Saturation)
|
||||
} else if cv.HS != nil {
|
||||
rgb := cv.HS.ToRGB()
|
||||
cv2 = ColorValue{RGB: &rgb}
|
||||
ok = true
|
||||
} else if cv.XY != nil {
|
||||
rgb := cv.XY.ToRGB()
|
||||
cv2 = ColorValue{RGB: &rgb}
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ColorValue) String() string {
|
||||
if c.Kelvin > 0 {
|
||||
return fmt.Sprintf("k:%d", c.Kelvin)
|
||||
func (cv *ColorValue) ToHS() (cv2 ColorValue, ok bool) {
|
||||
if cv.HS != nil {
|
||||
hs := *cv.HS
|
||||
cv2 = ColorValue{HS: &hs}
|
||||
ok = true
|
||||
} else if cv.RGB != nil {
|
||||
hs := cv.RGB.ToHS()
|
||||
cv2 = ColorValue{HS: &hs}
|
||||
ok = true
|
||||
} else if cv.XY != nil {
|
||||
hs := cv.XY.ToHS()
|
||||
cv2 = ColorValue{HS: &hs}
|
||||
ok = true
|
||||
}
|
||||
|
||||
return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation)
|
||||
return
|
||||
}
|
||||
|
||||
func ParseColorValue(raw string) (ColorValue, error) {
|
||||
func (cv *ColorValue) ToHSK() (cv2 ColorValue, ok bool) {
|
||||
k := 4000
|
||||
|
||||
if cv.HS != nil {
|
||||
hs := *cv.HS
|
||||
cv2 = ColorValue{HS: &hs}
|
||||
|
||||
if cv.K != nil {
|
||||
k = *cv.K
|
||||
}
|
||||
cv2.K = &k
|
||||
|
||||
ok = true
|
||||
} else if cv.RGB != nil {
|
||||
hs := cv.RGB.ToHS()
|
||||
cv2 = ColorValue{HS: &hs}
|
||||
cv2.K = &k
|
||||
ok = true
|
||||
} else if cv.XY != nil {
|
||||
hs := cv.XY.ToHS()
|
||||
cv2 = ColorValue{HS: &hs}
|
||||
cv2.K = &k
|
||||
ok = true
|
||||
} else if cv.K != nil {
|
||||
k = *cv.K
|
||||
cv2.HS = &ColorHS{Hue: 0, Sat: 0}
|
||||
cv2.K = &k
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToXY tries to copy the color to an XY color.
|
||||
func (cv *ColorValue) ToXY() (cv2 ColorValue, ok bool) {
|
||||
if cv.XY != nil {
|
||||
xy := *cv.XY
|
||||
cv2 = ColorValue{XY: &xy}
|
||||
ok = true
|
||||
} else if cv.HS != nil {
|
||||
xy := cv.HS.ToXY()
|
||||
cv2 = ColorValue{XY: &xy}
|
||||
ok = true
|
||||
} else if cv.RGB != nil {
|
||||
xy := cv.RGB.ToXY()
|
||||
cv2 = ColorValue{XY: &xy}
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (cv *ColorValue) colorful() colorful.Color {
|
||||
switch {
|
||||
case cv.HS != nil:
|
||||
return colorful.Hsv(cv.HS.Hue, cv.HS.Sat, 1)
|
||||
case cv.RGB != nil:
|
||||
return colorful.Color{R: cv.RGB.Red, G: cv.RGB.Green, B: cv.RGB.Blue}
|
||||
case cv.XY != nil:
|
||||
return colorful.Xyy(cv.XY.X, cv.XY.Y, 0.5)
|
||||
default:
|
||||
return colorful.Color{R: 255, B: 255, G: 255}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ColorValue) Interpolate(other ColorValue, fac float64) ColorValue {
|
||||
// Special case for kelvin values.
|
||||
if cv.IsKelvin() && other.IsKelvin() {
|
||||
k1 := *cv.K
|
||||
k2 := *cv.K
|
||||
k3 := k1 + int(float64(k2-k1)*fac)
|
||||
return ColorValue{K: &k3}
|
||||
}
|
||||
|
||||
// Get the colorful values.
|
||||
cvCF := cv.colorful()
|
||||
otherCF := other.colorful()
|
||||
|
||||
// Blend and normalize
|
||||
blended := cvCF.BlendLuv(otherCF, fac)
|
||||
blendedHue, blendedSat, _ := blended.Hsv()
|
||||
blendedHs := ColorHS{Hue: blendedHue, Sat: blendedSat}
|
||||
|
||||
// Convert to the first's type
|
||||
switch cv.Kind() {
|
||||
case "rgb":
|
||||
rgb := blendedHs.ToRGB()
|
||||
return ColorValue{RGB: &rgb}
|
||||
case "xy":
|
||||
xy := blendedHs.ToXY()
|
||||
return ColorValue{XY: &xy}
|
||||
default:
|
||||
return ColorValue{HS: &blendedHs}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ColorValue) Kind() string {
|
||||
switch {
|
||||
case cv.RGB != nil:
|
||||
return "rgb"
|
||||
case cv.XY != nil:
|
||||
return "xy"
|
||||
case cv.HS != nil && cv.K != nil:
|
||||
return "hsk"
|
||||
case cv.HS != nil:
|
||||
return "hs"
|
||||
case cv.K != nil:
|
||||
return "k"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ColorValue) String() string {
|
||||
switch {
|
||||
case cv.RGB != nil:
|
||||
return fmt.Sprintf("rgb:%.3f,%.3f,%.3f", cv.RGB.Red, cv.RGB.Green, cv.RGB.Blue)
|
||||
case cv.XY != nil:
|
||||
return fmt.Sprintf("xy:%.4f,%.4f", cv.XY.X, cv.XY.Y)
|
||||
case cv.HS != nil && cv.K != nil:
|
||||
return fmt.Sprintf("hsk:%.4f,%.3f,%d", cv.HS.Hue, cv.HS.Sat, *cv.K)
|
||||
case cv.HS != nil:
|
||||
return fmt.Sprintf("hs:%.4f,%.3f", cv.HS.Hue, cv.HS.Sat)
|
||||
case cv.K != nil:
|
||||
return fmt.Sprintf("k:%d", *cv.K)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func ParseColorValue(raw string) (cv2 ColorValue, err error) {
|
||||
if raw == "" {
|
||||
return
|
||||
}
|
||||
|
||||
tokens := strings.SplitN(raw, ":", 2)
|
||||
if len(tokens) != 2 {
|
||||
return ColorValue{}, ErrBadInput
|
||||
err = ErrBadInput
|
||||
return
|
||||
}
|
||||
|
||||
switch tokens[0] {
|
||||
@@ -65,65 +222,147 @@ func ParseColorValue(raw string) (ColorValue, error) {
|
||||
{
|
||||
parsedPart, err := strconv.Atoi(tokens[1])
|
||||
if err != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
err = ErrBadInput
|
||||
break
|
||||
}
|
||||
|
||||
return ColorValue{Kelvin: parsedPart}, nil
|
||||
cv2.K = &parsedPart
|
||||
}
|
||||
|
||||
case "xy":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
x, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
y, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
err = ErrBadInput
|
||||
break
|
||||
}
|
||||
|
||||
return ColorValue{XY: &ColorXY{X: x, Y: y}}, nil
|
||||
cv2.XY = &ColorXY{x, y}
|
||||
}
|
||||
|
||||
case "hs":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
err = ErrBadInput
|
||||
break
|
||||
}
|
||||
|
||||
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
|
||||
cv2.HS = &ColorHS{Hue: part1, Sat: part2}
|
||||
}
|
||||
|
||||
case "hsk":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 3 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
part3, err3 := strconv.Atoi(parts[2])
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
err = ErrBadInput
|
||||
break
|
||||
}
|
||||
|
||||
return ColorValue{
|
||||
Hue: math.Mod(part1, 360),
|
||||
Saturation: math.Min(math.Max(part2, 0), 1),
|
||||
Kelvin: part3,
|
||||
}, nil
|
||||
cv2.HS = &ColorHS{Hue: part1, Sat: part2}
|
||||
cv2.K = &part3
|
||||
}
|
||||
|
||||
case "rgb":
|
||||
{
|
||||
if strings.HasPrefix(tokens[1], "#") {
|
||||
hex := tokens[1][1:]
|
||||
if !validHex(hex) {
|
||||
err = ErrBadInput
|
||||
break
|
||||
}
|
||||
|
||||
if len(hex) == 6 {
|
||||
cv2.RGB = &ColorRGB{
|
||||
Red: float64(hex2num(hex[0:2])) / 255.0,
|
||||
Green: float64(hex2num(hex[2:4])) / 255.0,
|
||||
Blue: float64(hex2num(hex[4:6])) / 255.0,
|
||||
}
|
||||
} else if len(hex) == 3 {
|
||||
cv2.RGB = &ColorRGB{
|
||||
Red: float64(hex2digit(hex[0])) / 15.0,
|
||||
Green: float64(hex2digit(hex[1])) / 15.0,
|
||||
Blue: float64(hex2digit(hex[2])) / 15.0,
|
||||
}
|
||||
} else {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
} else {
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 3 {
|
||||
err = ErrUnknownColorFormat
|
||||
return
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
part3, err3 := strconv.ParseFloat(parts[2], 64)
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
err = ErrBadInput
|
||||
break
|
||||
}
|
||||
|
||||
cv2.RGB = &ColorRGB{Red: part1, Green: part2, Blue: part3}
|
||||
}
|
||||
|
||||
normalizedRGB := cv2.RGB.ToHS().ToRGB()
|
||||
cv2.RGB = &normalizedRGB
|
||||
}
|
||||
|
||||
default:
|
||||
err = ErrUnknownColorFormat
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validHex(h string) bool {
|
||||
for _, ch := range h {
|
||||
if !((ch >= 'a' && ch <= 'f') || (ch >= '0' || ch <= '9')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
return true
|
||||
}
|
||||
|
||||
func hex2num(s string) int {
|
||||
v := 0
|
||||
for _, h := range s {
|
||||
v *= 16
|
||||
v += hex2digit(byte(h))
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func hex2digit(h byte) int {
|
||||
if h >= 'a' && h <= 'f' {
|
||||
return 10 + int(h-'a')
|
||||
} else {
|
||||
return int(h - '0')
|
||||
}
|
||||
}
|
||||
|
||||
+11
-33
@@ -159,6 +159,17 @@ type ColorXY struct {
|
||||
Y float64 `json:"y"`
|
||||
}
|
||||
|
||||
func (xy ColorXY) ToRGB() ColorRGB {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
c := colorful.Hsv(h, s, 1)
|
||||
return ColorRGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
}
|
||||
|
||||
func (xy ColorXY) ToHS() ColorHS {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
return ColorHS{Hue: h, Sat: s}
|
||||
}
|
||||
|
||||
func (xy ColorXY) DistanceTo(other ColorXY) float64 {
|
||||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
||||
}
|
||||
@@ -169,36 +180,3 @@ func (xy ColorXY) Round() ColorXY {
|
||||
Y: math.Round(xy.Y*10000) / 10000,
|
||||
}
|
||||
}
|
||||
|
||||
func hsToXY(hue, sat float64) ColorXY {
|
||||
c := colorful.Hsv(hue, sat, 1)
|
||||
red255, green255, blue255 := c.RGB255()
|
||||
red := float64(red255) / 255.0
|
||||
green := float64(green255) / 255.0
|
||||
blue := float64(blue255) / 255.0
|
||||
|
||||
return rgbToXY(red, green, blue)
|
||||
}
|
||||
|
||||
func rgbToXY(red float64, green float64, blue float64) ColorXY {
|
||||
x := red*0.649926 + green*0.103455 + blue*0.197109
|
||||
y := red*0.234327 + green*0.743075 + blue*0.022598
|
||||
z := green*0.053077 + blue*1.035763
|
||||
|
||||
return ColorXY{
|
||||
X: x / (x + y + z),
|
||||
Y: y / (x + y + z),
|
||||
}
|
||||
}
|
||||
|
||||
func screenRGBToXY(red, green, blue float64) ColorXY {
|
||||
for _, component := range []*float64{&red, &green, &blue} {
|
||||
if *component > 0.04045 {
|
||||
*component = math.Pow((*component+0.055)/(1.055), 2.4)
|
||||
} else {
|
||||
*component /= 12.92
|
||||
}
|
||||
}
|
||||
|
||||
return rgbToXY(red, green, blue)
|
||||
}
|
||||
|
||||
+10
-6
@@ -87,6 +87,7 @@ var (
|
||||
DCColorHSK DeviceCapability = "ColorHSK"
|
||||
DCColorKelvin DeviceCapability = "ColorKelvin"
|
||||
DCColorXY DeviceCapability = "ColorXY"
|
||||
DCColorRGB DeviceCapability = "ColorRGB"
|
||||
DCButtons DeviceCapability = "Buttons"
|
||||
DCPresence DeviceCapability = "Presence"
|
||||
DCIntensity DeviceCapability = "Intensity"
|
||||
@@ -190,7 +191,14 @@ func (d *Device) SetState(newState NewDeviceState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if (parsed.IsKelvin() && d.HasCapability(DCColorKelvin, DCColorHSK)) || (parsed.IsHueSat() && d.HasCapability(DCColorHS)) {
|
||||
switch {
|
||||
case (parsed.RGB != nil || parsed.XY != nil || parsed.HS != nil) || d.HasCapability(DCColorHS, DCColorXY, DCColorRGB):
|
||||
d.State.Color = parsed
|
||||
case parsed.K != nil && d.HasCapability(DCColorKelvin, DCColorHSK):
|
||||
if !d.HasCapability(DCColorKelvin) {
|
||||
d.State.Color.HS = &ColorHS{Hue: 0, Sat: 0}
|
||||
}
|
||||
|
||||
d.State.Color = parsed
|
||||
}
|
||||
}
|
||||
@@ -232,11 +240,7 @@ func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDevic
|
||||
sc, err := ParseColorValue(*s.Color)
|
||||
oc, err2 := ParseColorValue(*other.Color)
|
||||
if err == nil && err2 == nil {
|
||||
rc := ColorValue{}
|
||||
rc.Hue = interpolateFloat(sc.Hue, oc.Hue, fac)
|
||||
rc.Saturation = interpolateFloat(sc.Saturation, oc.Saturation, fac)
|
||||
rc.Kelvin = interpolateInt(sc.Kelvin, oc.Kelvin, fac)
|
||||
|
||||
rc := sc.Interpolate(oc, fac)
|
||||
rcStr := rc.String()
|
||||
n.Color = &rcStr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user