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
}