Skip to main content

skia_safe/effects/
gradient.rs

1use crate::{scalar, Color4f, ColorSpace, TileMode};
2use skia_bindings as sb;
3
4/// Gradient interpolation settings.
5///
6/// Specifies how colors are interpolated in a gradient, including the color space
7/// and premultiplication mode.
8#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
9#[repr(C)]
10pub struct Interpolation {
11    pub in_premul: interpolation::InPremul,
12    pub color_space: interpolation::ColorSpace,
13    pub hue_method: interpolation::HueMethod,
14}
15
16native_transmutable!(sb::SkGradient_Interpolation, Interpolation);
17
18pub mod interpolation {
19    use skia_bindings as sb;
20
21    /// Whether to interpolate colors in premultiplied alpha space.
22    pub type InPremul = sb::SkGradient_Interpolation_InPremul;
23    variant_name!(InPremul::Yes);
24
25    /// Color space for gradient interpolation.
26    ///
27    /// See <https://www.w3.org/TR/css-color-4/#interpolation-space>
28    pub type ColorSpace = sb::SkGradient_Interpolation_ColorSpace;
29    variant_name!(ColorSpace::HSL);
30
31    /// Hue interpolation method for cylindrical color spaces (LCH, OKLCH, HSL, HWB).
32    ///
33    /// See <https://www.w3.org/TR/css-color-4/#hue-interpolation>
34    pub type HueMethod = sb::SkGradient_Interpolation_HueMethod;
35    variant_name!(HueMethod::Shorter);
36}
37
38impl Default for Interpolation {
39    fn default() -> Self {
40        Self {
41            in_premul: interpolation::InPremul::No,
42            color_space: interpolation::ColorSpace::Destination,
43            hue_method: interpolation::HueMethod::Shorter,
44        }
45    }
46}
47
48impl Interpolation {
49    /// Create interpolation settings from legacy flags.
50    pub fn from_flags(flags: u32) -> Self {
51        Self {
52            in_premul: if flags & 1 != 0 {
53                interpolation::InPremul::Yes
54            } else {
55                interpolation::InPremul::No
56            },
57            color_space: interpolation::ColorSpace::Destination,
58            hue_method: interpolation::HueMethod::Shorter,
59        }
60    }
61}
62
63/// Specification for the colors in a gradient.
64///
65/// Holds color data, positions, tile mode, and color space for gradient construction.
66/// All references must outlive any shader created from it.
67#[derive(Debug, Clone)]
68pub struct Colors<'a> {
69    colors: &'a [Color4f],
70    pos: Option<&'a [scalar]>,
71    color_space: Option<ColorSpace>,
72    tile_mode: TileMode,
73}
74
75impl<'a> Colors<'a> {
76    /// Create gradient colors with explicit positions.
77    ///
78    /// - `colors`: The colors for the gradient.
79    /// - `pos`: Relative positions of each color (0.0 to 1.0). Must be strictly increasing.
80    ///          If `None`, colors are distributed evenly.
81    /// - `tile_mode`: Tiling mode for the gradient.
82    /// - `color_space`: Optional color space. If `None`, colors are treated as sRGB.
83    pub fn new(
84        colors: &'a [Color4f],
85        pos: Option<&'a [scalar]>,
86        tile_mode: TileMode,
87        color_space: impl Into<Option<ColorSpace>>,
88    ) -> Self {
89        // Validate positions match colors if provided
90        assert!(pos.is_none_or(|pos| pos.len() == colors.len()));
91
92        Self {
93            colors,
94            pos,
95            color_space: color_space.into(),
96            tile_mode,
97        }
98    }
99
100    /// Create gradient colors with evenly distributed positions.
101    pub fn new_evenly_spaced(
102        colors: &'a [Color4f],
103        tile_mode: TileMode,
104        color_space: impl Into<Option<ColorSpace>>,
105    ) -> Self {
106        Self::new(colors, None, tile_mode, color_space)
107    }
108
109    /// Returns a reference to the colors.
110    pub fn colors(&self) -> &'a [Color4f] {
111        self.colors
112    }
113
114    /// Returns a reference to the positions.
115    pub fn positions(&self) -> Option<&'a [scalar]> {
116        self.pos
117    }
118
119    /// Returns a reference to the color space.
120    pub fn color_space(&self) -> Option<&ColorSpace> {
121        self.color_space.as_ref()
122    }
123
124    /// Returns the tile mode.
125    pub fn tile_mode(&self) -> TileMode {
126        self.tile_mode
127    }
128}
129
130/// Gradient specification combining colors and interpolation settings.
131///
132/// This type corresponds to the C++ `SkGradient` class and encapsulates
133/// all parameters needed to define a gradient's appearance.
134///
135/// Note: This is a lightweight wrapper around [`Colors`] and [`Interpolation`].
136/// The actual C++ `SkGradient` object is constructed on-demand when creating shaders.
137#[derive(Debug, Clone)]
138pub struct Gradient<'a> {
139    colors: Colors<'a>,
140    interpolation: Interpolation,
141}
142
143impl<'a> Gradient<'a> {
144    pub fn new(colors: Colors<'a>, interpolation: impl Into<Interpolation>) -> Self {
145        Self {
146            colors,
147            interpolation: interpolation.into(),
148        }
149    }
150
151    pub fn colors(&self) -> &Colors<'a> {
152        &self.colors
153    }
154
155    pub fn interpolation(&self) -> &Interpolation {
156        &self.interpolation
157    }
158}
159
160/// Shader factory functions that accept [`Gradient`] parameters.
161///
162/// These functions correspond to the C++ `SkShaders` namespace gradient functions.
163pub mod shaders {
164    use super::{scalar, Gradient};
165    use crate::{prelude::*, Matrix, Point, Shader};
166    use skia_bindings as sb;
167    use std::ptr;
168
169    /// Returns a shader that generates a linear gradient between the two specified points.
170    ///
171    /// - `points`: Array of 2 points, the end-points of the line segment
172    /// - `gradient`: Description of the colors and interpolation method
173    /// - `local_matrix`: Optional local matrix
174    pub fn linear_gradient<'a>(
175        points: (impl Into<Point>, impl Into<Point>),
176        gradient: &Gradient<'_>,
177        local_matrix: impl Into<Option<&'a Matrix>>,
178    ) -> Option<Shader> {
179        let points = [points.0.into(), points.1.into()];
180        let local_matrix = local_matrix.into();
181        let colors = gradient.colors();
182        let interpolation = gradient.interpolation();
183        let positions = colors.positions();
184        let color_space = colors.color_space().cloned();
185
186        Shader::from_ptr(unsafe {
187            sb::C_SkShaders_LinearGradient(
188                points.native().as_ptr(),
189                colors.colors().native().as_ptr(),
190                colors.colors().len(),
191                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
192                positions.map_or(0, |pos| pos.len()),
193                colors.tile_mode(),
194                color_space.into_ptr_or_null(),
195                interpolation.native(),
196                local_matrix.native_ptr_or_null(),
197            )
198        })
199    }
200
201    /// Returns a shader that generates a radial gradient given the center and radius.
202    ///
203    /// - `center`: The center of the circle for this gradient
204    /// - `radius`: Must be positive. The radius of the circle for this gradient
205    /// - `gradient`: Description of the colors and interpolation method
206    /// - `local_matrix`: Optional local matrix
207    pub fn radial_gradient<'a>(
208        (center, radius): (impl Into<Point>, scalar),
209        gradient: &Gradient<'_>,
210        local_matrix: impl Into<Option<&'a Matrix>>,
211    ) -> Option<Shader> {
212        let center = center.into();
213        let local_matrix = local_matrix.into();
214        let colors = gradient.colors();
215        let interpolation = gradient.interpolation();
216        let positions = colors.positions();
217        let color_space = colors.color_space().cloned();
218
219        Shader::from_ptr(unsafe {
220            sb::C_SkShaders_RadialGradient(
221                center.native(),
222                radius,
223                colors.colors().native().as_ptr(),
224                colors.colors().len(),
225                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
226                positions.map_or(0, |pos| pos.len()),
227                colors.tile_mode(),
228                color_space.into_ptr_or_null(),
229                interpolation.native(),
230                local_matrix.native_ptr_or_null(),
231            )
232        })
233    }
234
235    /// Returns a shader that generates a conical gradient given two circles.
236    ///
237    /// The gradient interprets the two circles according to the following HTML spec:
238    /// <http://dev.w3.org/html5/2dcontext/#dom-context-2d-createradialgradient>
239    ///
240    /// - `start`: The center of the start circle
241    /// - `start_radius`: Must be positive. The radius of the start circle
242    /// - `end`: The center of the end circle
243    /// - `end_radius`: Must be positive. The radius of the end circle
244    /// - `gradient`: Description of the colors and interpolation method
245    /// - `local_matrix`: Optional local matrix
246    #[allow(clippy::too_many_arguments)]
247    pub fn two_point_conical_gradient<'a>(
248        (start, start_radius): (impl Into<Point>, scalar),
249        (end, end_radius): (impl Into<Point>, scalar),
250        gradient: &Gradient<'_>,
251        local_matrix: impl Into<Option<&'a Matrix>>,
252    ) -> Option<Shader> {
253        let start = start.into();
254        let end = end.into();
255        let local_matrix = local_matrix.into();
256        let colors = gradient.colors();
257        let interpolation = gradient.interpolation();
258        let positions = colors.positions();
259        let color_space = colors.color_space().cloned();
260
261        Shader::from_ptr(unsafe {
262            sb::C_SkShaders_TwoPointConicalGradient(
263                start.native(),
264                start_radius,
265                end.native(),
266                end_radius,
267                colors.colors().native().as_ptr(),
268                colors.colors().len(),
269                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
270                positions.map_or(0, |pos| pos.len()),
271                colors.tile_mode(),
272                color_space.into_ptr_or_null(),
273                interpolation.native(),
274                local_matrix.native_ptr_or_null(),
275            )
276        })
277    }
278
279    /// Returns a shader that generates a sweep gradient given a center.
280    ///
281    /// The shader accepts negative angles and angles larger than 360, draws between 0 and 360
282    /// degrees, similar to the CSS conic-gradient semantics. 0 degrees means horizontal
283    /// positive x axis. The start angle must be less than the end angle.
284    ///
285    /// - `center`: The center of the sweep
286    /// - `start_angle`: Start of the angular range, corresponding to pos == 0
287    /// - `end_angle`: End of the angular range, corresponding to pos == 1
288    /// - `gradient`: Description of the colors and interpolation method
289    /// - `local_matrix`: Optional local matrix
290    pub fn sweep_gradient<'a>(
291        center: impl Into<Point>,
292        (start_angle, end_angle): (scalar, scalar),
293        gradient: &Gradient<'_>,
294        local_matrix: impl Into<Option<&'a Matrix>>,
295    ) -> Option<Shader> {
296        let center = center.into();
297        let local_matrix = local_matrix.into();
298        let colors = gradient.colors();
299        let interpolation = gradient.interpolation();
300        let positions = colors.positions();
301        let color_space = colors.color_space().cloned();
302
303        Shader::from_ptr(unsafe {
304            sb::C_SkShaders_SweepGradient(
305                center.native(),
306                start_angle,
307                end_angle,
308                colors.colors().native().as_ptr(),
309                colors.colors().len(),
310                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
311                positions.map_or(0, |pos| pos.len()),
312                colors.tile_mode(),
313                color_space.into_ptr_or_null(),
314                interpolation.native(),
315                local_matrix.native_ptr_or_null(),
316            )
317        })
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::{
325        prelude::{NativeAccess, RefCount},
326        Color, ColorSpace, Paint, Point, Rect, Shader,
327    };
328
329    #[test]
330    fn interpolation_from_flags() {
331        let interp_no_premul = Interpolation::from_flags(0);
332        assert_eq!(interp_no_premul.in_premul, interpolation::InPremul::No);
333
334        let interp_premul = Interpolation::from_flags(1);
335        assert_eq!(interp_premul.in_premul, interpolation::InPremul::Yes);
336    }
337
338    #[test]
339    #[should_panic]
340    fn colors_new_mismatched_positions() {
341        let colors = [Color::RED.into(), Color::BLUE.into()];
342        let positions = [0.0, 0.5, 1.0];
343        let _ = Colors::new(&colors, Some(&positions), TileMode::Clamp, None);
344    }
345
346    #[test]
347    fn linear_gradient_renders() {
348        let mut surface = crate::surfaces::raster_n32_premul((100, 100)).unwrap();
349        let canvas = surface.canvas();
350
351        let colors = [Color::RED.into(), Color::BLUE.into()];
352        let gradient_colors = Colors::new_evenly_spaced(&colors, TileMode::Clamp, None);
353        let gradient = Gradient::new(gradient_colors, Interpolation::default());
354
355        let shader = shaders::linear_gradient(
356            (Point::new(0.0, 0.0), Point::new(100.0, 0.0)),
357            &gradient,
358            None,
359        )
360        .unwrap();
361
362        let mut paint = Paint::default();
363        paint.set_shader(shader);
364
365        canvas.draw_rect(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), &paint);
366
367        let image = surface.image_snapshot();
368        let pixel_left = image.peek_pixels().unwrap().get_color((10, 50));
369        let pixel_right = image.peek_pixels().unwrap().get_color((90, 50));
370
371        assert_ne!(pixel_left, pixel_right);
372        assert!(pixel_left.r() > pixel_right.r());
373        assert!(pixel_left.b() < pixel_right.b());
374    }
375
376    #[test]
377    fn linear_gradient_with_explicit_colorspace_keeps_refcount_balanced() {
378        assert_refcount_balanced(|gradient| {
379            shaders::linear_gradient(
380                (Point::new(0.0, 0.0), Point::new(100.0, 0.0)),
381                gradient,
382                None,
383            )
384        });
385    }
386
387    #[test]
388    fn radial_gradient_with_explicit_colorspace_keeps_refcount_balanced() {
389        assert_refcount_balanced(|gradient| {
390            shaders::radial_gradient((Point::new(50.0, 50.0), 25.0), gradient, None)
391        });
392    }
393
394    #[test]
395    fn two_point_conical_gradient_with_explicit_colorspace_keeps_refcount_balanced() {
396        assert_refcount_balanced(|gradient| {
397            shaders::two_point_conical_gradient(
398                (Point::new(25.0, 50.0), 10.0),
399                (Point::new(75.0, 50.0), 40.0),
400                gradient,
401                None,
402            )
403        });
404    }
405
406    #[test]
407    fn sweep_gradient_with_explicit_colorspace_keeps_refcount_balanced() {
408        assert_refcount_balanced(|gradient| {
409            shaders::sweep_gradient(Point::new(50.0, 50.0), (0.0, 360.0), gradient, None)
410        });
411    }
412
413    fn test_color_space() -> ColorSpace {
414        ColorSpace::new_srgb().with_color_spin()
415    }
416
417    fn assert_refcount_balanced(build_shader: impl FnOnce(&Gradient<'_>) -> Option<Shader>) {
418        let colors = [Color::RED.into(), Color::BLUE.into()];
419        let color_space = test_color_space();
420        let gradient_colors =
421            Colors::new_evenly_spaced(&colors, TileMode::Clamp, Some(color_space.clone()));
422        let gradient = Gradient::new(gradient_colors, Interpolation::default());
423
424        let ref_cnt_before = color_space.native().ref_cnt();
425        let shader = build_shader(&gradient).unwrap();
426        drop(shader);
427        let ref_cnt_after = color_space.native().ref_cnt();
428
429        assert_eq!(ref_cnt_after, ref_cnt_before);
430    }
431}