refactor color system to be separate from models, also move errors out of models.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
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')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package color
|
||||
|
||||
import "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
type HueSat struct {
|
||||
Hue float64 `json:"hue"`
|
||||
Sat float64 `json:"sat"`
|
||||
}
|
||||
|
||||
func (hs HueSat) ToXY() XY {
|
||||
return hs.ToRGB().ToXY()
|
||||
}
|
||||
|
||||
func (hs HueSat) ToRGB() RGB {
|
||||
c := colorful.Hsv(hs.Hue, hs.Sat, 1)
|
||||
return RGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package color
|
||||
|
||||
import "github.com/lucasb-eyer/go-colorful"
|
||||
|
||||
type RGB struct {
|
||||
Red float64 `json:"red"`
|
||||
Green float64 `json:"green"`
|
||||
Blue float64 `json:"blue"`
|
||||
}
|
||||
|
||||
func (rgb RGB) AtIntensity(intensity float64) RGB {
|
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
hsv2 := colorful.Hsv(hue, sat, intensity)
|
||||
return RGB{Red: hsv2.R, Green: hsv2.G, Blue: hsv2.B}
|
||||
}
|
||||
|
||||
func (rgb RGB) ToHS() HueSat {
|
||||
hue, sat, _ := colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}.Hsv()
|
||||
return HueSat{Hue: hue, Sat: sat}
|
||||
}
|
||||
|
||||
func (rgb RGB) ToXY() XY {
|
||||
x, y, z := (colorful.Color{R: rgb.Red, G: rgb.Green, B: rgb.Blue}).Xyz()
|
||||
|
||||
return XY{
|
||||
X: x / (x + y + z),
|
||||
Y: y / (x + y + z),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package color
|
||||
|
||||
import (
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"math"
|
||||
)
|
||||
|
||||
const eps = 0.0001
|
||||
const epsSquare = eps * eps
|
||||
|
||||
type Gamut struct {
|
||||
Red XY `json:"red"`
|
||||
Green XY `json:"green"`
|
||||
Blue XY `json:"blue"`
|
||||
}
|
||||
|
||||
func (cg *Gamut) side(x1, y1, x2, y2, x, y float64) float64 {
|
||||
return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1)
|
||||
}
|
||||
|
||||
func (cg *Gamut) naiveContains(color XY) bool {
|
||||
x, y := color.X, color.Y
|
||||
x1, y1 := cg.Red.X, cg.Red.Y
|
||||
x2, y2 := cg.Green.X, cg.Green.Y
|
||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
||||
|
||||
checkSide1 := cg.side(x1, y1, x2, y2, x, y) < 0
|
||||
checkSide2 := cg.side(x2, y2, x3, y3, x, y) < 0
|
||||
checkSide3 := cg.side(x3, y3, x1, y1, x, y) < 0
|
||||
|
||||
return checkSide1 && checkSide2 && checkSide3
|
||||
}
|
||||
|
||||
func (cg *Gamut) getBounds() (xMin, xMax, yMin, yMax float64) {
|
||||
x1, y1 := cg.Red.X, cg.Red.Y
|
||||
x2, y2 := cg.Green.X, cg.Green.Y
|
||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
||||
|
||||
xMin = math.Min(x1, math.Min(x2, x3)) - eps
|
||||
xMax = math.Max(x1, math.Max(x2, x3)) + eps
|
||||
yMin = math.Min(y1, math.Min(y2, y3)) - eps
|
||||
yMax = math.Max(y1, math.Max(y2, y3)) + eps
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (cg *Gamut) isInBounds(color XY) bool {
|
||||
x, y := color.X, color.Y
|
||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
||||
|
||||
return !(x < xMin || xMax < x || y < yMin || yMax < y)
|
||||
}
|
||||
|
||||
func (cg *Gamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 {
|
||||
sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)
|
||||
dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1
|
||||
if dotProduct < 0 {
|
||||
return (x-x1)*(x-x1) + (y-y1)*(y-y1)
|
||||
} else if dotProduct <= 1 {
|
||||
sqLength2 := (x1-x)*(x1-x) + (y1-y)*(y1-y)
|
||||
return sqLength2 - dotProduct*dotProduct*sqLength1
|
||||
} else {
|
||||
return (x-x2)*(x-x2) + (y-y2)*(y-y2)
|
||||
}
|
||||
}
|
||||
|
||||
func (cg *Gamut) atTheEdge(color XY) bool {
|
||||
x, y := color.X, color.Y
|
||||
x1, y1 := cg.Red.X, cg.Red.Y
|
||||
x2, y2 := cg.Green.X, cg.Green.Y
|
||||
x3, y3 := cg.Blue.X, cg.Blue.Y
|
||||
|
||||
if cg.distanceSquarePointToSegment(x1, y1, x2, y2, x, y) <= epsSquare {
|
||||
return true
|
||||
}
|
||||
if cg.distanceSquarePointToSegment(x2, y2, x3, y3, x, y) <= epsSquare {
|
||||
return true
|
||||
}
|
||||
if cg.distanceSquarePointToSegment(x3, y3, x1, y1, x, y) <= epsSquare {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cg *Gamut) Contains(color XY) bool {
|
||||
if cg == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color))
|
||||
}
|
||||
|
||||
func (cg *Gamut) Conform(color XY) XY {
|
||||
if cg.Contains(color) {
|
||||
return color
|
||||
}
|
||||
|
||||
var best *XY
|
||||
|
||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
||||
|
||||
for x := xMin; x < xMax; x += 0.001 {
|
||||
for y := yMin; y < yMax; y += 0.001 {
|
||||
color2 := XY{X: x, Y: y}
|
||||
|
||||
if cg.Contains(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best == nil {
|
||||
centerX := (cg.Red.X + cg.Green.X + cg.Blue.X) / 3
|
||||
centerY := (cg.Red.Y + cg.Green.Y + cg.Blue.Y) / 3
|
||||
|
||||
stepX := (centerX - color.X) / 5000
|
||||
stepY := (centerY - color.Y) / 5000
|
||||
|
||||
for !cg.Contains(color) {
|
||||
color.X += stepX
|
||||
color.Y += stepY
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
|
||||
for x := best.X - 0.001; x < best.X+0.001; x += 0.0002 {
|
||||
for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0002 {
|
||||
color2 := XY{X: x, Y: y}
|
||||
|
||||
if cg.atTheEdge(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for x := best.X - 0.0001; x < best.X+0.0001; x += 0.00003 {
|
||||
for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00003 {
|
||||
color2 := XY{X: x, Y: y}
|
||||
|
||||
if cg.atTheEdge(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return *best
|
||||
}
|
||||
|
||||
type XY struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
}
|
||||
|
||||
func (xy XY) ToRGB() RGB {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
c := colorful.Hsv(h, s, 1)
|
||||
return RGB{Red: c.R, Green: c.G, Blue: c.B}
|
||||
}
|
||||
|
||||
func (xy XY) ToHS() HueSat {
|
||||
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
||||
return HueSat{Hue: h, Sat: s}
|
||||
}
|
||||
|
||||
func (xy XY) DistanceTo(other XY) float64 {
|
||||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
func (xy XY) Round() XY {
|
||||
return XY{
|
||||
X: math.Round(xy.X*10000) / 10000,
|
||||
Y: math.Round(xy.Y*10000) / 10000,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user