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

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