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/// Properties for the supported [gradient](Gradient) types.
142#[derive(Copy, Clone, PartialEq, Debug)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub enum GradientKind {
145    /// Gradient that transitions between two or more colors along a line.
146    Linear {
147        /// Starting point.
148        start: Point,
149        /// Ending point.
150        end: Point,
151    },
152    /// Gradient that transitions between two or more colors that radiate from an origin.
153    Radial {
154        /// Center of start circle.
155        start_center: Point,
156        /// Radius of start circle.
157        start_radius: f32,
158        /// Center of end circle.
159        end_center: Point,
160        /// Radius of end circle.
161        end_radius: f32,
162    },
163    /// Gradient that transitions between two or more colors that rotate around a center
164    /// point.
165    Sweep {
166        /// Center point.
167        center: Point,
168        /// Start angle of the sweep, counter-clockwise of the x-axis.
169        start_angle: f32,
170        /// End angle of the sweep, counter-clockwise of the x-axis.
171        end_angle: f32,
172    },
173}
174
175/// Definition of a gradient that transitions between two or more colors.
176#[derive(Clone, PartialEq, Debug)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178pub struct Gradient {
179    /// Kind and properties of the gradient.
180    pub kind: GradientKind,
181    /// Extend mode.
182    pub extend: Extend,
183    /// The color space to be used for interpolation.
184    ///
185    /// The colors in the color stops will be converted to this color space.
186    ///
187    /// This defaults to [sRGB](ColorSpaceTag::Srgb).
188    pub interpolation_cs: ColorSpaceTag,
189    /// When interpolating within a cylindrical color space, the direction for the hue.
190    ///
191    /// This is interpreted as described in [CSS Color Module Level 4 § 12.4].
192    ///
193    /// [CSS Color Module Level 4 § 12.4]: https://drafts.csswg.org/css-color/#hue-interpolation
194    pub hue_direction: HueDirection,
195    /// Color stop collection.
196    pub stops: ColorStops,
197}
198
199impl Default for Gradient {
200    fn default() -> Self {
201        Self {
202            kind: GradientKind::Linear {
203                start: Point::default(),
204                end: Point::default(),
205            },
206            extend: Extend::default(),
207            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
208            hue_direction: HueDirection::default(),
209            stops: ColorStops::default(),
210        }
211    }
212}
213
214impl Gradient {
215    /// Creates a new linear gradient for the specified start and end points.
216    pub fn new_linear(start: impl Into<Point>, end: impl Into<Point>) -> Self {
217        Self {
218            kind: GradientKind::Linear {
219                start: start.into(),
220                end: end.into(),
221            },
222            extend: Extend::default(),
223            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
224            hue_direction: HueDirection::default(),
225            stops: ColorStops::default(),
226        }
227    }
228
229    /// Creates a new radial gradient for the specified center point and radius.
230    pub fn new_radial(center: impl Into<Point>, radius: f32) -> Self {
231        let center = center.into();
232        Self {
233            kind: GradientKind::Radial {
234                start_center: center,
235                start_radius: 0.0,
236                end_center: center,
237                end_radius: radius,
238            },
239            extend: Extend::default(),
240            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
241            hue_direction: HueDirection::default(),
242            stops: ColorStops::default(),
243        }
244    }
245
246    /// Creates a new two point radial gradient for the specified center points and radii.
247    pub fn new_two_point_radial(
248        start_center: impl Into<Point>,
249        start_radius: f32,
250        end_center: impl Into<Point>,
251        end_radius: f32,
252    ) -> Self {
253        Self {
254            kind: GradientKind::Radial {
255                start_center: start_center.into(),
256                start_radius,
257                end_center: end_center.into(),
258                end_radius,
259            },
260            extend: Extend::default(),
261            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
262            hue_direction: HueDirection::default(),
263            stops: ColorStops::default(),
264        }
265    }
266
267    /// Creates a new sweep gradient for the specified center point, start and
268    /// end angles.
269    pub fn new_sweep(center: impl Into<Point>, start_angle: f32, end_angle: f32) -> Self {
270        Self {
271            kind: GradientKind::Sweep {
272                center: center.into(),
273                start_angle,
274                end_angle,
275            },
276            extend: Extend::default(),
277            interpolation_cs: DEFAULT_GRADIENT_COLOR_SPACE,
278            hue_direction: HueDirection::default(),
279            stops: ColorStops::default(),
280        }
281    }
282
283    /// Builder method for setting the gradient extend mode.
284    #[must_use]
285    pub const fn with_extend(mut self, mode: Extend) -> Self {
286        self.extend = mode;
287        self
288    }
289
290    /// Builder method for setting the interpolation color space.
291    #[must_use]
292    pub const fn with_interpolation_cs(mut self, interpolation_cs: ColorSpaceTag) -> Self {
293        self.interpolation_cs = interpolation_cs;
294        self
295    }
296
297    /// Builder method for setting the hue direction when interpolating within a cylindrical color space.
298    #[must_use]
299    pub const fn with_hue_direction(mut self, hue_direction: HueDirection) -> Self {
300        self.hue_direction = hue_direction;
301        self
302    }
303
304    /// Builder method for setting the color stop collection.
305    #[must_use]
306    pub fn with_stops(mut self, stops: impl ColorStopsSource) -> Self {
307        self.stops.clear();
308        stops.collect_stops(&mut self.stops);
309        self
310    }
311
312    /// Returns the gradient with the alpha component for all color stops set to `alpha`.
313    #[must_use]
314    pub fn with_alpha(mut self, alpha: f32) -> Self {
315        self.stops
316            .iter_mut()
317            .for_each(|stop| *stop = stop.with_alpha(alpha));
318        self
319    }
320
321    /// Returns the gradient with the alpha component for all color stops
322    /// multiplied by `alpha`.
323    #[must_use]
324    pub fn multiply_alpha(mut self, alpha: f32) -> Self {
325        self.stops
326            .iter_mut()
327            .for_each(|stop| *stop = stop.multiply_alpha(alpha));
328        self
329    }
330}
331
332/// Trait for types that represent a source of color stops.
333pub trait ColorStopsSource {
334    /// Append the stops represented within `self` into `stops`.
335    fn collect_stops(self, stops: &mut ColorStops);
336}
337
338impl<T> ColorStopsSource for &'_ [T]
339where
340    T: Into<ColorStop> + Copy,
341{
342    fn collect_stops(self, stops: &mut ColorStops) {
343        for &stop in self {
344            stops.push(stop.into());
345        }
346    }
347}
348
349impl<T, const N: usize> ColorStopsSource for [T; N]
350where
351    T: Into<ColorStop>,
352{
353    fn collect_stops(self, stops: &mut ColorStops) {
354        for stop in self.into_iter() {
355            stops.push(stop.into());
356        }
357    }
358}
359
360impl<CS: ColorSpace> ColorStopsSource for &'_ [AlphaColor<CS>] {
361    fn collect_stops(self, stops: &mut ColorStops) {
362        if !self.is_empty() {
363            let denom = (self.len() - 1).max(1) as f32;
364            stops.extend(self.iter().enumerate().map(|(i, c)| ColorStop {
365                offset: (i as f32) / denom,
366                color: DynamicColor::from_alpha_color(*c),
367            }));
368        }
369    }
370}
371
372impl ColorStopsSource for &'_ [DynamicColor] {
373    fn collect_stops(self, stops: &mut ColorStops) {
374        if !self.is_empty() {
375            let denom = (self.len() - 1).max(1) as f32;
376            stops.extend(self.iter().enumerate().map(|(i, c)| ColorStop {
377                offset: (i as f32) / denom,
378                color: (*c),
379            }));
380        }
381    }
382}
383
384impl<CS: ColorSpace> ColorStopsSource for &'_ [OpaqueColor<CS>] {
385    fn collect_stops(self, stops: &mut ColorStops) {
386        if !self.is_empty() {
387            let denom = (self.len() - 1).max(1) as f32;
388            stops.extend(self.iter().enumerate().map(|(i, c)| ColorStop {
389                offset: (i as f32) / denom,
390                color: DynamicColor::from_alpha_color((*c).with_alpha(1.)),
391            }));
392        }
393    }
394}
395
396impl<const N: usize, CS: ColorSpace> ColorStopsSource for [AlphaColor<CS>; N] {
397    fn collect_stops(self, stops: &mut ColorStops) {
398        (&self[..]).collect_stops(stops);
399    }
400}
401impl<const N: usize> ColorStopsSource for [DynamicColor; N] {
402    fn collect_stops(self, stops: &mut ColorStops) {
403        (&self[..]).collect_stops(stops);
404    }
405}
406impl<const N: usize, CS: ColorSpace> ColorStopsSource for [OpaqueColor<CS>; N] {
407    fn collect_stops(self, stops: &mut ColorStops) {
408        (&self[..]).collect_stops(stops);
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    extern crate alloc;
415    extern crate std;
416    use super::Gradient;
417    use alloc::vec;
418    use color::{cache_key::CacheKey, palette, parse_color};
419    use std::collections::HashSet;
420
421    #[test]
422    fn color_stops_cache() {
423        let mut set = HashSet::new();
424        let stops = Gradient::default()
425            .with_stops([palette::css::RED, palette::css::LIME, palette::css::BLUE])
426            .stops;
427        let stops_clone = stops.clone();
428        let parsed_gradient = Gradient::default().with_stops(
429            vec![
430                parse_color("red").unwrap(),
431                parse_color("lime").unwrap(),
432                parse_color("blue").unwrap(),
433            ]
434            .as_slice(),
435        );
436        let parsed_stops = parsed_gradient.stops.clone();
437        set.insert(CacheKey(stops));
438        // TODO: Ideally this wouldn't need to turn more_stops into a `CacheKey`;
439        assert!(set.contains(&CacheKey(stops_clone)));
440        set.insert(CacheKey(parsed_stops));
441        let new_grad = parsed_gradient.clone();
442        assert!(set.contains(&CacheKey(new_grad.stops)));
443    }
444}