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

  1. package models
  2. import (
  3. "github.com/lucasb-eyer/go-colorful"
  4. "math"
  5. )
  6. const eps = 0.0001
  7. const epsSquare = eps * eps
  8. type ColorGamut struct {
  9. Red ColorXY `json:"red"`
  10. Green ColorXY `json:"green"`
  11. Blue ColorXY `json:"blue"`
  12. }
  13. func (cg *ColorGamut) side(x1, y1, x2, y2, x, y float64) float64 {
  14. return (y2-y1)*(x-x1) + (-x2+x1)*(y-y1)
  15. }
  16. func (cg *ColorGamut) naiveContains(color ColorXY) 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 *ColorGamut) 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 *ColorGamut) isInBounds(color ColorXY) 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 *ColorGamut) 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 *ColorGamut) atTheEdge(color ColorXY) 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 *ColorGamut) Contains(color ColorXY) bool {
  70. if cg == nil {
  71. return true
  72. }
  73. return cg.isInBounds(color) && (cg.naiveContains(color) || cg.atTheEdge(color))
  74. }
  75. func (cg *ColorGamut) Conform(color ColorXY) ColorXY {
  76. if cg.Contains(color) {
  77. return color
  78. }
  79. var best *ColorXY
  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 := ColorXY{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 := ColorXY{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 := ColorXY{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 ColorXY struct {
  125. X float64 `json:"x"`
  126. Y float64 `json:"y"`
  127. }
  128. func (xy ColorXY) DistanceTo(other ColorXY) float64 {
  129. return math.Sqrt(math.Pow(xy.X-other.X, 2) + math.Pow(xy.Y-other.Y, 2))
  130. }
  131. func (xy ColorXY) Round() ColorXY {
  132. return ColorXY{
  133. X: math.Round(xy.X*10000) / 10000,
  134. Y: math.Round(xy.Y*10000) / 10000,
  135. }
  136. }
  137. func hsToXY(hue, sat float64) ColorXY {
  138. c := colorful.Hsv(hue, sat, 1)
  139. red255, green255, blue255 := c.RGB255()
  140. red := float64(red255) / 255.0
  141. green := float64(green255) / 255.0
  142. blue := float64(blue255) / 255.0
  143. return rgbToXY(red, green, blue)
  144. }
  145. func rgbToXY(red float64, green float64, blue float64) ColorXY {
  146. x := red*0.649926 + green*0.103455 + blue*0.197109
  147. y := red*0.234327 + green*0.743075 + blue*0.022598
  148. z := green*0.053077 + blue*1.035763
  149. return ColorXY{
  150. X: x / (x + y + z),
  151. Y: y / (x + y + z),
  152. }
  153. }
  154. func screenRGBToXY(red, green, blue float64) ColorXY {
  155. for _, component := range []*float64{&red, &green, &blue} {
  156. if *component > 0.04045 {
  157. *component = math.Pow((*component+0.055)/(1.055), 2.4)
  158. } else {
  159. *component /= 12.92
  160. }
  161. }
  162. return rgbToXY(red, green, blue)
  163. }