peniko/
gradient.rs

1// Copyright 2022 the Peniko Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use super::Extend;
5
6use color::{
7    cache_key::{BitEq, BitHash},
8    AlphaColor, ColorSpace, ColorSpaceTag, DynamicColor, HueDirection, OpaqueColor,
9};
10use kurbo::Point;
11use smallvec::SmallVec;
12
13use core::{
14    hash::Hasher,
15    ops::{Deref, DerefMut},
16};
17
18/// The default for `Gradient::interpolation_cs`.
19// This is intentionally not `pub` and is here in case we change it
20// in the future.
21const DEFAULT_GRADIENT_COLOR_SPACE: ColorSpaceTag = ColorSpaceTag::Srgb;
22
23/// Offset and color of a transition point in a [gradient](Gradient).
24///
25/// Color stops are compatible with use as a cache key.
26#[derive(Copy, Clone, Debug, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct ColorStop {
29    /// Normalized offset of the stop.
30    pub offset: f32,
31    /// Color at the specified offset.
32    pub color: DynamicColor,
33}
34
35impl BitHash for ColorStop {
36    fn bit_hash<H: Hasher>(&self, state: &mut H) {
37        self.offset.bit_hash(state);
38        self.color.bit_hash(state);
39    }
40}
41
42impl BitEq for ColorStop {
43    fn bit_eq(&self, other: &Self) -> bool {
44        self.offset.bit_eq(&other.offset) && self.color.bit_eq(&other.color)
45    }
46}
47
48impl ColorStop {
49    /// Returns the color stop with the alpha component set to `alpha`.
50    #[must_use]
51    pub const fn with_alpha(self, alpha: f32) -> Self {
52        Self {
53            offset: self.offset,
54            color: self.color.with_alpha(alpha),
55        }
56    }
57
58    /// Returns the color stop with the alpha component multiplied by `alpha`.
59    /// The behaviour of this transformation is undefined if `alpha` is negative.
60    ///
61    /// If any resulting alphas would overflow, these currently saturate (to opaque).
62    #[must_use]
63    pub const fn multiply_alpha(self, alpha: f32) -> Self {
64        Self {
65            offset: self.offset,
66            color: self.color.multiply_alpha(alpha),
67        }
68    }
69}
70
71impl<CS: ColorSpace> From<(f32, AlphaColor<CS>)> for ColorStop {
72    fn from(pair: (f32, AlphaColor<CS>)) -> Self {
73        Self {
74            offset: pair.0,
75            color: DynamicColor::from_alpha_color(pair.1),
76        }
77    }
78}
79
80impl From<(f32, DynamicColor)> for ColorStop {
81    fn from(pair: (f32, DynamicColor)) -> Self {
82        Self {
83            offset: pair.0,
84            color: pair.1,
85        }
86    }
87}
88
89impl<CS: ColorSpace> From<(f32, OpaqueColor<CS>)> for ColorStop {
90    fn from(pair: (f32, OpaqueColor<CS>)) -> Self {
91        Self {
92            offset: pair.0,
93            color: DynamicColor::from_alpha_color(pair.1.with_alpha(1.)),
94        }
95    }
96}
97
98/// Collection of color stops.
99#[derive(Clone, PartialEq, Debug, Default)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct ColorStops(pub SmallVec<[ColorStop; 4]>);
102
103impl Deref for ColorStops {
104    type Target = SmallVec<[ColorStop; 4]>;
105    fn deref(&self) -> &Self::Target {
106        &self.0
107    }
108}
109
110impl DerefMut for ColorStops {
111    fn deref_mut(&mut self) -> &mut Self::Target {
112        &mut self.0
113    }
114}
115
116impl ColorStops {
117    /// Construct an empty collection of stops.
118    pub fn new() -> Self {
119        Self::default()
120    }
121}
122
123impl BitEq for ColorStops {
124    fn bit_eq(&self, other: &Self) -> bool {
125        self.as_slice().bit_eq(other.as_slice())
126    }
127}
128
129impl BitHash for ColorStops {
130    fn bit_hash<H: Hasher>(&self, state: &mut H) {
131        self.as_slice().bit_hash(state);
132    }
133}
134
135impl From<&[ColorStop]> for ColorStops {
136    fn from(slice: &[ColorStop]) -> Self {
137        Self(slice.into())
138    }
139}
140
141/// Parameters that define the position of a linear gradient.
142#[derive(Copy, Clone, PartialEq, Debug)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub struct LinearGradientPosition {
145    /// Starting point.
146    pub start: Point,
147    /// Ending point.
148    pub end: Point,
149}
150
151impl LinearGradientPosition {
152    /// Creates a new linear gradient position for the specified start and end points.
153    pub fn new(start: impl Into<Point>, end: impl Into<Point>) -> Self {
154        Self {
155            start: start.into(),
156            end: end.into(),
157        }
158    }
159}
160
161/// Parameters that define the position of a radial gradient.
162#[derive(Copy, Clone, PartialEq, Debug)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub struct RadialGradientPosition {
165    /// Center of start circle.
166    pub start_center: Point,
167    /// Radius of start circle.
168    pub start_radius: f32,
169    /// Center of end circle.
170    pub end_center: Point,
171    /// Radius of end circle.
172    pub end_radius: f32,
173}
174
175impl RadialGradientPosition {
176    /// Creates a new radial gradient position for the specified center point and radius.
177    pub fn new(center: impl Into<Point>, radius: f32) -> Self {
178        let center = center.into();
179        Self {
180            start_center: center,
181            start_radius: 0.0,
182            end_center: center,
183            end_radius: radius,
184        }
185    }
186    /// Creates a new two point radial gradient position for the specified center points and radii.
187    pub fn new_two_point(
188        start_center: impl Into<Point>,
189        start_radius: f32,
190        end_center: impl Into<Point>,
191        end_radius: f32,
192    ) -> Self {
193        Self {
194            start_center: start_center.into(),
195            start_radius,
196            end_center: end_center.into(),
197            end_radius,
198        }
199    }
200}
201
202/// Parameters that define the position of a sweep gradient.
203///
204/// Conventionally, a positive increase in one of the sweep angles is a clockwise rotation in a
205/// Y-down, X-right coordinate system (as is common for graphics). More generally, the convention
206/// for rotations is that a positive angle rotates a positive X direction into positive Y.
207#[derive(Copy, Clone, PartialEq, Debug)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209pub struct SweepGradientPosition {
210    /// Center point.
211    pub center: Point,
212    /// Start angle of the sweep in radians, measuring from the positive X-axis.
213    ///
214    /// Clockwise in a Y-down coordinate system.
215    pub start_angle: f32,
216    /// End angle of the sweep in radians, measuring from the positive X-axis.
217    ///
218    /// Clockwise in a Y-down coordinate system.
219    pub end_angle: f32,
220}
221
222impl SweepGradientPosition {
223    /// Creates a new sweep gradient for the specified center point, start and end angles.
224    pub fn new(center: impl Into<Point>, start_angle: f32, end_angle: f32) -> Self {
225        Self {
226            center: center.into(),
227            start_angle,
228            end_angle,
229        }
230    }
231}
232
233/// Defines how color channels should be handled when interpolating
234/// between transparent colors.
235#[derive(Clone, Copy, Default, Debug, PartialEq)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237pub enum InterpolationAlphaSpace {
238    /// Colors are interpolated with their color channels premultiplied by the alpha
239    /// channel. This is almost always what you want.
240    ///
241    /// Used when interpolating colors in the premultiplied alpha space, which allows
242    /// for correct interpolation when colors are transparent. This matches behavior
243    /// described in [CSS Color Module Level 4 § 12.3].
244    ///
245    /// Following the convention of CSS Color Module Level 4, in cylindrical color
246    /// spaces the hue channel is not premultiplied. If it were, interpolation would
247    /// give undesirable results. See also [`color::PremulColor`].
248    ///
249    /// [CSS Color Module Level 4 § 12.3]: https://drafts.csswg.org/css-color/#interpolation-alpha
250    #[default]
251    Premultiplied = 0,
252    /// Colors are interpolated without premultiplying their color channels by the alpha channel.
253    ///
254    /// This causes color information to leak out of transparent colors. For example, when
255    /// interpolating from a fully transparent red to a fully opaque blue in sRGB, this
256    /// method will go through an intermediate purple.
257    ///
258    /// Used when interpolating colors in the unpremultiplied (straight) alpha space.
259    /// This matches behavior of gradients in the HTML `canvas` element.
260    /// See [The 2D rendering context § Fill and stroke styles].
261    ///
262    /// [The 2D rendering context § Fill and stroke styles]: https://html.spec.whatwg.org/multipage/#interpolation
263    Unpremultiplied = 1,
264}
265
266/// Properties for the supported [gradient](Gradient) types.
267#[derive(Copy, Clone, PartialEq, Debug)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
269pub enum GradientKind {
270    /// Gradient that transitions between two or more colors along a line.
271    Linear(LinearGradientPosition),
272    /// Gradient that transitions between two or more colors that radiate from an origin.
273    Radial(RadialGradientPosition),
274    /// Gradient that transitions between two or more colors that rotate around a center
275    /// point.
276    Sweep(SweepGradientPosition),
277}
278
279impl From<LinearGradientPosition> for GradientKind {
280    #[inline(always)]
281    fn from(value: LinearGradientPosition) -> Self {
282        Self::Linear(value)
283    }
284}
285impl From<RadialGradientPosition> for GradientKind {
286    #[inline(always)]
287    fn from(value: RadialGradientPosition) -> Self {
288        Self::Radial(value)
289    }
290}
291impl From<SweepGradientPosition> for GradientKind {
292    #[inline(always)]
293    fn from(value: SweepGradientPosition) -> Self {
294        Self::Sweep(value)
295    }
296}
297
298/// Definition of a gradient that transitions between two or more colors.
299#[derive(Clone, PartialEq, Debug)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
301pub struct Gradient {
302    /// Kind and properties of the gradient.
303    pub kind: GradientKind,
304    /// Extend mode.
305    pub extend: Extend,
306    /// The color space to be used for interpolation.
307    ///
308    /// The gradient's color ramps will be interpolated linearly in this color space between the
309    /// specified color stops.
310    ///
311    /// This defaults to [sRGB](ColorSpaceTag::Srgb).
312    pub interpolation_cs: ColorSpaceTag,
313    /// When interpolating within a cylindrical color space, the direction for the hue.
314    ///
315    /// This is interpreted as described in [CSS Color Module Level 4 § 12.4].
316    ///
317    /// [CSS Color Module Level 4 § 12.4]: https://drafts.csswg.org/css-color/#hue-interpolation
318    pub hue_direction: HueDirection,
319    /// Alpha space to be used for interpolation
320    pub interpolation_alpha_space: InterpolationAlphaSpace,
321    /// Color stop collection.
322    pub stops: ColorStops,
323}
324
325impl Default for Gradient {
326    fn default() -> Self {
327        Self {
328            kind: LinearGradientPosition {
329                start: Point::default(),
330                end: Point::default(),
331            }
332            .into(),
333            extend: Extend::default(),
334            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
335            hue_direction: HueDirection::default(),
336            interpolation_alpha_space: InterpolationAlphaSpace::default(),
337            stops: ColorStops::default(),
338        }
339    }
340}
341
342impl Gradient {
343    /// Creates a new linear gradient for the specified start and end points.
344    pub fn new_linear(start: impl Into<Point>, end: impl Into<Point>) -> Self {
345        Self {
346            kind: LinearGradientPosition::new(start, end).into(),
347            extend: Extend::default(),
348            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
349            hue_direction: HueDirection::default(),
350            interpolation_alpha_space: InterpolationAlphaSpace::default(),
351            stops: ColorStops::default(),
352        }
353    }
354
355    /// Creates a new radial gradient for the specified center point and radius.
356    pub fn new_radial(center: impl Into<Point>, radius: f32) -> Self {
357        let center = center.into();
358        Self {
359            kind: RadialGradientPosition::new(center, radius).into(),
360            extend: Extend::default(),
361            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
362            hue_direction: HueDirection::default(),
363            interpolation_alpha_space: InterpolationAlphaSpace::default(),
364            stops: ColorStops::default(),
365        }
366    }
367
368    /// Creates a new two point radial gradient for the specified center points and radii.
369    pub fn new_two_point_radial(
370        start_center: impl Into<Point>,
371        start_radius: f32,
372        end_center: impl Into<Point>,
373        end_radius: f32,
374    ) -> Self {
375        Self {
376            kind: RadialGradientPosition::new_two_point(
377                start_center,
378                start_radius,
379                end_center,
380                end_radius,
381            )
382            .into(),
383            extend: Extend::default(),
384            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
385            hue_direction: HueDirection::default(),
386            interpolation_alpha_space: InterpolationAlphaSpace::default(),
387            stops: ColorStops::default(),
388        }
389    }
390
391    /// Creates a new sweep gradient for the specified center point, start and
392    /// end angles.
393    pub fn new_sweep(center: impl Into<Point>, start_angle: f32, end_angle: f32) -> Self {
394        Self {
395            kind: SweepGradientPosition::new(center, start_angle, end_angle).into(),
396            extend: Extend::default(),
397            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
398            hue_direction: HueDirection::default(),
399            interpolation_alpha_space: InterpolationAlphaSpace::default(),
400            stops: ColorStops::default(),
401        }
402    }
403
404    /// Builder method for setting the gradient extend mode.
405    #[must_use]
406    pub const fn with_extend(mut self, mode: Extend) -> Self {
407        self.extend = mode;
408        self
409    }
410
411    /// Builder method for setting the interpolation color space.
412    #[must_use]
413    pub const fn with_interpolation_cs(mut self, interpolation_cs: ColorSpaceTag) -> Self {
414        self.interpolation_cs = interpolation_cs;
415        self
416    }
417
418    /// Builder method for setting the interpolation alpha space.
419    #[must_use]
420    pub const fn with_interpolation_alpha_space(
421        mut self,
422        interpolation_alpha_space: InterpolationAlphaSpace,
423    ) -> Self {
424        self.interpolation_alpha_space = interpolation_alpha_space;
425        self
426    }
427
428    /// Builder method for setting the hue direction when interpolating within a cylindrical color space.
429    #[must_use]
430    pub const fn with_hue_direction(mut self, hue_direction: HueDirection) -> Self {
431        self.hue_direction = hue_direction;
432        self
433    }
434
435    /// Builder method for setting the color stop collection.
436    #[must_use]
437    pub fn with_stops(mut self, stops: impl ColorStopsSource) -> Self {
438        self.stops.clear();
439        stops.collect_stops(&mut self.stops);
440        self
441    }
442
443    /// Returns the gradient with the alpha component for all color stops set to `alpha`.
444    #[must_use]
445    pub fn with_alpha(mut self, alpha: f32) -> Self {
446        self.stops
447            .iter_mut()
448            .for_each(|stop| *stop = stop.with_alpha(alpha));
449        self
450    }
451
452    /// Returns the gradient with the alpha component for all color stops
453    /// multiplied by `alpha`.
454    #[must_use]
455    pub fn multiply_alpha(mut self, alpha: f32) -> Self {
456        self.stops
457            .iter_mut()
458            .for_each(|stop| *stop = stop.multiply_alpha(alpha));
459        self
460    }
461}
462
463/// Trait for types that represent a source of color stops.
464pub trait ColorStopsSource {
465    /// Append the stops represented within `self` into `stops`.
466    fn collect_stops(self, stops: &mut ColorStops);
467}
468
469impl<T> ColorStopsSource for &'_ [T]
470where
471    T: Into<ColorStop> + Copy,
472{
473    fn collect_stops(self, stops: &mut ColorStops) {
474        for &stop in self {
475            stops.push(stop.into());
476        }
477    }
478}
479
480impl<T, const N: usize> ColorStopsSource for [T; N]
481where
482    T: Into<ColorStop>,
483{
484    fn collect_stops(self, stops: &mut ColorStops) {
485        for stop in self.into_iter() {
486            stops.push(stop.into());
487        }
488    }
489}
490
491impl<CS: ColorSpace> ColorStopsSource for &'_ [AlphaColor<CS>] {
492    fn collect_stops(self, stops: &mut ColorStops) {
493        if !self.is_empty() {
494            let denom = (self.len() - 1).max(1) as f32;
495            stops.extend(self.iter().enumerate().map(|(i, c)| ColorStop {
496                offset: (i as f32) / denom,
497                color: DynamicColor::from_alpha_color(*c),
498            }));
499        }
500    }
501}
502
503impl ColorStopsSource for &'_ [DynamicColor] {
504    fn collect_stops(self, stops: &mut ColorStops) {
505        if !self.is_empty() {
506            let denom = (self.len() - 1).max(1) as f32;
507            stops.extend(self.iter().enumerate().map(|(i, c)| ColorStop {
508                offset: (i as f32) / denom,
509                color: (*c),
510            }));
511        }
512    }
513}
514
515impl<CS: ColorSpace> ColorStopsSource for &'_ [OpaqueColor<CS>] {
516    fn collect_stops(self, stops: &mut ColorStops) {
517        if !self.is_empty() {
518            let denom = (self.len() - 1).max(1) as f32;
519            stops.extend(self.iter().enumerate().map(|(i, c)| ColorStop {
520                offset: (i as f32) / denom,
521                color: DynamicColor::from_alpha_color((*c).with_alpha(1.)),
522            }));
523        }
524    }
525}
526
527impl<const N: usize, CS: ColorSpace> ColorStopsSource for [AlphaColor<CS>; N] {
528    fn collect_stops(self, stops: &mut ColorStops) {
529        (&self[..]).collect_stops(stops);
530    }
531}
532impl<const N: usize> ColorStopsSource for [DynamicColor; N] {
533    fn collect_stops(self, stops: &mut ColorStops) {
534        (&self[..]).collect_stops(stops);
535    }
536}
537impl<const N: usize, CS: ColorSpace> ColorStopsSource for [OpaqueColor<CS>; N] {
538    fn collect_stops(self, stops: &mut ColorStops) {
539        (&self[..]).collect_stops(stops);
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    extern crate alloc;
546    extern crate std;
547    use super::Gradient;
548    use alloc::vec;
549    use color::{cache_key::CacheKey, palette, parse_color};
550    use std::collections::HashSet;
551
552    #[test]
553    fn color_stops_cache() {
554        let mut set = HashSet::new();
555        let stops = Gradient::default()
556            .with_stops([palette::css::RED, palette::css::LIME, palette::css::BLUE])
557            .stops;
558        let stops_clone = stops.clone();
559        let parsed_gradient = Gradient::default().with_stops(
560            vec![
561                parse_color("red").unwrap(),
562                parse_color("lime").unwrap(),
563                parse_color("blue").unwrap(),
564            ]
565            .as_slice(),
566        );
567        let parsed_stops = parsed_gradient.stops.clone();
568        set.insert(CacheKey(stops));
569        // TODO: Ideally this wouldn't need to turn more_stops into a `CacheKey`;
570        assert!(set.contains(&CacheKey(stops_clone)));
571        set.insert(CacheKey(parsed_stops));
572        let new_grad = parsed_gradient.clone();
573        assert!(set.contains(&CacheKey(new_grad.stops)));
574    }
575}