|
|
package color
import ( "fmt" "github.com/lucasb-eyer/go-colorful" "math" "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} }
func (col *Color) AlmostEquals(other Color) bool { if (col.K != nil) != (other.K != nil) { return false }
if col.HS != nil && other.HS != nil { if math.Abs(col.HS.Hue-other.HS.Hue) > 0.01 { return false } if math.Abs(col.HS.Sat-other.HS.Sat) > 0.01 { return false }
if col.K != nil && *col.K != *other.K { return false }
return true }
if col.K != nil { return *col.K == *other.K }
if col.RGB != nil && other.RGB != nil { if math.Abs(col.RGB.Red-other.RGB.Red) > 0.01 { return false } if math.Abs(col.RGB.Blue-other.RGB.Blue) > 0.01 { return false } if math.Abs(col.RGB.Green-other.RGB.Green) > 0.01 { return false }
return true }
xy1, _ := col.ToXY() xy2, _ := col.ToXY() if math.Abs(xy1.XY.X-xy2.XY.X) > 0.001 { return false } if math.Abs(xy1.XY.Y-xy2.XY.Y) > 0.001 { return false }
return true }
// 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 MustParse(raw string) Color { col, err := Parse(raw) if err != nil { panic(err) }
return col }
func Parse(raw string) (col Color, err error) { if raw == "" { return }
tokens := strings.SplitN(raw, ":", 2) if len(tokens) != 2 { err = ErrBadColorInput return }
switch tokens[0] { case "kelvin", "k": { parsedPart, err := strconv.Atoi(tokens[1]) if err != nil { err = ErrBadColorInput break }
col.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 = ErrBadColorInput break }
col.XY = &XY{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 = ErrBadColorInput break }
col.HS = &HueSat{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 = ErrBadColorInput 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 = ErrBadColorInput 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 = 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 = ErrBadColorInput break }
col.RGB = &RGB{Red: part1, Green: part2, Blue: part3} }
normalizedRGB := col.RGB.ToHS().ToRGB() col.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') } }
|