package color import ( "encoding/json" "fmt" "git.aiterp.net/lucifer3/server/internal/gentools" "github.com/lucasb-eyer/go-colorful" "log" "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) MarshalJSON() ([]byte, error) { return json.Marshal(col.String()) } func (col *Color) UnmarshalJSON(bytes []byte) error { var s string err := json.Unmarshal(bytes, &s) if err != nil { return err } *col, err = Parse(s) return err } 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.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 && other.K != nil && *col.K != *other.K { return false } return true } if col.K != nil { if other.K != nil { return false } 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, _ := other.ToXY() return math.Abs(xy1.XY.X-xy2.XY.X)+math.Abs(xy1.XY.Y-xy2.XY.Y) < 0.001 } // 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 } else if col.K != nil { rgb := kToRGB(*col.K) col2 = Color{RGB: &rgb} ok = true } if ok { col2 = Color{RGB: &RGB{ Red: math.Max(col2.RGB.Red, 0.0), Green: math.Max(col2.RGB.Green, 0.0), Blue: math.Max(col2.RGB.Blue, 0.0), }} } 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 } else if col.K != nil { hs := kToRGB(*col.K).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 } else if col.K != nil { xy := kToRGB(*col.K).ToXY() col2 = Color{XY: &xy} ok = true } return } func (col *Color) Interpolate(other Color, fac float64) Color { if col.AlmostEquals(other) { log.Println("ALMOST EQUAL", col.String(), other.String()) return *col } // Special case for both kelvin values. if col.IsKelvin() && other.IsKelvin() { k1 := *col.K k2 := *other.K if k2 > k1 { return Color{K: gentools.Ptr(k1 + int(float64(k2-k1)*fac))} } else { return Color{K: gentools.Ptr(k1 - int(float64(k1-k2)*fac))} } } if fac < 0.000001 { return *col } else if fac > 0.999999 { return other } // Get the colorful values. cvCF := col.colorful() otherCF := other.colorful() // Blend and normalize (clamping is hax to avoid issues with some colors) 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:%.2f,%.3f,%d", col.HS.Hue, col.HS.Sat, *col.K) case col.HS != nil: return fmt.Sprintf("hs:%.2f,%.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.K != nil: col2, _ := col.ToXY() return col2.colorful() 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: 1, B: 1, G: 1} } } 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 } convertTokens := strings.SplitN(raw, "|", 2) tokens := strings.SplitN(convertTokens[0], ":", 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 } if err == nil && len(convertTokens) == 2 { switch convertTokens[1] { case "xy": col2, ok := col.ToXY() if !ok { err = ErrUnknownColorFormat } else { col = col2 } case "rgb": col2, ok := col.ToRGB() if !ok { err = ErrUnknownColorFormat } else { col = col2 } case "hs": col2, ok := col.ToHS() if !ok { err = ErrUnknownColorFormat } else { col = col2 } 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') } } func kToRGB(kelvin int) RGB { if kelvin < 1000 { kelvin = 1000 } else if kelvin > 12000 { kelvin = 12000 } if kelvin%100 == 0 { return kelvinRGBTable[kelvin] } fac := float64(kelvin%100) / 100 floor := (kelvin / 100) * 100 ceil := floor + 100 floorRGB := kelvinRGBTable[floor] ceilRGB := kelvinRGBTable[ceil] return RGB{ Red: (floorRGB.Red * fac) + (ceilRGB.Red * (1 - fac)), Green: (floorRGB.Green * fac) + (ceilRGB.Green * (1 - fac)), Blue: (floorRGB.Blue * fac) + (ceilRGB.Blue * (1 - fac)), } } var kelvinRGBTable = map[int]RGB{ 1000: {Red: 1.000, Green: 0.220, Blue: 0.000}, 1100: {Red: 1.000, Green: 0.278, Blue: 0.000}, 1200: {Red: 1.000, Green: 0.325, Blue: 0.000}, 1300: {Red: 1.000, Green: 0.365, Blue: 0.000}, 1400: {Red: 1.000, Green: 0.396, Blue: 0.000}, 1500: {Red: 1.000, Green: 0.427, Blue: 0.000}, 1600: {Red: 1.000, Green: 0.451, Blue: 0.000}, 1700: {Red: 1.000, Green: 0.475, Blue: 0.000}, 1800: {Red: 1.000, Green: 0.494, Blue: 0.000}, 1900: {Red: 1.000, Green: 0.514, Blue: 0.000}, 2000: {Red: 1.000, Green: 0.541, Blue: 0.071}, 2100: {Red: 1.000, Green: 0.557, Blue: 0.129}, 2200: {Red: 1.000, Green: 0.576, Blue: 0.173}, 2300: {Red: 1.000, Green: 0.596, Blue: 0.212}, 2400: {Red: 1.000, Green: 0.616, Blue: 0.247}, 2500: {Red: 1.000, Green: 0.631, Blue: 0.282}, 2600: {Red: 1.000, Green: 0.647, Blue: 0.310}, 2700: {Red: 1.000, Green: 0.663, Blue: 0.341}, 2800: {Red: 1.000, Green: 0.678, Blue: 0.369}, 2900: {Red: 1.000, Green: 0.694, Blue: 0.396}, 3000: {Red: 1.000, Green: 0.706, Blue: 0.420}, 3100: {Red: 1.000, Green: 0.722, Blue: 0.447}, 3200: {Red: 1.000, Green: 0.733, Blue: 0.471}, 3300: {Red: 1.000, Green: 0.745, Blue: 0.494}, 3400: {Red: 1.000, Green: 0.757, Blue: 0.518}, 3500: {Red: 1.000, Green: 0.769, Blue: 0.537}, 3600: {Red: 1.000, Green: 0.780, Blue: 0.561}, 3700: {Red: 1.000, Green: 0.788, Blue: 0.580}, 3800: {Red: 1.000, Green: 0.800, Blue: 0.600}, 3900: {Red: 1.000, Green: 0.808, Blue: 0.624}, 4000: {Red: 1.000, Green: 0.820, Blue: 0.639}, 4100: {Red: 1.000, Green: 0.827, Blue: 0.659}, 4200: {Red: 1.000, Green: 0.835, Blue: 0.678}, 4300: {Red: 1.000, Green: 0.843, Blue: 0.694}, 4400: {Red: 1.000, Green: 0.851, Blue: 0.714}, 4500: {Red: 1.000, Green: 0.859, Blue: 0.729}, 4600: {Red: 1.000, Green: 0.867, Blue: 0.745}, 4700: {Red: 1.000, Green: 0.875, Blue: 0.761}, 4800: {Red: 1.000, Green: 0.882, Blue: 0.776}, 4900: {Red: 1.000, Green: 0.890, Blue: 0.792}, 5000: {Red: 1.000, Green: 0.894, Blue: 0.808}, 5100: {Red: 1.000, Green: 0.902, Blue: 0.824}, 5200: {Red: 1.000, Green: 0.910, Blue: 0.835}, 5300: {Red: 1.000, Green: 0.914, Blue: 0.851}, 5400: {Red: 1.000, Green: 0.922, Blue: 0.863}, 5500: {Red: 1.000, Green: 0.925, Blue: 0.878}, 5600: {Red: 1.000, Green: 0.933, Blue: 0.890}, 5700: {Red: 1.000, Green: 0.937, Blue: 0.902}, 5800: {Red: 1.000, Green: 0.941, Blue: 0.914}, 5900: {Red: 1.000, Green: 0.949, Blue: 0.925}, 6000: {Red: 1.000, Green: 0.953, Blue: 0.937}, 6100: {Red: 1.000, Green: 0.957, Blue: 0.949}, 6200: {Red: 1.000, Green: 0.961, Blue: 0.961}, 6300: {Red: 1.000, Green: 0.965, Blue: 0.969}, 6400: {Red: 1.000, Green: 0.973, Blue: 0.984}, 6500: {Red: 1.000, Green: 0.976, Blue: 0.992}, 6600: {Red: 0.996, Green: 0.976, Blue: 1.000}, 6700: {Red: 0.988, Green: 0.969, Blue: 1.000}, 6800: {Red: 0.976, Green: 0.965, Blue: 1.000}, 6900: {Red: 0.969, Green: 0.961, Blue: 1.000}, 7000: {Red: 0.961, Green: 0.953, Blue: 1.000}, 7100: {Red: 0.953, Green: 0.949, Blue: 1.000}, 7200: {Red: 0.941, Green: 0.945, Blue: 1.000}, 7300: {Red: 0.937, Green: 0.941, Blue: 1.000}, 7400: {Red: 0.929, Green: 0.937, Blue: 1.000}, 7500: {Red: 0.922, Green: 0.933, Blue: 1.000}, 7600: {Red: 0.914, Green: 0.929, Blue: 1.000}, 7700: {Red: 0.906, Green: 0.925, Blue: 1.000}, 7800: {Red: 0.902, Green: 0.922, Blue: 1.000}, 7900: {Red: 0.894, Green: 0.918, Blue: 1.000}, 8000: {Red: 0.890, Green: 0.914, Blue: 1.000}, 8100: {Red: 0.882, Green: 0.910, Blue: 1.000}, 8200: {Red: 0.878, Green: 0.906, Blue: 1.000}, 8300: {Red: 0.871, Green: 0.902, Blue: 1.000}, 8400: {Red: 0.867, Green: 0.902, Blue: 1.000}, 8500: {Red: 0.863, Green: 0.898, Blue: 1.000}, 8600: {Red: 0.855, Green: 0.898, Blue: 1.000}, 8700: {Red: 0.851, Green: 0.890, Blue: 1.000}, 8800: {Red: 0.847, Green: 0.890, Blue: 1.000}, 8900: {Red: 0.843, Green: 0.886, Blue: 1.000}, 9000: {Red: 0.839, Green: 0.882, Blue: 1.000}, 9100: {Red: 0.831, Green: 0.882, Blue: 1.000}, 9200: {Red: 0.827, Green: 0.878, Blue: 1.000}, 9300: {Red: 0.824, Green: 0.875, Blue: 1.000}, 9400: {Red: 0.820, Green: 0.875, Blue: 1.000}, 9500: {Red: 0.816, Green: 0.871, Blue: 1.000}, 9600: {Red: 0.812, Green: 0.867, Blue: 1.000}, 9700: {Red: 0.812, Green: 0.867, Blue: 1.000}, 9800: {Red: 0.808, Green: 0.863, Blue: 1.000}, 9900: {Red: 0.804, Green: 0.863, Blue: 1.000}, 10000: {Red: 0.812, Green: 0.855, Blue: 1.000}, 10100: {Red: 0.812, Green: 0.855, Blue: 1.000}, 10200: {Red: 0.808, Green: 0.851, Blue: 1.000}, 10300: {Red: 0.804, Green: 0.851, Blue: 1.000}, 10400: {Red: 0.800, Green: 0.847, Blue: 1.000}, 10500: {Red: 0.800, Green: 0.847, Blue: 1.000}, 10600: {Red: 0.796, Green: 0.843, Blue: 1.000}, 10700: {Red: 0.792, Green: 0.843, Blue: 1.000}, 10800: {Red: 0.792, Green: 0.839, Blue: 1.000}, 10900: {Red: 0.788, Green: 0.839, Blue: 1.000}, 11000: {Red: 0.784, Green: 0.835, Blue: 1.000}, 11100: {Red: 0.784, Green: 0.835, Blue: 1.000}, 11200: {Red: 0.780, Green: 0.831, Blue: 1.000}, 11300: {Red: 0.776, Green: 0.831, Blue: 1.000}, 11400: {Red: 0.776, Green: 0.831, Blue: 1.000}, 11500: {Red: 0.773, Green: 0.827, Blue: 1.000}, 11600: {Red: 0.773, Green: 0.827, Blue: 1.000}, 11700: {Red: 0.773, Green: 0.824, Blue: 1.000}, 11800: {Red: 0.769, Green: 0.824, Blue: 1.000}, 11900: {Red: 0.765, Green: 0.824, Blue: 1.000}, 12000: {Red: 0.765, Green: 0.820, Blue: 1.000}, }