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') } }