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.

182 lines
4.0 KiB

  1. package color
  2. import (
  3. "github.com/lucasb-eyer/go-colorful"
  4. "math"
  5. )
  6. const eps = 0.0001
  7. const epsSquare = eps * eps
  8. type Gamut struct {
  9. Red XY `json:"red"`
  10. Green XY `json:"green"`
  11. Blue XY `json:"blue"`
  12. }
  13. func (cg *Gamut) side(x1, y1, x2, y2, x, y float64) float64 {
  14. return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1)
  15. }
  16. func (cg *Gamut) naiveContains(color XY) bool {
  17. x, y := color.X, color.Y
  18. x1, y1 := cg.Red.X, cg.Red.Y
  19. x2, y2 := cg.Green.X, cg.Green.Y
  20. x3, y3 := cg.Blue.X, cg.Blue.Y
  21. checkSide1 := cg.side(x1, y1, x2, y2, x, y) < 0
  22. checkSide2 := cg.side(x2, y2, x3, y3, x, y) < 0
  23. checkSide3 := cg.side(x3, y3, x1, y1, x, y) < 0
  24. return checkSide1 && checkSide2 && checkSide3
  25. }
  26. func (cg *Gamut) getBounds() (xMin, xMax, yMin, yMax float64) {
  27. x1, y1 := cg.Red.X, cg.Red.Y
  28. x2, y2 := cg.Green.X, cg.Green.Y
  29. x3, y3 := cg.Blue.X, cg.Blue.Y
  30. xMin = math.Min(x1, math.Min(x2, x3)) - eps
  31. xMax = math.Max(x1, math.Max(x2, x3)) + eps
  32. yMin = math.Min(y1, math.Min(y2, y3)) - eps
  33. yMax = math.Max(y1, math.Max(y2, y3)) + eps
  34. return
  35. }
  36. func (cg *Gamut) isInBounds(color XY) bool {
  37. x, y := color.X, color.Y
  38. xMin, xMax, yMin, yMax := cg.getBounds()
  39. return !(x < xMin || xMax < x || y < yMin || yMax < y)
  40. }
  41. func (cg *Gamut) distanceSquarePointToSegment(x1, y1, x2, y2, x, y float64) float64 {
  42. sqLength1 := (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)
  43. dotProduct := ((x-x1)*(x2-x1) + (y-y1)*(y2-y1)) / sqLength1
  44. if dotProduct < 0 {
  45. return (x-x1)*(x-x1) + (y-y1)*(y-y1)
  46. } else if dotProduct <= 1 {
  47. sqLength2 := (x1-x)*(x1-x) + (y1-y)*(y1-y)
  48. return sqLength2 - dotProduct*dotProduct*sqLength1
  49. } else {
  50. return (x-x2)*(x-x2) + (y-y2)*(y-y2)
  51. }
  52. }
  53. func (cg *Gamut) atTheEdge(color XY) bool {
  54. x, y := color.X, color.Y
  55. x1, y1 := cg.Red.X, cg.Red.Y
  56. x2, y2 := cg.Green.X, cg.Green.Y
  57. x3, y3 := cg.Blue.X, cg.Blue.Y
  58. if cg.distanceSquarePointToSegment(x1, y1, x2, y2, x, y) <= epsSquare {
  59. return true
  60. }
  61. if cg.distanceSquarePointToSegment(x2, y2, x3, y3, x, y) <= epsSquare {
  62. return true
  63. }
  64. if cg.distanceSquarePointToSegment(x3, y3, x1, y1, x, y) <= epsSquare {
  65. return true
  66. }
  67. return false
  68. }
  69. func (cg *Gamut) Contains(color XY) bool {
  70. if cg == nil {
  71. return true
  72. }
  73. return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color))
  74. }
  75. func (cg *Gamut) Conform(color XY) XY {
  76. if cg.Contains(color) {
  77. return color
  78. }
  79. var best *XY
  80. xMin, xMax, yMin, yMax := cg.getBounds()
  81. for x := xMin; x < xMax; x += 0.001 {
  82. for y := yMin; y < yMax; y += 0.001 {
  83. color2 := XY{X: x, Y: y}
  84. if cg.Contains(color2) {
  85. if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
  86. best = &color2
  87. }
  88. }
  89. }
  90. }
  91. if best == nil {
  92. centerX := (cg.Red.X + cg.Green.X + cg.Blue.X) / 3
  93. centerY := (cg.Red.Y + cg.Green.Y + cg.Blue.Y) / 3
  94. stepX := (centerX - color.X) / 5000
  95. stepY := (centerY - color.Y) / 5000
  96. for !cg.Contains(color) {
  97. color.X += stepX
  98. color.Y += stepY
  99. }
  100. return color
  101. }
  102. for x := best.X - 0.001; x < best.X+0.001; x += 0.0002 {
  103. for y := best.Y - 0.001; y < best.Y+0.001; y += 0.0002 {
  104. color2 := XY{X: x, Y: y}
  105. if cg.atTheEdge(color2) {
  106. if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
  107. best = &color2
  108. }
  109. }
  110. }
  111. }
  112. for x := best.X - 0.0001; x < best.X+0.0001; x += 0.00003 {
  113. for y := best.Y - 0.0001; y < best.Y+0.0001; y += 0.00003 {
  114. color2 := XY{X: x, Y: y}
  115. if cg.atTheEdge(color2) {
  116. if best == nil || color.DistanceTo(color2) < color.DistanceTo(*best) {
  117. best = &color2
  118. }
  119. }
  120. }
  121. }
  122. return *best
  123. }
  124. type XY struct {
  125. X float64 `json:"x"`
  126. Y float64 `json:"y"`
  127. }
  128. func (xy XY) ToRGB() RGB {
  129. h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
  130. c := colorful.Hsv(h, s, 1)
  131. return RGB{Red: c.R, Green: c.G, Blue: c.B}
  132. }
  133. func (xy XY) ToHS() HueSat {
  134. h, s, _ := colorful.Xyy(xy.X, xy.Y, 0.5).Hsv()
  135. return HueSat{Hue: h, Sat: s}
  136. }
  137. func (xy XY) DistanceTo(other XY) float64 {
  138. return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
  139. }
  140. func (xy XY) Round() XY {
  141. return XY{
  142. X: math.Round(xy.X*10000) / 10000,
  143. Y: math.Round(xy.Y*10000) / 10000,
  144. }
  145. }