1use crate::easing::parse_easing;
4use crate::error::{JsResult, js_error, non_negative};
5use crate::types::{f32_array, normalize_name};
6use animato_core::Update;
7use animato_tween::Tween as CoreTween;
8use js_sys::Float32Array;
9use wasm_bindgen::prelude::*;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
12struct Rgb {
13 r: f32,
14 g: f32,
15 b: f32,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19enum ColorSpace {
20 Rgb,
21 Linear,
22 Lab,
23 Oklch,
24}
25
26impl ColorSpace {
27 fn parse(input: &str) -> JsResult<Self> {
28 match normalize_name(input).as_str() {
29 "rgb" | "srgb" => Ok(Self::Rgb),
30 "linear" | "linearrgb" => Ok(Self::Linear),
31 "lab" => Ok(Self::Lab),
32 "oklch" | "oklab" => Ok(Self::Oklch),
33 _ => Err(js_error(format!("unknown color space `{input}`"))),
34 }
35 }
36}
37
38#[wasm_bindgen(js_name = ColorTween)]
40#[derive(Clone, Debug)]
41pub struct ColorTween {
42 start: Rgb,
43 end: Rgb,
44 space: ColorSpace,
45 tween: CoreTween<f32>,
46}
47
48#[wasm_bindgen(js_class = ColorTween)]
49impl ColorTween {
50 #[wasm_bindgen(constructor)]
52 pub fn new(from: &str, to: &str, duration: f32, space: &str) -> Result<Self, JsValue> {
53 Ok(Self {
54 start: parse_color(from)?,
55 end: parse_color(to)?,
56 space: ColorSpace::parse(space)?,
57 tween: CoreTween::new(0.0, 1.0)
58 .duration(non_negative(duration, 1.0))
59 .build(),
60 })
61 }
62
63 pub fn update(&mut self, dt: f32) -> bool {
65 self.tween.update(dt)
66 }
67
68 #[wasm_bindgen(js_name = valueHex)]
70 pub fn value_hex(&self) -> String {
71 to_hex(interpolate(
72 self.start,
73 self.end,
74 self.tween.value(),
75 self.space,
76 ))
77 }
78
79 #[wasm_bindgen(js_name = rgbaArray)]
81 pub fn rgba_array(&self) -> Float32Array {
82 let color = interpolate(self.start, self.end, self.tween.value(), self.space);
83 f32_array(&[color.r, color.g, color.b, 1.0])
84 }
85
86 pub fn progress(&self) -> f32 {
88 self.tween.progress()
89 }
90
91 #[wasm_bindgen(js_name = isComplete)]
93 pub fn is_complete(&self) -> bool {
94 self.tween.is_complete()
95 }
96
97 pub fn reset(&mut self) {
99 self.tween.reset();
100 }
101
102 #[wasm_bindgen(js_name = setEasing)]
104 pub fn set_easing(&mut self, easing: &str) -> Result<(), JsValue> {
105 self.tween.easing = parse_easing(easing)?;
106 Ok(())
107 }
108}
109
110#[wasm_bindgen(js_name = interpolateColor)]
112pub fn interpolate_color(from: &str, to: &str, t: f32, space: &str) -> Result<String, JsValue> {
113 Ok(to_hex(interpolate(
114 parse_color(from)?,
115 parse_color(to)?,
116 t,
117 ColorSpace::parse(space)?,
118 )))
119}
120
121fn interpolate(from: Rgb, to: Rgb, t: f32, space: ColorSpace) -> Rgb {
122 let t = t.clamp(0.0, 1.0);
123 match space {
124 ColorSpace::Rgb => lerp_rgb(from, to, t),
125 ColorSpace::Linear => linear_to_srgb(lerp_rgb(srgb_to_linear(from), srgb_to_linear(to), t)),
126 ColorSpace::Lab => xyz_to_srgb(lab_to_xyz(lerp3(srgb_to_lab(from), srgb_to_lab(to), t))),
127 ColorSpace::Oklch => oklab_to_srgb(oklch_to_oklab(lerp_oklch(
128 oklab_to_oklch(srgb_to_oklab(from)),
129 oklab_to_oklch(srgb_to_oklab(to)),
130 t,
131 ))),
132 }
133}
134
135fn lerp_rgb(a: Rgb, b: Rgb, t: f32) -> Rgb {
136 Rgb {
137 r: a.r + (b.r - a.r) * t,
138 g: a.g + (b.g - a.g) * t,
139 b: a.b + (b.b - a.b) * t,
140 }
141}
142
143fn parse_color(input: &str) -> JsResult<Rgb> {
144 let hex = input.trim().trim_start_matches('#');
145 let (r, g, b) = match hex.len() {
146 3 => (
147 u8::from_str_radix(&hex[0..1].repeat(2), 16),
148 u8::from_str_radix(&hex[1..2].repeat(2), 16),
149 u8::from_str_radix(&hex[2..3].repeat(2), 16),
150 ),
151 6 => (
152 u8::from_str_radix(&hex[0..2], 16),
153 u8::from_str_radix(&hex[2..4], 16),
154 u8::from_str_radix(&hex[4..6], 16),
155 ),
156 _ => {
157 return Err(js_error(format!(
158 "expected #rgb or #rrggbb color, got `{input}`"
159 )));
160 }
161 };
162 Ok(Rgb {
163 r: r.map_err(|_| js_error(format!("invalid red channel in `{input}`")))? as f32 / 255.0,
164 g: g.map_err(|_| js_error(format!("invalid green channel in `{input}`")))? as f32 / 255.0,
165 b: b.map_err(|_| js_error(format!("invalid blue channel in `{input}`")))? as f32 / 255.0,
166 })
167}
168
169fn to_hex(color: Rgb) -> String {
170 format!(
171 "#{:02x}{:02x}{:02x}",
172 channel(color.r),
173 channel(color.g),
174 channel(color.b)
175 )
176}
177
178fn channel(value: f32) -> u8 {
179 (value.clamp(0.0, 1.0) * 255.0).round() as u8
180}
181
182fn srgb_channel_to_linear(c: f32) -> f32 {
183 if c <= 0.04045 {
184 c / 12.92
185 } else {
186 ((c + 0.055) / 1.055).powf(2.4)
187 }
188}
189
190fn linear_channel_to_srgb(c: f32) -> f32 {
191 if c <= 0.0031308 {
192 c * 12.92
193 } else {
194 1.055 * c.powf(1.0 / 2.4) - 0.055
195 }
196}
197
198fn srgb_to_linear(c: Rgb) -> Rgb {
199 Rgb {
200 r: srgb_channel_to_linear(c.r),
201 g: srgb_channel_to_linear(c.g),
202 b: srgb_channel_to_linear(c.b),
203 }
204}
205
206fn linear_to_srgb(c: Rgb) -> Rgb {
207 Rgb {
208 r: linear_channel_to_srgb(c.r),
209 g: linear_channel_to_srgb(c.g),
210 b: linear_channel_to_srgb(c.b),
211 }
212}
213
214fn srgb_to_xyz(c: Rgb) -> [f32; 3] {
215 let c = srgb_to_linear(c);
216 [
217 c.r * 0.4124564 + c.g * 0.3575761 + c.b * 0.1804375,
218 c.r * 0.2126729 + c.g * 0.7151522 + c.b * 0.0721750,
219 c.r * 0.0193339 + c.g * 0.119_192 + c.b * 0.9503041,
220 ]
221}
222
223fn xyz_to_srgb(xyz: [f32; 3]) -> Rgb {
224 linear_to_srgb(Rgb {
225 r: xyz[0] * 3.2404542 + xyz[1] * -1.5371385 + xyz[2] * -0.4985314,
226 g: xyz[0] * -0.969_266 + xyz[1] * 1.8760108 + xyz[2] * 0.0415560,
227 b: xyz[0] * 0.0556434 + xyz[1] * -0.2040259 + xyz[2] * 1.0572252,
228 })
229}
230
231fn srgb_to_lab(c: Rgb) -> [f32; 3] {
232 let xyz = srgb_to_xyz(c);
233 let x = lab_f(xyz[0] / 0.95047);
234 let y = lab_f(xyz[1]);
235 let z = lab_f(xyz[2] / 1.08883);
236 [116.0 * y - 16.0, 500.0 * (x - y), 200.0 * (y - z)]
237}
238
239fn lab_to_xyz(lab: [f32; 3]) -> [f32; 3] {
240 let y = (lab[0] + 16.0) / 116.0;
241 let x = lab[1] / 500.0 + y;
242 let z = y - lab[2] / 200.0;
243 [0.95047 * lab_f_inv(x), lab_f_inv(y), 1.08883 * lab_f_inv(z)]
244}
245
246fn lab_f(t: f32) -> f32 {
247 if t > 0.008856 {
248 t.cbrt()
249 } else {
250 7.787 * t + 16.0 / 116.0
251 }
252}
253
254fn lab_f_inv(t: f32) -> f32 {
255 let t3 = t * t * t;
256 if t3 > 0.008856 {
257 t3
258 } else {
259 (t - 16.0 / 116.0) / 7.787
260 }
261}
262
263fn srgb_to_oklab(c: Rgb) -> [f32; 3] {
264 let c = srgb_to_linear(c);
265 let l = 0.41222146 * c.r + 0.53633255 * c.g + 0.051445995 * c.b;
266 let m = 0.2119035 * c.r + 0.6806995 * c.g + 0.10739696 * c.b;
267 let s = 0.08830246 * c.r + 0.28171884 * c.g + 0.6299787 * c.b;
268 let l = l.cbrt();
269 let m = m.cbrt();
270 let s = s.cbrt();
271 [
272 0.21045426 * l + 0.7936178 * m - 0.004072047 * s,
273 1.9779985 * l - 2.4285922 * m + 0.4505937 * s,
274 0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
275 ]
276}
277
278fn oklab_to_srgb(oklab: [f32; 3]) -> Rgb {
279 let l = oklab[0] + 0.39633778 * oklab[1] + 0.21580376 * oklab[2];
280 let m = oklab[0] - 0.105561346 * oklab[1] - 0.06385417 * oklab[2];
281 let s = oklab[0] - 0.08948418 * oklab[1] - 1.2914855 * oklab[2];
282 let l = l * l * l;
283 let m = m * m * m;
284 let s = s * s * s;
285 linear_to_srgb(Rgb {
286 r: 4.0767417 * l - 3.3077116 * m + 0.23096994 * s,
287 g: -1.268438 * l + 2.6097574 * m - 0.34131938 * s,
288 b: -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s,
289 })
290}
291
292fn oklab_to_oklch(oklab: [f32; 3]) -> [f32; 3] {
293 [
294 oklab[0],
295 (oklab[1] * oklab[1] + oklab[2] * oklab[2]).sqrt(),
296 oklab[2].atan2(oklab[1]),
297 ]
298}
299
300fn oklch_to_oklab(oklch: [f32; 3]) -> [f32; 3] {
301 [
302 oklch[0],
303 oklch[1] * oklch[2].cos(),
304 oklch[1] * oklch[2].sin(),
305 ]
306}
307
308fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
309 [
310 a[0] + (b[0] - a[0]) * t,
311 a[1] + (b[1] - a[1]) * t,
312 a[2] + (b[2] - a[2]) * t,
313 ]
314}
315
316fn lerp_oklch(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
317 let mut dh = b[2] - a[2];
318 if dh > core::f32::consts::PI {
319 dh -= core::f32::consts::TAU;
320 } else if dh < -core::f32::consts::PI {
321 dh += core::f32::consts::TAU;
322 }
323 [
324 a[0] + (b[0] - a[0]) * t,
325 a[1] + (b[1] - a[1]) * t,
326 a[2] + dh * t,
327 ]
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn parses_hex() {
336 assert_eq!(to_hex(parse_color("#0f0").unwrap()), "#00ff00");
337 }
338
339 #[test]
340 fn interpolates_rgb() {
341 assert_eq!(
342 interpolate_color("#000", "#fff", 0.5, "rgb").unwrap(),
343 "#808080"
344 );
345 }
346}