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