548 lines
18 KiB
Rust
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)
|
|
}
|
|
}
|