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, } }