/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Color mixing/interpolation.
usesuper::{AbsoluteColor, ColorFlags, ColorSpace}; usecrate::color::ColorMixItemList; usecrate::derives::*; usecrate::parser::{Parse, ParserContext}; usecrate::values::generics::color::ColorMixFlags; use cssparser::Parser; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, ToCss};
/// Return the oklab interpolation method used for default color /// interpolcation. pubconstfn oklab() -> Self { Self {
space: ColorSpace::Oklab,
hue: HueInterpolationMethod::Shorter,
}
}
/// Return true if the this is the default method. pubfn is_default(&self) -> bool { self.space == ColorSpace::Oklab
}
/// Decides the best method for interpolating between the given colors. /// https://drafts.csswg.org/css-color-4/#interpolation-space pubfn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self { // The default color space to use for interpolation is Oklab. However, // if either of the colors are in legacy rgb(), hsl() or hwb(), then // interpolation is done in sRGB. if !left.is_legacy_syntax() || !right.is_legacy_syntax() { Self::default()
} else { Self::srgb()
}
}
}
/// A color and its weight for use in a color mix. pubstruct ColorMixItem { /// The color being mixed. pub color: AbsoluteColor, /// How much this color contributes to the final mix. pub weight: f32,
}
impl ColorMixItem { /// Create a new color item for mixing. #[inline] pubfn new(color: AbsoluteColor, weight: f32) -> Self { Self { color, weight }
}
}
/// Mix N colors into one (left-to-right fold). pubfn mix_many(
interpolation: ColorInterpolationMethod,
items: impl IntoIterator<Item = ColorMixItem>,
flags: ColorMixFlags,
) -> AbsoluteColor { let items = items.into_iter().collect::<ColorMixItemList<_>>();
// Match the behavior when the sum of weights equal 0. if items.is_empty() { return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space);
}
let normalize = flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS); letmut weight_scale = 1.0; letmut alpha_multiplier = 1.0; if normalize { // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm let sum: f32 = items.iter().map(|item| item.weight).sum(); if sum == 0.0 { return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space);
} if (sum - 1.0).abs() > f32::EPSILON {
weight_scale = 1.0 / sum; if sum < 1.0 {
alpha_multiplier = sum;
}
}
}
// We can unwrap here, because we already checked for no items. let (first, rest) = items.split_first().unwrap(); letmut accumulated_color = convert_for_mix(&first.color, interpolation.space); letmut accumulated_weight = first.weight * weight_scale;
for item in rest { let weight = item.weight * weight_scale; let combined = accumulated_weight + weight; if combined == 0.0 { // If both are 0, this fold doesn't contribute anything to the result. continue;
} let right = convert_for_mix(&item.color, interpolation.space);
let (left_weight, right_weight) = if normalize {
(accumulated_weight / combined, weight / combined)
} else {
(accumulated_weight, weight)
};
let components = accumulated_color.raw_components(); let alpha = components[3] * alpha_multiplier;
// FIXME: In rare cases we end up with 0.999995 in the alpha channel, // so we reduce the precision to avoid serializing to // rgba(?, ?, ?, 1). This is not ideal, so we should look into // ways to avoid it. Maybe pre-multiply all color components and // then divide after calculations? let alpha = (alpha.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { // If the result *MUST* be in modern syntax, then make sure it is in a // color space that allows the modern syntax. So hsl and hwb will be // converted to srgb. if result.is_legacy_syntax() {
result.to_color_space(ColorSpace::Srgb)
} else {
result
}
} elseif items.iter().all(|item| item.color.is_legacy_syntax()) { // If both sides of the mix is legacy then convert the result back into // legacy.
result.into_srgb_legacy()
} else {
result
}
}
/// What the outcome of each component should be in a mix result. #[derive(Clone, Copy, PartialEq)] #[repr(u8)] enum ComponentMixOutcome { /// Mix the left and right sides to give the result.
Mix, /// Carry the left side forward to the result.
UseLeft, /// Carry the right side forward to the result.
UseRight, /// The resulting component should also be none.
None,
}
impl AbsoluteColor { /// Calculate the flags that should be carried forward a color before converting /// it to the interpolation color space according to: /// <https://drafts.csswg.org/css-color-4/#interpolation-missing> fn carry_forward_analogous_missing_components(&mutself, source: &AbsoluteColor) { use ColorFlags as F; use ColorSpace as S;
if source.color_space == self.color_space { return;
}
// Reds r, x // Greens g, y // Blues b, z if source.color_space.is_rgb_or_xyz_like() && self.color_space.is_rgb_or_xyz_like() { return;
}
// Normalize hue into [0, 360) #[inline] fn normalize_hue(v: f32) -> f32 {
v - 360. * (v / 360.).floor()
}
fn adjust_hue(left: &mut f32, right: &mutf32, hue_interpolation: HueInterpolationMethod) { // Adjust the hue angle as per // https://drafts.csswg.org/css-color/#hue-interpolation. // // If both hue angles are NAN, they should be set to 0. Otherwise, if a // single hue angle is NAN, it should use the other hue angle. if left.is_nan() { if right.is_nan() {
*left = 0.;
*right = 0.;
} else {
*left = *right;
}
} elseif right.is_nan() {
*right = *left;
}
if hue_interpolation == HueInterpolationMethod::Specified { // Angles are not adjusted. They are interpolated like any other // component. return;
}
struct InterpolatedAlpha { /// The adjusted left alpha value.
left: f32, /// The adjusted right alpha value.
right: f32, /// The interpolated alpha value.
interpolated: f32, /// Whether the alpha component should be `none`.
is_none: bool,
}
for i in0..3 { match outcomes[i] {
ComponentMixOutcome::Mix => { let is_hue = hue_index == Some(i);
result[i] = if is_hue {
normalize_hue(interpolate_hue(
left[i],
left_weight,
right[i],
right_weight,
hue_interpolation,
))
} else { let interpolated = interpolate_premultiplied_component(
left[i],
left_weight,
alpha.left,
right[i],
right_weight,
alpha.right,
);
if alpha.interpolated == 0.0 {
interpolated
} else {
interpolated / alpha.interpolated
}
};
},
ComponentMixOutcome::UseLeft | ComponentMixOutcome::UseRight => { let used_component = if outcomes[i] == ComponentMixOutcome::UseLeft {
left[i]
} else {
right[i]
};
result[i] = if hue_interpolation == HueInterpolationMethod::Longer
&& hue_index == Some(i)
{ // If "longer hue" interpolation is required, we have to actually do // the computation even if we're using the same value at both ends, // so that interpolating from the starting hue back to the same value // produces a full cycle, rather than a constant hue.
normalize_hue(interpolate_hue(
used_component,
left_weight,
used_component,
right_weight,
hue_interpolation,
))
} else {
used_component
};
},
ComponentMixOutcome::None => {
result[i] = 0.0; match i { 0 => flags.insert(ColorFlags::C0_IS_NONE), 1 => flags.insert(ColorFlags::C1_IS_NONE), 2 => flags.insert(ColorFlags::C2_IS_NONE),
_ => unreachable!(),
}
},
}
}
result[3] = alpha.interpolated;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.