Skip to main content

tiny_skia/shaders/
radial_gradient.rs

1// Copyright 2006 The Android Open Source Project
2// Copyright 2020 Yevhenii Reizner
3//
4// Use of this source code is governed by a BSD-style license that can be
5// found in the LICENSE file.
6
7use alloc::vec::Vec;
8
9use tiny_skia_path::Scalar;
10
11use crate::{ColorSpace, GradientStop, Point, Shader, SpreadMode, Transform};
12
13use super::gradient::{Gradient, DEGENERATE_THRESHOLD};
14use crate::pipeline;
15use crate::pipeline::RasterPipelineBuilder;
16use crate::wide::u32x8;
17
18#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
19use tiny_skia_path::NoStdFloat;
20
21#[derive(Copy, Clone, PartialEq, Debug, Default)]
22struct FocalData {
23    r1: f32,      // r1 after mapping focal point to (0, 0)
24    focal_x: f32, // f
25    is_swapped: bool,
26}
27
28impl FocalData {
29    fn set(&mut self, mut r0: f32, mut r1: f32, matrix: &mut Transform) -> bool {
30        self.is_swapped = false;
31        self.focal_x = r0 / (r0 - r1);
32
33        if (self.focal_x - 1.0).is_nearly_zero() {
34            // swap r0, r1
35            *matrix = matrix.post_translate(-1.0, 0.0).post_scale(-1.0, 1.0);
36            core::mem::swap(&mut r0, &mut r1);
37
38            self.focal_x = 0.0; // because r0 is now 0
39            self.is_swapped = true;
40        }
41
42        // Map {focal point, (1, 0)} to {(0, 0), (1, 0)}
43        let from = [Point::from_xy(self.focal_x, 0.0), Point::from_xy(1.0, 0.0)];
44        let to = [Point::from_xy(0.0, 0.0), Point::from_xy(1.0, 0.0)];
45
46        let focal_matrix = match ts_from_poly_to_poly(from[0], from[1], to[0], to[1]) {
47            Some(m) => m,
48            None => return false,
49        };
50
51        *matrix = matrix.post_concat(focal_matrix);
52        self.r1 = r1 / (1.0 - self.focal_x).abs(); // focalMatrix has a scale of 1/(1-f).
53
54        // The following transformations are just to accelerate the shader computation by saving
55        // some arithmetic operations.
56        if self.is_focal_on_circle() {
57            *matrix = matrix.post_scale(0.5, 0.5);
58        } else {
59            *matrix = matrix.post_scale(
60                self.r1 / (self.r1 * self.r1 - 1.0),
61                1.0 / (self.r1 * self.r1 - 1.0).abs().sqrt(),
62            );
63        }
64
65        *matrix = matrix.post_scale((1.0 - self.focal_x).abs(), (1.0 - self.focal_x).abs()); // scale |1 - f|
66
67        true
68    }
69
70    fn is_focal_on_circle(&self) -> bool {
71        (1.0 - self.r1).is_nearly_zero()
72    }
73
74    fn is_well_behaved(&self) -> bool {
75        !self.is_focal_on_circle() && self.r1 > 1.0
76    }
77
78    fn is_natively_focal(&self) -> bool {
79        self.focal_x.is_nearly_zero()
80    }
81}
82
83#[derive(Clone, PartialEq, Debug)]
84enum GradientType {
85    Radial {
86        radius1: f32,
87        radius2: f32,
88    },
89    Strip {
90        /// Radius of the first circle scaled by the distance between centers (r0 / d_center)
91        scaled_r0: f32,
92    },
93    Focal(FocalData),
94}
95
96/// A 2-point conical gradient shader.
97#[derive(Clone, PartialEq, Debug)]
98pub struct RadialGradient {
99    pub(crate) base: Gradient,
100    gradient_type: GradientType,
101}
102
103impl RadialGradient {
104    /// Creates a new two-point conical gradient shader.
105    ///
106    /// A two-point conical gradient (also known as a radial gradient)
107    /// interpolates colors between two circles defined by their center points
108    /// and radii.
109    ///
110    /// Returns `Shader::SolidColor` when:
111    /// - `stops.len()` == 1
112    ///
113    /// Returns `None` when:
114    /// - `stops` is empty
115    /// - `start_radius` < 0 or `end_radius` < 0
116    /// - `transform` is not invertible
117    /// - The gradient is degenerate (both radii and centers are equal, except
118    ///   in specific pad mode cases)
119    #[allow(clippy::new_ret_no_self)]
120    pub fn new(
121        start_point: Point,
122        start_radius: f32,
123        end_point: Point,
124        end_radius: f32,
125        stops: Vec<GradientStop>,
126        mode: SpreadMode,
127        transform: Transform,
128    ) -> Option<Shader<'static>> {
129        if start_radius < 0.0 || end_radius < 0.0 {
130            return None;
131        }
132
133        match stops.as_slice() {
134            [] => return None,
135            [stop] => return Some(Shader::SolidColor(stop.color)),
136            _ => {}
137        }
138
139        transform.invert()?;
140
141        let length = (start_point - end_point).length();
142        if !length.is_finite() {
143            return None;
144        }
145        if length.is_nearly_zero_within_tolerance(DEGENERATE_THRESHOLD) {
146            if start_radius.is_nearly_equal_within_tolerance(end_radius, DEGENERATE_THRESHOLD) {
147                // Degenerate case, where the interpolation region area approaches zero. The proper
148                // behavior depends on the tile mode, which is consistent with the default degenerate
149                // gradient behavior, except when mode = clamp and the radii > 0.
150                if mode == SpreadMode::Pad && end_radius > DEGENERATE_THRESHOLD {
151                    // The interpolation region becomes an infinitely thin ring at the radius, so the
152                    // final gradient will be the first color repeated from p=0 to 1, and then a hard
153                    // stop switching to the last color at p=1.
154                    let start_color = stops.first()?.clone().color;
155                    let end_color = stops.last()?.clone().color;
156                    let mut new_stops = stops; // Reuse allocation from stops.
157                    new_stops.clear();
158                    new_stops.extend_from_slice(&[
159                        GradientStop::new(0.0, start_color),
160                        GradientStop::new(1.0, start_color),
161                        GradientStop::new(1.0, end_color),
162                    ]);
163                    // If the center positions are the same, then the gradient is the radial variant
164                    // of a 2 pt conical gradient, an actual radial gradient (startRadius == 0), or
165                    // it is fully degenerate (startRadius == endRadius).
166                    // We can treat this gradient as a simple radial, which is faster. If we got
167                    // here, we know that endRadius is not equal to 0, so this produces a meaningful
168                    // gradient
169                    return Self::new_radial_unchecked(
170                        start_point,
171                        end_radius,
172                        new_stops,
173                        mode,
174                        transform,
175                    );
176                }
177                // TODO: Consider making a degenerate gradient
178                return None;
179            }
180
181            if start_radius.is_nearly_zero_within_tolerance(DEGENERATE_THRESHOLD) {
182                // If the center positions are the same, then the gradient
183                // is the radial variant of a 2 pt conical gradient,
184                // an actual radial gradient (startRadius == 0),
185                // or it is fully degenerate (startRadius == endRadius).
186                // We can treat this gradient as a simple radial, which is faster. If we got here,
187                // we know that endRadius is not equal to 0, so this produces a meaningful gradient.
188                return Self::new_radial_unchecked(start_point, end_radius, stops, mode, transform);
189            }
190        }
191
192        create(
193            start_point,
194            start_radius,
195            end_point,
196            end_radius,
197            stops,
198            mode,
199            transform,
200        )
201    }
202
203    /// Creates a simple radial gradient shader without validation.
204    ///
205    /// This is an optimized path for creating radial gradients when the start radius is 0
206    /// and the gradient is known to be valid. The function computes the points-to-unit
207    /// transformation internally based on the center point and radius.
208    ///
209    /// # Parameters
210    /// - `center`: The center point of the radial gradient
211    /// - `radius`: The radius of the gradient (assumed to be > 0)
212    /// - `stops`: Color stops for the gradient (assumed to have length >= 2)
213    /// - `mode`: How the gradient extends beyond its bounds
214    /// - `transform`: The gradient's transformation matrix (assumed to be invertible)
215    fn new_radial_unchecked(
216        center: Point,
217        radius: f32,
218        stops: Vec<GradientStop>,
219        mode: SpreadMode,
220        transform: Transform,
221    ) -> Option<Shader<'static>> {
222        let inv = radius.invert();
223        let points_to_unit = Transform::from_translate(-center.x, -center.y).post_scale(inv, inv);
224
225        Some(Shader::RadialGradient(RadialGradient {
226            base: Gradient::new(stops, mode, transform, points_to_unit),
227            gradient_type: GradientType::Radial {
228                radius1: 0.0,
229                radius2: radius,
230            },
231        }))
232    }
233
234    pub(crate) fn push_stages(&self, cs: ColorSpace, p: &mut RasterPipelineBuilder) -> bool {
235        let (p0, p1) = match self.gradient_type {
236            GradientType::Radial { radius1, radius2 } => {
237                if radius1 == 0.0 {
238                    (1.0, 0.0)
239                } else {
240                    let d_radius = radius2 - radius1;
241                    // For concentric gradients: t = t * scale + bias
242                    let p0 = radius1.max(radius2) / d_radius;
243                    let p1 = -radius1 / d_radius;
244                    (p0, p1)
245                }
246            }
247            GradientType::Strip { scaled_r0 } => {
248                (scaled_r0 * scaled_r0, 0.0 /*unused*/)
249            }
250            GradientType::Focal(fd) => (1.0 / fd.r1, fd.focal_x),
251        };
252
253        p.ctx.two_point_conical_gradient = pipeline::TwoPointConicalGradientCtx {
254            mask: u32x8::default(),
255            p0,
256            p1,
257        };
258
259        self.base.push_stages(
260            p,
261            cs,
262            &|p| {
263                match self.gradient_type {
264                    GradientType::Radial { .. } => {
265                        p.push(pipeline::Stage::XYToRadius);
266                        // Apply scale/bias to map t from [0, 1] based on r_max to proper t where
267                        // t=0 at r0 and t=1 at r1
268                        if (p0, p1) != (1.0, 0.0) {
269                            p.push(pipeline::Stage::ApplyConcentricScaleBias);
270                        }
271                    }
272                    GradientType::Strip { .. } => {
273                        p.push(pipeline::Stage::XYTo2PtConicalStrip);
274                        p.push(pipeline::Stage::Mask2PtConicalNan);
275                    }
276                    GradientType::Focal(fd) => {
277                        if fd.is_focal_on_circle() {
278                            p.push(pipeline::Stage::XYTo2PtConicalFocalOnCircle);
279                        } else if fd.is_well_behaved() {
280                            p.push(pipeline::Stage::XYTo2PtConicalWellBehaved);
281                        } else if fd.is_swapped || (1.0 - fd.focal_x) < 0.0 {
282                            p.push(pipeline::Stage::XYTo2PtConicalSmaller);
283                        } else {
284                            p.push(pipeline::Stage::XYTo2PtConicalGreater);
285                        }
286
287                        if !fd.is_well_behaved() {
288                            p.push(pipeline::Stage::Mask2PtConicalDegenerates);
289                        }
290
291                        if (1.0 - fd.focal_x) < 0.0 {
292                            p.push(pipeline::Stage::NegateX);
293                        }
294
295                        if !fd.is_natively_focal() {
296                            p.push(pipeline::Stage::Alter2PtConicalCompensateFocal);
297                        }
298
299                        if fd.is_swapped {
300                            p.push(pipeline::Stage::Alter2PtConicalUnswap);
301                        }
302                    }
303                }
304            },
305            &|p| match self.gradient_type {
306                GradientType::Strip { .. } => p.push(pipeline::Stage::ApplyVectorMask),
307                GradientType::Focal(fd) if !fd.is_well_behaved() => {
308                    p.push(pipeline::Stage::ApplyVectorMask)
309                }
310                _ => {}
311            },
312        )
313    }
314}
315
316fn create(
317    c0: Point,
318    r0: f32,
319    c1: Point,
320    r1: f32,
321    stops: Vec<GradientStop>,
322    mode: SpreadMode,
323    transform: Transform,
324) -> Option<Shader<'static>> {
325    let mut gradient_type;
326    let mut gradient_matrix;
327
328    if (c0 - c1).length().is_nearly_zero() {
329        if r0.max(r1).is_nearly_zero() || r0.is_nearly_equal(r1) {
330            // Degenerate case; avoid dividing by zero. Should have been caught
331            // by caller but just in case, recheck here.
332            return None;
333        }
334
335        // Concentric case: we can pretend we're radial (with a tiny twist).
336        let scale = 1.0 / r0.max(r1);
337        gradient_matrix = Transform::from_translate(-c1.x, -c1.y).post_scale(scale, scale);
338        gradient_type = GradientType::Radial {
339            radius1: r0,
340            radius2: r1,
341        };
342    } else {
343        gradient_matrix = map_to_unit_x(c0, c1)?;
344        let d_center = (c0 - c1).length();
345        gradient_type = if (r0 - r1).is_nearly_zero() {
346            let scaled_r0 = r0 / d_center;
347            GradientType::Strip { scaled_r0 }
348        } else {
349            GradientType::Focal(FocalData::default())
350        };
351    }
352    if let GradientType::Focal(ref mut focal_data) = &mut gradient_type {
353        let d_center = (c0 - c1).length();
354        if !focal_data.set(r0 / d_center, r1 / d_center, &mut gradient_matrix) {
355            return None;
356        }
357    }
358
359    Some(Shader::RadialGradient(RadialGradient {
360        base: Gradient::new(stops, mode, transform, gradient_matrix),
361        gradient_type,
362    }))
363}
364
365fn map_to_unit_x(origin: Point, x_is_one: Point) -> Option<Transform> {
366    ts_from_poly_to_poly(
367        origin,
368        x_is_one,
369        Point::from_xy(0.0, 0.0),
370        Point::from_xy(1.0, 0.0),
371    )
372}
373
374fn ts_from_poly_to_poly(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Option<Transform> {
375    let tmp = from_poly2(src1, src2);
376    let res = tmp.invert()?;
377    let tmp = from_poly2(dst1, dst2);
378    Some(tmp.pre_concat(res))
379}
380
381fn from_poly2(p0: Point, p1: Point) -> Transform {
382    Transform::from_row(
383        p1.y - p0.y,
384        p0.x - p1.x,
385        p1.x - p0.x,
386        p1.y - p0.y,
387        p0.x,
388        p0.y,
389    )
390}