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::{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)]
22struct FocalData {
23    r1: f32, // r1 after mapping focal point to (0, 0)
24}
25
26impl FocalData {
27    // Whether the focal point (0, 0) is on the end circle with center (1, 0) and radius r1. If
28    // this is true, it's as if an aircraft is flying at Mach 1 and all circles (soundwaves)
29    // will go through the focal point (aircraft). In our previous implementations, this was
30    // known as the edge case where the inside circle touches the outside circle (on the focal
31    // point). If we were to solve for t bruteforcely using a quadratic equation, this case
32    // implies that the quadratic equation degenerates to a linear equation.
33    fn is_focal_on_circle(&self) -> bool {
34        (1.0 - self.r1).is_nearly_zero()
35    }
36
37    fn is_well_behaved(&self) -> bool {
38        !self.is_focal_on_circle() && self.r1 > 1.0
39    }
40}
41
42/// A radial gradient shader.
43///
44/// This is not `SkRadialGradient` like in Skia, but rather `SkTwoPointConicalGradient`
45/// without the start radius.
46#[derive(Clone, PartialEq, Debug)]
47pub struct RadialGradient {
48    pub(crate) base: Gradient,
49    focal_data: Option<FocalData>,
50}
51
52impl RadialGradient {
53    /// Creates a new radial gradient shader.
54    ///
55    /// Returns `Shader::SolidColor` when:
56    /// - `stops.len()` == 1
57    ///
58    /// Returns `None` when:
59    ///
60    /// - `stops` is empty
61    /// - `radius` <= 0
62    /// - `transform` is not invertible
63    #[allow(clippy::new_ret_no_self)]
64    pub fn new(
65        start: Point,
66        end: Point,
67        radius: f32,
68        stops: Vec<GradientStop>,
69        mode: SpreadMode,
70        transform: Transform,
71    ) -> Option<Shader<'static>> {
72        // From SkGradientShader::MakeTwoPointConical
73
74        if radius < 0.0 || radius.is_nearly_zero() {
75            return None;
76        }
77
78        if stops.is_empty() {
79            return None;
80        }
81
82        if stops.len() == 1 {
83            return Some(Shader::SolidColor(stops[0].color));
84        }
85
86        transform.invert()?;
87
88        let length = (end - start).length();
89        if !length.is_finite() {
90            return None;
91        }
92
93        if length.is_nearly_zero_within_tolerance(DEGENERATE_THRESHOLD) {
94            // If the center positions are the same, then the gradient
95            // is the radial variant of a 2 pt conical gradient,
96            // an actual radial gradient (startRadius == 0),
97            // or it is fully degenerate (startRadius == endRadius).
98
99            let inv = radius.invert();
100            let mut ts = Transform::from_translate(-start.x, -start.y);
101            ts = ts.post_scale(inv, inv);
102
103            // We can treat this gradient as radial, which is faster. If we got here, we know
104            // that endRadius is not equal to 0, so this produces a meaningful gradient
105            Some(Shader::RadialGradient(RadialGradient {
106                base: Gradient::new(stops, mode, transform, ts),
107                focal_data: None,
108            }))
109        } else {
110            // From SkTwoPointConicalGradient::Create
111            let mut ts = ts_from_poly_to_poly(
112                start,
113                end,
114                Point::from_xy(0.0, 0.0),
115                Point::from_xy(1.0, 0.0),
116            )?;
117
118            let d_center = (start - end).length();
119            let r1 = radius / d_center;
120            let focal_data = FocalData { r1 };
121
122            // The following transformations are just to accelerate the shader computation by saving
123            // some arithmetic operations.
124            if focal_data.is_focal_on_circle() {
125                ts = ts.post_scale(0.5, 0.5);
126            } else {
127                ts = ts.post_scale(r1 / (r1 * r1 - 1.0), 1.0 / ((r1 * r1 - 1.0).abs()).sqrt());
128            }
129
130            Some(Shader::RadialGradient(RadialGradient {
131                base: Gradient::new(stops, mode, transform, ts),
132                focal_data: Some(focal_data),
133            }))
134        }
135    }
136
137    pub(crate) fn push_stages(&self, p: &mut RasterPipelineBuilder) -> bool {
138        let p0 = if let Some(focal_data) = self.focal_data {
139            1.0 / focal_data.r1
140        } else {
141            1.0
142        };
143
144        p.ctx.two_point_conical_gradient = pipeline::TwoPointConicalGradientCtx {
145            mask: u32x8::default(),
146            p0,
147        };
148
149        self.base.push_stages(
150            p,
151            &|p| {
152                if let Some(focal_data) = self.focal_data {
153                    // Unlike Skia, we have only the Focal radial gradient type.
154
155                    if focal_data.is_focal_on_circle() {
156                        p.push(pipeline::Stage::XYTo2PtConicalFocalOnCircle);
157                    } else if focal_data.is_well_behaved() {
158                        p.push(pipeline::Stage::XYTo2PtConicalWellBehaved);
159                    } else {
160                        p.push(pipeline::Stage::XYTo2PtConicalGreater);
161                    }
162
163                    if !focal_data.is_well_behaved() {
164                        p.push(pipeline::Stage::Mask2PtConicalDegenerates);
165                    }
166                } else {
167                    p.push(pipeline::Stage::XYToRadius);
168                }
169            },
170            &|p| {
171                if let Some(focal_data) = self.focal_data {
172                    if !focal_data.is_well_behaved() {
173                        p.push(pipeline::Stage::ApplyVectorMask);
174                    }
175                }
176            },
177        )
178    }
179}
180
181fn ts_from_poly_to_poly(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Option<Transform> {
182    let tmp = from_poly2(src1, src2);
183    let res = tmp.invert()?;
184    let tmp = from_poly2(dst1, dst2);
185    Some(tmp.pre_concat(res))
186}
187
188fn from_poly2(p0: Point, p1: Point) -> Transform {
189    Transform::from_row(
190        p1.y - p0.y,
191        p0.x - p1.x,
192        p1.x - p0.x,
193        p1.y - p0.y,
194        p0.x,
195        p0.y,
196    )
197}