This commit is contained in:
+83
-39
@@ -8,9 +8,14 @@ import (
|
||||
)
|
||||
|
||||
type ColorValue struct {
|
||||
Hue float64 `json:"h,omitempty"` // 0..360
|
||||
Saturation float64 `json:"s,omitempty"` // 0..=1
|
||||
Kelvin int `json:"kelvin,omitempty"`
|
||||
Hue float64 `json:"h,omitempty"` // 0..360
|
||||
Saturation float64 `json:"s,omitempty"` // 0..=1
|
||||
Kelvin int `json:"kelvin,omitempty"`
|
||||
XY *ColorXY `json:"xy,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsEmpty() bool {
|
||||
return c.XY == nil && c.Kelvin == 0 && c.Saturation == 0 && c.Hue == 0
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsHueSat() bool {
|
||||
@@ -18,7 +23,27 @@ func (c *ColorValue) IsHueSat() bool {
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsKelvin() bool {
|
||||
return c.Kelvin > 0
|
||||
return !c.IsXY() && c.Kelvin > 0
|
||||
}
|
||||
|
||||
func (c *ColorValue) IsXY() bool {
|
||||
return c.XY != nil
|
||||
}
|
||||
|
||||
// ToXY converts the color to XY if possible. If the color already is XY, it returns
|
||||
// a copy of its held value. There are no guarantees of conforming to a gamut, however.
|
||||
func (c *ColorValue) ToXY() (xy ColorXY, ok bool) {
|
||||
if c.XY != nil {
|
||||
xy = *c.XY
|
||||
ok = true
|
||||
} else if c.Kelvin > 0 && c.Hue < 0.001 && c.Saturation <= 0.001 {
|
||||
ok = false
|
||||
} else {
|
||||
xy = hsToXY(c.Hue, c.Saturation)
|
||||
ok = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ColorValue) String() string {
|
||||
@@ -29,56 +54,75 @@ func (c *ColorValue) String() string {
|
||||
return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func ParseColorValue(raw string) (ColorValue, error) {
|
||||
tokens := strings.SplitN(raw, ":", 2)
|
||||
if len(tokens) != 2 {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
if tokens[0] == "kelvin" || tokens[0] == "k" {
|
||||
parsedPart, err := strconv.Atoi(tokens[1])
|
||||
if err != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
switch tokens[0] {
|
||||
case "kelvin", "k":
|
||||
{
|
||||
parsedPart, err := strconv.Atoi(tokens[1])
|
||||
if err != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
return ColorValue{Kelvin: parsedPart}, nil
|
||||
}
|
||||
|
||||
return ColorValue{Kelvin: parsedPart}, nil
|
||||
}
|
||||
case "xy":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
}
|
||||
|
||||
if tokens[0] == "hs" {
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
x, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
y, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
return ColorValue{XY: &ColorXY{X: x, Y: y}}, nil
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
case "hs":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 2 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
}
|
||||
|
||||
part1, err1 := strconv.ParseFloat(parts[0], 64)
|
||||
part2, err2 := strconv.ParseFloat(parts[1], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
|
||||
}
|
||||
|
||||
return ColorValue{Hue: math.Mod(part1, 360), Saturation: math.Min(math.Max(part2, 0), 1)}, nil
|
||||
}
|
||||
case "hsk":
|
||||
{
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 3 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
}
|
||||
|
||||
if tokens[0] == "hsk" {
|
||||
parts := strings.Split(tokens[1], ",")
|
||||
if len(parts) < 3 {
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
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 {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
return ColorValue{
|
||||
Hue: math.Mod(part1, 360),
|
||||
Saturation: math.Min(math.Max(part2, 0), 1),
|
||||
Kelvin: part3,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return ColorValue{}, ErrBadInput
|
||||
}
|
||||
|
||||
return ColorValue{
|
||||
Hue: math.Mod(part1, 360),
|
||||
Saturation: math.Min(math.Max(part2, 0), 1),
|
||||
Kelvin: part3,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return ColorValue{}, ErrUnknownColorFormat
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"math"
|
||||
)
|
||||
|
||||
const eps = 0.0001
|
||||
const epsSquare = eps * eps
|
||||
|
||||
type ColorGamut struct {
|
||||
Red ColorXY `json:"red"`
|
||||
Green ColorXY `json:"green"`
|
||||
Blue ColorXY `json:"blue"`
|
||||
}
|
||||
|
||||
func (cg *ColorGamut) side(x1, y1, x2, y2, x, y float64) float64 {
|
||||
return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1)
|
||||
}
|
||||
|
||||
func (cg *ColorGamut) naiveContains(color ColorXY) 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 *ColorGamut) 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 *ColorGamut) isInBounds(color ColorXY) bool {
|
||||
x, y := color.X, color.Y
|
||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
||||
|
||||
return !(x < xMin || xMax < x || y < yMin || yMax < y)
|
||||
}
|
||||
|
||||
func (cg *ColorGamut) 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 *ColorGamut) atTheEdge(color ColorXY) 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 *ColorGamut) Contains(color ColorXY) bool {
|
||||
if cg == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color))
|
||||
}
|
||||
|
||||
func (cg *ColorGamut) Conform(color ColorXY) ColorXY {
|
||||
if cg.Contains(color) {
|
||||
return color
|
||||
}
|
||||
|
||||
var best *ColorXY
|
||||
|
||||
xMin, xMax, yMin, yMax := cg.getBounds()
|
||||
|
||||
for x := xMin; x < xMax; x += 0.001 {
|
||||
for y := yMin; y < yMax; y += 0.001 {
|
||||
color2 := ColorXY{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.0001 {
|
||||
for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0001 {
|
||||
color2 := ColorXY{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.00001 {
|
||||
for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00001 {
|
||||
color2 := ColorXY{X: x, Y: y}
|
||||
|
||||
if cg.atTheEdge(color2) {
|
||||
if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
|
||||
best = &color2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return *best
|
||||
}
|
||||
|
||||
type ColorXY struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
}
|
||||
|
||||
func (xy ColorXY) DistanceTo(other ColorXY) float64 {
|
||||
return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
func (xy ColorXY) Round() ColorXY {
|
||||
return ColorXY{
|
||||
X: math.Round(xy.X*10000) / 10000,
|
||||
Y: math.Round(xy.Y*10000) / 10000,
|
||||
}
|
||||
}
|
||||
|
||||
func hsToXY(hue, sat float64) ColorXY {
|
||||
c := colorful.Hsv(hue, sat, 1)
|
||||
red255, green255, blue255 := c.RGB255()
|
||||
red := float64(red255) / 255.0
|
||||
green := float64(green255) / 255.0
|
||||
blue := float64(blue255) / 255.0
|
||||
|
||||
return screenRGBToXY(red, green, blue)
|
||||
}
|
||||
|
||||
func rgbToXY(red float64, green float64, blue float64) ColorXY {
|
||||
x := red*0.649926 + green*0.103455 + blue*0.197109
|
||||
y := red*0.234327 + green*0.743075 + blue*0.022598
|
||||
z := green*0.053077 + blue*1.035763
|
||||
|
||||
return ColorXY{
|
||||
X: x / (x + y + z),
|
||||
Y: y / (x + y + z),
|
||||
}
|
||||
}
|
||||
|
||||
func screenRGBToXY(red, green, blue float64) ColorXY {
|
||||
for _, component := range []*float64{&red, &green, &blue} {
|
||||
if *component > 0.04045 {
|
||||
*component = math.Pow((*component+0.055)/(1.055), 2.4)
|
||||
} else {
|
||||
*component /= 12.92
|
||||
}
|
||||
}
|
||||
|
||||
return rgbToXY(red, green, blue)
|
||||
}
|
||||
@@ -86,6 +86,7 @@ var (
|
||||
DCColorHS DeviceCapability = "ColorHS"
|
||||
DCColorHSK DeviceCapability = "ColorHSK"
|
||||
DCColorKelvin DeviceCapability = "ColorKelvin"
|
||||
DCColorXY DeviceCapability = "ColorXY"
|
||||
DCButtons DeviceCapability = "Buttons"
|
||||
DCPresence DeviceCapability = "Presence"
|
||||
DCIntensity DeviceCapability = "Intensity"
|
||||
|
||||
Reference in New Issue
Block a user