hsluv.go (6167B)
1 package colorful 2 3 import "math" 4 5 // Source: https://github.com/hsluv/hsluv-go 6 // Under MIT License 7 // Modified so that Saturation and Luminance are in [0..1] instead of [0..100]. 8 9 // HSLuv uses a rounded version of the D65. This has no impact on the final RGB 10 // values, but to keep high levels of accuracy for internal operations and when 11 // comparing to the test values, this modified white reference is used internally. 12 // 13 // See this GitHub thread for details on these values: 14 // https://github.com/hsluv/hsluv/issues/79 15 var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878} 16 17 func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) { 18 // [-1..1] but the code expects it to be [-100..100] 19 c *= 100.0 20 l *= 100.0 21 22 var s, max float64 23 if l > 99.9999999 || l < 0.00000001 { 24 s = 0.0 25 } else { 26 max = maxChromaForLH(l, h) 27 s = c / max * 100.0 28 } 29 return h, clamp01(s / 100.0), clamp01(l / 100.0) 30 } 31 32 func HSLuvToLuvLCh(h, s, l float64) (float64, float64, float64) { 33 l *= 100.0 34 s *= 100.0 35 36 var c, max float64 37 if l > 99.9999999 || l < 0.00000001 { 38 c = 0.0 39 } else { 40 max = maxChromaForLH(l, h) 41 c = max / 100.0 * s 42 } 43 44 // c is [-100..100], but for LCh it's supposed to be almost [-1..1] 45 return clamp01(l / 100.0), c / 100.0, h 46 } 47 48 func LuvLChToHPLuv(l, c, h float64) (float64, float64, float64) { 49 // [-1..1] but the code expects it to be [-100..100] 50 c *= 100.0 51 l *= 100.0 52 53 var s, max float64 54 if l > 99.9999999 || l < 0.00000001 { 55 s = 0.0 56 } else { 57 max = maxSafeChromaForL(l) 58 s = c / max * 100.0 59 } 60 return h, s / 100.0, l / 100.0 61 } 62 63 func HPLuvToLuvLCh(h, s, l float64) (float64, float64, float64) { 64 // [-1..1] but the code expects it to be [-100..100] 65 l *= 100.0 66 s *= 100.0 67 68 var c, max float64 69 if l > 99.9999999 || l < 0.00000001 { 70 c = 0.0 71 } else { 72 max = maxSafeChromaForL(l) 73 c = max / 100.0 * s 74 } 75 return l / 100.0, c / 100.0, h 76 } 77 78 // HSLuv creates a new Color from values in the HSLuv color space. 79 // Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1]. 80 // 81 // The returned color values are clamped (using .Clamped), so this will never output 82 // an invalid color. 83 func HSLuv(h, s, l float64) Color { 84 // HSLuv -> LuvLCh -> CIELUV -> CIEXYZ -> Linear RGB -> sRGB 85 l, u, v := LuvLChToLuv(HSLuvToLuvLCh(h, s, l)) 86 return LinearRgb(XyzToLinearRgb(LuvToXyzWhiteRef(l, u, v, hSLuvD65))).Clamped() 87 } 88 89 // HPLuv creates a new Color from values in the HPLuv color space. 90 // Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1]. 91 // 92 // The returned color values are clamped (using .Clamped), so this will never output 93 // an invalid color. 94 func HPLuv(h, s, l float64) Color { 95 // HPLuv -> LuvLCh -> CIELUV -> CIEXYZ -> Linear RGB -> sRGB 96 l, u, v := LuvLChToLuv(HPLuvToLuvLCh(h, s, l)) 97 return LinearRgb(XyzToLinearRgb(LuvToXyzWhiteRef(l, u, v, hSLuvD65))).Clamped() 98 } 99 100 // HSLuv returns the Hue, Saturation and Luminance of the color in the HSLuv 101 // color space. Hue in [0..360], a Saturation [0..1], and a Luminance 102 // (lightness) in [0..1]. 103 func (col Color) HSLuv() (h, s, l float64) { 104 // sRGB -> Linear RGB -> CIEXYZ -> CIELUV -> LuvLCh -> HSLuv 105 return LuvLChToHSLuv(col.LuvLChWhiteRef(hSLuvD65)) 106 } 107 108 // HPLuv returns the Hue, Saturation and Luminance of the color in the HSLuv 109 // color space. Hue in [0..360], a Saturation [0..1], and a Luminance 110 // (lightness) in [0..1]. 111 // 112 // Note that HPLuv can only represent pastel colors, and so the Saturation 113 // value could be much larger than 1 for colors it can't represent. 114 func (col Color) HPLuv() (h, s, l float64) { 115 return LuvLChToHPLuv(col.LuvLChWhiteRef(hSLuvD65)) 116 } 117 118 // DistanceHSLuv calculates Euclidan distance in the HSLuv colorspace. No idea 119 // how useful this is. 120 // 121 // The Hue value is divided by 100 before the calculation, so that H, S, and L 122 // have the same relative ranges. 123 func (c1 Color) DistanceHSLuv(c2 Color) float64 { 124 h1, s1, l1 := c1.HSLuv() 125 h2, s2, l2 := c2.HSLuv() 126 return math.Sqrt(sq((h1-h2)/100.0) + sq(s1-s2) + sq(l1-l2)) 127 } 128 129 // DistanceHPLuv calculates Euclidean distance in the HPLuv colorspace. No idea 130 // how useful this is. 131 // 132 // The Hue value is divided by 100 before the calculation, so that H, S, and L 133 // have the same relative ranges. 134 func (c1 Color) DistanceHPLuv(c2 Color) float64 { 135 h1, s1, l1 := c1.HPLuv() 136 h2, s2, l2 := c2.HPLuv() 137 return math.Sqrt(sq((h1-h2)/100.0) + sq(s1-s2) + sq(l1-l2)) 138 } 139 140 var m = [3][3]float64{ 141 {3.2409699419045214, -1.5373831775700935, -0.49861076029300328}, 142 {-0.96924363628087983, 1.8759675015077207, 0.041555057407175613}, 143 {0.055630079696993609, -0.20397695888897657, 1.0569715142428786}, 144 } 145 146 const kappa = 903.2962962962963 147 const epsilon = 0.0088564516790356308 148 149 func maxChromaForLH(l, h float64) float64 { 150 hRad := h / 360.0 * math.Pi * 2.0 151 minLength := math.MaxFloat64 152 for _, line := range getBounds(l) { 153 length := lengthOfRayUntilIntersect(hRad, line[0], line[1]) 154 if length > 0.0 && length < minLength { 155 minLength = length 156 } 157 } 158 return minLength 159 } 160 161 func getBounds(l float64) [6][2]float64 { 162 var sub2 float64 163 var ret [6][2]float64 164 sub1 := math.Pow(l+16.0, 3.0) / 1560896.0 165 if sub1 > epsilon { 166 sub2 = sub1 167 } else { 168 sub2 = l / kappa 169 } 170 for i := range m { 171 for k := 0; k < 2; k++ { 172 top1 := (284517.0*m[i][0] - 94839.0*m[i][2]) * sub2 173 top2 := (838422.0*m[i][2]+769860.0*m[i][1]+731718.0*m[i][0])*l*sub2 - 769860.0*float64(k)*l 174 bottom := (632260.0*m[i][2]-126452.0*m[i][1])*sub2 + 126452.0*float64(k) 175 ret[i*2+k][0] = top1 / bottom 176 ret[i*2+k][1] = top2 / bottom 177 } 178 } 179 return ret 180 } 181 182 func lengthOfRayUntilIntersect(theta, x, y float64) (length float64) { 183 length = y / (math.Sin(theta) - x*math.Cos(theta)) 184 return 185 } 186 187 func maxSafeChromaForL(l float64) float64 { 188 minLength := math.MaxFloat64 189 for _, line := range getBounds(l) { 190 m1 := line[0] 191 b1 := line[1] 192 x := intersectLineLine(m1, b1, -1.0/m1, 0.0) 193 dist := distanceFromPole(x, b1+x*m1) 194 if dist < minLength { 195 minLength = dist 196 } 197 } 198 return minLength 199 } 200 201 func intersectLineLine(x1, y1, x2, y2 float64) float64 { 202 return (y1 - y2) / (x2 - x1) 203 } 204 205 func distanceFromPole(x, y float64) float64 { 206 return math.Sqrt(math.Pow(x, 2.0) + math.Pow(y, 2.0)) 207 }