Lightningbeam/lightningbeam-ui/lightningbeam-core/src/animation.rs

548 lines
18 KiB
Rust

//! Animation system for Lightningbeam
//!
//! Provides keyframe-based animation curves with support for different
//! interpolation types and property targets.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
/// Interpolation type for keyframes
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum InterpolationType {
/// Linear interpolation between keyframes
Linear,
/// Smooth bezier interpolation with handles
Bezier,
/// Hold value until next keyframe (step function)
Hold,
}
/// Extrapolation type for values outside keyframe range
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum ExtrapolationType {
/// Hold the first/last keyframe value
Hold,
/// Continue with the slope from the first/last segment
Linear,
/// Repeat the curve pattern cyclically
Cyclic,
/// Repeat the curve, but offset each cycle by the change in the previous cycle
/// (each cycle starts where the previous one ended)
CyclicOffset,
}
impl Default for ExtrapolationType {
fn default() -> Self {
ExtrapolationType::Hold
}
}
/// A single keyframe in an animation curve
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Keyframe {
/// Time in seconds
pub time: f64,
/// Value at this keyframe
pub value: f64,
/// Interpolation type to use after this keyframe
pub interpolation: InterpolationType,
/// Bezier handle for smooth curves (in and out tangents)
/// Format: (in_time, in_value, out_time, out_value)
pub bezier_handles: Option<(f64, f64, f64, f64)>,
}
impl Keyframe {
/// Create a new linear keyframe
pub fn linear(time: f64, value: f64) -> Self {
Self {
time,
value,
interpolation: InterpolationType::Linear,
bezier_handles: None,
}
}
/// Create a new hold keyframe
pub fn hold(time: f64, value: f64) -> Self {
Self {
time,
value,
interpolation: InterpolationType::Hold,
bezier_handles: None,
}
}
/// Create a new bezier keyframe with handles
pub fn bezier(
time: f64,
value: f64,
in_time: f64,
in_value: f64,
out_time: f64,
out_value: f64,
) -> Self {
Self {
time,
value,
interpolation: InterpolationType::Bezier,
bezier_handles: Some((in_time, in_value, out_time, out_value)),
}
}
}
/// Transform properties that can be animated
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum TransformProperty {
X,
Y,
Rotation,
ScaleX,
ScaleY,
SkewX,
SkewY,
Opacity,
}
/// Shape properties that can be animated
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum ShapeProperty {
/// Whether the shape is visible (0 or 1, for animation)
Exists,
/// Z-order within the layer
ZOrder,
/// Morph between shape versions (fractional index)
ShapeIndex,
}
/// Layer-level properties that can be animated
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum LayerProperty {
/// Layer opacity (0.0 to 1.0)
Opacity,
/// Layer visibility (0 or 1, for animation)
Visibility,
}
/// Audio-specific properties that can be automated
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum AudioProperty {
/// Volume in dB (-60 to +12 typical range)
Volume,
/// Pan position (-1.0 left to +1.0 right)
Pan,
/// Pitch shift in semitones
Pitch,
}
/// Video-specific properties that can be animated
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum VideoProperty {
/// Fade/opacity (0.0 to 1.0)
Fade,
/// X position
PositionX,
/// Y position
PositionY,
/// Scale factor
Scale,
/// Rotation in degrees
Rotation,
}
/// Effect-specific properties that can be animated
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum EffectProperty {
/// Effect intensity (0.0 to 1.0)
Intensity,
/// Mix/blend amount (0.0 to 1.0)
Mix,
/// Custom effect parameter (effect-specific)
Custom(u32),
}
/// Target for an animation curve (type-safe property identification)
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum AnimationTarget {
/// Object transform property
Object {
id: Uuid,
property: TransformProperty,
},
/// Shape property
Shape { id: Uuid, property: ShapeProperty },
/// Layer property
Layer { property: LayerProperty },
/// Audio automation
Audio { id: Uuid, property: AudioProperty },
/// Video property
Video { id: Uuid, property: VideoProperty },
/// Effect parameter
Effect {
id: Uuid,
property: EffectProperty,
},
/// Generic automation node parameter
Automation { node_id: u32, parameter: String },
}
/// An animation curve with keyframes
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AnimationCurve {
/// What this curve animates
pub target: AnimationTarget,
/// Keyframes in chronological order
pub keyframes: Vec<Keyframe>,
/// Default value when no keyframes are present
pub default_value: f64,
/// How to extrapolate before the first keyframe
#[serde(default)]
pub pre_extrapolation: ExtrapolationType,
/// How to extrapolate after the last keyframe
#[serde(default)]
pub post_extrapolation: ExtrapolationType,
}
impl AnimationCurve {
/// Create a new animation curve
pub fn new(target: AnimationTarget, default_value: f64) -> Self {
Self {
target,
keyframes: Vec::new(),
default_value,
pre_extrapolation: ExtrapolationType::Hold,
post_extrapolation: ExtrapolationType::Hold,
}
}
/// Get the time range of keyframes (min, max)
fn get_keyframe_range(&self) -> Option<(f64, f64)> {
if self.keyframes.is_empty() {
None
} else {
Some((
self.keyframes.first().unwrap().time,
self.keyframes.last().unwrap().time,
))
}
}
/// Get the keyframes that bracket the given time
/// Returns (before, after) where:
/// - (None, Some(kf)) if time is before the first keyframe
/// - (Some(kf), None) if time is after the last keyframe
/// - (Some(before), Some(after)) if time is between two keyframes
/// - (None, None) if there are no keyframes
pub fn get_bracketing_keyframes(&self, time: f64) -> (Option<&Keyframe>, Option<&Keyframe>) {
if self.keyframes.is_empty() {
return (None, None);
}
// Find the first keyframe after the given time
let after_idx = self.keyframes.iter().position(|kf| kf.time > time);
match after_idx {
None => {
// Time is after all keyframes
(self.keyframes.last(), None)
}
Some(0) => {
// Time is before all keyframes
(None, self.keyframes.first())
}
Some(idx) => {
// Time is between two keyframes
(Some(&self.keyframes[idx - 1]), Some(&self.keyframes[idx]))
}
}
}
/// Interpolate between two keyframes
fn interpolate(&self, before_kf: &Keyframe, after_kf: &Keyframe, time: f64) -> f64 {
let t = (time - before_kf.time) / (after_kf.time - before_kf.time);
match before_kf.interpolation {
InterpolationType::Linear => {
// Linear interpolation
before_kf.value + t * (after_kf.value - before_kf.value)
}
InterpolationType::Bezier => {
// Bezier interpolation using handles
if let Some((_, in_val, _, out_val)) = before_kf.bezier_handles {
// Cubic bezier interpolation
let p0 = before_kf.value;
let p1 = out_val;
let p2 = in_val;
let p3 = after_kf.value;
let t2 = t * t;
let t3 = t2 * t;
let mt = 1.0 - t;
let mt2 = mt * mt;
let mt3 = mt2 * mt;
mt3 * p0 + 3.0 * mt2 * t * p1 + 3.0 * mt * t2 * p2 + t3 * p3
} else {
// Fallback to linear if no handles
before_kf.value + t * (after_kf.value - before_kf.value)
}
}
InterpolationType::Hold => {
// Hold until next keyframe
before_kf.value
}
}
}
/// Evaluate the curve at a given time
pub fn eval(&self, time: f64) -> f64 {
if self.keyframes.is_empty() {
return self.default_value;
}
let (before, after) = self.get_bracketing_keyframes(time);
match (before, after) {
(None, None) => self.default_value,
(None, Some(first_kf)) => {
// Before first keyframe - use pre-extrapolation
self.extrapolate_pre(time, first_kf)
}
(Some(last_kf), None) => {
// After last keyframe - use post-extrapolation
self.extrapolate_post(time, last_kf)
}
(Some(before_kf), Some(after_kf)) => {
// Between keyframes - interpolate
self.interpolate(before_kf, after_kf, time)
}
}
}
/// Extrapolate before the first keyframe
fn extrapolate_pre(&self, time: f64, first_kf: &Keyframe) -> f64 {
match self.pre_extrapolation {
ExtrapolationType::Hold => first_kf.value,
ExtrapolationType::Linear => {
// Use slope from first segment if available
if self.keyframes.len() >= 2 {
let second_kf = &self.keyframes[1];
let slope = (second_kf.value - first_kf.value)
/ (second_kf.time - first_kf.time);
first_kf.value + slope * (time - first_kf.time)
} else {
first_kf.value
}
}
ExtrapolationType::Cyclic => {
let (start_time, end_time) = self.get_keyframe_range().unwrap();
let duration = end_time - start_time;
if duration <= 0.0 {
return first_kf.value;
}
// Map time into the keyframe range
let offset = ((start_time - time) / duration).ceil() * duration;
let mapped_time = time + offset;
self.eval(mapped_time)
}
ExtrapolationType::CyclicOffset => {
let (start_time, end_time) = self.get_keyframe_range().unwrap();
let duration = end_time - start_time;
if duration <= 0.0 {
return first_kf.value;
}
let first_val = self.keyframes.first().unwrap().value;
let last_val = self.keyframes.last().unwrap().value;
let cycle_delta = last_val - first_val;
// Calculate which cycle we're in (negative for pre-extrapolation)
let cycles = ((start_time - time) / duration).ceil();
let offset = cycles * duration;
let mapped_time = time + offset;
// Evaluate and offset by accumulated cycles
self.eval(mapped_time) - cycles * cycle_delta
}
}
}
/// Extrapolate after the last keyframe
fn extrapolate_post(&self, time: f64, last_kf: &Keyframe) -> f64 {
match self.post_extrapolation {
ExtrapolationType::Hold => last_kf.value,
ExtrapolationType::Linear => {
// Use slope from last segment if available
let n = self.keyframes.len();
if n >= 2 {
let second_last_kf = &self.keyframes[n - 2];
let slope = (last_kf.value - second_last_kf.value)
/ (last_kf.time - second_last_kf.time);
last_kf.value + slope * (time - last_kf.time)
} else {
last_kf.value
}
}
ExtrapolationType::Cyclic => {
let (start_time, end_time) = self.get_keyframe_range().unwrap();
let duration = end_time - start_time;
if duration <= 0.0 {
return last_kf.value;
}
// Map time into the keyframe range
let offset = ((time - start_time) / duration).floor() * duration;
let mapped_time = time - offset;
self.eval(mapped_time)
}
ExtrapolationType::CyclicOffset => {
let (start_time, end_time) = self.get_keyframe_range().unwrap();
let duration = end_time - start_time;
if duration <= 0.0 {
return last_kf.value;
}
let first_val = self.keyframes.first().unwrap().value;
let last_val = self.keyframes.last().unwrap().value;
let cycle_delta = last_val - first_val;
// Calculate which cycle we're in
let cycles = ((time - start_time) / duration).floor();
let offset = cycles * duration;
let mapped_time = time - offset;
// Evaluate and offset by accumulated cycles
self.eval(mapped_time) + cycles * cycle_delta
}
}
}
/// Add or update a keyframe
pub fn set_keyframe(&mut self, keyframe: Keyframe) {
// Find existing keyframe at this time or insert new one
if let Some(existing) = self
.keyframes
.iter_mut()
.find(|kf| (kf.time - keyframe.time).abs() < 0.001)
{
*existing = keyframe;
} else {
self.keyframes.push(keyframe);
// Keep keyframes sorted by time
self.keyframes
.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
}
}
/// Remove a keyframe at the given time (within tolerance)
pub fn remove_keyframe(&mut self, time: f64, tolerance: f64) -> bool {
if let Some(idx) = self
.keyframes
.iter()
.position(|kf| (kf.time - time).abs() < tolerance)
{
self.keyframes.remove(idx);
true
} else {
false
}
}
/// Get the keyframe closest to the given time, if within tolerance
pub fn get_keyframe_at(&self, time: f64, tolerance: f64) -> Option<&Keyframe> {
let (before, after) = self.get_bracketing_keyframes(time);
// Check if before keyframe is within tolerance
if let Some(kf) = before {
if (kf.time - time).abs() < tolerance {
return Some(kf);
}
}
// Check if after keyframe is within tolerance
if let Some(kf) = after {
if (kf.time - time).abs() < tolerance {
return Some(kf);
}
}
None
}
}
/// Collection of animation curves for a layer
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct AnimationData {
/// Map of animation targets to their curves
pub curves: HashMap<AnimationTarget, AnimationCurve>,
}
impl AnimationData {
/// Create new empty animation data
pub fn new() -> Self {
Self {
curves: HashMap::new(),
}
}
/// Get a curve for a specific target
pub fn get_curve(&self, target: &AnimationTarget) -> Option<&AnimationCurve> {
self.curves.get(target)
}
/// Get a mutable curve for a specific target
pub fn get_curve_mut(&mut self, target: &AnimationTarget) -> Option<&mut AnimationCurve> {
self.curves.get_mut(target)
}
/// Add or replace a curve
pub fn set_curve(&mut self, curve: AnimationCurve) {
let target = curve.target.clone();
self.curves.insert(target, curve);
}
/// Remove a curve
pub fn remove_curve(&mut self, target: &AnimationTarget) -> Option<AnimationCurve> {
self.curves.remove(target)
}
/// Evaluate a property at a given time
pub fn eval(&self, target: &AnimationTarget, time: f64, default: f64) -> f64 {
self.curves
.get(target)
.map(|curve| curve.eval(time))
.unwrap_or(default)
}
/// Evaluate the effective transform for a clip instance at a given time.
/// Uses animation curves if they exist, falling back to the clip instance's base transform.
pub fn eval_clip_instance_transform(
&self,
instance_id: uuid::Uuid,
time: f64,
base: &crate::object::Transform,
base_opacity: f64,
) -> (crate::object::Transform, f64) {
let mut t = base.clone();
t.x = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::X }, time, base.x);
t.y = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::Y }, time, base.y);
t.rotation = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::Rotation }, time, base.rotation);
t.scale_x = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::ScaleX }, time, base.scale_x);
t.scale_y = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::ScaleY }, time, base.scale_y);
t.skew_x = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::SkewX }, time, base.skew_x);
t.skew_y = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::SkewY }, time, base.skew_y);
let opacity = self.eval(&AnimationTarget::Object { id: instance_id, property: TransformProperty::Opacity }, time, base_opacity);
(t, opacity)
}
}