You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
4.2 KiB
184 lines
4.2 KiB
package color
|
|
|
|
import (
|
|
"github.com/lucasb-eyer/go-colorful"
|
|
"math"
|
|
)
|
|
|
|
const eps = 0.0001
|
|
const epsSquare = eps * eps
|
|
|
|
type Gamut struct {
|
|
Label string `json:"label,omitempty"`
|
|
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, math.Max(xy.Y, 0.0001), 0.5).Hsv()
|
|
c := colorful.Hsv(math.Mod(h, 360), s, 1)
|
|
return RGB{Red: math.Max(c.R, 0), Green: math.Max(c.G, 0), Blue: math.Max(c.B, 0)}
|
|
}
|
|
|
|
func (xy XY) ToHS() HueSat {
|
|
h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
|
|
return HueSat{Hue: math.Mod(h, 360), Sat: s}
|
|
}
|
|
|
|
func (xy XY) DistanceTo(other XY) float64 {
|
|
return math.Abs(xy.X-other.X) + math.Abs(xy.Y-other.Y)
|
|
//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,
|
|
}
|
|
}
|