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;
#[derive(Clone, PartialEq, Debug)]
pub struct LinearGradient {
pub(crate) base: Gradient,
}
impl LinearGradient {
#[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) {
match mode {
SpreadMode::Pad => {
return Some(Shader::SolidColor(stops.last().unwrap().color));
}
SpreadMode::Reflect | SpreadMode::Repeat => {
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());
let mut blend = f32x4::splat(0.0);
let w_scale = f32x4::splat(0.5);
for i in 0..points.len() - 1 {
let c0 = load_color(points[i].color);
let c1 = load_color(points[i + 1].color);
let w = points[i + 1].position.get() - points[i].position.get();
blend += w_scale * f32x4::splat(w) * (c1 + c0);
}
if points[0].position.get() > 0.0 {
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 {
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
}