use crate::{Alignment, AlignmentGeometry, Color, TextDirection};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TileMode {
#[default]
Clamp,
Repeated,
Mirror,
Decal,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearGradient {
pub begin: AlignmentGeometry,
pub end: AlignmentGeometry,
pub colors: Vec<Color>,
pub stops: Option<Vec<f32>>,
pub tile_mode: TileMode,
pub rotation: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RadialGradient {
pub center: AlignmentGeometry,
pub radius: f32,
pub colors: Vec<Color>,
pub stops: Option<Vec<f32>>,
pub tile_mode: TileMode,
pub focal: Option<AlignmentGeometry>,
pub focal_radius: f32,
pub rotation: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SweepGradient {
pub center: AlignmentGeometry,
pub start_angle: f32,
pub end_angle: f32,
pub colors: Vec<Color>,
pub stops: Option<Vec<f32>>,
pub tile_mode: TileMode,
pub rotation: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Gradient {
Linear(LinearGradient),
Radial(RadialGradient),
Sweep(SweepGradient),
}
fn physical(a: Alignment) -> AlignmentGeometry {
AlignmentGeometry::Physical(a)
}
impl LinearGradient {
pub fn new(colors: impl Into<Vec<Color>>) -> Self {
Self {
begin: physical(Alignment::CENTER_LEFT),
end: physical(Alignment::CENTER_RIGHT),
colors: colors.into(),
stops: None,
tile_mode: TileMode::Clamp,
rotation: 0.0,
}
}
#[must_use]
pub fn between(
mut self,
begin: impl Into<AlignmentGeometry>,
end: impl Into<AlignmentGeometry>,
) -> Self {
self.begin = begin.into();
self.end = end.into();
self
}
}
impl RadialGradient {
pub fn new(colors: impl Into<Vec<Color>>) -> Self {
Self {
center: physical(Alignment::CENTER),
radius: 0.5,
colors: colors.into(),
stops: None,
tile_mode: TileMode::Clamp,
focal: None,
focal_radius: 0.0,
rotation: 0.0,
}
}
}
impl SweepGradient {
pub fn new(colors: impl Into<Vec<Color>>) -> Self {
Self {
center: physical(Alignment::CENTER),
start_angle: 0.0,
end_angle: std::f32::consts::TAU,
colors: colors.into(),
stops: None,
tile_mode: TileMode::Clamp,
rotation: 0.0,
}
}
}
impl From<LinearGradient> for Gradient {
fn from(g: LinearGradient) -> Self {
Gradient::Linear(g)
}
}
impl From<RadialGradient> for Gradient {
fn from(g: RadialGradient) -> Self {
Gradient::Radial(g)
}
}
impl From<SweepGradient> for Gradient {
fn from(g: SweepGradient) -> Self {
Gradient::Sweep(g)
}
}
impl Gradient {
pub fn colors(&self) -> &[Color] {
match self {
Gradient::Linear(g) => &g.colors,
Gradient::Radial(g) => &g.colors,
Gradient::Sweep(g) => &g.colors,
}
}
fn colors_mut(&mut self) -> &mut Vec<Color> {
match self {
Gradient::Linear(g) => &mut g.colors,
Gradient::Radial(g) => &mut g.colors,
Gradient::Sweep(g) => &mut g.colors,
}
}
pub fn stops(&self) -> Option<&[f32]> {
match self {
Gradient::Linear(g) => g.stops.as_deref(),
Gradient::Radial(g) => g.stops.as_deref(),
Gradient::Sweep(g) => g.stops.as_deref(),
}
}
pub fn tile_mode(&self) -> TileMode {
match self {
Gradient::Linear(g) => g.tile_mode,
Gradient::Radial(g) => g.tile_mode,
Gradient::Sweep(g) => g.tile_mode,
}
}
pub fn rotation(&self) -> f32 {
match self {
Gradient::Linear(g) => g.rotation,
Gradient::Radial(g) => g.rotation,
Gradient::Sweep(g) => g.rotation,
}
}
pub fn resolved_stops(&self) -> Vec<f32> {
let n = self.colors().len();
match self.stops() {
Some(stops) if stops.len() == n => stops.to_vec(),
_ if n <= 1 => vec![0.0; n],
_ => (0..n).map(|i| i as f32 / (n - 1) as f32).collect(),
}
}
pub fn sample(&self, t: f32) -> Color {
let colors = self.colors();
match colors {
[] => Color::TRANSPARENT,
[only] => *only,
_ => {
let stops = self.resolved_stops();
if t <= stops[0] {
return colors[0];
}
for i in 1..colors.len() {
if t <= stops[i] {
let span = stops[i] - stops[i - 1];
let k = if span > 0.0 {
(t - stops[i - 1]) / span
} else {
1.0
};
return colors[i - 1].lerp(colors[i], k);
}
}
colors[colors.len() - 1]
}
}
}
#[must_use]
pub fn resolve(&self, direction: TextDirection) -> Gradient {
let fix = |a: AlignmentGeometry| physical(a.resolve(direction));
match self {
Gradient::Linear(g) => Gradient::Linear(LinearGradient {
begin: fix(g.begin),
end: fix(g.end),
..g.clone()
}),
Gradient::Radial(g) => Gradient::Radial(RadialGradient {
center: fix(g.center),
focal: g.focal.map(fix),
..g.clone()
}),
Gradient::Sweep(g) => Gradient::Sweep(SweepGradient {
center: fix(g.center),
..g.clone()
}),
}
}
#[must_use]
pub fn scale(&self, factor: f32) -> Gradient {
let mut g = self.clone();
for c in g.colors_mut() {
*c = c.fade(factor);
}
g
}
#[must_use]
pub fn lerp(a: Option<&Gradient>, b: Option<&Gradient>, t: f32) -> Option<Gradient> {
let mix = |x: f32, y: f32| x + (y - x) * t;
let mix_a = |x: AlignmentGeometry, y: AlignmentGeometry| {
let (x, y) = (x.resolve(TextDirection::Ltr), y.resolve(TextDirection::Ltr));
physical(Alignment::new(mix(x.x, y.x), mix(x.y, y.y)))
};
let colors = |x: &[Color], y: &[Color]| -> Vec<Color> {
x.iter().zip(y).map(|(p, q)| p.lerp(*q, t)).collect()
};
let stops = |x: &Gradient, y: &Gradient| -> Option<Vec<f32>> {
if x.stops().is_none() && y.stops().is_none() {
return None;
}
Some(
x.resolved_stops()
.iter()
.zip(y.resolved_stops())
.map(|(p, q)| mix(*p, q))
.collect(),
)
};
match (a, b) {
(None, None) => None,
(Some(a), None) => Some(a.scale(1.0 - t)),
(None, Some(b)) => Some(b.scale(t)),
(Some(a), Some(b)) if a.colors().len() == b.colors().len() => {
let tile = if t < 0.5 {
a.tile_mode()
} else {
b.tile_mode()
};
match (a, b) {
(Gradient::Linear(x), Gradient::Linear(y)) => {
Some(Gradient::Linear(LinearGradient {
begin: mix_a(x.begin, y.begin),
end: mix_a(x.end, y.end),
colors: colors(&x.colors, &y.colors),
stops: stops(a, b),
tile_mode: tile,
rotation: mix(x.rotation, y.rotation),
}))
}
(Gradient::Radial(x), Gradient::Radial(y)) => {
Some(Gradient::Radial(RadialGradient {
center: mix_a(x.center, y.center),
radius: mix(x.radius, y.radius),
colors: colors(&x.colors, &y.colors),
stops: stops(a, b),
tile_mode: tile,
focal: match (x.focal, y.focal) {
(None, None) => None,
(p, q) => Some(mix_a(p.unwrap_or(x.center), q.unwrap_or(y.center))),
},
focal_radius: mix(x.focal_radius, y.focal_radius),
rotation: mix(x.rotation, y.rotation),
}))
}
(Gradient::Sweep(x), Gradient::Sweep(y)) => {
Some(Gradient::Sweep(SweepGradient {
center: mix_a(x.center, y.center),
start_angle: mix(x.start_angle, y.start_angle),
end_angle: mix(x.end_angle, y.end_angle),
colors: colors(&x.colors, &y.colors),
stops: stops(a, b),
tile_mode: tile,
rotation: mix(x.rotation, y.rotation),
}))
}
_ => Some(if t < 0.5 {
a.scale(1.0 - 2.0 * t)
} else {
b.scale(2.0 * t - 1.0)
}),
}
}
(Some(a), Some(b)) => Some(if t < 0.5 {
a.scale(1.0 - 2.0 * t)
} else {
b.scale(2.0 * t - 1.0)
}),
}
}
}
pub fn tile(t: f32, mode: TileMode) -> Option<f32> {
match mode {
TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
TileMode::Repeated => Some(t - t.floor()),
TileMode::Mirror => {
let m = t.rem_euclid(2.0);
Some(if m <= 1.0 { m } else { 2.0 - m })
}
TileMode::Decal => (0.0..=1.0).contains(&t).then_some(t),
}
}
#[cfg(test)]
mod tests {
use super::*;
const RED: Color = Color::rgb(1.0, 0.0, 0.0);
const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
#[test]
fn colours_are_sampled_between_their_stops() {
let even = Gradient::from(LinearGradient::new(vec![RED, GREEN, BLUE]));
assert_eq!(even.resolved_stops(), vec![0.0, 0.5, 1.0]);
assert_eq!(even.sample(0.0), RED);
assert_eq!(even.sample(0.5), GREEN);
assert_eq!(even.sample(0.25), RED.lerp(GREEN, 0.5));
assert_eq!(even.sample(1.0), BLUE);
let mut stopped = LinearGradient::new(vec![RED, BLUE]);
stopped.stops = Some(vec![0.2, 0.6]);
let stopped = Gradient::from(stopped);
assert_eq!(stopped.sample(0.1), RED, "held before the first stop");
let mid = stopped.sample(0.4);
assert!(
(mid.r - 0.5).abs() < 1e-6 && (mid.b - 0.5).abs() < 1e-6,
"half way between the stops: {mid:?}"
);
assert_eq!(stopped.sample(0.9), BLUE, "and after the last");
let mut wrong = LinearGradient::new(vec![RED, GREEN, BLUE]);
wrong.stops = Some(vec![0.0, 1.0]);
assert_eq!(Gradient::from(wrong).resolved_stops(), vec![0.0, 0.5, 1.0]);
}
#[test]
fn the_tile_modes() {
assert_eq!(tile(1.4, TileMode::Clamp), Some(1.0));
assert_eq!(tile(-0.3, TileMode::Clamp), Some(0.0));
assert!((tile(1.25, TileMode::Repeated).unwrap() - 0.25).abs() < 1e-6);
assert!((tile(1.25, TileMode::Mirror).unwrap() - 0.75).abs() < 1e-6);
assert!((tile(-0.25, TileMode::Mirror).unwrap() - 0.25).abs() < 1e-6);
assert_eq!(tile(1.25, TileMode::Decal), None);
assert_eq!(tile(0.5, TileMode::Decal), Some(0.5));
}
#[test]
fn the_defaults_are_the_reference_s() {
let l = LinearGradient::new(vec![RED, BLUE]);
assert_eq!(l.begin, physical(Alignment::CENTER_LEFT));
assert_eq!(l.end, physical(Alignment::CENTER_RIGHT));
let r = RadialGradient::new(vec![RED, BLUE]);
assert_eq!((r.radius, r.focal, r.focal_radius), (0.5, None, 0.0));
let s = SweepGradient::new(vec![RED, BLUE]);
assert_eq!((s.start_angle, s.end_angle), (0.0, std::f32::consts::TAU));
assert_eq!(l.tile_mode, TileMode::Clamp);
}
#[test]
fn gradients_lerp_as_the_reference_s() {
let a = Gradient::from(LinearGradient::new(vec![RED, RED]));
let b = Gradient::from(
LinearGradient::new(vec![BLUE, BLUE])
.between(Alignment::TOP_CENTER, Alignment::BOTTOM_CENTER),
);
let mid = Gradient::lerp(Some(&a), Some(&b), 0.5).unwrap();
match &mid {
Gradient::Linear(g) => {
assert_eq!(g.colors[0], RED.lerp(BLUE, 0.5));
assert_eq!(g.begin, physical(Alignment::new(-0.5, -0.5)));
}
other => panic!("{other:?}"),
}
let fading = Gradient::lerp(Some(&a), None, 0.25).unwrap();
assert!((fading.colors()[0].a - 0.75).abs() < 1e-6);
let sweep = Gradient::from(SweepGradient::new(vec![GREEN, GREEN]));
assert!(matches!(
Gradient::lerp(Some(&a), Some(&sweep), 0.25),
Some(Gradient::Linear(_))
));
assert!(matches!(
Gradient::lerp(Some(&a), Some(&sweep), 0.75),
Some(Gradient::Sweep(_))
));
}
#[test]
fn a_faded_gradient_rectangle_carries_the_fade() {
let mut scene = crate::Scene::new();
scene.shaded_rect(
crate::Rect::new(0.0, 0.0, 10.0, 10.0),
Gradient::from(LinearGradient::new(vec![RED, BLUE])),
0.5,
0.0,
0.0,
Color::TRANSPARENT,
);
match &scene.primitives()[0] {
crate::Primitive::Rect {
shader: Some(g),
color,
..
} => {
assert_eq!(g.colors()[1].a, 0.5);
assert_eq!(
*color,
RED.fade(0.5),
"the first colour, for what reads a colour"
);
}
other => panic!("{other:?}"),
}
}
}