1#![warn(missing_docs)]
8
9use glam::{Vec3, Vec4};
10use std::f32::consts::PI;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Rgba {
17 pub r: f32,
19 pub g: f32,
21 pub b: f32,
23 pub a: f32,
25}
26
27impl Rgba {
28 pub const WHITE: Rgba = Rgba { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
30 pub const BLACK: Rgba = Rgba { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
32 pub const RED: Rgba = Rgba { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
34 pub const GREEN: Rgba = Rgba { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
36 pub const BLUE: Rgba = Rgba { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
38 pub const YELLOW: Rgba = Rgba { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };
40 pub const CYAN: Rgba = Rgba { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
42 pub const MAGENTA: Rgba = Rgba { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
44 pub const TRANSPARENT: Rgba = Rgba { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
46
47 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { Self { r, g, b, a } }
49 pub fn rgb(r: f32, g: f32, b: f32) -> Self { Self { r, g, b, a: 1.0 } }
51
52 pub fn from_vec4(v: Vec4) -> Self { Self { r: v.x, g: v.y, b: v.z, a: v.w } }
54 pub fn to_vec4(self) -> Vec4 { Vec4::new(self.r, self.g, self.b, self.a) }
56 pub fn to_vec3(self) -> Vec3 { Vec3::new(self.r, self.g, self.b) }
58
59 pub fn from_hex(hex: u32) -> Self {
61 let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
62 let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
63 let b = ( hex & 0xFF) as f32 / 255.0;
64 Self::rgb(r, g, b)
65 }
66
67 pub fn from_hex_alpha(hex: u32) -> Self {
69 let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
70 let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
71 let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
72 let a = ( hex & 0xFF) as f32 / 255.0;
73 Self { r, g, b, a }
74 }
75
76 pub fn with_alpha(self, a: f32) -> Self { Self { a, ..self } }
78 pub fn lerp(self, other: Rgba, t: f32) -> Self {
80 Rgba {
81 r: self.r + (other.r - self.r) * t,
82 g: self.g + (other.g - self.g) * t,
83 b: self.b + (other.b - self.b) * t,
84 a: self.a + (other.a - self.a) * t,
85 }
86 }
87
88 pub fn over(self, other: Rgba) -> Rgba {
90 let ia = 1.0 - self.a;
91 Rgba {
92 r: self.r * self.a + other.r * ia,
93 g: self.g * self.a + other.g * ia,
94 b: self.b * self.a + other.b * ia,
95 a: self.a + other.a * ia,
96 }
97 }
98
99 pub fn luminance(self) -> f32 {
101 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
102 }
103
104 pub fn to_u8(self) -> [u8; 4] {
106 [
107 (self.r.clamp(0.0, 1.0) * 255.0) as u8,
108 (self.g.clamp(0.0, 1.0) * 255.0) as u8,
109 (self.b.clamp(0.0, 1.0) * 255.0) as u8,
110 (self.a.clamp(0.0, 1.0) * 255.0) as u8,
111 ]
112 }
113}
114
115impl From<Vec4> for Rgba {
116 fn from(v: Vec4) -> Self { Self::from_vec4(v) }
117}
118
119impl From<Rgba> for Vec4 {
120 fn from(c: Rgba) -> Self { c.to_vec4() }
121}
122
123#[inline]
127pub fn linear_to_srgb_channel(x: f32) -> f32 {
128 if x <= 0.003_130_8 {
129 x * 12.92
130 } else {
131 1.055 * x.powf(1.0 / 2.4) - 0.055
132 }
133}
134
135#[inline]
137pub fn srgb_to_linear_channel(x: f32) -> f32 {
138 if x <= 0.040_45 {
139 x / 12.92
140 } else {
141 ((x + 0.055) / 1.055).powf(2.4)
142 }
143}
144
145pub fn linear_to_srgb(c: Rgba) -> Rgba {
147 Rgba::new(
148 linear_to_srgb_channel(c.r),
149 linear_to_srgb_channel(c.g),
150 linear_to_srgb_channel(c.b),
151 c.a,
152 )
153}
154
155pub fn srgb_to_linear(c: Rgba) -> Rgba {
157 Rgba::new(
158 srgb_to_linear_channel(c.r),
159 srgb_to_linear_channel(c.g),
160 srgb_to_linear_channel(c.b),
161 c.a,
162 )
163}
164
165#[derive(Debug, Clone, Copy)]
169pub struct Hsv {
170 pub h: f32,
172 pub s: f32,
174 pub v: f32,
176}
177
178impl Hsv {
179 pub fn new(h: f32, s: f32, v: f32) -> Self { Self { h, s, v } }
181
182 pub fn to_rgb(self) -> Rgba {
184 let (r, g, b) = hsv_to_rgb(self.h, self.s, self.v);
185 Rgba::rgb(r, g, b)
186 }
187
188 pub fn from_rgb(c: Rgba) -> Self {
190 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
191 Self { h, s, v }
192 }
193}
194
195pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
197 if s == 0.0 { return (v, v, v); }
198 let h = ((h % 360.0) + 360.0) % 360.0;
199 let i = (h / 60.0) as u32;
200 let f = h / 60.0 - i as f32;
201 let p = v * (1.0 - s);
202 let q = v * (1.0 - s * f);
203 let t = v * (1.0 - s * (1.0 - f));
204 match i {
205 0 => (v, t, p),
206 1 => (q, v, p),
207 2 => (p, v, t),
208 3 => (p, q, v),
209 4 => (t, p, v),
210 _ => (v, p, q),
211 }
212}
213
214pub fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
216 let max = r.max(g).max(b);
217 let min = r.min(g).min(b);
218 let delta = max - min;
219
220 let v = max;
221 let s = if max < 1e-8 { 0.0 } else { delta / max };
222 let h = if delta < 1e-8 {
223 0.0
224 } else if max == r {
225 60.0 * (((g - b) / delta) % 6.0)
226 } else if max == g {
227 60.0 * ((b - r) / delta + 2.0)
228 } else {
229 60.0 * ((r - g) / delta + 4.0)
230 };
231 (((h % 360.0) + 360.0) % 360.0, s, v)
232}
233
234#[derive(Debug, Clone, Copy)]
238pub struct Hsl {
239 pub h: f32,
241 pub s: f32,
243 pub l: f32,
245}
246
247impl Hsl {
248 pub fn new(h: f32, s: f32, l: f32) -> Self { Self { h, s, l } }
250
251 pub fn to_rgb(self) -> Rgba {
253 let (r, g, b) = hsl_to_rgb(self.h, self.s, self.l);
254 Rgba::rgb(r, g, b)
255 }
256}
257
258fn hue_to_rgb(p: f32, q: f32, t: f32) -> f32 {
259 let t = ((t % 1.0) + 1.0) % 1.0;
260 if t < 1.0 / 6.0 { return p + (q - p) * 6.0 * t; }
261 if t < 1.0 / 2.0 { return q; }
262 if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
263 p
264}
265
266pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
268 if s == 0.0 { return (l, l, l); }
269 let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
270 let p = 2.0 * l - q;
271 let h = h / 360.0;
272 (
273 hue_to_rgb(p, q, h + 1.0 / 3.0),
274 hue_to_rgb(p, q, h),
275 hue_to_rgb(p, q, h - 1.0 / 3.0),
276 )
277}
278
279pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
281 let max = r.max(g).max(b);
282 let min = r.min(g).min(b);
283 let l = (max + min) * 0.5;
284 let delta = max - min;
285
286 if delta < 1e-8 { return (0.0, 0.0, l); }
287
288 let s = if l < 0.5 { delta / (max + min) } else { delta / (2.0 - max - min) };
289 let h = if max == r {
290 60.0 * ((g - b) / delta + if g < b { 6.0 } else { 0.0 })
291 } else if max == g {
292 60.0 * ((b - r) / delta + 2.0)
293 } else {
294 60.0 * ((r - g) / delta + 4.0)
295 };
296 (h, s, l)
297}
298
299#[derive(Debug, Clone, Copy)]
304pub struct Oklab {
305 pub l: f32,
307 pub a: f32,
309 pub b: f32,
311}
312
313impl Oklab {
314 pub fn from_linear_rgb(c: Rgba) -> Self {
316 let l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
317 let m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
318 let s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;
319
320 let l_ = l.cbrt();
321 let m_ = m.cbrt();
322 let s_ = s.cbrt();
323
324 Self {
325 l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
326 a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
327 b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
328 }
329 }
330
331 pub fn to_linear_rgb(self) -> Rgba {
333 let l_ = self.l + 0.3963377774 * self.a + 0.2158037573 * self.b;
334 let m_ = self.l - 0.1055613458 * self.a - 0.0638541728 * self.b;
335 let s_ = self.l - 0.0894841775 * self.a - 1.2914855480 * self.b;
336
337 let l = l_ * l_ * l_;
338 let m = m_ * m_ * m_;
339 let s = s_ * s_ * s_;
340
341 Rgba::rgb(
342 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
343 -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
344 -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
345 )
346 }
347
348 pub fn lerp(self, other: Oklab, t: f32) -> Oklab {
350 Oklab {
351 l: self.l + (other.l - self.l) * t,
352 a: self.a + (other.a - self.a) * t,
353 b: self.b + (other.b - self.b) * t,
354 }
355 }
356}
357
358#[derive(Debug, Clone, Copy)]
362pub struct Xyz {
363 pub x: f32,
365 pub y: f32,
367 pub z: f32,
369}
370
371impl Xyz {
372 pub fn from_linear_rgb(c: Rgba) -> Self {
374 Self {
375 x: c.r * 0.4124 + c.g * 0.3576 + c.b * 0.1805,
376 y: c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722,
377 z: c.r * 0.0193 + c.g * 0.1192 + c.b * 0.9505,
378 }
379 }
380
381 pub fn to_linear_rgb(self) -> Rgba {
383 Rgba::rgb(
384 self.x * 3.2406 + self.y * -1.5372 + self.z * -0.4986,
385 self.x * -0.9689 + self.y * 1.8758 + self.z * 0.0415,
386 self.x * 0.0557 + self.y * -0.2040 + self.z * 1.0570,
387 )
388 }
389}
390
391#[derive(Debug, Clone, Copy)]
395pub struct Lab {
396 pub l: f32,
398 pub a: f32,
400 pub b: f32,
402}
403
404const D65_X: f32 = 0.95047;
405const D65_Y: f32 = 1.00000;
406const D65_Z: f32 = 1.08883;
407
408fn xyz_to_lab_f(t: f32) -> f32 {
409 if t > 0.008856 { t.cbrt() } else { 7.787 * t + 16.0 / 116.0 }
410}
411
412impl Lab {
413 pub fn from_xyz(xyz: Xyz) -> Self {
415 let fx = xyz_to_lab_f(xyz.x / D65_X);
416 let fy = xyz_to_lab_f(xyz.y / D65_Y);
417 let fz = xyz_to_lab_f(xyz.z / D65_Z);
418 Self {
419 l: 116.0 * fy - 16.0,
420 a: 500.0 * (fx - fy),
421 b: 200.0 * (fy - fz),
422 }
423 }
424
425 pub fn to_xyz(self) -> Xyz {
427 let fy = (self.l + 16.0) / 116.0;
428 let fx = self.a / 500.0 + fy;
429 let fz = fy - self.b / 200.0;
430 let cube = |v: f32| if v > 0.2069 { v * v * v } else { (v - 16.0 / 116.0) / 7.787 };
431 Xyz { x: cube(fx) * D65_X, y: cube(fy) * D65_Y, z: cube(fz) * D65_Z }
432 }
433
434 pub fn from_rgb(c: Rgba) -> Self {
436 Self::from_xyz(Xyz::from_linear_rgb(c))
437 }
438
439 pub fn to_rgb(self) -> Rgba {
441 self.to_xyz().to_linear_rgb()
442 }
443
444 pub fn delta_e(&self, other: &Lab) -> f32 {
446 let dl = self.l - other.l;
447 let da = self.a - other.a;
448 let db = self.b - other.b;
449 (dl * dl + da * da + db * db).sqrt()
450 }
451}
452
453#[derive(Debug, Clone, Copy)]
455pub struct Lch {
456 pub l: f32,
458 pub c: f32,
460 pub h: f32,
462}
463
464impl Lch {
465 pub fn from_lab(lab: Lab) -> Self {
467 let c = (lab.a * lab.a + lab.b * lab.b).sqrt();
468 let h = lab.b.atan2(lab.a).to_degrees();
469 let h = ((h % 360.0) + 360.0) % 360.0;
470 Self { l: lab.l, c, h }
471 }
472
473 pub fn to_lab(self) -> Lab {
475 let h_rad = self.h.to_radians();
476 Lab { l: self.l, a: self.c * h_rad.cos(), b: self.c * h_rad.sin() }
477 }
478
479 pub fn from_rgb(c: Rgba) -> Self { Self::from_lab(Lab::from_rgb(c)) }
481 pub fn to_rgb(self) -> Rgba { self.to_lab().to_rgb() }
483
484 pub fn lerp_hue(self, other: Lch, t: f32) -> Lch {
487 let mut dh = other.h - self.h;
489 if dh > 180.0 { dh -= 360.0; }
490 if dh < -180.0 { dh += 360.0; }
491 Lch {
492 l: self.l + (other.l - self.l) * t,
493 c: self.c + (other.c - self.c) * t,
494 h: self.h + dh * t,
495 }
496 }
497}
498
499#[derive(Debug, Clone, Copy, PartialEq)]
503pub enum GradientMode {
504 LinearRgb,
506 Oklab,
508 Lch,
510 Hsv,
512}
513
514#[derive(Debug, Clone, Copy)]
516pub struct ColorStop {
517 pub t: f32,
519 pub color: Rgba,
521}
522
523#[derive(Debug, Clone)]
525pub struct Gradient {
526 pub stops: Vec<ColorStop>,
528 pub mode: GradientMode,
530}
531
532impl Gradient {
533 pub fn new(mode: GradientMode) -> Self {
535 Self { stops: Vec::new(), mode }
536 }
537
538 pub fn add_stop(mut self, t: f32, color: Rgba) -> Self {
540 self.stops.push(ColorStop { t: t.clamp(0.0, 1.0), color });
541 self.stops.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap());
542 self
543 }
544
545 pub fn sample(&self, t: f32) -> Rgba {
547 if self.stops.is_empty() { return Rgba::BLACK; }
548 if self.stops.len() == 1 { return self.stops[0].color; }
549
550 let t = t.clamp(0.0, 1.0);
551
552 let i = self.stops.partition_point(|s| s.t <= t);
554 if i == 0 { return self.stops[0].color; }
555 if i >= self.stops.len() { return self.stops.last().unwrap().color; }
556
557 let lo = &self.stops[i - 1];
558 let hi = &self.stops[i];
559 let f = (t - lo.t) / (hi.t - lo.t).max(1e-8);
560
561 match self.mode {
562 GradientMode::LinearRgb => lo.color.lerp(hi.color, f),
563 GradientMode::Oklab => {
564 let a = Oklab::from_linear_rgb(lo.color);
565 let b = Oklab::from_linear_rgb(hi.color);
566 a.lerp(b, f).to_linear_rgb()
567 }
568 GradientMode::Lch => {
569 let a = Lch::from_rgb(lo.color);
570 let b = Lch::from_rgb(hi.color);
571 a.lerp_hue(b, f).to_rgb()
572 }
573 GradientMode::Hsv => {
574 let (ha, sa, va) = rgb_to_hsv(lo.color.r, lo.color.g, lo.color.b);
575 let (hb, sb, vb) = rgb_to_hsv(hi.color.r, hi.color.g, hi.color.b);
576 let mut dh = hb - ha;
577 if dh > 180.0 { dh -= 360.0; }
578 if dh < -180.0 { dh += 360.0; }
579 let h = ha + dh * f;
580 let s = sa + (sb - sa) * f;
581 let v = va + (vb - va) * f;
582 let (r, g, b) = hsv_to_rgb(h, s, v);
583 Rgba::rgb(r, g, b)
584 }
585 }
586 }
587
588 pub fn bake_lut(&self, n: usize) -> Vec<Rgba> {
590 (0..n).map(|i| self.sample(i as f32 / (n - 1) as f32)).collect()
591 }
592}
593
594pub use colorgrad;
605
606pub const PRESET_GRADIENTS: &[&str] = &[
608 "viridis", "inferno", "magma", "plasma", "cividis", "turbo",
610 "sinebow", "rainbow", "cubehelix", "warm", "cool",
612 "spectral", "rd_bu", "rd_yl_bu", "rd_yl_gn", "br_bg", "pr_gn", "pi_yg", "pu_or", "rd_gy",
614 "blues", "greens", "greys", "oranges", "purples", "reds",
616 "bu_gn", "bu_pu", "gn_bu", "or_rd", "pu_bu_gn", "pu_bu", "pu_rd", "rd_pu",
617 "yl_gn_bu", "yl_gn", "yl_or_br", "yl_or_rd",
618];
619
620const PRESET_STOPS: usize = 64;
623
624pub fn preset_gradient(name: &str) -> Option<Gradient> {
637 use colorgrad::preset as p;
638 let key = name.trim().to_ascii_lowercase().replace(['-', ' '], "_");
639 let n = PRESET_STOPS;
640 Some(match key.as_str() {
641 "viridis" => Gradient::from_colorgrad(&p::viridis(), n),
642 "inferno" => Gradient::from_colorgrad(&p::inferno(), n),
643 "magma" => Gradient::from_colorgrad(&p::magma(), n),
644 "plasma" => Gradient::from_colorgrad(&p::plasma(), n),
645 "cividis" => Gradient::from_colorgrad(&p::cividis(), n),
646 "turbo" => Gradient::from_colorgrad(&p::turbo(), n),
647 "sinebow" => Gradient::from_colorgrad(&p::sinebow(), n),
648 "rainbow" => Gradient::from_colorgrad(&p::rainbow(), n),
649 "cubehelix" => Gradient::from_colorgrad(&p::cubehelix_default(), n),
650 "warm" => Gradient::from_colorgrad(&p::warm(), n),
651 "cool" => Gradient::from_colorgrad(&p::cool(), n),
652 "spectral" => Gradient::from_colorgrad(&p::spectral(), n),
653 "rd_bu" => Gradient::from_colorgrad(&p::rd_bu(), n),
654 "rd_yl_bu" => Gradient::from_colorgrad(&p::rd_yl_bu(), n),
655 "rd_yl_gn" => Gradient::from_colorgrad(&p::rd_yl_gn(), n),
656 "br_bg" => Gradient::from_colorgrad(&p::br_bg(), n),
657 "pr_gn" => Gradient::from_colorgrad(&p::pr_gn(), n),
658 "pi_yg" => Gradient::from_colorgrad(&p::pi_yg(), n),
659 "pu_or" => Gradient::from_colorgrad(&p::pu_or(), n),
660 "rd_gy" => Gradient::from_colorgrad(&p::rd_gy(), n),
661 "blues" => Gradient::from_colorgrad(&p::blues(), n),
662 "greens" => Gradient::from_colorgrad(&p::greens(), n),
663 "greys" | "grays" => Gradient::from_colorgrad(&p::greys(), n),
664 "oranges" => Gradient::from_colorgrad(&p::oranges(), n),
665 "purples" => Gradient::from_colorgrad(&p::purples(), n),
666 "reds" => Gradient::from_colorgrad(&p::reds(), n),
667 "bu_gn" => Gradient::from_colorgrad(&p::bu_gn(), n),
668 "bu_pu" => Gradient::from_colorgrad(&p::bu_pu(), n),
669 "gn_bu" => Gradient::from_colorgrad(&p::gn_bu(), n),
670 "or_rd" => Gradient::from_colorgrad(&p::or_rd(), n),
671 "pu_bu_gn" => Gradient::from_colorgrad(&p::pu_bu_gn(), n),
672 "pu_bu" => Gradient::from_colorgrad(&p::pu_bu(), n),
673 "pu_rd" => Gradient::from_colorgrad(&p::pu_rd(), n),
674 "rd_pu" => Gradient::from_colorgrad(&p::rd_pu(), n),
675 "yl_gn_bu" => Gradient::from_colorgrad(&p::yl_gn_bu(), n),
676 "yl_gn" => Gradient::from_colorgrad(&p::yl_gn(), n),
677 "yl_or_br" => Gradient::from_colorgrad(&p::yl_or_br(), n),
678 "yl_or_rd" => Gradient::from_colorgrad(&p::yl_or_rd(), n),
679 _ => return None,
680 })
681}
682
683impl Gradient {
684 pub fn from_colorgrad<G: colorgrad::Gradient + ?Sized>(g: &G, samples: usize) -> Gradient {
690 let (lo, hi) = g.domain();
691 let n = samples.max(2);
692 let mut out = Gradient::new(GradientMode::LinearRgb);
693 out.stops = (0..n)
694 .map(|i| {
695 let t = i as f32 / (n - 1) as f32;
696 let c = g.at(lo + (hi - lo) * t);
697 let c = c.clamp();
699 ColorStop { t, color: Rgba::new(c.r, c.g, c.b, c.a) }
700 })
701 .collect();
702 out
703 }
704
705 pub fn from_css(css: &str) -> Result<Gradient, String> {
717 let g = colorgrad::GradientBuilder::new()
718 .css(css)
719 .build::<colorgrad::LinearGradient>()
720 .map_err(|e| format!("bad gradient {css:?}: {e}"))?;
721 Ok(Gradient::from_colorgrad(&g, 256))
724 }
725}
726
727pub fn gradient_plasma() -> Gradient {
729 preset_gradient("plasma").expect("built-in preset")
730}
731
732pub fn gradient_inferno() -> Gradient {
734 preset_gradient("inferno").expect("built-in preset")
735}
736
737pub fn gradient_viridis() -> Gradient {
739 preset_gradient("viridis").expect("built-in preset")
740}
741
742pub fn gradient_fire() -> Gradient {
744 Gradient::new(GradientMode::LinearRgb)
745 .add_stop(0.0, Rgba::BLACK)
746 .add_stop(0.3, Rgba::rgb(0.5, 0.0, 0.0))
747 .add_stop(0.6, Rgba::rgb(1.0, 0.3, 0.0))
748 .add_stop(0.8, Rgba::rgb(1.0, 0.8, 0.0))
749 .add_stop(1.0, Rgba::WHITE)
750}
751
752pub fn gradient_ice() -> Gradient {
754 Gradient::new(GradientMode::Oklab)
755 .add_stop(0.0, Rgba::BLACK)
756 .add_stop(0.4, Rgba::rgb(0.0, 0.2, 0.5))
757 .add_stop(0.7, Rgba::rgb(0.2, 0.6, 1.0))
758 .add_stop(1.0, Rgba::WHITE)
759}
760
761pub fn gradient_neon() -> Gradient {
763 Gradient::new(GradientMode::Oklab)
764 .add_stop(0.0, Rgba::from_hex(0xff00ff))
765 .add_stop(0.5, Rgba::from_hex(0x00ffff))
766 .add_stop(1.0, Rgba::from_hex(0xff00ff))
767}
768
769pub fn gradient_health() -> Gradient {
771 Gradient::new(GradientMode::Oklab)
772 .add_stop(0.0, Rgba::rgb(1.0, 0.0, 0.0))
773 .add_stop(0.5, Rgba::rgb(1.0, 0.8, 0.0))
774 .add_stop(1.0, Rgba::rgb(0.0, 1.0, 0.2))
775}
776
777pub fn complementary(c: Rgba) -> Rgba {
781 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
782 let (r, g, b) = hsv_to_rgb((h + 180.0) % 360.0, s, v);
783 Rgba::rgb(r, g, b)
784}
785
786pub fn split_complementary(c: Rgba) -> (Rgba, Rgba) {
788 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
789 let mk = |dh: f32| {
790 let (r, g, b) = hsv_to_rgb((h + dh) % 360.0, s, v);
791 Rgba::rgb(r, g, b)
792 };
793 (mk(150.0), mk(210.0))
794}
795
796pub fn triadic(c: Rgba) -> (Rgba, Rgba) {
798 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
799 let mk = |dh: f32| {
800 let (r, g, b) = hsv_to_rgb((h + dh) % 360.0, s, v);
801 Rgba::rgb(r, g, b)
802 };
803 (mk(120.0), mk(240.0))
804}
805
806pub fn analogous(c: Rgba) -> (Rgba, Rgba) {
808 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
809 let mk = |dh: f32| {
810 let (r, g, b) = hsv_to_rgb((h + dh + 360.0) % 360.0, s, v);
811 Rgba::rgb(r, g, b)
812 };
813 (mk(-30.0), mk(30.0))
814}
815
816pub fn tetradic(c: Rgba) -> [Rgba; 4] {
818 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
819 std::array::from_fn(|i| {
820 let (r, g, b) = hsv_to_rgb((h + i as f32 * 90.0) % 360.0, s, v);
821 Rgba::rgb(r, g, b)
822 })
823}
824
825#[derive(Debug, Clone)]
829pub struct Palette {
830 pub name: String,
832 pub colors: Vec<Rgba>,
834}
835
836impl Palette {
837 pub fn new(name: impl Into<String>, colors: Vec<Rgba>) -> Self {
839 Self { name: name.into(), colors }
840 }
841
842 pub fn get(&self, i: usize) -> Rgba {
844 if self.colors.is_empty() { return Rgba::WHITE; }
845 self.colors[i % self.colors.len()]
846 }
847
848 pub fn sample(&self, t: f32) -> Rgba {
850 if self.colors.is_empty() { return Rgba::WHITE; }
851 if self.colors.len() == 1 { return self.colors[0]; }
852 let t = t.fract().abs();
853 let f = t * (self.colors.len() - 1) as f32;
854 let i = f as usize;
855 let j = (i + 1).min(self.colors.len() - 1);
856 self.colors[i].lerp(self.colors[j], f.fract())
857 }
858}
859
860pub fn palette_crt() -> Palette {
862 Palette::new("CRT", vec![
863 Rgba::from_hex(0x00ff00), Rgba::from_hex(0x00ffff), Rgba::from_hex(0xff6600), Rgba::from_hex(0xffffff), ])
868}
869
870pub fn palette_ansi16() -> Palette {
872 Palette::new("ANSI16", vec![
873 Rgba::from_hex(0x000000), Rgba::from_hex(0xaa0000),
874 Rgba::from_hex(0x00aa00), Rgba::from_hex(0xaa5500),
875 Rgba::from_hex(0x0000aa), Rgba::from_hex(0xaa00aa),
876 Rgba::from_hex(0x00aaaa), Rgba::from_hex(0xaaaaaa),
877 Rgba::from_hex(0x555555), Rgba::from_hex(0xff5555),
878 Rgba::from_hex(0x55ff55), Rgba::from_hex(0xffff55),
879 Rgba::from_hex(0x5555ff), Rgba::from_hex(0xff55ff),
880 Rgba::from_hex(0x55ffff), Rgba::from_hex(0xffffff),
881 ])
882}
883
884pub fn palette_chaos_elements() -> Palette {
886 Palette::new("ChaosElements", vec![
887 Rgba::from_hex(0xff4400), Rgba::from_hex(0x00aaff), Rgba::from_hex(0x44ff44), Rgba::from_hex(0xaa00ff), Rgba::from_hex(0xffcc00), Rgba::from_hex(0x22ffcc), Rgba::from_hex(0xff00aa), ])
895}
896
897pub fn tonemap_reinhard(c: Rgba) -> Rgba {
901 let map = |x: f32| x / (x + 1.0);
902 Rgba::new(map(c.r), map(c.g), map(c.b), c.a)
903}
904
905pub fn tonemap_aces(c: Rgba) -> Rgba {
907 let aces = |x: f32| -> f32 {
908 const A: f32 = 2.51;
909 const B: f32 = 0.03;
910 const C: f32 = 2.43;
911 const D: f32 = 0.59;
912 const E: f32 = 0.14;
913 ((x * (A * x + B)) / (x * (C * x + D) + E)).clamp(0.0, 1.0)
914 };
915 Rgba::new(aces(c.r), aces(c.g), aces(c.b), c.a)
916}
917
918pub fn tonemap_uncharted2(c: Rgba) -> Rgba {
920 fn partial(x: f32) -> f32 {
921 const A: f32 = 0.15; const B: f32 = 0.50;
922 const C: f32 = 0.10; const D: f32 = 0.20;
923 const E: f32 = 0.02; const F: f32 = 0.30;
924 ((x*(A*x+C*B)+D*E) / (x*(A*x+B)+D*F)) - E/F
925 }
926 let exposure_bias = 2.0_f32;
927 let curr = |x: f32| partial(x * exposure_bias);
928 let white = partial(11.2);
929 let scale = 1.0 / white;
930 Rgba::new(curr(c.r)*scale, curr(c.g)*scale, curr(c.b)*scale, c.a)
931}
932
933pub fn distance_rgb(a: Rgba, b: Rgba) -> f32 {
937 let dr = a.r - b.r; let dg = a.g - b.g; let db = a.b - b.b;
938 (dr*dr + dg*dg + db*db).sqrt()
939}
940
941pub fn distance_lab_e76(a: Rgba, b: Rgba) -> f32 {
943 Lab::from_rgb(a).delta_e(&Lab::from_rgb(b))
944}
945
946pub fn nearest_in_palette(color: Rgba, palette: &Palette) -> usize {
948 let lab = Lab::from_rgb(color);
949 palette.colors.iter()
950 .enumerate()
951 .min_by(|(_, &a), (_, &b)| {
952 let da = lab.delta_e(&Lab::from_rgb(a));
953 let db = lab.delta_e(&Lab::from_rgb(b));
954 da.partial_cmp(&db).unwrap()
955 })
956 .map(|(i, _)| i)
957 .unwrap_or(0)
958}
959
960pub fn adjust_hue(c: Rgba, delta: f32) -> Rgba {
964 let (h, s, v) = rgb_to_hsv(c.r, c.g, c.b);
965 let (r, g, b) = hsv_to_rgb((h + delta + 360.0) % 360.0, s, v);
966 Rgba::new(r, g, b, c.a)
967}
968
969pub fn adjust_saturation(c: Rgba, factor: f32) -> Rgba {
971 let lum = c.luminance();
972 Rgba::new(
973 lum + (c.r - lum) * factor,
974 lum + (c.g - lum) * factor,
975 lum + (c.b - lum) * factor,
976 c.a,
977 )
978}
979
980pub fn adjust_brightness(c: Rgba, delta: f32) -> Rgba {
982 Rgba::new((c.r + delta).clamp(0.0, 1.0),
983 (c.g + delta).clamp(0.0, 1.0),
984 (c.b + delta).clamp(0.0, 1.0),
985 c.a)
986}
987
988pub fn adjust_contrast(c: Rgba, factor: f32) -> Rgba {
990 let adj = |x: f32| ((x - 0.5) * factor + 0.5).clamp(0.0, 1.0);
991 Rgba::new(adj(c.r), adj(c.g), adj(c.b), c.a)
992}
993
994pub fn tint_white(c: Rgba, factor: f32) -> Rgba {
996 c.lerp(Rgba::WHITE, factor)
997}
998
999pub fn shade_black(c: Rgba, factor: f32) -> Rgba {
1001 c.lerp(Rgba::BLACK, factor)
1002}
1003
1004#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 fn close(a: Rgba, hex: u32) -> bool {
1011 let b = Rgba::from_hex(hex);
1012 (a.r - b.r).abs() < 0.02 && (a.g - b.g).abs() < 0.02 && (a.b - b.b).abs() < 0.02
1013 }
1014
1015 #[test]
1016 fn presets_hit_the_published_end_points() {
1017 let v = gradient_viridis();
1020 assert!(close(v.sample(0.0), 0x440154) && close(v.sample(1.0), 0xfde725));
1021 let p = gradient_plasma();
1022 assert!(close(p.sample(0.0), 0x0d0887) && close(p.sample(1.0), 0xf0f921));
1023 let i = gradient_inferno();
1024 assert!(close(i.sample(0.0), 0x000004) && close(i.sample(1.0), 0xfcffa4));
1025 let (m, want) = (v.sample(0.5), Rgba::from_hex(0x21918c));
1029 let err = (m.r - want.r).abs().max((m.g - want.g).abs()).max((m.b - want.b).abs());
1030 assert!(err < 0.06, "{m:?} is {err} from #21918c");
1031 }
1032
1033 #[test]
1034 fn every_listed_preset_resolves_and_is_well_formed() {
1035 for name in PRESET_GRADIENTS {
1036 let g = preset_gradient(name).unwrap_or_else(|| panic!("{name} missing"));
1037 assert_eq!(g.stops.len(), PRESET_STOPS);
1038 for c in g.bake_lut(17) {
1039 for ch in [c.r, c.g, c.b, c.a] {
1040 assert!((0.0..=1.0).contains(&ch), "{name}: {ch}");
1041 }
1042 }
1043 }
1044 assert!(preset_gradient("Rd-Yl-Bu").is_some());
1045 assert!(preset_gradient("nope").is_none());
1046 }
1047
1048 #[test]
1049 fn css_gradients_parse_with_positions_and_hard_stops() {
1050 let g = Gradient::from_css("red, red 50%, blue 50%, blue").unwrap();
1051 assert!(close(g.sample(0.25), 0xff0000));
1052 assert!(close(g.sample(0.75), 0x0000ff));
1053 let g = Gradient::from_css("#000, #fff").unwrap();
1054 assert!((g.sample(0.5).r - 0.5).abs() < 0.01);
1055 assert!(Gradient::from_css("").is_err() || Gradient::from_css("").unwrap().stops.len() >= 2);
1056 assert!(Gradient::from_css("bogus, nonsense").is_err());
1057 }
1058
1059 fn approx_eq(a: f32, b: f32) -> bool { (a - b).abs() < 0.005 }
1060
1061 #[test]
1062 fn hsv_roundtrip() {
1063 let (h0, s0, v0) = (200.0f32, 0.7, 0.8);
1064 let (r, g, b) = hsv_to_rgb(h0, s0, v0);
1065 let (h1, s1, v1) = rgb_to_hsv(r, g, b);
1066 assert!(approx_eq(h0, h1), "hue mismatch: {h0} vs {h1}");
1067 assert!(approx_eq(s0, s1));
1068 assert!(approx_eq(v0, v1));
1069 }
1070
1071 #[test]
1072 fn hsl_roundtrip() {
1073 let (r, g, b) = hsl_to_rgb(120.0, 0.5, 0.5);
1074 let (h, s, l) = rgb_to_hsl(r, g, b);
1075 assert!(approx_eq(h, 120.0), "hue mismatch: {h}");
1076 assert!(approx_eq(s, 0.5));
1077 assert!(approx_eq(l, 0.5));
1078 }
1079
1080 #[test]
1081 fn oklab_roundtrip() {
1082 let c = Rgba::from_hex(0x3a7bd5);
1083 let oklab = Oklab::from_linear_rgb(c);
1084 let back = oklab.to_linear_rgb();
1085 assert!(approx_eq(c.r, back.r), "r mismatch: {} vs {}", c.r, back.r);
1086 assert!(approx_eq(c.g, back.g));
1087 assert!(approx_eq(c.b, back.b));
1088 }
1089
1090 #[test]
1091 fn gradient_endpoints() {
1092 let g = gradient_fire();
1093 let lo = g.sample(0.0);
1094 let hi = g.sample(1.0);
1095 assert!(lo.luminance() < 0.01);
1096 assert!(hi.luminance() > 0.9);
1097 }
1098
1099 #[test]
1100 fn complementary_is_180_degrees() {
1101 let c = Rgba::from_hex(0xff0000); let comp = complementary(c);
1103 let (h, _, _) = rgb_to_hsv(comp.r, comp.g, comp.b);
1104 assert!((h - 180.0).abs() < 2.0, "hue={h}");
1106 }
1107
1108 #[test]
1109 fn tonemap_aces_bounds() {
1110 let bright = Rgba::rgb(10.0, 5.0, 2.0); let tm = tonemap_aces(bright);
1112 assert!(tm.r <= 1.0);
1113 assert!(tm.g <= 1.0);
1114 assert!(tm.b <= 1.0);
1115 }
1116}