| 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 | |