Skip to main content

gpui_kit_material_color/dynamic/
utils.rs

1// Copyright 2021 Google LLC
2// SPDX-License-Identifier: Apache-2.0
3// Ported to Rust from material-color-utilities (typescript/utils), modified for
4// this crate. See licenses/material-color-utilities.txt.
5
6//! Math and color-space conversions shared by the dynamic color port.
7
8const SRGB_TO_XYZ: [[f64; 3]; 3] = [
9    [0.41233895, 0.35762064, 0.18051042],
10    [0.2126, 0.7152, 0.0722],
11    [0.01932141, 0.11916382, 0.95034478],
12];
13
14const XYZ_TO_SRGB: [[f64; 3]; 3] = [
15    [
16        3.2413774792388685,
17        -1.5376652402851851,
18        -0.49885366846268053,
19    ],
20    [-0.9691452513005321, 1.8758853451067872, 0.04156585616912061],
21    [
22        0.05562093689691305,
23        -0.20395524564742123,
24        1.0571799111220335,
25    ],
26];
27
28/// The D65 white point in XYZ: white on a sunny day.
29pub const WHITE_POINT_D65: [f64; 3] = [95.047, 100.0, 108.883];
30
31/// -1 below zero, 0 at zero, 1 above zero.
32pub fn signum(num: f64) -> f64 {
33    if num < 0. {
34        -1.
35    } else if num == 0. {
36        0.
37    } else {
38        1.
39    }
40}
41
42/// `start` at `amount` 0 and `stop` at `amount` 1.
43pub fn lerp(start: f64, stop: f64, amount: f64) -> f64 {
44    (1. - amount) * start + amount * stop
45}
46
47/// `input` limited to `min..=max`. Unlike [`f64::clamp`] this accepts
48/// `min > max` (returning `min` for anything below it), which the 2025 color
49/// spec relies on and which must not panic.
50pub fn clamp_double(min: f64, max: f64, input: f64) -> f64 {
51    if input < min {
52        min
53    } else if input > max {
54        max
55    } else {
56        input
57    }
58}
59
60/// An integer angle in `0..360`.
61pub fn sanitize_degrees_int(degrees: i32) -> i32 {
62    degrees.rem_euclid(360)
63}
64
65/// An angle in `0.0..360.0`.
66pub fn sanitize_degrees_double(degrees: f64) -> f64 {
67    let degrees = degrees % 360.;
68    if degrees < 0. {
69        degrees + 360.
70    } else {
71        degrees
72    }
73}
74
75/// The sign of the shortest rotation from one angle to another; 1 when they
76/// are exactly opposite.
77pub fn rotation_direction(from: f64, to: f64) -> f64 {
78    if sanitize_degrees_double(to - from) <= 180. {
79        1.
80    } else {
81        -1.
82    }
83}
84
85/// The distance of two angles on a circle, in degrees.
86pub fn difference_degrees(a: f64, b: f64) -> f64 {
87    180. - ((a - b).abs() - 180.).abs()
88}
89
90/// A 1x3 row vector times a 3x3 matrix.
91pub(crate) fn matrix_multiply(row: [f64; 3], matrix: &[[f64; 3]; 3]) -> [f64; 3] {
92    [
93        row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2],
94        row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2],
95        row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2],
96    ]
97}
98
99/// Opaque ARGB from 8-bit channels.
100pub const fn argb_from_rgb(red: u8, green: u8, blue: u8) -> u32 {
101    0xff00_0000 | (red as u32) << 16 | (green as u32) << 8 | blue as u32
102}
103
104/// Opaque ARGB from linear RGB channels in `0.0..=100.0`.
105pub fn argb_from_linrgb(linrgb: [f64; 3]) -> u32 {
106    argb_from_rgb(
107        delinearized(linrgb[0]),
108        delinearized(linrgb[1]),
109        delinearized(linrgb[2]),
110    )
111}
112
113/// The red channel of an ARGB color.
114pub const fn red_from_argb(argb: u32) -> u8 {
115    (argb >> 16) as u8
116}
117
118/// The green channel of an ARGB color.
119pub const fn green_from_argb(argb: u32) -> u8 {
120    (argb >> 8) as u8
121}
122
123/// The blue channel of an ARGB color.
124pub const fn blue_from_argb(argb: u32) -> u8 {
125    argb as u8
126}
127
128/// Opaque ARGB from XYZ.
129pub fn argb_from_xyz(x: f64, y: f64, z: f64) -> u32 {
130    argb_from_linrgb(matrix_multiply([x, y, z], &XYZ_TO_SRGB))
131}
132
133/// XYZ of an ARGB color.
134pub fn xyz_from_argb(argb: u32) -> [f64; 3] {
135    matrix_multiply(
136        [
137            linearized(red_from_argb(argb)),
138            linearized(green_from_argb(argb)),
139            linearized(blue_from_argb(argb)),
140        ],
141        &SRGB_TO_XYZ,
142    )
143}
144
145/// L*a*b* of an ARGB color.
146pub fn lab_from_argb(argb: u32) -> [f64; 3] {
147    let [x, y, z] = xyz_from_argb(argb);
148    let fx = lab_f(x / WHITE_POINT_D65[0]);
149    let fy = lab_f(y / WHITE_POINT_D65[1]);
150    let fz = lab_f(z / WHITE_POINT_D65[2]);
151    [116. * fy - 16., 500. * (fx - fy), 200. * (fy - fz)]
152}
153
154/// The gray with lightness `lstar`.
155pub fn argb_from_lstar(lstar: f64) -> u32 {
156    let component = delinearized(y_from_lstar(lstar));
157    argb_from_rgb(component, component, component)
158}
159
160/// L* of an ARGB color.
161pub fn lstar_from_argb(argb: u32) -> f64 {
162    116. * lab_f(xyz_from_argb(argb)[1] / 100.) - 16.
163}
164
165/// Y in XYZ (relative luminance) of an L* value.
166pub fn y_from_lstar(lstar: f64) -> f64 {
167    100. * lab_invf((lstar + 16.) / 116.)
168}
169
170/// L* of a Y value.
171pub fn lstar_from_y(y: f64) -> f64 {
172    lab_f(y / 100.) * 116. - 16.
173}
174
175/// An 8-bit sRGB channel as linear RGB in `0.0..=100.0`.
176pub fn linearized(component: u8) -> f64 {
177    let normalized = f64::from(component) / 255.;
178    if normalized <= 0.040449936 {
179        normalized / 12.92 * 100.
180    } else {
181        ((normalized + 0.055) / 1.055).powf(2.4) * 100.
182    }
183}
184
185/// A linear RGB channel in `0.0..=100.0` as 8-bit sRGB, clamped.
186pub fn delinearized(component: f64) -> u8 {
187    let normalized = component / 100.;
188    let delinearized = if normalized <= 0.0031308 {
189        normalized * 12.92
190    } else {
191        1.055 * normalized.powf(1. / 2.4) - 0.055
192    };
193    // A saturating cast: out-of-range values clamp and NaN becomes 0.
194    (delinearized * 255.).round() as u8
195}
196
197fn lab_f(t: f64) -> f64 {
198    const E: f64 = 216. / 24389.;
199    const KAPPA: f64 = 24389. / 27.;
200    if t > E {
201        t.powf(1. / 3.)
202    } else {
203        (KAPPA * t + 16.) / 116.
204    }
205}
206
207fn lab_invf(ft: f64) -> f64 {
208    const E: f64 = 216. / 24389.;
209    const KAPPA: f64 = 24389. / 27.;
210    let ft3 = ft * ft * ft;
211    if ft3 > E {
212        ft3
213    } else {
214        (116. * ft - 16.) / KAPPA
215    }
216}