use half::f16;
use std::cmp::Ordering;
use std::f32::consts::FRAC_PI_2;
use super::color::PackedSrgb;
use crate::math::{Angle, Point, Rect};
pub const MAX_STOPS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Gradient {
Linear(LinearGradient),
}
impl Gradient {
pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self {
match &mut self {
Gradient::Linear(linear) => {
for stop in linear.stops.iter_mut().flatten() {
*stop.color.a_mut() *= alpha_multiplier;
}
}
}
self
}
pub fn packed(&self, bounds: Rect) -> PackedGradient {
PackedGradient::new(self, bounds)
}
}
impl From<LinearGradient> for Gradient {
fn from(gradient: LinearGradient) -> Self {
Self::Linear(gradient)
}
}
impl Default for Gradient {
fn default() -> Self {
Gradient::Linear(LinearGradient::new(Angle::default()))
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct ColorStop {
pub offset: f32,
pub color: PackedSrgb,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LinearGradient {
pub angle: Angle,
pub stops: [Option<ColorStop>; MAX_STOPS],
}
impl LinearGradient {
pub const fn new(angle: Angle) -> Self {
Self {
angle: angle,
stops: [None; 8],
}
}
pub fn add_stop(mut self, offset: f32, color: impl Into<PackedSrgb>) -> Self {
if offset.is_finite() && (0.0..=1.0).contains(&offset) {
let (Ok(index) | Err(index)) = self.stops.binary_search_by(|stop| match stop {
None => Ordering::Greater,
Some(stop) => stop.offset.partial_cmp(&offset).unwrap(),
});
if index < 8 {
self.stops[index] = Some(ColorStop {
offset,
color: color.into(),
});
}
} else {
log::warn!("Gradient color stop must be within 0.0..=1.0 range.");
};
self
}
pub fn add_stops(mut self, stops: impl IntoIterator<Item = ColorStop>) -> Self {
for stop in stops {
self = self.add_stop(stop.offset, stop.color);
}
self
}
}
#[repr(C)]
#[derive(Default, Debug, Copy, Clone, PartialEq, bytemuck::Zeroable, bytemuck::Pod)]
pub struct PackedGradient {
pub colors: [[u32; 2]; 8],
pub offsets: [u32; 4],
pub direction: [f32; 4],
}
impl PackedGradient {
pub fn new(gradient: &Gradient, bounds: Rect) -> Self {
match gradient {
Gradient::Linear(linear) => {
let mut colors = [[0u32; 2]; 8];
let mut offsets = [f16::from(0u8); 8];
for (index, stop) in linear.stops.iter().enumerate() {
let packed_color = stop.map(|s| s.color).unwrap_or(PackedSrgb::default());
colors[index] = [
pack_f16s([
f16::from_f32(packed_color.r()),
f16::from_f32(packed_color.g()),
]),
pack_f16s([
f16::from_f32(packed_color.b()),
f16::from_f32(packed_color.a()),
]),
];
offsets[index] = f16::from_f32(stop.map(|s| s.offset).unwrap_or(2.0));
}
let offsets = [
pack_f16s([offsets[0], offsets[1]]),
pack_f16s([offsets[2], offsets[3]]),
pack_f16s([offsets[4], offsets[5]]),
pack_f16s([offsets[6], offsets[7]]),
];
let (start, end) = to_distance(linear.angle, &bounds);
let direction = [start.x, start.y, end.x, end.y];
PackedGradient {
colors,
offsets,
direction,
}
}
}
}
}
fn to_distance(angle: Angle, bounds: &Rect) -> (Point, Point) {
let angle = angle - Angle { radians: FRAC_PI_2 };
let r = Point::new(f32::cos(angle.radians), f32::sin(angle.radians));
let bounds_center = bounds.center();
let distance_to_rect = f32::max(
f32::abs(r.x * bounds.size.width / 2.0),
f32::abs(r.y * bounds.size.height / 2.0),
);
let start = Point::new(
bounds_center.x - (r.x * distance_to_rect),
bounds_center.y - (r.y * distance_to_rect),
);
let end = Point::new(
bounds_center.x + (r.x * distance_to_rect),
bounds_center.y + (r.y * distance_to_rect),
);
(start, end)
}
fn pack_f16s(f: [f16; 2]) -> u32 {
let one = (f[0].to_bits() as u32) << 16;
let two = f[1].to_bits() as u32;
one | two
}