use std::f32::consts::TAU;
#[derive(Clone, Debug, PartialEq)]
pub struct TextGradient {
pub kind: TextGradientKind,
pub stops: Vec<TextGradientStop>,
pub spread: TextSpread,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TextGradientKind {
Linear {
start: [f32; 2],
end: [f32; 2],
},
Radial {
center: [f32; 2],
radius: f32,
},
Conic {
center: [f32; 2],
angle: f32,
},
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TextGradientStop {
pub offset: f32,
pub color: [f32; 4],
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum TextSpread {
#[default]
Pad,
Repeat,
Reflect,
}
pub(crate) const MAX_STOPS: usize = 16;
impl TextGradient {
#[allow(clippy::float_cmp)]
pub(crate) fn is_valid(&self) -> bool {
let finite = |point: [f32; 2]| point.iter().all(|value| value.is_finite());
let geometry = match self.kind {
TextGradientKind::Linear { start, end } => finite(start) && finite(end) && start != end,
TextGradientKind::Radial { center, radius } => {
finite(center) && radius.is_finite() && radius > 0.0
}
TextGradientKind::Conic { center, angle } => finite(center) && angle.is_finite(),
};
geometry
&& (2..=MAX_STOPS).contains(&self.stops.len())
&& self.stops.iter().all(|stop| {
(0.0..=1.0).contains(&stop.offset)
&& stop.color.iter().all(|value| (0.0..=1.0).contains(value))
})
&& self
.stops
.windows(2)
.all(|pair| pair[0].offset <= pair[1].offset)
}
pub(crate) fn prepared(&self, size: [f32; 2]) -> PreparedGradient<'_> {
let at = |point: [f32; 2]| [point[0] * size[0], point[1] * size[1]];
let kind = match self.kind {
TextGradientKind::Linear { start, end } => TextGradientKind::Linear {
start: at(start),
end: at(end),
},
TextGradientKind::Radial { center, radius } => TextGradientKind::Radial {
center: at(center),
radius: radius * size[0].min(size[1]),
},
TextGradientKind::Conic { center, angle } => TextGradientKind::Conic {
center: at(center),
angle,
},
};
PreparedGradient {
gradient: self,
kind,
colors: self
.stops
.iter()
.map(|stop| premultiplied_linear(stop.color))
.collect(),
}
}
}
pub(crate) struct PreparedGradient<'a> {
gradient: &'a TextGradient,
kind: TextGradientKind,
colors: Vec<[f32; 4]>,
}
impl PreparedGradient<'_> {
pub(crate) fn rgba8_at(&self, point: [f32; 2]) -> [u8; 4] {
stored_rgba8(self.linear_at(point))
}
pub(crate) fn linear_at(&self, point: [f32; 2]) -> [f32; 4] {
let gradient = self.gradient;
let t = spread(self.kind, gradient.spread, position(self.kind, point));
let stops = &gradient.stops;
if t <= stops[0].offset {
return self.colors[0];
}
for index in 1..stops.len() {
if t <= stops[index].offset {
let span = stops[index].offset - stops[index - 1].offset;
let weight = if span <= 0.0 {
1.0
} else {
(t - stops[index - 1].offset) / span
};
let (from, to) = (self.colors[index - 1], self.colors[index]);
return std::array::from_fn(|channel| {
from[channel] * (1.0 - weight) + to[channel] * weight
});
}
}
self.colors[stops.len() - 1]
}
}
fn position(kind: TextGradientKind, point: [f32; 2]) -> f32 {
match kind {
TextGradientKind::Linear { start, end } => {
let direction = [end[0] - start[0], end[1] - start[1]];
let from = [point[0] - start[0], point[1] - start[1]];
dot(from, direction) / dot(direction, direction).max(0.000_001)
}
TextGradientKind::Radial { center, radius } => {
let from = [point[0] - center[0], point[1] - center[1]];
dot(from, from).sqrt() / radius.max(0.000_001)
}
TextGradientKind::Conic { center, angle } => {
let from = [point[0] - center[0], point[1] - center[1]];
if dot(from, from) <= 0.0 {
return 0.0;
}
let turn = (libm::atan2f(from[0], -from[1]) - angle) / TAU;
turn - turn.floor()
}
}
}
fn spread(kind: TextGradientKind, spread: TextSpread, t: f32) -> f32 {
if matches!(kind, TextGradientKind::Conic { .. }) {
return t;
}
match spread {
TextSpread::Pad => t.clamp(0.0, 1.0),
TextSpread::Repeat => t - t.floor(),
TextSpread::Reflect => {
let turn = t - 2.0 * (t * 0.5).floor();
if turn > 1.0 { 2.0 - turn } else { turn }
}
}
}
fn dot(left: [f32; 2], right: [f32; 2]) -> f32 {
left[0] * right[0] + left[1] * right[1]
}
pub(crate) fn premultiplied_linear(color: [f32; 4]) -> [f32; 4] {
let alpha = color[3].clamp(0.0, 1.0);
let linear = |value: f32| {
let value = value.clamp(0.0, 1.0);
if value >= 1.0 {
1.0
} else if value <= 0.040_45 {
value / 12.92
} else {
libm::powf((value + 0.055) / 1.055, 2.4)
}
};
[
linear(color[0]) * alpha,
linear(color[1]) * alpha,
linear(color[2]) * alpha,
alpha,
]
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn stored_rgba8(color: [f32; 4]) -> [u8; 4] {
let alpha = color[3].clamp(0.0, 1.0);
if alpha <= 0.0 {
return [0; 4];
}
let encode = |value: f32| {
let value = (value / alpha).clamp(0.0, 1.0);
if value >= 1.0 {
1.0
} else if value <= 0.003_130_8 {
value * 12.92
} else {
1.055 * libm::powf(value, 1.0 / 2.4) - 0.055
}
};
let byte = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
[
byte(encode(color[0])),
byte(encode(color[1])),
byte(encode(color[2])),
byte(alpha),
]
}