use crate::display_list::ImageSampling;
use incular_core::finite_or_zero;
use incular_core::{Color, Offset};
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
static NEXT_GRADIENT_ID: AtomicU64 = AtomicU64::new(1);
fn next_gradient_id() -> GradientId {
GradientId(
NEXT_GRADIENT_ID
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
.expect("gradient identity space exhausted"),
)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct GradientId(u64);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GradientStop {
pub offset: f32,
pub color: Color,
}
#[derive(Clone, Debug)]
pub struct GradientStops {
id: GradientId,
stops: Arc<[GradientStop]>,
}
impl PartialEq for GradientStops {
fn eq(&self, other: &Self) -> bool {
self.stops == other.stops
}
}
#[must_use]
pub fn sample_gradient_stops(stops: &GradientStops, t: f32) -> [f32; 4] {
let stops = stops.as_slice();
let t = finite_or_zero(t);
if t <= stops[0].offset {
return premultiplied(stops[0].color);
}
let last = stops.last().expect("normalized stops are non-empty");
if t >= last.offset {
return premultiplied(last.color);
}
let mut left = 0;
for index in 1..stops.len() {
if stops[index].offset <= t {
left = index;
} else {
let a = stops[left];
let b = stops[index];
let amount = ((t - a.offset) / (b.offset - a.offset)).clamp(0., 1.);
let a = premultiplied(a.color);
let b = premultiplied(b.color);
return [
a[0] + (b[0] - a[0]) * amount,
a[1] + (b[1] - a[1]) * amount,
a[2] + (b[2] - a[2]) * amount,
a[3] + (b[3] - a[3]) * amount,
];
}
}
premultiplied(last.color)
}
fn premultiplied(color: Color) -> [f32; 4] {
crate::premultiplied_working_rgba(color)
}
#[must_use]
pub fn linear_gradient_t(start: Offset, end: Offset, point: Offset) -> f32 {
let dx = end.x - start.x;
let dy = end.y - start.y;
let length_squared = dx * dx + dy * dy;
if !length_squared.is_finite() || length_squared <= f32::EPSILON {
return 1.;
}
(((point.x - start.x) * dx + (point.y - start.y) * dy) / length_squared).clamp(0., 1.)
}
#[must_use]
pub fn radial_gradient_t(center: Offset, radius: f32, point: Offset) -> f32 {
if !radius.is_finite() || radius <= 0. {
return 1.;
}
(((point.x - center.x).hypot(point.y - center.y)) / radius).clamp(0., 1.)
}
impl GradientStops {
#[must_use]
pub fn new(mut stops: Vec<GradientStop>) -> Self {
for s in &mut stops {
s.offset = if s.offset.is_finite() {
s.offset.clamp(0., 1.)
} else {
0.
};
}
stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
if stops.is_empty() {
stops.push(GradientStop {
offset: 0.,
color: Color::TRANSPARENT,
});
}
if stops.len() == 1 {
stops.push(GradientStop {
offset: 1.,
color: stops[0].color,
});
}
Self {
id: next_gradient_id(),
stops: stops.into(),
}
}
#[must_use]
pub fn as_slice(&self) -> &[GradientStop] {
&self.stops
}
#[must_use]
pub const fn id(&self) -> GradientId {
self.id
}
}
impl GradientId {
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum TileMode {
#[default]
Clamp,
Repeated,
Mirror,
Decal,
}
impl TileMode {
#[must_use]
pub fn apply(self, t: f32) -> Option<f32> {
let t = finite_or_zero(t);
match self {
Self::Clamp => Some(t.clamp(0., 1.)),
Self::Repeated => Some(t - t.floor()),
Self::Mirror => {
let period = t * 0.5 - (t * 0.5).floor();
Some(1. - (period * 2. - 1.).abs())
}
Self::Decal => (0. ..=1.).contains(&t).then_some(t),
}
}
#[must_use]
pub const fn shader_code(self) -> f32 {
match self {
Self::Clamp => 0.,
Self::Repeated => 1.,
Self::Mirror => 2.,
Self::Decal => 3.,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearGradient {
pub start: Offset,
pub end: Offset,
pub stops: GradientStops,
pub tile_mode: TileMode,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RadialGradient {
pub center: Offset,
pub radius: f32,
pub stops: GradientStops,
pub tile_mode: TileMode,
pub focal: Option<Offset>,
pub focal_radius: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SweepGradient {
pub center: Offset,
pub start_angle: f32,
pub stops: GradientStops,
pub tile_mode: TileMode,
}
impl LinearGradient {
#[must_use]
pub fn new(start: Offset, end: Offset, stops: GradientStops) -> Self {
Self {
start,
end,
stops,
tile_mode: TileMode::Clamp,
}
}
}
impl RadialGradient {
#[must_use]
pub fn new(center: Offset, radius: f32, stops: GradientStops) -> Self {
Self {
center,
radius,
stops,
tile_mode: TileMode::Clamp,
focal: None,
focal_radius: 0.,
}
}
#[must_use]
pub fn is_focal(&self) -> bool {
self.focal.is_some_and(|focal| focal != self.center) || self.focal_radius != 0.
}
}
impl SweepGradient {
#[must_use]
pub fn new(center: Offset, start_angle: f32, stops: GradientStops) -> Self {
Self {
center,
start_angle,
stops,
tile_mode: TileMode::Clamp,
}
}
}
#[must_use]
pub fn two_point_conical_gradient_t(
focal: Offset,
focal_radius: f32,
center: Offset,
radius: f32,
point: Offset,
) -> Option<f32> {
let cd = Offset::new(center.x - focal.x, center.y - focal.y);
let dr = radius - focal_radius;
let pd = Offset::new(point.x - focal.x, point.y - focal.y);
let a = cd.x * cd.x + cd.y * cd.y - dr * dr;
let b = pd.x * cd.x + pd.y * cd.y + focal_radius * dr;
let c = pd.x * pd.x + pd.y * pd.y - focal_radius * focal_radius;
let radius_at = |t: f32| focal_radius + t * dr;
if a.abs() <= f32::EPSILON {
if b == 0. {
return None;
}
let t = c / (2. * b);
return (radius_at(t) >= 0.).then_some(t);
}
let discriminant = b * b - a * c;
if discriminant < 0. {
return None;
}
let root = discriminant.sqrt();
let (high, low) = {
let first = (b + root) / a;
let second = (b - root) / a;
(first.max(second), first.min(second))
};
if radius_at(high) >= 0. {
Some(high)
} else if radius_at(low) >= 0. {
Some(low)
} else {
None
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Brush {
Solid(Color),
LinearGradient(LinearGradient),
RadialGradient(RadialGradient),
SweepGradient(SweepGradient),
}
pub type Shader = Brush;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FilterQuality {
None,
#[default]
Low,
Medium,
High,
}
impl FilterQuality {
#[must_use]
pub const fn sampling(self) -> ImageSampling {
match self {
Self::None => ImageSampling::Nearest,
Self::Low | Self::Medium | Self::High => ImageSampling::Linear,
}
}
}
impl From<FilterQuality> for ImageSampling {
fn from(value: FilterQuality) -> Self {
value.sampling()
}
}
impl From<Color> for Brush {
fn from(color: Color) -> Self {
Self::Solid(color)
}
}
impl From<LinearGradient> for Brush {
fn from(gradient: LinearGradient) -> Self {
Self::LinearGradient(gradient)
}
}
impl From<RadialGradient> for Brush {
fn from(gradient: RadialGradient) -> Self {
Self::RadialGradient(gradient)
}
}
impl From<SweepGradient> for Brush {
fn from(gradient: SweepGradient) -> Self {
Self::SweepGradient(gradient)
}
}
#[must_use]
pub fn sweep_gradient_t(center: Offset, start_angle: f32, point: Offset) -> f32 {
((point.y - center.y).atan2(point.x - center.x) - start_angle).rem_euclid(std::f32::consts::TAU)
/ std::f32::consts::TAU
}