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: Point = end - start;
101 let mag: f32 = vec.length();
102 let inv: f32 = if mag != 0.0 { mag.invert() } else { 0.0 };
103
104 vec.scale(inv);
105
106 let mut ts: Transform = ts_from_sin_cos_at(-vec.y, cos:vec.x, px:start.x, py:start.y);
107 ts = ts.post_translate(-start.x, -start.y);
108 ts = ts.post_scale(sx:inv, sy: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: f32 = 1.0 - cos;
166 Transform::from_row(
167 sx:cos,
168 ky:sin,
169 -sin,
170 sy:cos,
171 tx:sdot(sin, py, cos_inv, px),
172 ty:sdot(-sin, b:px, c:cos_inv, d:py),
173 )
174}
175
176fn sdot(a: f32, b: f32, c: f32, d: f32) -> f32 {
177 a * b + c * d
178}
179