1use super::{AbsoluteColor, ColorFlags, ColorSpace};
8use crate::color::ColorMixItemList;
9use crate::derives::*;
10use crate::parser::{Parse, ParserContext};
11use crate::values::generics::color::ColorMixFlags;
12use cssparser::Parser;
13use std::fmt::{self, Write};
14use style_traits::{CssWriter, ParseError, ToCss};
15
16#[derive(
20 Clone,
21 Copy,
22 Debug,
23 Eq,
24 MallocSizeOf,
25 Parse,
26 PartialEq,
27 ToAnimatedValue,
28 ToComputedValue,
29 ToCss,
30 ToResolvedValue,
31 ToShmem,
32)]
33#[repr(u8)]
34pub enum HueInterpolationMethod {
35 Shorter,
37 Longer,
39 Increasing,
41 Decreasing,
43 Specified,
45}
46
47#[derive(
49 Clone,
50 Copy,
51 Debug,
52 Eq,
53 MallocSizeOf,
54 PartialEq,
55 ToShmem,
56 ToAnimatedValue,
57 ToComputedValue,
58 ToResolvedValue,
59)]
60#[repr(C)]
61pub struct ColorInterpolationMethod {
62 pub space: ColorSpace,
64 pub hue: HueInterpolationMethod,
66}
67
68impl ColorInterpolationMethod {
69 pub const fn srgb() -> Self {
71 Self {
72 space: ColorSpace::Srgb,
73 hue: HueInterpolationMethod::Shorter,
74 }
75 }
76
77 pub const fn oklab() -> Self {
80 Self {
81 space: ColorSpace::Oklab,
82 hue: HueInterpolationMethod::Shorter,
83 }
84 }
85
86 pub fn is_default(&self) -> bool {
88 self.space == ColorSpace::Oklab
89 }
90
91 pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self {
94 if !left.is_legacy_syntax() || !right.is_legacy_syntax() {
98 Self::default()
99 } else {
100 Self::srgb()
101 }
102 }
103}
104
105impl Default for ColorInterpolationMethod {
106 fn default() -> Self {
107 Self::oklab()
108 }
109}
110
111impl Parse for ColorInterpolationMethod {
112 fn parse<'i, 't>(
113 _: &ParserContext,
114 input: &mut Parser<'i, 't>,
115 ) -> Result<Self, ParseError<'i>> {
116 input.expect_ident_matching("in")?;
117 let space = ColorSpace::parse(input)?;
118 let hue = if space.is_polar() {
122 input
123 .try_parse(|input| -> Result<_, ParseError<'i>> {
124 let hue = HueInterpolationMethod::parse(input)?;
125 input.expect_ident_matching("hue")?;
126 Ok(hue)
127 })
128 .unwrap_or(HueInterpolationMethod::Shorter)
129 } else {
130 HueInterpolationMethod::Shorter
131 };
132 Ok(Self { space, hue })
133 }
134}
135
136impl ToCss for ColorInterpolationMethod {
137 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
138 where
139 W: Write,
140 {
141 dest.write_str("in ")?;
142 self.space.to_css(dest)?;
143 if self.hue != HueInterpolationMethod::Shorter {
144 dest.write_char(' ')?;
145 self.hue.to_css(dest)?;
146 dest.write_str(" hue")?;
147 }
148 Ok(())
149 }
150}
151
152pub struct ColorMixItem {
154 pub color: AbsoluteColor,
156 pub weight: f32,
158}
159
160impl ColorMixItem {
161 #[inline]
163 pub fn new(color: AbsoluteColor, weight: f32) -> Self {
164 Self { color, weight }
165 }
166}
167
168pub fn mix_many(
170 interpolation: ColorInterpolationMethod,
171 items: impl IntoIterator<Item = ColorMixItem>,
172 flags: ColorMixFlags,
173) -> AbsoluteColor {
174 let items = items.into_iter().collect::<ColorMixItemList<_>>();
175
176 if items.is_empty() {
178 return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space);
179 }
180
181 let normalize = flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS);
182 let mut weight_scale = 1.0;
183 let mut alpha_multiplier = 1.0;
184 if normalize {
185 let sum: f32 = items.iter().map(|item| item.weight).sum();
187 if sum == 0.0 {
188 return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space);
189 }
190 if (sum - 1.0).abs() > f32::EPSILON {
191 weight_scale = 1.0 / sum;
192 if sum < 1.0 {
193 alpha_multiplier = sum;
194 }
195 }
196 }
197
198 let (first, rest) = items.split_first().unwrap();
200 let mut accumulated_color = convert_for_mix(&first.color, interpolation.space);
201 let mut accumulated_weight = first.weight * weight_scale;
202
203 for item in rest {
204 let weight = item.weight * weight_scale;
205 let combined = accumulated_weight + weight;
206 if combined == 0.0 {
207 continue;
209 }
210 let right = convert_for_mix(&item.color, interpolation.space);
211
212 let (left_weight, right_weight) = if normalize {
213 (accumulated_weight / combined, weight / combined)
214 } else {
215 (accumulated_weight, weight)
216 };
217
218 accumulated_color = mix_with_weights(
219 &accumulated_color,
220 left_weight,
221 &right,
222 right_weight,
223 interpolation.hue,
224 );
225 accumulated_weight = combined;
226 }
227
228 let components = accumulated_color.raw_components();
229 let alpha = components[3] * alpha_multiplier;
230
231 let alpha = (alpha.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
237
238 let mut result = AbsoluteColor::new(
239 interpolation.space,
240 components[0],
241 components[1],
242 components[2],
243 alpha,
244 );
245 result.flags = accumulated_color.flags;
246
247 if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
248 if result.is_legacy_syntax() {
252 result.to_color_space(ColorSpace::Srgb)
253 } else {
254 result
255 }
256 } else if items.iter().all(|item| item.color.is_legacy_syntax()) {
257 result.into_srgb_legacy()
260 } else {
261 result
262 }
263}
264
265#[derive(Clone, Copy, PartialEq)]
267#[repr(u8)]
268enum ComponentMixOutcome {
269 Mix,
271 UseLeft,
273 UseRight,
275 None,
277}
278
279impl ComponentMixOutcome {
280 fn from_colors(
281 left: &AbsoluteColor,
282 right: &AbsoluteColor,
283 flags_to_check: ColorFlags,
284 ) -> Self {
285 match (
286 left.flags.contains(flags_to_check),
287 right.flags.contains(flags_to_check),
288 ) {
289 (true, true) => Self::None,
290 (true, false) => Self::UseRight,
291 (false, true) => Self::UseLeft,
292 (false, false) => Self::Mix,
293 }
294 }
295}
296
297impl AbsoluteColor {
298 fn carry_forward_analogous_missing_components(&mut self, source: &AbsoluteColor) {
302 use ColorFlags as F;
303 use ColorSpace as S;
304
305 if source.color_space == self.color_space {
306 return;
307 }
308
309 if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() {
313 return;
314 }
315
316 if matches!(source.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
318 if matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch) {
319 self.flags |= source.flags & F::C0_IS_NONE;
320 } else if matches!(self.color_space, S::Hsl) {
321 if source.flags.contains(F::C0_IS_NONE) {
322 self.flags.insert(F::C2_IS_NONE)
323 }
324 }
325 } else if matches!(source.color_space, S::Hsl)
326 && matches!(self.color_space, S::Lab | S::Lch | S::Oklab | S::Oklch)
327 {
328 if source.flags.contains(F::C2_IS_NONE) {
329 self.flags.insert(F::C0_IS_NONE)
330 }
331 }
332
333 if matches!(source.color_space, S::Hsl | S::Lch | S::Oklch)
335 && matches!(self.color_space, S::Hsl | S::Lch | S::Oklch)
336 {
337 self.flags |= source.flags & F::C1_IS_NONE;
338 }
339
340 if matches!(source.color_space, S::Hsl | S::Hwb) {
342 if matches!(self.color_space, S::Hsl | S::Hwb) {
343 self.flags |= source.flags & F::C0_IS_NONE;
344 } else if matches!(self.color_space, S::Lch | S::Oklch) {
345 if source.flags.contains(F::C0_IS_NONE) {
346 self.flags.insert(F::C2_IS_NONE)
347 }
348 }
349 } else if matches!(source.color_space, S::Lch | S::Oklch) {
350 if matches!(self.color_space, S::Hsl | S::Hwb) {
351 if source.flags.contains(F::C2_IS_NONE) {
352 self.flags.insert(F::C0_IS_NONE)
353 }
354 } else if matches!(self.color_space, S::Lch | S::Oklch) {
355 self.flags |= source.flags & F::C2_IS_NONE;
356 }
357 }
358
359 if matches!(source.color_space, S::Lab | S::Oklab)
362 && matches!(self.color_space, S::Lab | S::Oklab)
363 {
364 self.flags |= source.flags & F::C1_IS_NONE;
365 self.flags |= source.flags & F::C2_IS_NONE;
366 }
367 }
368}
369
370fn mix_with_weights(
372 left: &AbsoluteColor,
373 left_weight: f32,
374 right: &AbsoluteColor,
375 right_weight: f32,
376 hue_interpolation: HueInterpolationMethod,
377) -> AbsoluteColor {
378 debug_assert!(right.color_space == left.color_space);
379 let color_space = left.color_space;
380
381 let outcomes = [
382 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
383 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE),
384 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE),
385 ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE),
386 ];
387
388 let left = left.raw_components();
390 let right = right.raw_components();
391
392 let (result, result_flags) = interpolate_premultiplied(
393 &left,
394 left_weight,
395 &right,
396 right_weight,
397 color_space.hue_index(),
398 hue_interpolation,
399 &outcomes,
400 );
401
402 let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], result[3]);
403 result.flags = result_flags;
404 result
405}
406
407fn convert_for_mix(color: &AbsoluteColor, color_space: ColorSpace) -> AbsoluteColor {
408 let mut converted = color.to_color_space(color_space);
409 converted.carry_forward_analogous_missing_components(color);
410 converted
411}
412
413fn interpolate_premultiplied_component(
414 left: f32,
415 left_weight: f32,
416 left_alpha: f32,
417 right: f32,
418 right_weight: f32,
419 right_alpha: f32,
420) -> f32 {
421 left * left_weight * left_alpha + right * right_weight * right_alpha
422}
423
424#[inline]
426fn normalize_hue(v: f32) -> f32 {
427 v - 360. * (v / 360.).floor()
428}
429
430fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
431 if left.is_nan() {
437 if right.is_nan() {
438 *left = 0.;
439 *right = 0.;
440 } else {
441 *left = *right;
442 }
443 } else if right.is_nan() {
444 *right = *left;
445 }
446
447 if hue_interpolation == HueInterpolationMethod::Specified {
448 return;
451 }
452
453 *left = normalize_hue(*left);
454 *right = normalize_hue(*right);
455
456 match hue_interpolation {
457 HueInterpolationMethod::Shorter => {
459 let delta = *right - *left;
460
461 if delta > 180. {
462 *left += 360.;
463 } else if delta < -180. {
464 *right += 360.;
465 }
466 },
467 HueInterpolationMethod::Longer => {
469 let delta = *right - *left;
470 if 0. < delta && delta < 180. {
471 *left += 360.;
472 } else if -180. < delta && delta <= 0. {
473 *right += 360.;
474 }
475 },
476 HueInterpolationMethod::Increasing => {
478 if *right < *left {
479 *right += 360.;
480 }
481 },
482 HueInterpolationMethod::Decreasing => {
484 if *left < *right {
485 *left += 360.;
486 }
487 },
488 HueInterpolationMethod::Specified => unreachable!("Handled above"),
489 }
490}
491
492fn interpolate_hue(
493 mut left: f32,
494 left_weight: f32,
495 mut right: f32,
496 right_weight: f32,
497 hue_interpolation: HueInterpolationMethod,
498) -> f32 {
499 adjust_hue(&mut left, &mut right, hue_interpolation);
500 left * left_weight + right * right_weight
501}
502
503struct InterpolatedAlpha {
504 left: f32,
506 right: f32,
508 interpolated: f32,
510 is_none: bool,
512}
513
514fn interpolate_alpha(
515 left: f32,
516 left_weight: f32,
517 right: f32,
518 right_weight: f32,
519 outcome: ComponentMixOutcome,
520) -> InterpolatedAlpha {
521 let mut result = match outcome {
523 ComponentMixOutcome::Mix => {
524 let interpolated = left * left_weight + right * right_weight;
525 InterpolatedAlpha {
526 left,
527 right,
528 interpolated,
529 is_none: false,
530 }
531 },
532 ComponentMixOutcome::UseLeft => InterpolatedAlpha {
533 left,
534 right: left,
535 interpolated: left,
536 is_none: false,
537 },
538 ComponentMixOutcome::UseRight => InterpolatedAlpha {
539 left: right,
540 right,
541 interpolated: right,
542 is_none: false,
543 },
544 ComponentMixOutcome::None => InterpolatedAlpha {
545 left: 1.0,
546 right: 1.0,
547 interpolated: 0.0,
548 is_none: true,
549 },
550 };
551
552 result.left = result.left.clamp(0.0, 1.0);
554 result.right = result.right.clamp(0.0, 1.0);
555 result.interpolated = result.interpolated.clamp(0.0, 1.0);
556
557 result
558}
559
560fn interpolate_premultiplied(
561 left: &[f32; 4],
562 left_weight: f32,
563 right: &[f32; 4],
564 right_weight: f32,
565 hue_index: Option<usize>,
566 hue_interpolation: HueInterpolationMethod,
567 outcomes: &[ComponentMixOutcome; 4],
568) -> ([f32; 4], ColorFlags) {
569 let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]);
570 let mut flags = if alpha.is_none {
571 ColorFlags::ALPHA_IS_NONE
572 } else {
573 ColorFlags::empty()
574 };
575
576 let mut result = [0.; 4];
577
578 for i in 0..3 {
579 match outcomes[i] {
580 ComponentMixOutcome::Mix => {
581 let is_hue = hue_index == Some(i);
582 result[i] = if is_hue {
583 normalize_hue(interpolate_hue(
584 left[i],
585 left_weight,
586 right[i],
587 right_weight,
588 hue_interpolation,
589 ))
590 } else {
591 let interpolated = interpolate_premultiplied_component(
592 left[i],
593 left_weight,
594 alpha.left,
595 right[i],
596 right_weight,
597 alpha.right,
598 );
599
600 if alpha.interpolated == 0.0 {
601 interpolated
602 } else {
603 interpolated / alpha.interpolated
604 }
605 };
606 },
607 ComponentMixOutcome::UseLeft | ComponentMixOutcome::UseRight => {
608 let used_component = if outcomes[i] == ComponentMixOutcome::UseLeft {
609 left[i]
610 } else {
611 right[i]
612 };
613 result[i] = if hue_interpolation == HueInterpolationMethod::Longer
614 && hue_index == Some(i)
615 {
616 normalize_hue(interpolate_hue(
621 used_component,
622 left_weight,
623 used_component,
624 right_weight,
625 hue_interpolation,
626 ))
627 } else {
628 used_component
629 };
630 },
631 ComponentMixOutcome::None => {
632 result[i] = 0.0;
633 match i {
634 0 => flags.insert(ColorFlags::C0_IS_NONE),
635 1 => flags.insert(ColorFlags::C1_IS_NONE),
636 2 => flags.insert(ColorFlags::C2_IS_NONE),
637 _ => unreachable!(),
638 }
639 },
640 }
641 }
642 result[3] = alpha.interpolated;
643
644 (result, flags)
645}