tiny_skia/shaders/
linear_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::{Color, GradientStop, Point, Shader, SpreadMode, Transform};
12
13use super::gradient::{Gradient, DEGENERATE_THRESHOLD};
14use crate::pipeline::RasterPipelineBuilder;
15
16/// A linear gradient shader.
17#[derive(Clone, PartialEq, Debug)]
18pub struct LinearGradient {
19    pub(crate) base: Gradient,
20}
21
22impl LinearGradient {
23    /// Creates a new linear gradient shader.
24    ///
25    /// Returns `Shader::SolidColor` when:
26    /// - `stops.len()` == 1
27    /// - `start` and `end` are very close
28    ///
29    /// Returns `None` when:
30    ///
31    /// - `stops` is empty
32    /// - `start` == `end`
33    /// - `transform` is not invertible
34    #[allow(clippy::new_ret_no_self)]
35    pub fn new(
36        start: Point,
37        end: Point,
38        stops: Vec<GradientStop>,
39        mode: SpreadMode,
40        transform: Transform,
41    ) -> Option<Shader<'static>> {
42        if stops.is_empty() {
43            return None;
44        }
45
46        if stops.len() == 1 {
47            return Some(Shader::SolidColor(stops[0].color));
48        }
49
50        let length = (end - start).length();
51        if !length.is_finite() {
52            return None;
53        }
54
55        if length.is_nearly_zero_within_tolerance(DEGENERATE_THRESHOLD) {
56            // Degenerate gradient, the only tricky complication is when in clamp mode,
57            // the limit of the gradient approaches two half planes of solid color
58            // (first and last). However, they are divided by the line perpendicular
59            // to the start and end point, which becomes undefined once start and end
60            // are exactly the same, so just use the end color for a stable solution.
61
62            // Except for special circumstances of clamped gradients,
63            // every gradient shape (when degenerate) can be mapped to the same fallbacks.
64            // The specific shape factories must account for special clamped conditions separately,
65            // this will always return the last color for clamped gradients.
66            match mode {
67                SpreadMode::Pad => {
68                    // Depending on how the gradient shape degenerates,
69                    // there may be a more specialized fallback representation
70                    // for the factories to use, but this is a reasonable default.
71                    return Some(Shader::SolidColor(stops.last().unwrap().color));
72                }
73                SpreadMode::Reflect | SpreadMode::Repeat => {
74                    // repeat and mirror are treated the same: the border colors are never visible,
75                    // but approximate the final color as infinite repetitions of the colors, so
76                    // it can be represented as the average color of the gradient.
77                    return Some(Shader::SolidColor(average_gradient_color(&stops)));
78                }
79            }
80        }
81
82        transform.invert()?;
83
84        let unit_ts = points_to_unit_ts(start, end)?;
85        Some(Shader::LinearGradient(LinearGradient {
86            base: Gradient::new(stops, mode, transform, unit_ts),
87        }))
88    }
89
90    pub(crate) fn is_opaque(&self) -> bool {
91        self.base.colors_are_opaque
92    }
93
94    pub(crate) fn push_stages(&self, p: &mut RasterPipelineBuilder) -> bool {
95        self.base.push_stages(p, &|_| {}, &|_| {})
96    }
97}
98
99fn points_to_unit_ts(start: Point, end: Point) -> Option<Transform> {
100    let mut vec = end - start;
101    let mag = vec.length();
102    let inv = if mag != 0.0 { mag.invert() } else { 0.0 };
103
104    vec.scale(inv);
105
106    let mut ts = ts_from_sin_cos_at(-vec.y, vec.x, start.x, start.y);
107    ts = ts.post_translate(-start.x, -start.y);
108    ts = ts.post_scale(inv, inv);
109    Some(ts)
110}
111
112fn average_gradient_color(points: &[GradientStop]) -> Color {
113    use crate::wide::f32x4;
114
115    fn load_color(c: Color) -> f32x4 {
116        f32x4::from([c.red(), c.green(), c.blue(), c.alpha()])
117    }
118
119    fn store_color(c: f32x4) -> Color {
120        let c: [f32; 4] = c.into();
121        Color::from_rgba(c[0], c[1], c[2], c[3]).unwrap()
122    }
123
124    assert!(!points.is_empty());
125
126    // The gradient is a piecewise linear interpolation between colors. For a given interval,
127    // the integral between the two endpoints is 0.5 * (ci + cj) * (pj - pi), which provides that
128    // intervals average color. The overall average color is thus the sum of each piece. The thing
129    // to keep in mind is that the provided gradient definition may implicitly use p=0 and p=1.
130    let mut blend = f32x4::splat(0.0);
131
132    // Bake 1/(colorCount - 1) uniform stop difference into this scale factor
133    let w_scale = f32x4::splat(0.5);
134
135    for i in 0..points.len() - 1 {
136        // Calculate the average color for the interval between pos(i) and pos(i+1)
137        let c0 = load_color(points[i].color);
138        let c1 = load_color(points[i + 1].color);
139        // when pos == null, there are colorCount uniformly distributed stops, going from 0 to 1,
140        // so pos[i + 1] - pos[i] = 1/(colorCount-1)
141        let w = points[i + 1].position.get() - points[i].position.get();
142        blend += w_scale * f32x4::splat(w) * (c1 + c0);
143    }
144
145    // Now account for any implicit intervals at the start or end of the stop definitions
146    if points[0].position.get() > 0.0 {
147        // The first color is fixed between p = 0 to pos[0], so 0.5 * (ci + cj) * (pj - pi)
148        // becomes 0.5 * (c + c) * (pj - 0) = c * pj
149        let c = load_color(points[0].color);
150        blend += f32x4::splat(points[0].position.get()) * c;
151    }
152
153    let last_idx = points.len() - 1;
154    if points[last_idx].position.get() < 1.0 {
155        // The last color is fixed between pos[n-1] to p = 1, so 0.5 * (ci + cj) * (pj - pi)
156        // becomes 0.5 * (c + c) * (1 - pi) = c * (1 - pi)
157        let c = load_color(points[last_idx].color);
158        blend += (f32x4::splat(1.0) - f32x4::splat(points[last_idx].position.get())) * c;
159    }
160
161    store_color(blend)
162}
163
164fn ts_from_sin_cos_at(sin: f32, cos: f32, px: f32, py: f32) -> Transform {
165    let cos_inv = 1.0 - cos;
166    Transform::from_row(
167        cos,
168        sin,
169        -sin,
170        cos,
171        sdot(sin, py, cos_inv, px),
172        sdot(-sin, px, cos_inv, py),
173    )
174}
175
176fn sdot(a: f32, b: f32, c: f32, d: f32) -> f32 {
177    a * b + c * d
178}