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