Skip to content

Commit a0842a4

Browse files
committed
Bug 2007772 - Add support for mixing multiple colors r=layout-reviewers,firefox-style-system-reviewers,dshin
Mixing is not done by the exhanced mixing algorithm specified in: https://drafts.csswg.org/css-color-5/#color-mix This patch only implements the algorithm and forwards the original 2 color function to it, yielding the exact results as before. Differential Revision: https://phabricator.services.mozilla.com/D278115
1 parent 08b0f82 commit a0842a4

File tree

2 files changed

+118
-43
lines changed

2 files changed

+118
-43
lines changed

servo/components/style/color/mix.rs

Lines changed: 112 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
//! Color mixing/interpolation.
66
77
use super::{AbsoluteColor, ColorFlags, ColorSpace};
8+
use crate::color::ColorMixItemList;
89
use crate::derives::*;
910
use crate::parser::{Parse, ParserContext};
1011
use crate::values::generics::color::ColorMixFlags;
1112
use cssparser::Parser;
13+
use smallvec::SmallVec;
1214
use std::fmt::{self, Write};
1315
use style_traits::{CssWriter, ParseError, ToCss};
1416

@@ -148,38 +150,118 @@ impl ToCss for ColorInterpolationMethod {
148150
}
149151
}
150152

153+
/// A color and its weight for use in a color mix.
154+
pub struct ColorMixItem {
155+
/// The color being mixed.
156+
pub color: AbsoluteColor,
157+
/// How much this color contributes to the final mix.
158+
pub weight: f32,
159+
}
160+
161+
impl ColorMixItem {
162+
/// Create a new color item for mixing.
163+
#[inline]
164+
pub fn new(color: AbsoluteColor, weight: f32) -> Self {
165+
Self { color, weight }
166+
}
167+
}
168+
151169
/// Mix two colors into one.
170+
#[inline]
152171
pub fn mix(
153172
interpolation: ColorInterpolationMethod,
154173
left_color: &AbsoluteColor,
155-
mut left_weight: f32,
174+
left_weight: f32,
156175
right_color: &AbsoluteColor,
157-
mut right_weight: f32,
176+
right_weight: f32,
158177
flags: ColorMixFlags,
159178
) -> AbsoluteColor {
160-
// https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
179+
let items = [
180+
ColorMixItem::new(*left_color, left_weight),
181+
ColorMixItem::new(*right_color, right_weight),
182+
];
183+
184+
mix_many(interpolation, items, flags)
185+
}
186+
187+
/// Mix N colors into one (left-to-right fold).
188+
pub fn mix_many(
189+
interpolation: ColorInterpolationMethod,
190+
items: impl IntoIterator<Item = ColorMixItem>,
191+
flags: ColorMixFlags,
192+
) -> AbsoluteColor {
193+
let items = items.into_iter().collect::<ColorMixItemList<_>>();
194+
195+
// Match the behavior when the sum of weights equal 0.
196+
if items.is_empty() {
197+
return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space);
198+
}
199+
200+
let normalize = flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS);
201+
let mut weight_scale = 1.0;
161202
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;
203+
if normalize {
204+
// https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
205+
let sum: f32 = items.iter().map(|item| item.weight).sum();
206+
if sum == 0.0 {
207+
return AbsoluteColor::TRANSPARENT_BLACK.to_color_space(interpolation.space);
208+
}
209+
if (sum - 1.0).abs() > f32::EPSILON {
210+
weight_scale = 1.0 / sum;
168211
if sum < 1.0 {
169212
alpha_multiplier = sum;
170213
}
171214
}
172215
}
173216

174-
let result = mix_in(
217+
// We can unwrap here, because we already checked for no items.
218+
let (first, rest) = items.split_first().unwrap();
219+
let mut accumulated_color = convert_for_mix(&first.color, interpolation.space);
220+
let mut accumulated_weight = first.weight * weight_scale;
221+
222+
for item in rest {
223+
let weight = item.weight * weight_scale;
224+
let combined = accumulated_weight + weight;
225+
if combined == 0.0 {
226+
// If both are 0, this fold doesn't contribute anything to the result.
227+
continue;
228+
}
229+
let right = convert_for_mix(&item.color, interpolation.space);
230+
231+
let (left_weight, right_weight) = if normalize {
232+
(accumulated_weight / combined, weight / combined)
233+
} else {
234+
(accumulated_weight, weight)
235+
};
236+
237+
accumulated_color = mix_with_weights(
238+
&accumulated_color,
239+
left_weight,
240+
&right,
241+
right_weight,
242+
interpolation.hue,
243+
);
244+
accumulated_weight = combined;
245+
}
246+
247+
let components = accumulated_color.raw_components();
248+
let alpha = components[3] * alpha_multiplier;
249+
250+
// FIXME: In rare cases we end up with 0.999995 in the alpha channel,
251+
// so we reduce the precision to avoid serializing to
252+
// rgba(?, ?, ?, 1). This is not ideal, so we should look into
253+
// ways to avoid it. Maybe pre-multiply all color components and
254+
// then divide after calculations?
255+
let alpha = (alpha.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
256+
257+
let mut result = AbsoluteColor::new(
175258
interpolation.space,
176-
left_color,
177-
left_weight,
178-
right_color,
179-
right_weight,
180-
interpolation.hue,
181-
alpha_multiplier,
259+
components[0],
260+
components[1],
261+
components[2],
262+
alpha,
182263
);
264+
result.flags = accumulated_color.flags;
183265

184266
if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
185267
// If the result *MUST* be in modern syntax, then make sure it is in a
@@ -190,7 +272,7 @@ pub fn mix(
190272
} else {
191273
result
192274
}
193-
} else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() {
275+
} else if items.iter().all(|item| item.color.is_legacy_syntax()) {
194276
// If both sides of the mix is legacy then convert the result back into
195277
// legacy.
196278
result.into_srgb_legacy()
@@ -304,20 +386,16 @@ impl AbsoluteColor {
304386
}
305387
}
306388

307-
fn mix_in(
308-
color_space: ColorSpace,
309-
left_color: &AbsoluteColor,
389+
/// Mix two colors already in the interpolation color space.
390+
fn mix_with_weights(
391+
left: &AbsoluteColor,
310392
left_weight: f32,
311-
right_color: &AbsoluteColor,
393+
right: &AbsoluteColor,
312394
right_weight: f32,
313395
hue_interpolation: HueInterpolationMethod,
314-
alpha_multiplier: f32,
315396
) -> 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);
397+
debug_assert!(right.color_space == left.color_space);
398+
let color_space = left.color_space;
321399

322400
let outcomes = [
323401
ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
@@ -340,26 +418,17 @@ fn mix_in(
340418
&outcomes,
341419
);
342420

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-
421+
let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], result[3]);
358422
result.flags = result_flags;
359-
360423
result
361424
}
362425

426+
fn convert_for_mix(color: &AbsoluteColor, color_space: ColorSpace) -> AbsoluteColor {
427+
let mut converted = color.to_color_space(color_space);
428+
converted.carry_forward_analogous_missing_components(color);
429+
converted
430+
}
431+
363432
fn interpolate_premultiplied_component(
364433
left: f32,
365434
left_weight: f32,

servo/components/style/color/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ pub use color_function::*;
1919
use component::ColorComponent;
2020
use cssparser::color::PredefinedColorSpace;
2121

22+
/// Number of color-mix items to reserve on the stack to avoid heap allocations.
23+
pub const PRE_ALLOCATED_COLOR_MIX_ITEMS: usize = 3;
24+
25+
/// Conveniece type to use for collecting color mix items.
26+
pub type ColorMixItemList<T> = smallvec::SmallVec<[T; PRE_ALLOCATED_COLOR_MIX_ITEMS]>;
27+
2228
/// The 3 components that make up a color. (Does not include the alpha component)
2329
#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
2430
#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]

0 commit comments

Comments
 (0)