You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
434 lines
8.1 KiB
434 lines
8.1 KiB
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}
|
|
}
|
|
|
|
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:%.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: 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
|
|
}
|
|
|
|
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')
|
|
}
|
|
}
|