add very partial hue2 driver wip.
continuous-integration/drone/push Build is passing

This commit is contained in:
2022-02-14 22:03:41 +01:00
parent 44bd327a71
commit 73e6cba770
7 changed files with 895 additions and 39 deletions
+83 -39
View File
@@ -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
+204
View File
@@ -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)
}
+1
View File
@@ -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"