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.
 
 
 
 

369 lines
7.2 KiB

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