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.
204 lines
4.7 KiB
204 lines
4.7 KiB
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)
|
|
}
|