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 | |
7 | use alloc::vec::Vec; |
8 | |
9 | use tiny_skia_path::Scalar; |
10 | |
11 | use crate::{Color, GradientStop, Point, Shader, SpreadMode, Transform}; |
12 | |
13 | use super::gradient::{Gradient, DEGENERATE_THRESHOLD}; |
14 | use crate::pipeline::RasterPipelineBuilder; |
15 | |
16 | /// A linear gradient shader. |
17 | #[derive (Clone, PartialEq, Debug)] |
18 | pub struct LinearGradient { |
19 | pub(crate) base: Gradient, |
20 | } |
21 | |
22 | impl 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 | |
99 | fn 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 | |
112 | fn 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 | |
164 | fn 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 | |
176 | fn sdot(a: f32, b: f32, c: f32, d: f32) -> f32 { |
177 | a * b + c * d |
178 | } |
179 | |