use crate::color::OpalineColor;
use crate::error::OpalineError;
use serde::de::Deserializer;
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Gradient {
stops: Vec<OpalineColor>,
}
impl Gradient {
pub fn new(stops: Vec<OpalineColor>) -> Self {
assert!(!stops.is_empty(), "gradient must have at least one stop");
Self { stops }
}
pub fn try_new(stops: Vec<OpalineColor>) -> Result<Self, OpalineError> {
if stops.is_empty() {
return Err(OpalineError::EmptyGradient);
}
Ok(Self { stops })
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::as_conversions
)]
pub fn at(&self, t: f32) -> OpalineColor {
let t = t.clamp(0.0, 1.0);
if self.stops.len() == 1 {
return self.stops[0];
}
let segments = self.stops.len() - 1;
let scaled = t * segments as f32;
let index = (scaled.floor() as usize).min(segments - 1);
let local_t = scaled - index as f32;
self.stops[index].lerp(self.stops[index + 1], local_t)
}
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
pub fn generate(&self, n: usize) -> Vec<OpalineColor> {
match n {
0 => vec![],
1 => vec![self.at(0.5)],
_ => (0..n).map(|i| self.at(i as f32 / (n - 1) as f32)).collect(),
}
}
pub fn len(&self) -> usize {
self.stops.len()
}
pub fn is_empty(&self) -> bool {
self.stops.is_empty()
}
pub fn stops(&self) -> &[OpalineColor] {
&self.stops
}
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: vec![OpalineColor::FALLBACK],
}
}
}
impl<'de> serde::Deserialize<'de> for Gradient {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct GradientRepr {
stops: Vec<OpalineColor>,
}
let GradientRepr { stops } = GradientRepr::deserialize(deserializer)?;
Self::try_new(stops).map_err(serde::de::Error::custom)
}
}