mix.rs (18692B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 //! Color mixing/interpolation. 6 7 use super::{AbsoluteColor, ColorFlags, ColorSpace}; 8 use crate::derives::*; 9 use crate::parser::{Parse, ParserContext}; 10 use crate::values::generics::color::ColorMixFlags; 11 use cssparser::Parser; 12 use std::fmt::{self, Write}; 13 use style_traits::{CssWriter, ParseError, ToCss}; 14 15 /// A hue-interpolation-method as defined in [1]. 16 /// 17 /// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method 18 #[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)] 33 pub enum HueInterpolationMethod { 34 /// https://drafts.csswg.org/css-color-4/#shorter 35 Shorter, 36 /// https://drafts.csswg.org/css-color-4/#longer 37 Longer, 38 /// https://drafts.csswg.org/css-color-4/#increasing 39 Increasing, 40 /// https://drafts.csswg.org/css-color-4/#decreasing 41 Decreasing, 42 /// https://drafts.csswg.org/css-color-4/#specified 43 Specified, 44 } 45 46 /// https://drafts.csswg.org/css-color-4/#color-interpolation-method 47 #[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)] 60 pub struct ColorInterpolationMethod { 61 /// The color-space the interpolation should be done in. 62 pub space: ColorSpace, 63 /// The hue interpolation method. 64 pub hue: HueInterpolationMethod, 65 } 66 67 impl ColorInterpolationMethod { 68 /// Returns the srgb interpolation method. 69 pub const fn srgb() -> Self { 70 Self { 71 space: ColorSpace::Srgb, 72 hue: HueInterpolationMethod::Shorter, 73 } 74 } 75 76 /// Return the oklab interpolation method used for default color 77 /// interpolcation. 78 pub const fn oklab() -> Self { 79 Self { 80 space: ColorSpace::Oklab, 81 hue: HueInterpolationMethod::Shorter, 82 } 83 } 84 85 /// Return true if the this is the default method. 86 pub fn is_default(&self) -> bool { 87 self.space == ColorSpace::Oklab 88 } 89 90 /// Decides the best method for interpolating between the given colors. 91 /// https://drafts.csswg.org/css-color-4/#interpolation-space 92 pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self { 93 // The default color space to use for interpolation is Oklab. However, 94 // if either of the colors are in legacy rgb(), hsl() or hwb(), then 95 // interpolation is done in sRGB. 96 if !left.is_legacy_syntax() || !right.is_legacy_syntax() { 97 Self::default() 98 } else { 99 Self::srgb() 100 } 101 } 102 } 103 104 impl Default for ColorInterpolationMethod { 105 fn default() -> Self { 106 Self::oklab() 107 } 108 } 109 110 impl 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 // https://drafts.csswg.org/css-color-4/#hue-interpolation 118 // Unless otherwise specified, if no specific hue interpolation 119 // algorithm is selected by the host syntax, the default is shorter. 120 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 135 impl 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 151 /// Mix two colors into one. 152 pub 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 // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm 161 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 the result *MUST* be in modern syntax, then make sure it is in a 186 // color space that allows the modern syntax. So hsl and hwb will be 187 // converted to srgb. 188 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 // If both sides of the mix is legacy then convert the result back into 195 // legacy. 196 result.into_srgb_legacy() 197 } else { 198 result 199 } 200 } 201 202 /// What the outcome of each component should be in a mix result. 203 #[derive(Clone, Copy, PartialEq)] 204 #[repr(u8)] 205 enum ComponentMixOutcome { 206 /// Mix the left and right sides to give the result. 207 Mix, 208 /// Carry the left side forward to the result. 209 UseLeft, 210 /// Carry the right side forward to the result. 211 UseRight, 212 /// The resulting component should also be none. 213 None, 214 } 215 216 impl 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 234 impl AbsoluteColor { 235 /// Calculate the flags that should be carried forward a color before converting 236 /// it to the interpolation color space according to: 237 /// <https://drafts.csswg.org/css-color-4/#interpolation-missing> 238 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 // Reds r, x 247 // Greens g, y 248 // Blues b, z 249 if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() { 250 return; 251 } 252 253 // Lightness L 254 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 // Colorfulness C, S 271 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 // Hue H 278 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 // Opponent a, a 297 // Opponent b, b 298 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 307 fn 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 // Convert both colors into the interpolation color space. 317 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 // Convert both sides into just components. 330 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 // FIXME: In rare cases we end up with 0.999995 in the alpha channel, 350 // so we reduce the precision to avoid serializing to 351 // rgba(?, ?, ?, 1). This is not ideal, so we should look into 352 // ways to avoid it. Maybe pre-multiply all color components and 353 // then divide after calculations? 354 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 363 fn 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 // Normalize hue into [0, 360) 375 #[inline] 376 fn normalize_hue(v: f32) -> f32 { 377 v - 360. * (v / 360.).floor() 378 } 379 380 fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { 381 // Adjust the hue angle as per 382 // https://drafts.csswg.org/css-color/#hue-interpolation. 383 // 384 // If both hue angles are NAN, they should be set to 0. Otherwise, if a 385 // single hue angle is NAN, it should use the other hue angle. 386 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 // Angles are not adjusted. They are interpolated like any other 399 // component. 400 return; 401 } 402 403 *left = normalize_hue(*left); 404 *right = normalize_hue(*right); 405 406 match hue_interpolation { 407 // https://drafts.csswg.org/css-color/#shorter 408 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 // https://drafts.csswg.org/css-color/#longer 418 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 // https://drafts.csswg.org/css-color/#increasing 427 HueInterpolationMethod::Increasing => { 428 if *right < *left { 429 *right += 360.; 430 } 431 }, 432 // https://drafts.csswg.org/css-color/#decreasing 433 HueInterpolationMethod::Decreasing => { 434 if *left < *right { 435 *left += 360.; 436 } 437 }, 438 HueInterpolationMethod::Specified => unreachable!("Handled above"), 439 } 440 } 441 442 fn 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 453 struct InterpolatedAlpha { 454 /// The adjusted left alpha value. 455 left: f32, 456 /// The adjusted right alpha value. 457 right: f32, 458 /// The interpolated alpha value. 459 interpolated: f32, 460 /// Whether the alpha component should be `none`. 461 is_none: bool, 462 } 463 464 fn interpolate_alpha( 465 left: f32, 466 left_weight: f32, 467 right: f32, 468 right_weight: f32, 469 outcome: ComponentMixOutcome, 470 ) -> InterpolatedAlpha { 471 // <https://drafts.csswg.org/css-color-4/#interpolation-missing> 472 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 // Clip all alpha values to [0.0..1.0]. 503 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 510 fn 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 // If "longer hue" interpolation is required, we have to actually do 567 // the computation even if we're using the same value at both ends, 568 // so that interpolating from the starting hue back to the same value 569 // produces a full cycle, rather than a constant hue. 570 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 }