1use super::{AbsoluteColor, ColorFlags, ColorSpace};
8use crate::parser::{Parse, ParserContext};
9use crate::values::generics::color::ColorMixFlags;
10use cssparser::Parser;
11use std::fmt::{self, Write};
12use style_traits::{CssWriter, ParseError, ToCss};
13
14#[derive(
18 Clone,
19 Copy,
20 Debug,
21 Eq,
22 MallocSizeOf,
23 Parse,
24 PartialEq,
25 ToAnimatedValue,
26 ToComputedValue,
27 ToCss,
28 ToResolvedValue,
29 ToShmem,
30)]
31#[repr(u8)]
32pub enum HueInterpolationMethod {
33 Shorter,
35 Longer,
37 Increasing,
39 Decreasing,
41 Specified,
43}
44
45#[derive(
47 Clone,
48 Copy,
49 Debug,
50 Eq,
51 MallocSizeOf,
52 PartialEq,
53 ToShmem,
54 ToAnimatedValue,
55 ToComputedValue,
56 ToResolvedValue,
57)]
58#[repr(C)]
59pub struct ColorInterpolationMethod {
60 pub space: ColorSpace,
62 pub hue: HueInterpolationMethod,
64}
65
66impl ColorInterpolationMethod {
67 pub const fn srgb() -> Self {
69 Self {
70 space: ColorSpace::Srgb,
71 hue: HueInterpolationMethod::Shorter,
72 }
73 }
74
75 pub const fn oklab() -> Self {
78 Self {
79 space: ColorSpace::Oklab,
80 hue: HueInterpolationMethod::Shorter,
81 }
82 }
83
84 pub fn is_default(&self) -> bool {
86 self.space == ColorSpace::Oklab
87 }
88
89 pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self {
92 if !left.is_legacy_syntax() || !right.is_legacy_syntax() {
96 Self::default()
97 } else {
98 Self::srgb()
99 }
100 }
101}
102
103impl Default for ColorInterpolationMethod {
104 fn default() -> Self {
105 Self::oklab()
106 }
107}
108
109impl Parse for ColorInterpolationMethod {
110 fn parse<'i, 't>(
111 _: &ParserContext,
112 input: &mut Parser<'i, 't>,
113 ) -> Result<Self, ParseError<'i>> {
114 input.expect_ident_matching("in")?;
115 let space = ColorSpace::parse(input)?;
116 let hue = if space.is_polar() {
120 input
121 .try_parse(|input| -> Result<_, ParseError<'i>> {
122 let hue = HueInterpolationMethod::parse(input)?;
123 input.expect_ident_matching("hue")?;
124 Ok(hue)
125 })
126 .unwrap_or(HueInterpolationMethod::Shorter)
127 } else {
128 HueInterpolationMethod::Shorter
129 };
130 Ok(Self { space, hue })
131 }
132}
133
134impl ToCss for ColorInterpolationMethod {
135 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
136 where
137 W: Write,
138 {
139 dest.write_str("in ")?;
140 self.space.to_css(dest)?;
141 if self.hue != HueInterpolationMethod::Shorter {
142 dest.write_char(' ')?;
143 self.hue.to_css(dest)?;
144 dest.write_str(" hue")?;
145 }
146 Ok(())
147 }
148}
149
150pub fn mix(
152 interpolation: ColorInterpolationMethod,
153 left_color: &AbsoluteColor,
154 mut left_weight: f32,
155 right_color: &AbsoluteColor,
156 mut right_weight: f32,
157 flags: ColorMixFlags,
158) -> AbsoluteColor {
159 let mut alpha_multiplier = 1.0;
161 if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) {
162 let sum = left_weight + right_weight;
163 if sum != 1.0 {
164 let scale = 1.0 / sum;
165 left_weight *= scale;
166 right_weight *= scale;
167 if sum < 1.0 {
168 alpha_multiplier = sum;
169 }
170 }
171 }
172
173 let result = mix_in(
174 interpolation.space,
175 left_color,
176 left_weight,
177 right_color,
178 right_weight,
179 interpolation.hue,
180 alpha_multiplier,
181 );
182
183 if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
184 if result.is_legacy_syntax() {
188 result.to_color_space(ColorSpace::Srgb)
189 } else {
190 result
191 }
192 } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() {
193 result.into_srgb_legacy()
196 } else {
197 result
198 }
199}
200
201#[derive(Clone, Copy, PartialEq)]
203#[repr(u8)]
204enum ComponentMixOutcome {
205 Mix,
207 UseLeft,
209 UseRight,
211 None,
213}
214
215impl ComponentMixOutcome {
216 fn from_colors(
217 left: &AbsoluteColor,
218 right: &AbsoluteColor,
219 flags_to_check: ColorFlags,
220 ) -> Self {
221 match (
222 left.flags.contains(flags_to_check),
223 right.flags.contains(flags_to_check),
224 ) {
225 (true, true) => Self::None,
226 (true, false) => Self::UseRight,
227 (false, true) => Self::UseLeft,
228 (false, false) => Self::Mix,
229 }
230 }
231}
232
233impl AbsoluteColor {
234 fn carry_forward_analogous_missing_components(&mut self, source: &AbsoluteColor) {
238 use ColorFlags as F;
239 use ColorSpace as S;
240
241 if source.color_space == self.color_space {
242 return;
243 }
244
245 if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() {
249 return;
250 }
251
252 if matches!(source.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
254 if matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
255 self.flags
256 .set(F::C0_IS_NONE, source.flags.contains(F::C0_IS_NONE));
257 } else if matches!(self.color_space, S::Hsl) {
258 self.flags
259 .set(F::C2_IS_NONE, source.flags.contains(F::C0_IS_NONE));
260 }
261 } else if matches!(source.color_space, S::Hsl)
262 && matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch)
263 {
264 self.flags
265 .set(F::C0_IS_NONE, source.flags.contains(F::C2_IS_NONE));
266 }
267
268 if matches!(source.color_space, S::Hsl | S::Lch | S::Oklch)
270 && matches!(self.color_space, S::Hsl | S::Lch | S::Oklch)
271 {
272 self.flags
273 .set(F::C1_IS_NONE, source.flags.contains(F::C1_IS_NONE));
274 }
275
276 if matches!(source.color_space, S::Hsl | S::Hwb) {
278 if matches!(self.color_space, S::Hsl | S::Hwb) {
279 self.flags
280 .set(F::C0_IS_NONE, source.flags.contains(F::C0_IS_NONE));
281 } else if matches!(self.color_space, S::Lch | S::Oklch) {
282 self.flags
283 .set(F::C2_IS_NONE, source.flags.contains(F::C0_IS_NONE));
284 }
285 } else if matches!(source.color_space, S::Lch | S::Oklch) {
286 if matches!(self.color_space, S::Hsl | S::Hwb) {
287 self.flags
288 .set(F::C0_IS_NONE, source.flags.contains(F::C2_IS_NONE));
289 } else if matches!(self.color_space, S::Lch | S::Oklch) {
290 self.flags
291 .set(F::C2_IS_NONE, source.flags.contains(F::C2_IS_NONE));
292 }
293 }
294
295 if matches!(source.color_space, S::Lab | S::Oklab)
298 && matches!(self.color_space, S::Lab | S::Oklab)
299 {
300 self.flags
301 .set(F::C1_IS_NONE, source.flags.contains(F::C1_IS_NONE));
302 self.flags
303 .set(F::C2_IS_NONE, source.flags.contains(F::C2_IS_NONE));
304 }
305 }
306}
307
308fn mix_in(
309 color_space: ColorSpace,
310 left_color: &AbsoluteColor,
311 left_weight: f32,
312 right_color: &AbsoluteColor,
313 right_weight: f32,
314 hue_interpolation: HueInterpolationMethod,
315 alpha_multiplier: f32,
316) -> AbsoluteColor {
317 let mut left = left_color.to_color_space(color_space);
319 left.carry_forward_analogous_missing_components(&left_color);
320 let mut right = right_color.to_color_space(color_space);
321 right.carry_forward_analogous_missing_components(&right_color);
322
323 let outcomes = [
324 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
325 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE),
326 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE),
327 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE),
328 ];
329
330 let left = left.raw_components();
332 let right = right.raw_components();
333
334 let (result, result_flags) = interpolate_premultiplied(
335 &left,
336 left_weight,
337 &right,
338 right_weight,
339 color_space.hue_index(),
340 hue_interpolation,
341 &outcomes,
342 );
343
344 let alpha = if alpha_multiplier != 1.0 {
345 result[3] * alpha_multiplier
346 } else {
347 result[3]
348 };
349
350 let alpha = (alpha * 1000.0).round() / 1000.0;
356
357 let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha);
358
359 result.flags = result_flags;
360
361 result
362}
363
364fn interpolate_premultiplied_component(
365 left: f32,
366 left_weight: f32,
367 left_alpha: f32,
368 right: f32,
369 right_weight: f32,
370 right_alpha: f32,
371) -> f32 {
372 left * left_weight * left_alpha + right * right_weight * right_alpha
373}
374
375#[inline]
377fn normalize_hue(v: f32) -> f32 {
378 v - 360. * (v / 360.).floor()
379}
380
381fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
382 if left.is_nan() {
388 if right.is_nan() {
389 *left = 0.;
390 *right = 0.;
391 } else {
392 *left = *right;
393 }
394 } else if right.is_nan() {
395 *right = *left;
396 }
397
398 if hue_interpolation == HueInterpolationMethod::Specified {
399 return;
402 }
403
404 *left = normalize_hue(*left);
405 *right = normalize_hue(*right);
406
407 match hue_interpolation {
408 HueInterpolationMethod::Shorter => {
410 let delta = *right - *left;
411
412 if delta > 180. {
413 *left += 360.;
414 } else if delta < -180. {
415 *right += 360.;
416 }
417 },
418 HueInterpolationMethod::Longer => {
420 let delta = *right - *left;
421 if 0. < delta && delta < 180. {
422 *left += 360.;
423 } else if -180. < delta && delta <= 0. {
424 *right += 360.;
425 }
426 },
427 HueInterpolationMethod::Increasing => {
429 if *right < *left {
430 *right += 360.;
431 }
432 },
433 HueInterpolationMethod::Decreasing => {
435 if *left < *right {
436 *left += 360.;
437 }
438 },
439 HueInterpolationMethod::Specified => unreachable!("Handled above"),
440 }
441}
442
443fn interpolate_hue(
444 mut left: f32,
445 left_weight: f32,
446 mut right: f32,
447 right_weight: f32,
448 hue_interpolation: HueInterpolationMethod,
449) -> f32 {
450 adjust_hue(&mut left, &mut right, hue_interpolation);
451 left * left_weight + right * right_weight
452}
453
454struct InterpolatedAlpha {
455 left: f32,
457 right: f32,
459 interpolated: f32,
461 is_none: bool,
463}
464
465fn interpolate_alpha(
466 left: f32,
467 left_weight: f32,
468 right: f32,
469 right_weight: f32,
470 outcome: ComponentMixOutcome,
471) -> InterpolatedAlpha {
472 let mut result = match outcome {
474 ComponentMixOutcome::Mix => {
475 let interpolated = left * left_weight + right * right_weight;
476 InterpolatedAlpha {
477 left,
478 right,
479 interpolated,
480 is_none: false,
481 }
482 },
483 ComponentMixOutcome::UseLeft => InterpolatedAlpha {
484 left,
485 right: left,
486 interpolated: left,
487 is_none: false,
488 },
489 ComponentMixOutcome::UseRight => InterpolatedAlpha {
490 left: right,
491 right,
492 interpolated: right,
493 is_none: false,
494 },
495 ComponentMixOutcome::None => InterpolatedAlpha {
496 left: 1.0,
497 right: 1.0,
498 interpolated: 0.0,
499 is_none: true,
500 },
501 };
502
503 result.left = result.left.clamp(0.0, 1.0);
505 result.right = result.right.clamp(0.0, 1.0);
506 result.interpolated = result.interpolated.clamp(0.0, 1.0);
507
508 result
509}
510
511fn interpolate_premultiplied(
512 left: &[f32; 4],
513 left_weight: f32,
514 right: &[f32; 4],
515 right_weight: f32,
516 hue_index: Option<usize>,
517 hue_interpolation: HueInterpolationMethod,
518 outcomes: &[ComponentMixOutcome; 4],
519) -> ([f32; 4], ColorFlags) {
520 let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]);
521 let mut flags = if alpha.is_none {
522 ColorFlags::ALPHA_IS_NONE
523 } else {
524 ColorFlags::empty()
525 };
526
527 let mut result = [0.; 4];
528
529 for i in 0..3 {
530 match outcomes[i] {
531 ComponentMixOutcome::Mix => {
532 let is_hue = hue_index == Some(i);
533 result[i] = if is_hue {
534 normalize_hue(interpolate_hue(
535 left[i],
536 left_weight,
537 right[i],
538 right_weight,
539 hue_interpolation,
540 ))
541 } else {
542 let interpolated = interpolate_premultiplied_component(
543 left[i],
544 left_weight,
545 alpha.left,
546 right[i],
547 right_weight,
548 alpha.right,
549 );
550
551 if alpha.interpolated == 0.0 {
552 interpolated
553 } else {
554 interpolated / alpha.interpolated
555 }
556 };
557 },
558 ComponentMixOutcome::UseLeft | ComponentMixOutcome::UseRight => {
559 let used_component = if outcomes[i] == ComponentMixOutcome::UseLeft {
560 left[i]
561 } else {
562 right[i]
563 };
564 result[i] = if hue_interpolation == HueInterpolationMethod::Longer
565 && hue_index == Some(i)
566 {
567 normalize_hue(interpolate_hue(
572 used_component,
573 left_weight,
574 used_component,
575 right_weight,
576 hue_interpolation,
577 ))
578 } else {
579 used_component
580 };
581 },
582 ComponentMixOutcome::None => {
583 result[i] = 0.0;
584 match i {
585 0 => flags.insert(ColorFlags::C0_IS_NONE),
586 1 => flags.insert(ColorFlags::C1_IS_NONE),
587 2 => flags.insert(ColorFlags::C2_IS_NONE),
588 _ => unreachable!(),
589 }
590 },
591 }
592 }
593 result[3] = alpha.interpolated;
594
595 (result, flags)
596}