use std::sync::Arc;
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::system::{Commands, Query};
use bevy::pbr::StandardMaterial;
use bevy::prelude::{Handle, Res};
use bevy::time::Time;
use crate::cache::TextureCacheKey;
use crate::material::{PatchMaterialTextures, TextureConfig};
pub trait ParameterCurve<T>: Send + Sync {
fn eval(&self, t: f32) -> T;
}
#[derive(Clone, Copy, Debug)]
pub struct Linear<T> {
pub from: T,
pub to: T,
pub duration: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct EaseInOut<T> {
pub from: T,
pub to: T,
pub duration: f32,
}
#[derive(Clone, Debug)]
pub struct Stepped<T: Clone> {
pub steps: Vec<(f32, T)>,
}
pub struct ScriptedFn<T, F>(pub F)
where
F: Fn(f32) -> T + Send + Sync;
impl ParameterCurve<f32> for Linear<f32> {
fn eval(&self, t: f32) -> f32 {
if self.duration <= 0.0 {
return self.to;
}
let u = (t / self.duration).clamp(0.0, 1.0);
self.from + (self.to - self.from) * u
}
}
impl ParameterCurve<f64> for Linear<f64> {
fn eval(&self, t: f32) -> f64 {
if self.duration <= 0.0 {
return self.to;
}
let u = (t / self.duration).clamp(0.0, 1.0) as f64;
self.from + (self.to - self.from) * u
}
}
impl ParameterCurve<f32> for EaseInOut<f32> {
fn eval(&self, t: f32) -> f32 {
if self.duration <= 0.0 {
return self.to;
}
let u = (t / self.duration).clamp(0.0, 1.0);
let s = u * u * (3.0 - 2.0 * u);
self.from + (self.to - self.from) * s
}
}
impl ParameterCurve<f64> for EaseInOut<f64> {
fn eval(&self, t: f32) -> f64 {
if self.duration <= 0.0 {
return self.to;
}
let u = (t / self.duration).clamp(0.0, 1.0) as f64;
let s = u * u * (3.0 - 2.0 * u);
self.from + (self.to - self.from) * s
}
}
impl<T: Clone + Send + Sync> ParameterCurve<T> for Stepped<T> {
fn eval(&self, t: f32) -> T {
debug_assert!(
!self.steps.is_empty(),
"Stepped curve requires at least one step"
);
let idx = self
.steps
.partition_point(|(s, _)| *s <= t)
.saturating_sub(1);
self.steps[idx].1.clone()
}
}
impl<T, F> ParameterCurve<T> for ScriptedFn<T, F>
where
F: Fn(f32) -> T + Send + Sync,
{
fn eval(&self, t: f32) -> T {
(self.0)(t)
}
}
pub type TextureCurve = Arc<dyn Fn(f32) -> TextureConfig + Send + Sync>;
#[derive(Component)]
pub struct AnimatedProceduralMaterial {
pub material: Handle<StandardMaterial>,
pub width: u32,
pub height: u32,
pub texture_curve: TextureCurve,
pub min_regen_interval: f32,
pub elapsed: f32,
pub last_regen_at: f32,
pub last_fingerprint: u64,
}
impl AnimatedProceduralMaterial {
pub const DEFAULT_REGEN_INTERVAL: f32 = 0.25;
pub fn new(
material: Handle<StandardMaterial>,
width: u32,
height: u32,
texture_curve: impl Fn(f32) -> TextureConfig + Send + Sync + 'static,
) -> Self {
Self {
material,
width,
height,
texture_curve: Arc::new(texture_curve),
min_regen_interval: Self::DEFAULT_REGEN_INTERVAL,
elapsed: 0.0,
last_regen_at: f32::NEG_INFINITY,
last_fingerprint: 0,
}
}
pub fn with_min_regen_interval(mut self, interval: f32) -> Self {
self.min_regen_interval = interval.max(0.0);
self
}
}
pub fn tick_animated_procedural_materials(
time: Res<Time>,
mut commands: Commands,
mut anim_q: Query<(Entity, &mut AnimatedProceduralMaterial)>,
) {
let dt = time.delta_secs();
for (_entity, mut anim) in &mut anim_q {
anim.elapsed += dt;
let since = anim.elapsed - anim.last_regen_at;
if since < anim.min_regen_interval {
continue;
}
let cfg = (anim.texture_curve)(anim.elapsed);
let fp = cfg.fingerprint();
anim.last_regen_at = anim.elapsed;
if fp == anim.last_fingerprint {
continue;
}
anim.last_fingerprint = fp;
let key = TextureCacheKey {
kind: cfg.label(),
fingerprint: fp,
width: anim.width,
height: anim.height,
};
if let Some(pending) = cfg.spawn(anim.width, anim.height) {
commands.spawn((
pending,
PatchMaterialTextures {
target: anim.material.clone(),
cache_key: Some(key),
},
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_clamps_and_interpolates() {
let c = Linear {
from: 0.0_f32,
to: 4.0,
duration: 2.0,
};
assert_eq!(c.eval(0.0), 0.0);
assert_eq!(c.eval(2.0), 4.0);
assert!((c.eval(1.0) - 2.0).abs() < 1e-6);
assert_eq!(c.eval(-5.0), 0.0);
assert_eq!(c.eval(99.0), 4.0);
}
#[test]
fn ease_in_out_hits_endpoints_and_midpoint() {
let c = EaseInOut {
from: 0.0_f32,
to: 1.0,
duration: 1.0,
};
assert_eq!(c.eval(0.0), 0.0);
assert_eq!(c.eval(1.0), 1.0);
assert!((c.eval(0.5) - 0.5).abs() < 1e-6);
}
#[test]
fn stepped_holds_until_next_step() {
let c = Stepped {
steps: vec![(0.0, 0_i32), (1.0, 1), (3.0, 2), (5.0, 3)],
};
assert_eq!(c.eval(0.0), 0);
assert_eq!(c.eval(0.99), 0);
assert_eq!(c.eval(1.0), 1);
assert_eq!(c.eval(2.5), 1);
assert_eq!(c.eval(3.0), 2);
assert_eq!(c.eval(100.0), 3);
}
#[test]
fn scripted_fn_forwards() {
let c = ScriptedFn(|t: f32| t * 2.0 + 1.0);
assert_eq!(c.eval(0.0), 1.0);
assert_eq!(c.eval(3.0), 7.0);
}
}