Skip to main content

tiny_skia/shaders/
sweep_gradient.rs

1// Copyright 2006 The Android Open Source Project
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5
6use alloc::vec::Vec;
7
8use tiny_skia_path::Scalar;
9
10use crate::{ColorSpace, GradientStop, Point, Shader, SpreadMode, Transform};
11
12use super::gradient::{Gradient, DEGENERATE_THRESHOLD};
13use crate::pipeline::{RasterPipelineBuilder, Stage};
14
15#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
16use tiny_skia_path::NoStdFloat;
17
18/// A radial gradient.
19#[derive(Clone, PartialEq, Debug)]
20pub struct SweepGradient {
21    pub(crate) base: Gradient,
22    t0: f32,
23    t1: f32,
24}
25
26impl SweepGradient {
27    /// Creates a new 2-point conical gradient shader.
28    #[allow(clippy::new_ret_no_self)]
29    pub fn new(
30        center: Point,
31        start_angle: f32,
32        end_angle: f32,
33        stops: Vec<GradientStop>,
34        mut mode: SpreadMode,
35        transform: Transform,
36    ) -> Option<Shader<'static>> {
37        if !start_angle.is_finite() || !end_angle.is_finite() || start_angle > end_angle {
38            return None;
39        }
40
41        match stops.as_slice() {
42            [] => return None,
43            [stop] => return Some(Shader::SolidColor(stop.color)),
44            _ => (),
45        }
46        transform.invert()?;
47        if start_angle.is_nearly_equal_within_tolerance(end_angle, DEGENERATE_THRESHOLD) {
48            if mode == SpreadMode::Pad && end_angle > DEGENERATE_THRESHOLD {
49                // In this case, the first color is repeated from 0 to the angle, then a hardstop
50                // switches to the last color (all other colors are compressed to the infinitely
51                // thin interpolation region).
52                let front_color = stops.first().unwrap().color;
53                let back_color = stops.last().unwrap().color;
54                let mut new_stops = stops;
55                new_stops.clear();
56                new_stops.extend_from_slice(&[
57                    GradientStop::new(0.0, front_color),
58                    GradientStop::new(1.0, front_color),
59                    GradientStop::new(1.0, back_color),
60                ]);
61                return SweepGradient::new(center, 0.0, end_angle, new_stops, mode, transform);
62            }
63            // TODO: Consider making a degenerate fallback shader similar to Skia. Tiny Skia
64            // currently opts to return `None` in some places.
65            return None;
66        }
67        if start_angle <= 0.0 && end_angle >= 360.0 {
68            mode = SpreadMode::Pad;
69        }
70        let t0 = start_angle / 360.0;
71        let t1 = end_angle / 360.0;
72        Some(Shader::SweepGradient(SweepGradient {
73            base: Gradient::new(
74                stops,
75                mode,
76                transform,
77                Transform::from_translate(-center.x, -center.y),
78            ),
79            t0,
80            t1,
81        }))
82    }
83
84    pub(crate) fn is_opaque(&self) -> bool {
85        self.base.colors_are_opaque
86    }
87
88    pub(crate) fn push_stages(&self, cs: ColorSpace, p: &mut RasterPipelineBuilder) -> bool {
89        let scale = 1.0 / (self.t1 - self.t0);
90        let bias = -scale * self.t0;
91        p.ctx.two_point_conical_gradient.p0 = scale;
92        p.ctx.two_point_conical_gradient.p1 = bias;
93        self.base.push_stages(
94            p,
95            cs,
96            &|p| {
97                p.push(Stage::XYToUnitAngle);
98                if scale != 1.0 && bias != 0.0 {
99                    p.push(Stage::ApplyConcentricScaleBias)
100                }
101            },
102            &|_| {},
103        )
104    }
105}