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.
368 lines
7.2 KiB
368 lines
7.2 KiB
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/lucasb-eyer/go-colorful"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type ColorValue struct {
|
|
RGB *ColorRGB `json:"rgb,omitempty"`
|
|
HS *ColorHS `json:"hs,omitempty"`
|
|
K *int `json:"k,omitempty"`
|
|
XY *ColorXY `json:"xy,omitempty"`
|
|
}
|
|
|
|
func (cv *ColorValue) IsHueSat() bool {
|
|
return cv.HS != nil
|
|
}
|
|
|
|
func (cv *ColorValue) IsHueSatKelvin() bool {
|
|
return cv.HS != nil && cv.K != nil
|
|
}
|
|
|
|
func (cv *ColorValue) IsKelvin() bool {
|
|
return cv.K != nil
|
|
}
|
|
|
|
func (cv *ColorValue) IsEmpty() bool {
|
|
return *cv == ColorValue{}
|
|
}
|
|
|
|
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 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 (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
|
|
}
|
|
|
|
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 {
|
|
err = ErrBadInput
|
|
return
|
|
}
|
|
|
|
switch tokens[0] {
|
|
case "kelvin", "k":
|
|
{
|
|
parsedPart, err := strconv.Atoi(tokens[1])
|
|
if err != nil {
|
|
err = ErrBadInput
|
|
break
|
|
}
|
|
|
|
cv2.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 = ErrBadInput
|
|
break
|
|
}
|
|
|
|
cv2.XY = &ColorXY{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 = ErrBadInput
|
|
break
|
|
}
|
|
|
|
cv2.HS = &ColorHS{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 = 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}
|
|
}
|
|
|
|
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 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')
|
|
}
|
|
}
|