Browse Source
refactor color system to be separate from models, also move errors out of models.
feature-colorvalue2
refactor color system to be separate from models, also move errors out of models.
feature-colorvalue2
Gisle Aune
3 years ago
32 changed files with 532 additions and 516 deletions
-
3app/api/devices.go
-
5app/api/presets.go
-
28app/api/util.go
-
3cmd/bridgetest/main.go
-
10cmd/xy/main.go
-
369internal/color/color.go
-
10internal/color/hs.go
-
16internal/color/rgb.go
-
50internal/color/xy.go
-
5internal/drivers/hue/bridge.go
-
3internal/drivers/hue/driver.go
-
4internal/drivers/hue2/bridge.go
-
4internal/drivers/hue2/client.go
-
8internal/drivers/hue2/data.go
-
3internal/drivers/hue2/driver.go
-
12internal/drivers/lifx/bridge.go
-
6internal/drivers/lifx/client.go
-
6internal/drivers/lifx/packet.go
-
14internal/drivers/lifx/payloads.go
-
13internal/drivers/lifx/state.go
-
17internal/drivers/mill/bridge.go
-
10internal/drivers/nanoleaf/bridge.go
-
5internal/drivers/nanoleaf/driver.go
-
8internal/drivers/provider.go
-
2internal/lerrors/errors.go
-
3internal/mysql/devicerepo.go
-
3internal/mysql/presetrepo.go
-
6internal/mysql/util.go
-
3models/colorpreset.go
-
368models/colorvalue.go
-
20models/device.go
-
15models/scene.go
@ -1,21 +1,15 @@ |
|||||
package main |
package main |
||||
|
|
||||
import ( |
import ( |
||||
"encoding/json" |
|
||||
"git.aiterp.net/lucifer/new-server/models" |
|
||||
|
"git.aiterp.net/lucifer/new-server/internal/color" |
||||
"log" |
"log" |
||||
) |
) |
||||
|
|
||||
func main() { |
func main() { |
||||
cv, _ := models.ParseColorValue("rgb:#fff") |
|
||||
|
cv, _ := color.Parse("xy:0.1944,0.0942") |
||||
hs, _ := cv.ToHS() |
hs, _ := cv.ToHS() |
||||
log.Println(cv.String()) |
log.Println(cv.String()) |
||||
log.Println(hs.String()) |
log.Println(hs.String()) |
||||
xy, _ := hs.ToXY() |
xy, _ := hs.ToXY() |
||||
log.Println(xy.String()) |
log.Println(xy.String()) |
||||
} |
} |
||||
|
|
||||
func toJSON(v interface{}) string { |
|
||||
j, _ := json.Marshal(v) |
|
||||
return string(j) |
|
||||
} |
|
@ -0,0 +1,369 @@ |
|||||
|
package color |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"git.aiterp.net/lucifer/new-server/internal/lerrors" |
||||
|
"github.com/lucasb-eyer/go-colorful" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
type Color struct { |
||||
|
RGB *RGB `json:"rgb,omitempty"` |
||||
|
HS *HueSat `json:"hs,omitempty"` |
||||
|
K *int `json:"k,omitempty"` |
||||
|
XY *XY `json:"xy,omitempty"` |
||||
|
} |
||||
|
|
||||
|
func (col *Color) IsHueSat() bool { |
||||
|
return col.HS != nil |
||||
|
} |
||||
|
|
||||
|
func (col *Color) IsHueSatKelvin() bool { |
||||
|
return col.HS != nil && col.K != nil |
||||
|
} |
||||
|
|
||||
|
func (col *Color) IsKelvin() bool { |
||||
|
return col.K != nil |
||||
|
} |
||||
|
|
||||
|
func (col *Color) IsEmpty() bool { |
||||
|
return *col == Color{} |
||||
|
} |
||||
|
|
||||
|
func (col *Color) SetK(k int) { |
||||
|
*col = Color{K: &k} |
||||
|
} |
||||
|
|
||||
|
func (col *Color) SetXY(xy XY) { |
||||
|
*col = Color{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 (col *Color) ToRGB() (col2 Color, ok bool) { |
||||
|
if col.RGB != nil { |
||||
|
rgb := *col.RGB |
||||
|
col2 = Color{RGB: &rgb} |
||||
|
ok = true |
||||
|
} else if col.HS != nil { |
||||
|
rgb := col.HS.ToRGB() |
||||
|
col2 = Color{RGB: &rgb} |
||||
|
ok = true |
||||
|
} else if col.XY != nil { |
||||
|
rgb := col.XY.ToRGB() |
||||
|
col2 = Color{RGB: &rgb} |
||||
|
ok = true |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (col *Color) ToHS() (col2 Color, ok bool) { |
||||
|
if col.HS != nil { |
||||
|
hs := *col.HS |
||||
|
col2 = Color{HS: &hs} |
||||
|
ok = true |
||||
|
} else if col.RGB != nil { |
||||
|
hs := col.RGB.ToHS() |
||||
|
col2 = Color{HS: &hs} |
||||
|
ok = true |
||||
|
} else if col.XY != nil { |
||||
|
hs := col.XY.ToHS() |
||||
|
col2 = Color{HS: &hs} |
||||
|
ok = true |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (col *Color) ToHSK() (col2 Color, ok bool) { |
||||
|
k := 4000 |
||||
|
|
||||
|
if col.HS != nil { |
||||
|
hs := *col.HS |
||||
|
col2 = Color{HS: &hs} |
||||
|
|
||||
|
if col.K != nil { |
||||
|
k = *col.K |
||||
|
} |
||||
|
col2.K = &k |
||||
|
|
||||
|
ok = true |
||||
|
} else if col.RGB != nil { |
||||
|
hs := col.RGB.ToHS() |
||||
|
col2 = Color{HS: &hs} |
||||
|
col2.K = &k |
||||
|
ok = true |
||||
|
} else if col.XY != nil { |
||||
|
hs := col.XY.ToHS() |
||||
|
col2 = Color{HS: &hs} |
||||
|
col2.K = &k |
||||
|
ok = true |
||||
|
} else if col.K != nil { |
||||
|
k = *col.K |
||||
|
col2.HS = &HueSat{Hue: 0, Sat: 0} |
||||
|
col2.K = &k |
||||
|
ok = true |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// ToXY tries to copy the color to an XY color.
|
||||
|
func (col *Color) ToXY() (col2 Color, ok bool) { |
||||
|
if col.XY != nil { |
||||
|
xy := *col.XY |
||||
|
col2 = Color{XY: &xy} |
||||
|
ok = true |
||||
|
} else if col.HS != nil { |
||||
|
xy := col.HS.ToXY() |
||||
|
col2 = Color{XY: &xy} |
||||
|
ok = true |
||||
|
} else if col.RGB != nil { |
||||
|
xy := col.RGB.ToXY() |
||||
|
col2 = Color{XY: &xy} |
||||
|
ok = true |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (col *Color) Interpolate(other Color, fac float64) Color { |
||||
|
// Special case for kelvin values.
|
||||
|
if col.IsKelvin() && other.IsKelvin() { |
||||
|
k1 := *col.K |
||||
|
k2 := *col.K |
||||
|
k3 := k1 + int(float64(k2-k1)*fac) |
||||
|
return Color{K: &k3} |
||||
|
} |
||||
|
|
||||
|
// Get the colorful values.
|
||||
|
cvCF := col.colorful() |
||||
|
otherCF := other.colorful() |
||||
|
|
||||
|
// Blend and normalize
|
||||
|
blended := cvCF.BlendLuv(otherCF, fac) |
||||
|
blendedHue, blendedSat, _ := blended.Hsv() |
||||
|
blendedHs := HueSat{Hue: blendedHue, Sat: blendedSat} |
||||
|
|
||||
|
// Convert to the first's type
|
||||
|
switch col.Kind() { |
||||
|
case "rgb": |
||||
|
rgb := blendedHs.ToRGB() |
||||
|
return Color{RGB: &rgb} |
||||
|
case "xy": |
||||
|
xy := blendedHs.ToXY() |
||||
|
return Color{XY: &xy} |
||||
|
default: |
||||
|
return Color{HS: &blendedHs} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (col *Color) Kind() string { |
||||
|
switch { |
||||
|
case col.RGB != nil: |
||||
|
return "rgb" |
||||
|
case col.XY != nil: |
||||
|
return "xy" |
||||
|
case col.HS != nil && col.K != nil: |
||||
|
return "hsk" |
||||
|
case col.HS != nil: |
||||
|
return "hs" |
||||
|
case col.K != nil: |
||||
|
return "k" |
||||
|
default: |
||||
|
return "" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (col *Color) String() string { |
||||
|
switch { |
||||
|
case col.RGB != nil: |
||||
|
return fmt.Sprintf("rgb:%.3f,%.3f,%.3f", col.RGB.Red, col.RGB.Green, col.RGB.Blue) |
||||
|
case col.XY != nil: |
||||
|
return fmt.Sprintf("xy:%.4f,%.4f", col.XY.X, col.XY.Y) |
||||
|
case col.HS != nil && col.K != nil: |
||||
|
return fmt.Sprintf("hsk:%.4f,%.3f,%d", col.HS.Hue, col.HS.Sat, *col.K) |
||||
|
case col.HS != nil: |
||||
|
return fmt.Sprintf("hs:%.4f,%.3f", col.HS.Hue, col.HS.Sat) |
||||
|
case col.K != nil: |
||||
|
return fmt.Sprintf("k:%d", *col.K) |
||||
|
default: |
||||
|
return "" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (col *Color) colorful() colorful.Color { |
||||
|
switch { |
||||
|
case col.HS != nil: |
||||
|
return colorful.Hsv(col.HS.Hue, col.HS.Sat, 1) |
||||
|
case col.RGB != nil: |
||||
|
return colorful.Color{R: col.RGB.Red, G: col.RGB.Green, B: col.RGB.Blue} |
||||
|
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} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Parse(raw string) (col Color, err error) { |
||||
|
if raw == "" { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
tokens := strings.SplitN(raw, ":", 2) |
||||
|
if len(tokens) != 2 { |
||||
|
err = lerrors.ErrBadInput |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch tokens[0] { |
||||
|
case "kelvin", "k": |
||||
|
{ |
||||
|
parsedPart, err := strconv.Atoi(tokens[1]) |
||||
|
if err != nil { |
||||
|
err = lerrors.ErrBadInput |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
col.K = &parsedPart |
||||
|
} |
||||
|
|
||||
|
case "xy": |
||||
|
{ |
||||
|
parts := strings.Split(tokens[1], ",") |
||||
|
if len(parts) < 2 { |
||||
|
err = lerrors.ErrUnknownColorFormat |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
x, err1 := strconv.ParseFloat(parts[0], 64) |
||||
|
y, err2 := strconv.ParseFloat(parts[1], 64) |
||||
|
if err1 != nil || err2 != nil { |
||||
|
err = lerrors.ErrBadInput |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
col.XY = &XY{x, y} |
||||
|
} |
||||
|
|
||||
|
case "hs": |
||||
|
{ |
||||
|
parts := strings.Split(tokens[1], ",") |
||||
|
if len(parts) < 2 { |
||||
|
err = lerrors.ErrUnknownColorFormat |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
part1, err1 := strconv.ParseFloat(parts[0], 64) |
||||
|
part2, err2 := strconv.ParseFloat(parts[1], 64) |
||||
|
if err1 != nil || err2 != nil { |
||||
|
err = lerrors.ErrBadInput |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
col.HS = &HueSat{Hue: part1, Sat: part2} |
||||
|
} |
||||
|
|
||||
|
case "hsk": |
||||
|
{ |
||||
|
parts := strings.Split(tokens[1], ",") |
||||
|
if len(parts) < 3 { |
||||
|
err = lerrors.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 { |
||||
|
err = lerrors.ErrBadInput |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
col.HS = &HueSat{Hue: part1, Sat: part2} |
||||
|
col.K = &part3 |
||||
|
} |
||||
|
|
||||
|
case "rgb": |
||||
|
{ |
||||
|
if strings.HasPrefix(tokens[1], "#") { |
||||
|
hex := tokens[1][1:] |
||||
|
if !validHex(hex) { |
||||
|
err = lerrors.ErrBadInput |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if len(hex) == 6 { |
||||
|
col.RGB = &RGB{ |
||||
|
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 { |
||||
|
col.RGB = &RGB{ |
||||
|
Red: float64(hex2digit(hex[0])) / 15.0, |
||||
|
Green: float64(hex2digit(hex[1])) / 15.0, |
||||
|
Blue: float64(hex2digit(hex[2])) / 15.0, |
||||
|
} |
||||
|
} else { |
||||
|
err = lerrors.ErrUnknownColorFormat |
||||
|
return |
||||
|
} |
||||
|
} else { |
||||
|
parts := strings.Split(tokens[1], ",") |
||||
|
if len(parts) < 3 { |
||||
|
err = lerrors.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 = lerrors.ErrBadInput |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
col.RGB = &RGB{Red: part1, Green: part2, Blue: part3} |
||||
|
} |
||||
|
|
||||
|
normalizedRGB := col.RGB.ToHS().ToRGB() |
||||
|
col.RGB = &normalizedRGB |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
err = lerrors.ErrUnknownColorFormat |
||||
|
} |
||||
|
|
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func validHex(h string) bool { |
||||
|
for _, ch := range h { |
||||
|
if !((ch >= 'a' && ch <= 'f') || (ch >= '0' || ch <= '9')) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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') |
||||
|
} |
||||
|
} |
@ -1,17 +1,17 @@ |
|||||
package models |
|
||||
|
package color |
||||
|
|
||||
import "github.com/lucasb-eyer/go-colorful" |
import "github.com/lucasb-eyer/go-colorful" |
||||
|
|
||||
type ColorHS struct { |
|
||||
|
type HueSat struct { |
||||
Hue float64 `json:"hue"` |
Hue float64 `json:"hue"` |
||||
Sat float64 `json:"sat"` |
Sat float64 `json:"sat"` |
||||
} |
} |
||||
|
|
||||
func (hs ColorHS) ToXY() ColorXY { |
|
||||
|
func (hs HueSat) ToXY() XY { |
||||
return hs.ToRGB().ToXY() |
return hs.ToRGB().ToXY() |
||||
} |
} |
||||
|
|
||||
func (hs ColorHS) ToRGB() ColorRGB { |
|
||||
|
func (hs HueSat) ToRGB() RGB { |
||||
c := colorful.Hsv(hs.Hue, hs.Sat, 1) |
c := colorful.Hsv(hs.Hue, hs.Sat, 1) |
||||
return ColorRGB{Red: c.R, Green: c.G, Blue: c.B} |
|
||||
|
return RGB{Red: c.R, Green: c.G, Blue: c.B} |
||||
} |
} |
@ -1,28 +1,28 @@ |
|||||
package models |
|
||||
|
package color |
||||
|
|
||||
import "github.com/lucasb-eyer/go-colorful" |
import "github.com/lucasb-eyer/go-colorful" |
||||
|
|
||||
type ColorRGB struct { |
|
||||
|
type RGB struct { |
||||
Red float64 `json:"red"` |
Red float64 `json:"red"` |
||||
Green float64 `json:"green"` |
Green float64 `json:"green"` |
||||
Blue float64 `json:"blue"` |
Blue float64 `json:"blue"` |
||||
} |
} |
||||
|
|
||||
func (rgb ColorRGB) AtIntensity(intensity float64) ColorRGB { |
|
||||
|
func (rgb RGB) AtIntensity(intensity float64) RGB { |
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() |
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() |
||||
hsv2 := colorful.Hsv(hue, sat, intensity) |
hsv2 := colorful.Hsv(hue, sat, intensity) |
||||
return ColorRGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B} |
|
||||
|
return RGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B} |
||||
} |
} |
||||
|
|
||||
func (rgb ColorRGB) ToHS() ColorHS { |
|
||||
|
func (rgb RGB) ToHS() HueSat { |
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() |
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv() |
||||
return ColorHS{Hue: hue, Sat: sat} |
|
||||
|
return HueSat{Hue: hue, Sat: sat} |
||||
} |
} |
||||
|
|
||||
func (rgb ColorRGB) ToXY() ColorXY { |
|
||||
|
func (rgb RGB) ToXY() XY { |
||||
x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz() |
x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz() |
||||
|
|
||||
return ColorXY{ |
|
||||
|
return XY{ |
||||
X: x / (x + y + z), |
X: x / (x + y + z), |
||||
Y: y / (x + y + z), |
Y: y / (x + y + z), |
||||
} |
} |
@ -1,14 +1,16 @@ |
|||||
package drivers |
package drivers |
||||
|
|
||||
import "git.aiterp.net/lucifer/new-server/models" |
|
||||
|
import ( |
||||
|
"git.aiterp.net/lucifer/new-server/internal/lerrors" |
||||
|
"git.aiterp.net/lucifer/new-server/models" |
||||
|
) |
||||
|
|
||||
type DriverMap map[models.DriverKind]models.Driver |
type DriverMap map[models.DriverKind]models.Driver |
||||
|
|
||||
func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) { |
func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) { |
||||
if m[kind] == nil { |
if m[kind] == nil { |
||||
return nil, models.ErrUnknownDriver |
|
||||
|
return nil, lerrors.ErrUnknownDriver |
||||
} |
} |
||||
|
|
||||
return m[kind], nil |
return m[kind], nil |
||||
} |
} |
||||
|
|
@ -1,4 +1,4 @@ |
|||||
package models |
|
||||
|
package lerrors |
||||
|
|
||||
import "errors" |
import "errors" |
||||
|
|
@ -1,368 +0,0 @@ |
|||||
package models |
|
||||
|
|
||||
import ( |
|
||||
"fmt" |
|
||||
"github.com/lucasb-eyer/go-colorful" |
|
||||
"strconv" |
|
||||
"strings" |
|
||||
) |
|
||||
|
|
||||
type ColorValue struct { |
|
||||
RGB *ColorRGB `json:"rgb,omitempty"` |
|
||||
HS *ColorHS `json:"hs,omitempty"` |
|
||||
K *int `json:"k,omitempty"` |
|
||||
XY *ColorXY `json:"xy,omitempty"` |
|
||||
} |
|
||||
|
|
||||
func (cv *ColorValue) IsHueSat() bool { |
|
||||
return cv.HS != nil |
|
||||
} |
|
||||
|
|
||||
func (cv *ColorValue) IsHueSatKelvin() bool { |
|
||||
return cv.HS != nil && cv.K != nil |
|
||||
} |
|
||||
|
|
||||
func (cv *ColorValue) IsKelvin() bool { |
|
||||
return cv.K != nil |
|
||||
} |
|
||||
|
|
||||
func (cv *ColorValue) IsEmpty() bool { |
|
||||
return *cv == ColorValue{} |
|
||||
} |
|
||||
|
|
||||
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 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 (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 |
|
||||
} |
|
||||
|
|
||||
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 { |
|
||||
err = ErrBadInput |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
switch tokens[0] { |
|
||||
case "kelvin", "k": |
|
||||
{ |
|
||||
parsedPart, err := strconv.Atoi(tokens[1]) |
|
||||
if err != nil { |
|
||||
err = ErrBadInput |
|
||||
break |
|
||||
} |
|
||||
|
|
||||
cv2.K = &parsedPart |
|
||||
} |
|
||||
|
|
||||
case "xy": |
|
||||
{ |
|
||||
parts := strings.Split(tokens[1], ",") |
|
||||
if len(parts) < 2 { |
|
||||
err = ErrUnknownColorFormat |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
x, err1 := strconv.ParseFloat(parts[0], 64) |
|
||||
y, err2 := strconv.ParseFloat(parts[1], 64) |
|
||||
if err1 != nil || err2 != nil { |
|
||||
err = ErrBadInput |
|
||||
break |
|
||||
} |
|
||||
|
|
||||
cv2.XY = &ColorXY{x, y} |
|
||||
} |
|
||||
|
|
||||
case "hs": |
|
||||
{ |
|
||||
parts := strings.Split(tokens[1], ",") |
|
||||
if len(parts) < 2 { |
|
||||
err = ErrUnknownColorFormat |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64) |
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64) |
|
||||
if err1 != nil || err2 != nil { |
|
||||
err = ErrBadInput |
|
||||
break |
|
||||
} |
|
||||
|
|
||||
cv2.HS = &ColorHS{Hue: part1, Sat: part2} |
|
||||
} |
|
||||
|
|
||||
case "hsk": |
|
||||
{ |
|
||||
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.Atoi(parts[2]) |
|
||||
if err1 != nil || err2 != nil || err3 != nil { |
|
||||
err = ErrBadInput |
|
||||
break |
|
||||
} |
|
||||
|
|
||||
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 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') |
|
||||
} |
|
||||
} |
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue