|
|
@ -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 |
|
|
|
} |
|
|
|
|
|
|
|
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} |
|
|
|
} |
|
|
|
|
|
|
|
return ColorValue{ |
|
|
|
Hue: math.Mod(part1, 360), |
|
|
|
Saturation: math.Min(math.Max(part2, 0), 1), |
|
|
|
Kelvin: part3, |
|
|
|
}, nil |
|
|
|
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') |
|
|
|
} |
|
|
|
} |
xxxxxxxxxx