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.0002 { for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0002 { 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.00003 { for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00003 { 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 rgbToXY(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) }