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
185        Shader::from_ptr(unsafe {
186            sb::C_SkShaders_LinearGradient(
187                points.native().as_ptr(),
188                colors.colors().as_ptr() as *const _,
189                colors.colors().len(),
190                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
191                positions.map_or(0, |pos| pos.len()),
192                colors.tile_mode(),
193                colors.color_space().native_ptr_or_null() as *mut _,
194                interpolation.native(),
195                local_matrix.native_ptr_or_null(),
196            )
197        })
198    }
199
200    /// Returns a shader that generates a radial gradient given the center and radius.
201    ///
202    /// - `center`: The center of the circle for this gradient
203    /// - `radius`: Must be positive. The radius of the circle for this gradient
204    /// - `gradient`: Description of the colors and interpolation method
205    /// - `local_matrix`: Optional local matrix
206    pub fn radial_gradient<'a>(
207        (center, radius): (impl Into<Point>, scalar),
208        gradient: &Gradient<'_>,
209        local_matrix: impl Into<Option<&'a Matrix>>,
210    ) -> Option<Shader> {
211        let center = center.into();
212        let local_matrix = local_matrix.into();
213        let colors = gradient.colors();
214        let interpolation = gradient.interpolation();
215        let positions = colors.positions();
216
217        Shader::from_ptr(unsafe {
218            sb::C_SkShaders_RadialGradient(
219                center.native(),
220                radius,
221                colors.colors().as_ptr() as *const _,
222                colors.colors().len(),
223                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
224                positions.map_or(0, |pos| pos.len()),
225                colors.tile_mode(),
226                colors.color_space().native_ptr_or_null() as *mut _,
227                interpolation.native(),
228                local_matrix.native_ptr_or_null(),
229            )
230        })
231    }
232
233    /// Returns a shader that generates a conical gradient given two circles.
234    ///
235    /// The gradient interprets the two circles according to the following HTML spec:
236    /// <http://dev.w3.org/html5/2dcontext/#dom-context-2d-createradialgradient>
237    ///
238    /// - `start`: The center of the start circle
239    /// - `start_radius`: Must be positive. The radius of the start circle
240    /// - `end`: The center of the end circle
241    /// - `end_radius`: Must be positive. The radius of the end circle
242    /// - `gradient`: Description of the colors and interpolation method
243    /// - `local_matrix`: Optional local matrix
244    #[allow(clippy::too_many_arguments)]
245    pub fn two_point_conical_gradient<'a>(
246        (start, start_radius): (impl Into<Point>, scalar),
247        (end, end_radius): (impl Into<Point>, scalar),
248        gradient: &Gradient<'_>,
249        local_matrix: impl Into<Option<&'a Matrix>>,
250    ) -> Option<Shader> {
251        let start = start.into();
252        let end = end.into();
253        let local_matrix = local_matrix.into();
254        let colors = gradient.colors();
255        let interpolation = gradient.interpolation();
256        let positions = colors.positions();
257
258        Shader::from_ptr(unsafe {
259            sb::C_SkShaders_TwoPointConicalGradient(
260                start.native(),
261                start_radius,
262                end.native(),
263                end_radius,
264                colors.colors().as_ptr() as *const _,
265                colors.colors().len(),
266                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
267                positions.map_or(0, |pos| pos.len()),
268                colors.tile_mode(),
269                colors.color_space().native_ptr_or_null() as *mut _,
270                interpolation.native(),
271                local_matrix.native_ptr_or_null(),
272            )
273        })
274    }
275
276    /// Returns a shader that generates a sweep gradient given a center.
277    ///
278    /// The shader accepts negative angles and angles larger than 360, draws between 0 and 360
279    /// degrees, similar to the CSS conic-gradient semantics. 0 degrees means horizontal
280    /// positive x axis. The start angle must be less than the end angle.
281    ///
282    /// - `center`: The center of the sweep
283    /// - `start_angle`: Start of the angular range, corresponding to pos == 0
284    /// - `end_angle`: End of the angular range, corresponding to pos == 1
285    /// - `gradient`: Description of the colors and interpolation method
286    /// - `local_matrix`: Optional local matrix
287    pub fn sweep_gradient<'a>(
288        center: impl Into<Point>,
289        (start_angle, end_angle): (scalar, scalar),
290        gradient: &Gradient<'_>,
291        local_matrix: impl Into<Option<&'a Matrix>>,
292    ) -> Option<Shader> {
293        let center = center.into();
294        let local_matrix = local_matrix.into();
295        let colors = gradient.colors();
296        let interpolation = gradient.interpolation();
297        let positions = colors.positions();
298
299        Shader::from_ptr(unsafe {
300            sb::C_SkShaders_SweepGradient(
301                center.native(),
302                start_angle,
303                end_angle,
304                colors.colors().as_ptr() as *const _,
305                colors.colors().len(),
306                positions.map_or(ptr::null(), |pos| pos.as_ptr()),
307                positions.map_or(0, |pos| pos.len()),
308                colors.tile_mode(),
309                colors.color_space().native_ptr_or_null() as *mut _,
310                interpolation.native(),
311                local_matrix.native_ptr_or_null(),
312            )
313        })
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::{Color, Paint, Point, Rect};
321
322    #[test]
323    fn interpolation_from_flags() {
324        let interp_no_premul = Interpolation::from_flags(0);
325        assert_eq!(interp_no_premul.in_premul, interpolation::InPremul::No);
326
327        let interp_premul = Interpolation::from_flags(1);
328        assert_eq!(interp_premul.in_premul, interpolation::InPremul::Yes);
329    }
330
331    #[test]
332    #[should_panic]
333    fn colors_new_mismatched_positions() {
334        let colors = [Color::RED.into(), Color::BLUE.into()];
335        let positions = [0.0, 0.5, 1.0];
336        let _ = Colors::new(&colors, Some(&positions), TileMode::Clamp, None);
337    }
338
339    #[test]
340    fn linear_gradient_renders() {
341        let mut surface = crate::surfaces::raster_n32_premul((100, 100)).unwrap();
342        let canvas = surface.canvas();
343
344        let colors = [Color::RED.into(), Color::BLUE.into()];
345        let gradient_colors = Colors::new_evenly_spaced(&colors, TileMode::Clamp, None);
346        let gradient = Gradient::new(gradient_colors, Interpolation::default());
347
348        let shader = shaders::linear_gradient(
349            (Point::new(0.0, 0.0), Point::new(100.0, 0.0)),
350            &gradient,
351            None,
352        )
353        .unwrap();
354
355        let mut paint = Paint::default();
356        paint.set_shader(shader);
357
358        canvas.draw_rect(Rect::from_xywh(0.0, 0.0, 100.0, 100.0), &paint);
359
360        let image = surface.image_snapshot();
361        let pixel_left = image.peek_pixels().unwrap().get_color((10, 50));
362        let pixel_right = image.peek_pixels().unwrap().get_color((90, 50));
363
364        assert_ne!(pixel_left, pixel_right);
365        assert!(pixel_left.r() > pixel_right.r());
366        assert!(pixel_left.b() < pixel_right.b());
367    }
368}