55//! Color mixing/interpolation.
66
77use super :: { AbsoluteColor , ColorFlags , ColorSpace } ;
8+ use crate :: color:: ColorMixItemList ;
89use crate :: derives:: * ;
910use crate :: parser:: { Parse , ParserContext } ;
1011use crate :: values:: generics:: color:: ColorMixFlags ;
1112use cssparser:: Parser ;
13+ use smallvec:: SmallVec ;
1214use std:: fmt:: { self , Write } ;
1315use 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]
152171pub 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+
363432fn interpolate_premultiplied_component (
364433 left : f32 ,
365434 left_weight : f32 ,
0 commit comments