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 best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self {
87 if !left.is_legacy_syntax() || !right.is_legacy_syntax() {
91 Self::oklab()
92 } else {
93 Self::srgb()
94 }
95 }
96}
97
98impl Parse for ColorInterpolationMethod {
99 fn parse<'i, 't>(
100 _: &ParserContext,
101 input: &mut Parser<'i, 't>,
102 ) -> Result<Self, ParseError<'i>> {
103 input.expect_ident_matching("in")?;
104 let space = ColorSpace::parse(input)?;
105 let hue = if space.is_polar() {
109 input
110 .try_parse(|input| -> Result<_, ParseError<'i>> {
111 let hue = HueInterpolationMethod::parse(input)?;
112 input.expect_ident_matching("hue")?;
113 Ok(hue)
114 })
115 .unwrap_or(HueInterpolationMethod::Shorter)
116 } else {
117 HueInterpolationMethod::Shorter
118 };
119 Ok(Self { space, hue })
120 }
121}
122
123impl ToCss for ColorInterpolationMethod {
124 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
125 where
126 W: Write,
127 {
128 dest.write_str("in ")?;
129 self.space.to_css(dest)?;
130 if self.hue != HueInterpolationMethod::Shorter {
131 dest.write_char(' ')?;
132 self.hue.to_css(dest)?;
133 dest.write_str(" hue")?;
134 }
135 Ok(())
136 }
137}
138
139pub fn mix(
141 interpolation: ColorInterpolationMethod,
142 left_color: &AbsoluteColor,
143 mut left_weight: f32,
144 right_color: &AbsoluteColor,
145 mut right_weight: f32,
146 flags: ColorMixFlags,
147) -> AbsoluteColor {
148 let mut alpha_multiplier = 1.0;
150 if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) {
151 let sum = left_weight + right_weight;
152 if sum != 1.0 {
153 let scale = 1.0 / sum;
154 left_weight *= scale;
155 right_weight *= scale;
156 if sum < 1.0 {
157 alpha_multiplier = sum;
158 }
159 }
160 }
161
162 let result = mix_in(
163 interpolation.space,
164 left_color,
165 left_weight,
166 right_color,
167 right_weight,
168 interpolation.hue,
169 alpha_multiplier,
170 );
171
172 if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
173 if result.is_legacy_syntax() {
177 result.to_color_space(ColorSpace::Srgb)
178 } else {
179 result
180 }
181 } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() {
182 result.into_srgb_legacy()
185 } else {
186 result
187 }
188}
189
190#[derive(Clone, Copy, PartialEq)]
192#[repr(u8)]
193enum ComponentMixOutcome {
194 Mix,
196 UseLeft,
198 UseRight,
200 None,
202}
203
204impl ComponentMixOutcome {
205 fn from_colors(
206 left: &AbsoluteColor,
207 right: &AbsoluteColor,
208 flags_to_check: ColorFlags,
209 ) -> Self {
210 match (
211 left.flags.contains(flags_to_check),
212 right.flags.contains(flags_to_check),
213 ) {
214 (true, true) => Self::None,
215 (true, false) => Self::UseRight,
216 (false, true) => Self::UseLeft,
217 (false, false) => Self::Mix,
218 }
219 }
220}
221
222impl AbsoluteColor {
223 fn carry_forward_analogous_missing_components(&mut self, source: &AbsoluteColor) {
227 use ColorFlags as F;
228 use ColorSpace as S;
229
230 if source.color_space == self.color_space {
231 return;
232 }
233
234 if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() {
238 return;
239 }
240
241 if matches!(source.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
243 if matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
244 self.flags
245 .set(F::C0_IS_NONE, source.flags.contains(F::C0_IS_NONE));
246 } else if matches!(self.color_space, S::Hsl) {
247 self.flags
248 .set(F::C2_IS_NONE, source.flags.contains(F::C0_IS_NONE));
249 }
250 } else if matches!(source.color_space, S::Hsl)
251 && matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch)
252 {
253 self.flags
254 .set(F::C0_IS_NONE, source.flags.contains(F::C2_IS_NONE));
255 }
256
257 if matches!(source.color_space, S::Hsl | S::Lch | S::Oklch)
259 && matches!(self.color_space, S::Hsl | S::Lch | S::Oklch)
260 {
261 self.flags
262 .set(F::C1_IS_NONE, source.flags.contains(F::C1_IS_NONE));
263 }
264
265 if matches!(source.color_space, S::Hsl | S::Hwb) {
267 if matches!(self.color_space, S::Hsl | S::Hwb) {
268 self.flags
269 .set(F::C0_IS_NONE, source.flags.contains(F::C0_IS_NONE));
270 } else if matches!(self.color_space, S::Lch | S::Oklch) {
271 self.flags
272 .set(F::C2_IS_NONE, source.flags.contains(F::C0_IS_NONE));
273 }
274 } else if matches!(source.color_space, S::Lch | S::Oklch) {
275 if matches!(self.color_space, S::Hsl | S::Hwb) {
276 self.flags
277 .set(F::C0_IS_NONE, source.flags.contains(F::C2_IS_NONE));
278 } else if matches!(self.color_space, S::Lch | S::Oklch) {
279 self.flags
280 .set(F::C2_IS_NONE, source.flags.contains(F::C2_IS_NONE));
281 }
282 }
283
284 if matches!(source.color_space, S::Lab | S::Oklab)
287 && matches!(self.color_space, S::Lab | S::Oklab)
288 {
289 self.flags
290 .set(F::C1_IS_NONE, source.flags.contains(F::C1_IS_NONE));
291 self.flags
292 .set(F::C2_IS_NONE, source.flags.contains(F::C2_IS_NONE));
293 }
294 }
295}
296
297fn mix_in(
298 color_space: ColorSpace,
299 left_color: &AbsoluteColor,
300 left_weight: f32,
301 right_color: &AbsoluteColor,
302 right_weight: f32,
303 hue_interpolation: HueInterpolationMethod,
304 alpha_multiplier: f32,
305) -> AbsoluteColor {
306 let mut left = left_color.to_color_space(color_space);
308 left.carry_forward_analogous_missing_components(&left_color);
309 let mut right = right_color.to_color_space(color_space);
310 right.carry_forward_analogous_missing_components(&right_color);
311
312 let outcomes = [
313 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
314 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE),
315 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE),
316 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE),
317 ];
318
319 let left = left.raw_components();
321 let right = right.raw_components();
322
323 let (result, result_flags) = interpolate_premultiplied(
324 &left,
325 left_weight,
326 &right,
327 right_weight,
328 color_space.hue_index(),
329 hue_interpolation,
330 &outcomes,
331 );
332
333 let alpha = if alpha_multiplier != 1.0 {
334 result[3] * alpha_multiplier
335 } else {
336 result[3]
337 };
338
339 let alpha = (alpha * 1000.0).round() / 1000.0;
345
346 let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha);
347
348 result.flags = result_flags;
349
350 result
351}
352
353fn interpolate_premultiplied_component(
354 left: f32,
355 left_weight: f32,
356 left_alpha: f32,
357 right: f32,
358 right_weight: f32,
359 right_alpha: f32,
360) -> f32 {
361 left * left_weight * left_alpha + right * right_weight * right_alpha
362}
363
364#[inline]
366fn normalize_hue(v: f32) -> f32 {
367 v - 360. * (v / 360.).floor()
368}
369
370fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
371 if left.is_nan() {
377 if right.is_nan() {
378 *left = 0.;
379 *right = 0.;
380 } else {
381 *left = *right;
382 }
383 } else if right.is_nan() {
384 *right = *left;
385 }
386
387 if hue_interpolation == HueInterpolationMethod::Specified {
388 return;
391 }
392
393 *left = normalize_hue(*left);
394 *right = normalize_hue(*right);
395
396 match hue_interpolation {
397 HueInterpolationMethod::Shorter => {
399 let delta = *right - *left;
400
401 if delta > 180. {
402 *left += 360.;
403 } else if delta < -180. {
404 *right += 360.;
405 }
406 },
407 HueInterpolationMethod::Longer => {
409 let delta = *right - *left;
410 if 0. < delta && delta < 180. {
411 *left += 360.;
412 } else if -180. < delta && delta <= 0. {
413 *right += 360.;
414 }
415 },
416 HueInterpolationMethod::Increasing => {
418 if *right < *left {
419 *right += 360.;
420 }
421 },
422 HueInterpolationMethod::Decreasing => {
424 if *left < *right {
425 *left += 360.;
426 }
427 },
428 HueInterpolationMethod::Specified => unreachable!("Handled above"),
429 }
430}
431
432fn interpolate_hue(
433 mut left: f32,
434 left_weight: f32,
435 mut right: f32,
436 right_weight: f32,
437 hue_interpolation: HueInterpolationMethod,
438) -> f32 {
439 adjust_hue(&mut left, &mut right, hue_interpolation);
440 left * left_weight + right * right_weight
441}
442
443struct InterpolatedAlpha {
444 left: f32,
446 right: f32,
448 interpolated: f32,
450 is_none: bool,
452}
453
454fn interpolate_alpha(
455 left: f32,
456 left_weight: f32,
457 right: f32,
458 right_weight: f32,
459 outcome: ComponentMixOutcome,
460) -> InterpolatedAlpha {
461 let mut result = match outcome {
463 ComponentMixOutcome::Mix => {
464 let interpolated = left * left_weight + right * right_weight;
465 InterpolatedAlpha {
466 left,
467 right,
468 interpolated,
469 is_none: false,
470 }
471 },
472 ComponentMixOutcome::UseLeft => InterpolatedAlpha {
473 left,
474 right: left,
475 interpolated: left,
476 is_none: false,
477 },
478 ComponentMixOutcome::UseRight => InterpolatedAlpha {
479 left: right,
480 right,
481 interpolated: right,
482 is_none: false,
483 },
484 ComponentMixOutcome::None => InterpolatedAlpha {
485 left: 1.0,
486 right: 1.0,
487 interpolated: 0.0,
488 is_none: true,
489 },
490 };
491
492 result.left = result.left.clamp(0.0, 1.0);
494 result.right = result.right.clamp(0.0, 1.0);
495 result.interpolated = result.interpolated.clamp(0.0, 1.0);
496
497 result
498}
499
500fn interpolate_premultiplied(
501 left: &[f32; 4],
502 left_weight: f32,
503 right: &[f32; 4],
504 right_weight: f32,
505 hue_index: Option<usize>,
506 hue_interpolation: HueInterpolationMethod,
507 outcomes: &[ComponentMixOutcome; 4],
508) -> ([f32; 4], ColorFlags) {
509 let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]);
510 let mut flags = if alpha.is_none {
511 ColorFlags::ALPHA_IS_NONE
512 } else {
513 ColorFlags::empty()
514 };
515
516 let mut result = [0.; 4];
517
518 for i in 0..3 {
519 match outcomes[i] {
520 ComponentMixOutcome::Mix => {
521 let is_hue = hue_index == Some(i);
522 result[i] = if is_hue {
523 normalize_hue(interpolate_hue(
524 left[i],
525 left_weight,
526 right[i],
527 right_weight,
528 hue_interpolation,
529 ))
530 } else {
531 let interpolated = interpolate_premultiplied_component(
532 left[i],
533 left_weight,
534 alpha.left,
535 right[i],
536 right_weight,
537 alpha.right,
538 );
539
540 if alpha.interpolated == 0.0 {
541 interpolated
542 } else {
543 interpolated / alpha.interpolated
544 }
545 };
546 },
547 ComponentMixOutcome::UseLeft | ComponentMixOutcome::UseRight => {
548 let used_component = if outcomes[i] == ComponentMixOutcome::UseLeft {
549 left[i]
550 } else {
551 right[i]
552 };
553 result[i] = if hue_interpolation == HueInterpolationMethod::Longer
554 && hue_index == Some(i)
555 {
556 normalize_hue(interpolate_hue(
561 used_component,
562 left_weight,
563 used_component,
564 right_weight,
565 hue_interpolation,
566 ))
567 } else {
568 used_component
569 };
570 },
571 ComponentMixOutcome::None => {
572 result[i] = 0.0;
573 match i {
574 0 => flags.insert(ColorFlags::C0_IS_NONE),
575 1 => flags.insert(ColorFlags::C1_IS_NONE),
576 2 => flags.insert(ColorFlags::C2_IS_NONE),
577 _ => unreachable!(),
578 }
579 },
580 }
581 }
582 result[3] = alpha.interpolated;
583
584 (result, flags)
585}