use crate::Color; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)] #[widget(Radio)] pub enum GradientType { #[default] Linear, Radial, } // TODO: Someday we could switch this to a Box[T] to avoid over-allocation // TODO: Use linear not gamma colors /// A list of colors associated with positions (in the range 0 to 1) along a gradient. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] pub struct GradientStops(pub Vec<(f64, Color)>); impl std::hash::Hash for GradientStops { fn hash(&self, state: &mut H) { self.0.len().hash(state); self.0.iter().for_each(|(position, color)| { position.to_bits().hash(state); color.hash(state); }); } } impl Default for GradientStops { fn default() -> Self { Self(vec![(0., Color::BLACK), (1., Color::WHITE)]) } } impl IntoIterator for GradientStops { type Item = (f64, Color); type IntoIter = std::vec::IntoIter<(f64, Color)>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl<'a> IntoIterator for &'a GradientStops { type Item = &'a (f64, Color); type IntoIter = std::slice::Iter<'a, (f64, Color)>; fn into_iter(self) -> Self::IntoIter { self.0.iter() } } impl std::ops::Index for GradientStops { type Output = (f64, Color); fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } impl std::ops::Deref for GradientStops { type Target = Vec<(f64, Color)>; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for GradientStops { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl GradientStops { pub fn new(stops: Vec<(f64, Color)>) -> Self { let mut stops = Self(stops); stops.sort(); stops } pub fn evaluate(&self, t: f64) -> Color { if self.0.is_empty() { return Color::BLACK; } if t <= self.0[0].0 { return self.0[0].1; } if t >= self.0[self.0.len() - 1].0 { return self.0[self.0.len() - 1].1; } for i in 0..self.0.len() - 1 { let (t1, c1) = self.0[i]; let (t2, c2) = self.0[i + 1]; if t >= t1 && t <= t2 { let normalized_t = (t - t1) / (t2 - t1); return c1.lerp(&c2, normalized_t as f32); } } Color::BLACK } pub fn sort(&mut self) { self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); } pub fn reversed(&self) -> Self { Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect()) } pub fn map_colors Color>(&self, f: F) -> Self { Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect()) } } /// A gradient fill. /// /// Contains the start and end points, along with the colors at varying points along the length. #[repr(C)] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] pub struct Gradient { pub stops: GradientStops, pub gradient_type: GradientType, pub start: DVec2, pub end: DVec2, pub transform: DAffine2, } impl Default for Gradient { fn default() -> Self { Self { stops: GradientStops::default(), gradient_type: GradientType::Linear, start: DVec2::new(0., 0.5), end: DVec2::new(1., 0.5), transform: DAffine2::IDENTITY, } } } impl std::hash::Hash for Gradient { fn hash(&self, state: &mut H) { self.stops.0.len().hash(state); [].iter() .chain(self.start.to_array().iter()) .chain(self.end.to_array().iter()) .chain(self.transform.to_cols_array().iter()) .chain(self.stops.0.iter().map(|(position, _)| position)) .for_each(|x| x.to_bits().hash(state)); self.stops.0.iter().for_each(|(_, color)| color.hash(state)); self.gradient_type.hash(state); } } impl std::fmt::Display for Gradient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let round = |x: f64| (x * 1e3).round() / 1e3; let stops = self .stops .0 .iter() .map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb())) .collect::>() .join(", "); write!(f, "{} Gradient: {stops}", self.gradient_type) } } impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self { Gradient { start, end, stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]), transform, gradient_type, } } pub fn lerp(&self, other: &Self, time: f64) -> Self { let start = self.start + (other.start - self.start) * time; let end = self.end + (other.end - self.end) * time; let transform = self.transform; let stops = self .stops .0 .iter() .zip(other.stops.0.iter()) .map(|((a_pos, a_color), (b_pos, b_color))| { let position = a_pos + (b_pos - a_pos) * time; let color = a_color.lerp(b_color, time as f32); (position, color) }) .collect::>(); let stops = GradientStops::new(stops); let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; Self { start, end, transform, stops, gradient_type, } } /// Insert a stop into the gradient, the index if successful pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option { // Transform the start and end positions to the same coordinate space as the mouse. let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end)); // Calculate the new position by finding the closest point on the line let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); // Don't insert point past end of line if !(0. ..=1.).contains(&new_position) { return None; } // Compute the color of the inserted stop let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) { // Lerp between the nearest colors if applicable (a, Some(b)) => a.lerp( &b, ((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32, ), // Use the start or the end color if applicable (v, _) => v, }; // Compute the correct index to keep the positions in order let mut index = 0; while self.stops.0.len() > index && self.stops.0[index].0 <= new_position { index += 1; } let new_color = get_color(index - 1, new_position); // Insert the new stop self.stops.0.insert(index, (new_position, new_color)); Some(index) } }