1package colorful 2 3import "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 15var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878} 16 17func 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 32func 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 48func 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 63func 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. 83func 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. 94func 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]. 103func (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. 114func (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. 123func (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. 134func (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 140var 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 146const kappa = 903.2962962962963 147const epsilon = 0.0088564516790356308 148 149func 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 161func 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 182func lengthOfRayUntilIntersect(theta, x, y float64) (length float64) { 183 length = y / (math.Sin(theta) - x*math.Cos(theta)) 184 return 185} 186 187func 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 201func intersectLineLine(x1, y1, x2, y2 float64) float64 { 202 return (y1 - y2) / (x2 - x1) 203} 204 205func distanceFromPole(x, y float64) float64 { 206 return math.Sqrt(math.Pow(x, 2.0) + math.Pow(y, 2.0)) 207} 208