use super::*;
use motion_core::Easing;
pub use crate::FieldCoordinate;
#[derive(Clone)]
pub struct ParticleCurve {
pub stops: Arc<[(f32, f32)]>,
pub easing: Easing,
}
impl ParticleCurve {
pub fn linear(start: f32, end: f32) -> Self {
Self {
stops: Arc::from([(0.0, start), (1.0, end)]),
easing: Easing::Linear,
}
}
pub fn sample(&self, progress: f32) -> Result<f32, ValidationError> {
validate_times(self.stops.iter().map(|stop| stop.0), self.stops.len())?;
if !progress.is_finite()
|| self
.stops
.iter()
.any(|stop| !stop.1.is_finite() || !(0.0..=65504.0).contains(&stop.1))
{
return Err(invalid(
"curve",
"Curve progress/values must be finite and nonnegative",
));
}
let (first, second, amount) = interval(&self.stops, progress, &self.easing)?;
let value = first.1 * (1.0 - amount) + second.1 * amount;
if !value.is_finite() || !(0.0..=65504.0).contains(&value) {
return Err(invalid("curve", "Easing produced an invalid scalar value"));
}
Ok(value)
}
}
fn validate_times(times: impl Iterator<Item = f32>, count: usize) -> Result<(), ValidationError> {
let mut previous = -1.0;
if count == 0 || count > 256 {
return Err(invalid("curve", "Require 1..=256 keyframes"));
}
for time in times {
if !time.is_finite() || !(0.0..=1.0).contains(&time) || time <= previous {
return Err(invalid(
"curve",
"Keyframe times must be finite, normalized and strictly increasing",
));
}
previous = time;
}
Ok(())
}
type Interval<'a, Value> = (&'a (f32, Value), &'a (f32, Value), f32);
fn interval<'a, Value>(
stops: &'a [(f32, Value)],
progress: f32,
easing: &Easing,
) -> Result<Interval<'a, Value>, ValidationError> {
let upper = stops
.partition_point(|stop| stop.0 <= progress)
.min(stops.len() - 1);
let lower = if progress >= stops[upper].0 {
upper
} else {
upper.saturating_sub(1)
};
let amount = if lower == upper {
0.0
} else {
(progress - stops[lower].0) / (stops[upper].0 - stops[lower].0)
};
Ok((
&stops[lower],
&stops[upper],
easing.try_apply(f64::from(amount.clamp(0.0, 1.0)))? as f32,
))
}
#[derive(Clone)]
pub struct ColorCurve {
pub stops: Arc<[(f32, [f32; 4])]>,
pub easing: Easing,
}
impl ColorCurve {
pub fn sample(&self, progress: f32) -> Result<[f32; 4], ValidationError> {
validate_times(self.stops.iter().map(|stop| stop.0), self.stops.len())?;
if !progress.is_finite() {
return Err(invalid("curve", "Nonfinite color progress"));
}
let (first, second, amount) = interval(&self.stops, progress, &self.easing)?;
for stop in [first, second] {
Particle {
color: stop.1,
..Particle::default()
}
.validate()?;
}
let color = std::array::from_fn(|channel| {
first.1[channel] * (1.0 - amount) + second.1[channel] * amount
});
Particle {
color,
..Particle::default()
}
.validate()?;
Ok(color)
}
}
impl ParticleModifier for ColorCurve {
fn apply(
&self,
particle: &mut Particle,
_: f32,
_: Time,
_: &mut Random,
) -> Result<(), ValidationError> {
particle.color = self.sample(particle.age / particle.lifetime)?;
particle.validate()
}
}
impl ColorOverLifetime {
pub fn curve(self, easing: Easing) -> ColorCurve {
let denominator = self.stops.len().saturating_sub(1).max(1) as f32;
ColorCurve {
stops: self
.stops
.iter()
.enumerate()
.map(|(index, color)| (index as f32 / denominator, *color))
.collect(),
easing,
}
}
}
#[derive(Clone)]
pub struct IntensityOverLifetime {
pub curve: ParticleCurve,
}
impl IntensityOverLifetime {
pub fn new(curve: ParticleCurve) -> Self {
Self { curve }
}
}
impl ParticleModifier for IntensityOverLifetime {
fn apply(
&self,
particle: &mut Particle,
_: f32,
_: Time,
_: &mut Random,
) -> Result<(), ValidationError> {
let intensity = self.curve.sample(particle.age / particle.lifetime)?;
for channel in &mut particle.color[..3] {
*channel *= intensity;
}
particle.validate()
}
fn resets_color(&self) -> bool {
true
}
}
#[derive(Clone)]
pub struct Temperature {
pub curve: ParticleCurve,
}
impl Temperature {
pub fn new(curve: ParticleCurve) -> Self {
Self { curve }
}
pub fn rgb(kelvin: f32) -> Result<[f32; 3], ValidationError> {
if !kelvin.is_finite() || !(1000.0..=40000.0).contains(&kelvin) {
return Err(invalid("temperature", "Require 1000..=40000 Kelvin"));
}
let temperature = f64::from(kelvin) / 100.0;
let red = if temperature <= 66.0 {
255.0
} else {
329.698727446 * libm::pow(temperature - 60.0, -0.1332047592)
};
let green = if temperature <= 66.0 {
99.4708025861 * libm::log(temperature) - 161.1195681661
} else {
288.1221695283 * libm::pow(temperature - 60.0, -0.0755148492)
};
let blue = if temperature >= 66.0 {
255.0
} else if temperature <= 19.0 {
0.0
} else {
138.5177312231 * libm::log(temperature - 10.0) - 305.0447927307
};
Ok([red, green, blue]
.map(|value| motion_core::srgb_to_linear((value / 255.0).clamp(0.0, 1.0) as f32)))
}
}
impl ParticleModifier for Temperature {
fn apply(
&self,
particle: &mut Particle,
_: f32,
_: Time,
_: &mut Random,
) -> Result<(), ValidationError> {
let rgb = Self::rgb(self.curve.sample(particle.age / particle.lifetime)?)?;
for (channel, value) in rgb.into_iter().enumerate() {
particle.color[channel] = value * particle.color[3];
}
particle.validate()
}
}
#[derive(Clone)]
pub struct ColorField {
pub coordinate: FieldCoordinate,
pub range: [f32; 2],
pub colors: ColorCurve,
}
impl ParticleModifier for ColorField {
fn apply(
&self,
particle: &mut Particle,
_: f32,
_: Time,
_: &mut Random,
) -> Result<(), ValidationError> {
if self.range.iter().any(|value| !value.is_finite()) || self.range[0] >= self.range[1] {
return Err(invalid("field", "Invalid coordinate range"));
}
let value = match self.coordinate {
FieldCoordinate::X => particle.position[0],
FieldCoordinate::Y => particle.position[1],
FieldCoordinate::Speed => libm::hypotf(particle.velocity[0], particle.velocity[1]),
};
particle.color = self
.colors
.sample(((value - self.range[0]) / (self.range[1] - self.range[0])).clamp(0.0, 1.0))?;
particle.validate()
}
}