tiny_skia/shaders/
linear_gradient.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// Copyright 2006 The Android Open Source Project
// Copyright 2020 Yevhenii Reizner
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use alloc::vec::Vec;

use tiny_skia_path::Scalar;

use crate::{Color, GradientStop, Point, Shader, SpreadMode, Transform};

use super::gradient::{Gradient, DEGENERATE_THRESHOLD};
use crate::pipeline::RasterPipelineBuilder;

/// A linear gradient shader.
#[derive(Clone, PartialEq, Debug)]
pub struct LinearGradient {
    pub(crate) base: Gradient,
}

impl LinearGradient {
    /// Creates a new linear gradient shader.
    ///
    /// Returns `Shader::SolidColor` when:
    /// - `stops.len()` == 1
    /// - `start` and `end` are very close
    ///
    /// Returns `None` when:
    ///
    /// - `stops` is empty
    /// - `start` == `end`
    /// - `transform` is not invertible
    #[allow(clippy::new_ret_no_self)]
    pub fn new(
        start: Point,
        end: Point,
        stops: Vec<GradientStop>,
        mode: SpreadMode,
        transform: Transform,
    ) -> Option<Shader<'static>> {
        if stops.is_empty() {
            return None;
        }

        if stops.len() == 1 {
            return Some(Shader::SolidColor(stops[0].color));
        }

        let length = (end - start).length();
        if !length.is_finite() {
            return None;
        }

        if length.is_nearly_zero_within_tolerance(DEGENERATE_THRESHOLD) {
            // Degenerate gradient, the only tricky complication is when in clamp mode,
            // the limit of the gradient approaches two half planes of solid color
            // (first and last). However, they are divided by the line perpendicular
            // to the start and end point, which becomes undefined once start and end
            // are exactly the same, so just use the end color for a stable solution.

            // Except for special circumstances of clamped gradients,
            // every gradient shape (when degenerate) can be mapped to the same fallbacks.
            // The specific shape factories must account for special clamped conditions separately,
            // this will always return the last color for clamped gradients.
            match mode {
                SpreadMode::Pad => {
                    // Depending on how the gradient shape degenerates,
                    // there may be a more specialized fallback representation
                    // for the factories to use, but this is a reasonable default.
                    return Some(Shader::SolidColor(stops.last().unwrap().color));
                }
                SpreadMode::Reflect | SpreadMode::Repeat => {
                    // repeat and mirror are treated the same: the border colors are never visible,
                    // but approximate the final color as infinite repetitions of the colors, so
                    // it can be represented as the average color of the gradient.
                    return Some(Shader::SolidColor(average_gradient_color(&stops)));
                }
            }
        }

        transform.invert()?;

        let unit_ts = points_to_unit_ts(start, end)?;
        Some(Shader::LinearGradient(LinearGradient {
            base: Gradient::new(stops, mode, transform, unit_ts),
        }))
    }

    pub(crate) fn is_opaque(&self) -> bool {
        self.base.colors_are_opaque
    }

    pub(crate) fn push_stages(&self, p: &mut RasterPipelineBuilder) -> bool {
        self.base.push_stages(p, &|_| {}, &|_| {})
    }
}

fn points_to_unit_ts(start: Point, end: Point) -> Option<Transform> {
    let mut vec = end - start;
    let mag = vec.length();
    let inv = if mag != 0.0 { mag.invert() } else { 0.0 };

    vec.scale(inv);

    let mut ts = ts_from_sin_cos_at(-vec.y, vec.x, start.x, start.y);
    ts = ts.post_translate(-start.x, -start.y);
    ts = ts.post_scale(inv, inv);
    Some(ts)
}

fn average_gradient_color(points: &[GradientStop]) -> Color {
    use crate::wide::f32x4;

    fn load_color(c: Color) -> f32x4 {
        f32x4::from([c.red(), c.green(), c.blue(), c.alpha()])
    }

    fn store_color(c: f32x4) -> Color {
        let c: [f32; 4] = c.into();
        Color::from_rgba(c[0], c[1], c[2], c[3]).unwrap()
    }

    assert!(!points.is_empty());

    // The gradient is a piecewise linear interpolation between colors. For a given interval,
    // the integral between the two endpoints is 0.5 * (ci + cj) * (pj - pi), which provides that
    // intervals average color. The overall average color is thus the sum of each piece. The thing
    // to keep in mind is that the provided gradient definition may implicitly use p=0 and p=1.
    let mut blend = f32x4::splat(0.0);

    // Bake 1/(colorCount - 1) uniform stop difference into this scale factor
    let w_scale = f32x4::splat(0.5);

    for i in 0..points.len() - 1 {
        // Calculate the average color for the interval between pos(i) and pos(i+1)
        let c0 = load_color(points[i].color);
        let c1 = load_color(points[i + 1].color);
        // when pos == null, there are colorCount uniformly distributed stops, going from 0 to 1,
        // so pos[i + 1] - pos[i] = 1/(colorCount-1)
        let w = points[i + 1].position.get() - points[i].position.get();
        blend += w_scale * f32x4::splat(w) * (c1 + c0);
    }

    // Now account for any implicit intervals at the start or end of the stop definitions
    if points[0].position.get() > 0.0 {
        // The first color is fixed between p = 0 to pos[0], so 0.5 * (ci + cj) * (pj - pi)
        // becomes 0.5 * (c + c) * (pj - 0) = c * pj
        let c = load_color(points[0].color);
        blend += f32x4::splat(points[0].position.get()) * c;
    }

    let last_idx = points.len() - 1;
    if points[last_idx].position.get() < 1.0 {
        // The last color is fixed between pos[n-1] to p = 1, so 0.5 * (ci + cj) * (pj - pi)
        // becomes 0.5 * (c + c) * (1 - pi) = c * (1 - pi)
        let c = load_color(points[last_idx].color);
        blend += (f32x4::splat(1.0) - f32x4::splat(points[last_idx].position.get())) * c;
    }

    store_color(blend)
}

fn ts_from_sin_cos_at(sin: f32, cos: f32, px: f32, py: f32) -> Transform {
    let cos_inv = 1.0 - cos;
    Transform::from_row(
        cos,
        sin,
        -sin,
        cos,
        sdot(sin, py, cos_inv, px),
        sdot(-sin, px, cos_inv, py),
    )
}

fn sdot(a: f32, b: f32, c: f32, d: f32) -> f32 {
    a * b + c * d
}