Skip to main content

hsluv/
lib.rs

1use std::f64::consts::PI;
2use std::cmp::Ordering;
3use std::str;
4
5const M: [[f64;3];3] = [
6	[3.240969941904521, -1.537383177570093, -0.498610760293],
7	[-0.96924363628087, 1.87596750150772, 0.041555057407175],
8	[0.055630079696993, -0.20397695888897, 1.056971514242878],
9];
10
11const M_INV: [[f64;3];3] = [
12
13	[0.41239079926595, 0.35758433938387, 0.18048078840183],
14	[0.21263900587151, 0.71516867876775, 0.072192315360733],
15	[0.019330818715591, 0.11919477979462, 0.95053215224966],
16];
17
18const REF_Y : f64   = 1.0;
19const REF_U : f64   = 0.19783000664283;
20const REF_V : f64   = 0.46831999493879;
21// CIE LUV constants
22const KAPPA : f64   = 903.2962962;
23const EPSILON : f64 = 0.0088564516;
24
25/// Convert HSLUV to HEX
26pub fn hsluv_to_hex(hsl: (f64, f64, f64)) -> String {
27	rgb_to_hex(
28		hsluv_to_rgb(hsl)
29	)
30}
31
32/// Convert HPLUV to HEX
33pub fn hpluv_to_hex(hsl: (f64, f64, f64)) -> String {
34	rgb_to_hex(
35		hpluv_to_rgb(hsl)
36	)
37}
38
39/// Convert HEX to HSLUV
40pub fn hex_to_hsluv(hex: &str) -> (f64, f64, f64) {
41	rgb_to_hsluv(
42		hex_to_rgb(hex)
43	)
44}
45
46/// Convert HEX to HPLUV
47pub fn hex_to_hpluv(hex: &str) -> (f64, f64, f64) {
48	rgb_to_hpluv(
49		hex_to_rgb(hex)
50	)
51}
52
53/// Convert HSLUV to LCH
54pub fn hsluv_to_lch(hsl: (f64, f64, f64)) ->(f64, f64, f64) {
55	let (h, s, l) = hsl;
56	match l {
57		l if l > 99.9999999 => (100.0, 0.0, h),
58		l if l < 0.00000001 => (0.0, 0.0, h),
59		_ => {
60			let mx = max_chroma_for(l, h);
61			let c = mx/100.0 * s;
62			(l, c, h)
63		}
64	}
65}
66
67/// Convert HPLUV to LCH
68pub fn hpluv_to_lch(hpl: (f64, f64, f64) ) -> (f64, f64, f64) {
69	let (h, p, l) = hpl;
70	match l {
71		l if l > 99.9999999 => (100.0, 0.0, h),
72		l if l < 0.00000001 => (0.0, 0.0, h),
73		_ => {
74			let mx = max_safe_chroma_for(l);
75			let c = mx/100.0 * p;
76			(l, c, h)
77		}
78	}
79}
80
81/// Convert LCH to LUV
82pub fn lch_to_luv(lch: (f64, f64, f64)) -> (f64, f64, f64) {
83	let (l, c, h) = lch;
84	let hrad = degrees_to_radians(h);
85	let u = hrad.cos() * c;
86	let v = hrad.sin() * c;
87
88	(l, u, v)
89}
90
91/// Convert LCH to HSLUV
92pub fn lch_to_hsluv(lch: (f64, f64, f64)) -> (f64, f64, f64) {
93	let (l, c, h) = lch;
94	match l {
95		l if l > 99.9999999 => (h, 0.0, 100.0),
96		l if l < 0.00000001 => (h, 0.0, 0.0),
97		_ => {
98			let mx = max_chroma_for(l, h);
99			let s = c / mx * 100.0;
100			(h, s, l)
101		}
102	}
103}
104
105/// Convert LCH to HPLUV
106pub fn lch_to_hpluv(lch: (f64, f64, f64)) -> (f64, f64, f64) {
107	let (l, c, h) = lch;
108	match l {
109		l if l > 99.9999999 => (h, 0.0, 100.0),
110		l if l < 0.00000001 => (h, 0.0, 0.0),
111		_ => {
112			let mx = max_safe_chroma_for(l);
113			let s = c / mx * 100.0;
114			(h, s, l)
115		}
116	}
117}
118
119/// Convert LCH to RGB
120pub fn lch_to_rgb(lch: (f64, f64, f64)) -> (f64, f64, f64) {
121	xyz_to_rgb(
122		luv_to_xyz(
123			lch_to_luv(lch)
124		)
125	)
126}
127
128/// Convert HSLUV to RGB
129pub fn hsluv_to_rgb(hsl: (f64, f64, f64)) -> (f64, f64, f64) {
130	xyz_to_rgb(
131		luv_to_xyz(
132			lch_to_luv(
133				hsluv_to_lch(hsl)
134			)
135		)
136	)
137}
138
139/// Convert HPLUV to RGB
140pub fn hpluv_to_rgb(hsl: (f64, f64, f64)) -> (f64, f64, f64) {
141	lch_to_rgb(
142		hpluv_to_lch(hsl)
143	)
144}
145
146/// Convert XYZ to RGB
147pub fn xyz_to_rgb(xyz: (f64, f64, f64)) -> (f64, f64, f64) {
148	let xyz_vec = vec![xyz.0, xyz.1, xyz.2];
149	let abc: Vec<f64> = M.iter().map(|i| from_linear(dot_product(&i.to_vec(), &xyz_vec))).collect();
150	(abc[0], abc[1], abc[2])
151}
152
153/// Convert LUV to XYZ
154pub fn luv_to_xyz(luv: (f64, f64, f64)) -> (f64, f64, f64) {
155	let (l, u, v) = luv;
156
157	if l == 0.0 {
158		return (0.0, 0.0, 0.0);
159	}
160
161	let var_y = f_inv(l);
162	let var_u = u / (13.0 * l) + REF_U;
163	let var_v = v / (13.0 * l) + REF_V;
164
165	let y = var_y * REF_Y;
166	let x = 0.0 - (9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v);
167	let z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v);
168
169	(x, y, z)
170}
171
172/// Convert XYZ to LUV
173pub fn xyz_to_luv(xyz: (f64, f64, f64)) -> (f64, f64, f64) {
174	let (x, y, z) = xyz;
175	let l = f(y);
176
177	if l == 0.0 || (xyz == (0.0, 0.0, 0.0)) {
178		return (0.0, 0.0, 0.0);
179	}
180
181	let var_u = (4.0 * x) / (x + (15.0 * y) + (3.0 *z));
182	let var_v = (9.0 * y) / (x + (15.0 * y) + (3.0 *z));
183	let u = 13.0 * l * (var_u - REF_U);
184	let v = 13.0 * l * (var_v - REF_V);
185
186	(l, u, v)
187}
188
189/// Convert RGB to HSLUV
190pub fn rgb_to_hsluv(rgb: (f64, f64, f64)) -> (f64, f64, f64) {
191	lch_to_hsluv(rgb_to_lch(rgb))
192}
193
194/// Convert RGB to HPLUV
195pub fn rgb_to_hpluv(rgb: (f64, f64, f64)) -> (f64, f64, f64) {
196	lch_to_hpluv(rgb_to_lch(rgb))
197}
198
199/// Convert RGB to LCH
200pub fn rgb_to_lch(rgb: (f64, f64, f64)) -> (f64, f64, f64) {
201	luv_to_lch(
202		xyz_to_luv(
203			rgb_to_xyz(rgb)
204		)
205	)
206}
207
208/// Convert RGB to XYZ
209pub fn rgb_to_xyz(rgb: (f64, f64, f64)) -> (f64, f64, f64) {
210	let rgbl = vec![to_linear(rgb.0), to_linear(rgb.1), to_linear(rgb.2)];
211	let mapping : Vec<f64> = M_INV.iter().map(|i| dot_product(&i.to_vec(), &rgbl)).collect();
212	(mapping[0], mapping[1], mapping[2])
213}
214
215/// Convert LUV to LCH
216pub fn luv_to_lch(luv: (f64, f64, f64)) -> (f64, f64, f64) {
217	let (l, u, v) = luv;
218	let c = (u*u + v*v).sqrt();
219	if c < 0.00000001 {
220		(l, c, 0.0)
221	} else {
222		let hrad = f64::atan2(v, u);
223		let mut h = radians_to_degrees(hrad);
224		if h < 0.0 {
225			h += 360.0;
226		}
227		(l, c, h)
228	}
229}
230
231/// Convert RGB to HEX
232pub fn rgb_to_hex(rgb: (f64, f64, f64)) -> String {
233	let (r,g,b) = rgb_prepare(rgb);
234	format!("#{:02x}{:02x}{:02x}", r,g,b)
235}
236
237/// Convert HEX to RGB
238pub fn hex_to_rgb(raw_hex: &str) -> (f64, f64, f64) {
239	let hex = raw_hex.trim_start_matches('#');
240	if hex.len() != 6 {
241		println!("Not a hex string!");
242		return (0.0,0.0,0.0)
243	}
244	let mut chunks = hex.as_bytes().chunks(2);
245	let red = i64::from_str_radix(str::from_utf8(chunks.next().unwrap()).unwrap(), 16);
246	let green = i64::from_str_radix(str::from_utf8(chunks.next().unwrap()).unwrap(), 16);
247	let blue = i64::from_str_radix(str::from_utf8(chunks.next().unwrap()).unwrap(), 16);
248	( (red.unwrap_or(0) as f64) / 255.0, (green.unwrap_or(0) as f64) / 255.0, (blue.unwrap_or(0) as f64) / 255.0 )
249}
250
251
252fn f_inv(t: f64) -> f64 {
253	if t > 8.0 {
254		REF_Y * ( (t + 16.0) / 116.0 ).powf(3.0)
255	} else {
256		REF_Y * t / KAPPA
257	}
258}
259
260fn to_linear(c: f64) -> f64 {
261	if c > 0.04045 {
262		( (c + 0.055) / 1.055).powf(2.4)
263	} else {
264		c / 12.92
265	}
266}
267
268fn from_linear(c: f64) -> f64 {
269	if c <= 0.0031308 {
270		12.92 * c
271	} else {
272		1.055 * (c.powf(1.0/2.4)) - 0.055
273	}
274}
275
276fn f(t:f64) -> f64 {
277	if t > EPSILON {
278		116.0 * ( (t / REF_Y).powf(1.0/3.0) ) - 16.0
279	} else {
280		t / REF_Y * KAPPA
281	}
282}
283
284fn dot_product(a: &[f64], b: &[f64] ) -> f64 {
285	a.iter().zip(b.iter()).map(|(i, j)| i * j).sum()
286}
287
288fn rgb_prepare(rgb: (f64, f64, f64)) ->  (u8, u8, u8) {
289	(clamp(rgb.0), clamp(rgb.1), clamp(rgb.2))
290}
291
292fn clamp(v: f64) -> u8 {
293	let mut rounded = (v * 1000.0).round() / 1000.0;
294	if rounded < 0.0 {
295		rounded = 0.0;
296	}
297	if rounded > 1.0 {
298		rounded = 1.0;
299	}
300	(rounded * 255.0).round() as u8
301}
302
303fn max_chroma_for(l: f64, h: f64) -> f64 {
304	let hrad = h / 360.0 * PI * 2.0;
305
306	let mut lengths : Vec<f64> = get_bounds(l)
307		.iter()
308		.map(|line| length_of_ray_until_intersect(hrad, line))
309		.filter(|length| length > &0.0 )
310		.collect::<Vec<f64>>();
311
312		lengths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal) );
313		lengths[0]
314}
315
316fn max_safe_chroma_for(l: f64) -> f64 {
317	let mut lengths = Vec::new();
318	get_bounds(l).iter().for_each(|line| {
319		let x = intersect_line_line((line.0, line.1), (-1.0/line.0, 0.0) );
320		lengths.push(distance_from_pole((x, line.1 + x * line.0)));
321	});
322	lengths.sort_by(|a,b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
323	lengths[0]
324}
325
326fn intersect_line_line(line1: (f64, f64), line2: (f64, f64)) -> f64 {
327	(line1.1 - line2.1) / (line2.0 - line1.0)
328}
329
330fn distance_from_pole(point: (f64, f64)) -> f64 {
331	(point.0.powi(2) + point.1.powi(2)).sqrt()
332}
333
334fn get_bounds(l: f64) -> Vec<(f64, f64)> {
335	let sub1 = ((l + 16.0).powi(3))/ 1560896.0;
336	let sub2 = match sub1 {
337		s if s > EPSILON => s,
338		_ => l / KAPPA
339	};
340
341	let mut bounds = Vec::new();
342
343	for ms in &M {
344		let (m1, m2, m3) = (ms[0], ms[1], ms[2]);
345		for t in 0..2 {
346			let top1 = (284517.0 * m1 - 94839.0 * m3) * sub2;
347			let top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * f64::from(t) * l;
348			let bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * f64::from(t);
349
350			bounds.push((top1/bottom, top2/bottom));
351		}
352	}
353	bounds
354}
355
356fn length_of_ray_until_intersect(theta: f64, line: &(f64, f64)) -> f64 {
357	let (m1, b1) = *line;
358	let length = b1 / (theta.sin() - m1 * theta.cos());
359	if length < 0.0 {
360		-0.0001
361	} else {
362		length
363	}
364}
365
366fn radians_to_degrees(rad: f64) -> f64 {
367	rad * 180.0 / PI
368}
369
370fn degrees_to_radians(deg: f64) -> f64 {
371	deg * PI / 180.0
372}