From 8d5b04e65b3610c2eed1a0bdc1d35af65ac1085e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 2 Jul 2026 00:54:42 -0700 Subject: [PATCH] New node: 'Animation Delta Time' --- .../animation/animation_message_handler.rs | 13 ++++- node-graph/graph-craft/src/proto.rs | 2 +- node-graph/graphene-cli/src/export.rs | 1 + .../libraries/application-io/src/lib.rs | 2 + .../libraries/core-types/src/context.rs | 48 +++++++++++++++++-- node-graph/node-macro/src/parsing.rs | 2 + node-graph/nodes/gcore/src/animation.rs | 8 +++- node-graph/nodes/gstd/src/render_node.rs | 1 + 8 files changed, 70 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/animation/animation_message_handler.rs b/editor/src/messages/animation/animation_message_handler.rs index 89fa807b73..c7a4d089e0 100644 --- a/editor/src/messages/animation/animation_message_handler.rs +++ b/editor/src/messages/animation/animation_message_handler.rs @@ -33,6 +33,8 @@ pub struct AnimationMessageHandler { animation_state: AnimationState, fps: f64, animation_time_mode: AnimationTimeMode, + /// Seconds of animation time elapsed between the previous frame and the current one. + animation_delta_time: f64, } impl AnimationMessageHandler { pub(crate) fn timing_information(&self) -> TimingInformation { @@ -41,7 +43,11 @@ impl AnimationMessageHandler { AnimationTimeMode::TimeBased => Duration::from_millis(animation_time as u64), AnimationTimeMode::FrameBased => Duration::from_secs((self.frame_index / self.fps) as u64), }; - TimingInformation { time: self.timestamp, animation_time } + TimingInformation { + time: self.timestamp, + animation_time, + animation_delta_time: self.animation_delta_time, + } } pub(crate) fn animation_start(&self) -> f64 { @@ -89,7 +95,11 @@ impl MessageHandler for AnimationMessageHandler { responses.add(PortfolioMessage::UpdateDocumentWidgets); } AnimationMessage::SetTime { time } => { + // Elapsed animation time since the previous frame, in seconds (frozen while paused, so it reads 0 then) + let previous_animation_time = self.timestamp - self.animation_start(); self.timestamp = time; + let current_animation_time = self.timestamp - self.animation_start(); + self.animation_delta_time = ((current_animation_time - previous_animation_time) / 1000.).max(0.); responses.add(AnimationMessage::UpdateTime); } AnimationMessage::IncrementFrameCounter => { @@ -110,6 +120,7 @@ impl MessageHandler for AnimationMessageHandler { } AnimationMessage::RestartAnimation => { self.frame_index = 0.; + self.animation_delta_time = 0.; self.animation_state = match self.animation_state { AnimationState::Playing { .. } => AnimationState::Playing { start: self.timestamp }, _ => AnimationState::Stopped, diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 2fa7eb93a3..803e522936 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -742,7 +742,7 @@ impl TypingContext { // List of all implementations that match the input types let valid_output_types = impls .keys() - .filter(|node_io| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2))) + .filter(|node_io| valid_type(&node_io.call_argument, call_argument) && inputs.len() == node_io.inputs.len() && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2))) .collect::>(); // Attempt to substitute generic types with concrete types and save the list of results diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index c09c291cde..d277b0a27a 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -187,6 +187,7 @@ pub async fn export_gif( time: TimingInformation { time: animation_time.as_secs_f64(), animation_time, + animation_delta_time: 0., }, ..Default::default() }; diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 3ca7dcbd49..005552ab96 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -92,6 +92,8 @@ pub enum ExportFormat { pub struct TimingInformation { pub time: f64, pub animation_time: Duration, + /// Seconds of animation time elapsed since the previous frame. + pub animation_delta_time: f64, } #[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index ab8b022239..ff4f242309 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -28,6 +28,9 @@ pub trait ExtractRealTime { pub trait ExtractAnimationTime { fn try_animation_time(&self) -> Option; } +pub trait ExtractAnimationDeltaTime { + fn try_animation_delta_time(&self) -> Option; +} pub trait ExtractPointerPosition { fn try_pointer_position(&self) -> Option; } @@ -60,6 +63,7 @@ pub trait CloneVarArgs: ExtractVarArgs { pub trait InjectFootprint {} pub trait InjectRealTime {} pub trait InjectAnimationTime {} +pub trait InjectAnimationDeltaTime {} pub trait InjectPointerPosition {} pub trait InjectPosition {} pub trait InjectIndex {} @@ -74,6 +78,7 @@ pub trait ExtractAll: ExtractFootprint + ExtractRealTime + ExtractAnimationTime + + ExtractAnimationDeltaTime + ExtractPointerPosition + ExtractPosition + ExtractIndex + @@ -84,6 +89,7 @@ impl< + ExtractFootprint + ExtractRealTime + ExtractAnimationTime + + ExtractAnimationDeltaTime + ExtractPointerPosition + ExtractPosition + ExtractIndex @@ -99,6 +105,7 @@ impl< impl InjectFootprint for T {} impl InjectRealTime for T {} impl InjectAnimationTime for T {} +impl InjectAnimationDeltaTime for T {} impl InjectPointerPosition for T {} impl InjectPosition for T {} impl InjectIndex for T {} @@ -112,6 +119,7 @@ impl InjectVarArgs for T {} pub trait ModifyFootprint: ExtractFootprint + InjectFootprint {} pub trait ModifyRealTime: ExtractRealTime + InjectRealTime {} pub trait ModifyAnimationTime: ExtractAnimationTime + InjectAnimationTime {} +pub trait ModifyAnimationDeltaTime: ExtractAnimationDeltaTime + InjectAnimationDeltaTime {} pub trait ModifyPointerPosition: ExtractPointerPosition + InjectPointerPosition {} pub trait ModifyPosition: ExtractPosition + InjectPosition {} pub trait ModifyIndex: ExtractIndex + InjectIndex {} @@ -120,6 +128,7 @@ pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {} impl ModifyFootprint for T {} impl ModifyRealTime for T {} impl ModifyAnimationTime for T {} +impl ModifyAnimationDeltaTime for T {} impl ModifyPointerPosition for T {} impl ModifyPosition for T {} impl ModifyIndex for T {} @@ -136,6 +145,7 @@ pub enum ContextFeature { ExtractFootprint, ExtractRealTime, ExtractAnimationTime, + ExtractAnimationDeltaTime, ExtractPointerPosition, ExtractPosition, ExtractIndex, @@ -143,6 +153,7 @@ pub enum ContextFeature { InjectFootprint, InjectRealTime, InjectAnimationTime, + InjectAnimationDeltaTime, InjectPointerPosition, InjectPosition, InjectIndex, @@ -158,10 +169,11 @@ bitflags! { const FOOTPRINT = 1 << 0; const REAL_TIME = 1 << 1; const ANIMATION_TIME = 1 << 2; - const POINTER_POSITION = 1 << 3; - const POSITION = 1 << 4; - const INDEX = 1 << 5; - const VARARGS = 1 << 6; + const ANIMATION_DELTA_TIME = 1 << 3; + const POINTER_POSITION = 1 << 4; + const POSITION = 1 << 5; + const INDEX = 1 << 6; + const VARARGS = 1 << 7; } } @@ -177,6 +189,7 @@ impl ContextFeatures { ContextFeatures::FOOTPRINT => "Footprint", ContextFeatures::REAL_TIME => "RealTime", ContextFeatures::ANIMATION_TIME => "AnimationTime", + ContextFeatures::ANIMATION_DELTA_TIME => "AnimationDeltaTime", ContextFeatures::POINTER_POSITION => "PointerPosition", ContextFeatures::POSITION => "Position", ContextFeatures::INDEX => "Index", @@ -206,6 +219,7 @@ impl From<&[ContextFeature]> for ContextDependencies { ContextFeature::ExtractFootprint => ContextFeatures::FOOTPRINT, ContextFeature::ExtractRealTime => ContextFeatures::REAL_TIME, ContextFeature::ExtractAnimationTime => ContextFeatures::ANIMATION_TIME, + ContextFeature::ExtractAnimationDeltaTime => ContextFeatures::ANIMATION_DELTA_TIME, ContextFeature::ExtractPointerPosition => ContextFeatures::POINTER_POSITION, ContextFeature::ExtractPosition => ContextFeatures::POSITION, ContextFeature::ExtractIndex => ContextFeatures::INDEX, @@ -216,6 +230,7 @@ impl From<&[ContextFeature]> for ContextDependencies { ContextFeature::InjectFootprint => ContextFeatures::FOOTPRINT, ContextFeature::InjectRealTime => ContextFeatures::REAL_TIME, ContextFeature::InjectAnimationTime => ContextFeatures::ANIMATION_TIME, + ContextFeature::InjectAnimationDeltaTime => ContextFeatures::ANIMATION_DELTA_TIME, ContextFeature::InjectPointerPosition => ContextFeatures::POINTER_POSITION, ContextFeature::InjectPosition => ContextFeatures::POSITION, ContextFeature::InjectIndex => ContextFeatures::INDEX, @@ -253,6 +268,11 @@ impl ExtractAnimationTime for Option { self.as_ref().and_then(|x| x.try_animation_time()) } } +impl ExtractAnimationDeltaTime for Option { + fn try_animation_delta_time(&self) -> Option { + self.as_ref().and_then(|x| x.try_animation_delta_time()) + } +} impl ExtractPointerPosition for Option { fn try_pointer_position(&self) -> Option { self.as_ref().and_then(|x| x.try_pointer_position()) @@ -311,6 +331,11 @@ impl ExtractAnimationTime for Arc { (**self).try_animation_time() } } +impl ExtractAnimationDeltaTime for Arc { + fn try_animation_delta_time(&self) -> Option { + (**self).try_animation_delta_time() + } +} impl ExtractPointerPosition for Arc { fn try_pointer_position(&self) -> Option { (**self).try_pointer_position() @@ -446,6 +471,11 @@ impl ExtractAnimationTime for OwnedContextImpl { self.animation_time } } +impl ExtractAnimationDeltaTime for OwnedContextImpl { + fn try_animation_delta_time(&self) -> Option { + self.animation_delta_time + } +} impl ExtractPointerPosition for OwnedContextImpl { fn try_pointer_position(&self) -> Option { self.pointer_position @@ -517,6 +547,7 @@ pub struct OwnedContextImpl { footprint: Option, real_time: Option, animation_time: Option, + animation_delta_time: Option, pointer_position: Option, position: Option>, // This could be converted into a single enum to save extra bytes @@ -531,6 +562,7 @@ impl std::fmt::Debug for OwnedContextImpl { .field("footprint", &self.footprint) .field("real_time", &self.real_time) .field("animation_time", &self.animation_time) + .field("animation_delta_time", &self.animation_delta_time) .field("pointer_position", &self.pointer_position) .field("index", &self.index) .field("varargs_len", &self.varargs.as_ref().map(|x| x.len())) @@ -550,6 +582,7 @@ impl graphene_hash::CacheHash for OwnedContextImpl { self.footprint.cache_hash(state); self.real_time.cache_hash(state); self.animation_time.cache_hash(state); + self.animation_delta_time.cache_hash(state); self.pointer_position.cache_hash(state); self.position.cache_hash(state); self.index.cache_hash(state); @@ -575,6 +608,7 @@ impl OwnedContextImpl { let footprint = bitflags.contains(ContextFeatures::FOOTPRINT).then(|| value.try_footprint().copied()).flatten(); let real_time = bitflags.contains(ContextFeatures::REAL_TIME).then(|| value.try_real_time()).flatten(); let animation_time = bitflags.contains(ContextFeatures::ANIMATION_TIME).then(|| value.try_animation_time()).flatten(); + let animation_delta_time = bitflags.contains(ContextFeatures::ANIMATION_DELTA_TIME).then(|| value.try_animation_delta_time()).flatten(); let pointer_position = bitflags.contains(ContextFeatures::POINTER_POSITION).then(|| value.try_pointer_position()).flatten(); let position = bitflags.contains(ContextFeatures::POSITION).then(|| value.try_position()).flatten().map(|x| x.collect()); let index = bitflags.contains(ContextFeatures::INDEX).then(|| value.try_index()).flatten().map(|x| x.collect()); @@ -584,6 +618,7 @@ impl OwnedContextImpl { footprint, real_time, animation_time, + animation_delta_time, pointer_position, position, index, @@ -597,6 +632,7 @@ impl OwnedContextImpl { footprint: None, real_time: None, animation_time: None, + animation_delta_time: None, pointer_position: None, position: None, index: None, @@ -646,6 +682,10 @@ impl OwnedContextImpl { self.animation_time = Some(animation_time); self } + pub fn with_animation_delta_time(mut self, animation_delta_time: f64) -> Self { + self.animation_delta_time = Some(animation_delta_time); + self + } pub fn with_pointer_position(mut self, pointer_position: DVec2) -> Self { self.pointer_position = Some(pointer_position); self diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index f0641685aa..b49bcaf8a9 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -565,6 +565,7 @@ fn parse_context_feature_idents(ty: &Type) -> Vec { "ExtractFootprint" | "ExtractRealTime" | "ExtractAnimationTime" + | "ExtractAnimationDeltaTime" | "ExtractPointerPosition" | "ExtractPosition" | "ExtractIndex" @@ -572,6 +573,7 @@ fn parse_context_feature_idents(ty: &Type) -> Vec { | "InjectFootprint" | "InjectRealTime" | "InjectAnimationTime" + | "InjectAnimationDeltaTime" | "InjectPointerPosition" | "InjectPosition" | "InjectIndex" diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 4182847d00..8f5656925d 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,6 +1,6 @@ use core_types::list::List; use core_types::transform::Footprint; -use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationDeltaTime, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; use graphic_types::{Artboard, Graphic, Vector}; @@ -60,6 +60,12 @@ fn animation_time( ctx.try_animation_time().unwrap_or_default() * rate } +/// Produces the elapsed animation time, in seconds, since the previous frame was rendered (or delta time-dependent node was evaluated). +#[node_macro::node(category("Animation"))] +fn animation_delta_time(ctx: impl Ctx + ExtractAnimationDeltaTime) -> f64 { + ctx.try_animation_delta_time().unwrap_or_default() +} + #[node_macro::node(category("Debug"))] async fn quantize_real_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index c0eda33a0b..29f0ed4114 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -172,6 +172,7 @@ async fn create_context<'a: 'n>( .with_footprint(footprint) .with_real_time(render_config.time.time) .with_animation_time(render_config.time.animation_time.as_secs_f64()) + .with_animation_delta_time(render_config.time.animation_delta_time) .with_pointer_position(render_config.pointer) .with_vararg(Box::new(render_params)) .into_context();