Experimental animation support (#2443)

* Implement experimental time routing to the node graph

* Allow toggling live preview with SHIFT + SPACE

* Add animation message handler

* Fix hotkeys

* Fix milisecond node

* Adevertize set frame index action

* Fix frame index

* Fix year calculation

* Add comment for why month and day are not exposed

* Combine animation nodes and fix animation time implementation

* Fix animation time interaction with playback

* Add set animation time mode message

* Captalize UTC

* Fix compiling

* Fix crash and add text nodes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert 2025-03-19 09:19:49 +01:00 committed by GitHub
parent b98711dbdb
commit 44694ff8d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 428 additions and 62 deletions

View file

@ -0,0 +1,64 @@
use crate::{Ctx, ExtractAnimationTime, ExtractTime};
const DAY: f64 = 1000. * 3600. * 24.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RealTimeMode {
Utc,
Year,
Hour,
Minute,
#[default]
Second,
Millisecond,
}
impl core::fmt::Display for RealTimeMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RealTimeMode::Utc => write!(f, "UTC"),
RealTimeMode::Year => write!(f, "Year"),
RealTimeMode::Hour => write!(f, "Hour"),
RealTimeMode::Minute => write!(f, "Minute"),
RealTimeMode::Second => write!(f, "Second"),
RealTimeMode::Millisecond => write!(f, "Millisecond"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimationTimeMode {
AnimationTime,
FrameNumber,
}
#[node_macro::node(category("Animation"))]
fn real_time(ctx: impl Ctx + ExtractTime, _primary: (), mode: RealTimeMode) -> f64 {
let time = ctx.try_time().unwrap_or_default();
// TODO: Implement proper conversion using and existing time implementation
match mode {
RealTimeMode::Utc => time,
RealTimeMode::Year => (time / DAY / 365.25).floor() + 1970.,
RealTimeMode::Hour => (time / 1000. / 3600.).floor() % 24.,
RealTimeMode::Minute => (time / 1000. / 60.).floor() % 60.,
RealTimeMode::Second => (time / 1000.).floor() % 60.,
RealTimeMode::Millisecond => time % 1000.,
}
}
#[node_macro::node(category("Animation"))]
fn animation_time(ctx: impl Ctx + ExtractAnimationTime) -> f64 {
ctx.try_animation_time().unwrap_or_default()
}
// These nodes require more sophistcated algorithms for giving the correct result
// #[node_macro::node(category("Animation"))]
// fn month(ctx: impl Ctx + ExtractTime) -> f64 {
// ((ctx.try_time().unwrap_or_default() / DAY / 365.25 % 1.) * 12.).floor()
// }
// #[node_macro::node(category("Animation"))]
// fn day(ctx: impl Ctx + ExtractTime) -> f64 {
// (ctx.try_time().unwrap_or_default() / DAY
// }

View file

@ -8,6 +8,7 @@ use core::future::Future;
use core::hash::{Hash, Hasher};
use core::pin::Pin;
use core::ptr::addr_of;
use core::time::Duration;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{DAffine2, UVec2};
@ -250,10 +251,17 @@ pub enum ExportFormat {
Canvas,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct TimingInformation {
pub time: f64,
pub animation_time: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)]
pub struct RenderConfig {
pub viewport: Footprint,
pub export_format: ExportFormat,
pub time: TimingInformation,
pub view_mode: ViewMode,
pub hide_artboards: bool,
pub for_export: bool,

View file

@ -22,6 +22,10 @@ pub trait ExtractTime {
fn try_time(&self) -> Option<f64>;
}
pub trait ExtractAnimationTime {
fn try_animation_time(&self) -> Option<f64>;
}
pub trait ExtractIndex {
fn try_index(&self) -> Option<usize>;
}
@ -38,9 +42,9 @@ pub trait CloneVarArgs: ExtractVarArgs {
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>>;
}
pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractTime + ExtractVarArgs {}
pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractTime + ExtractAnimationTime + ExtractVarArgs {}
impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractTime + ExtractVarArgs> ExtractAll for T {}
impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractTime + ExtractAnimationTime + ExtractVarArgs> ExtractAll for T {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VarArgsResult {
@ -81,6 +85,11 @@ impl<T: ExtractTime + Sync> ExtractTime for Option<T> {
self.as_ref().and_then(|x| x.try_time())
}
}
impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Option<T> {
fn try_animation_time(&self) -> Option<f64> {
self.as_ref().and_then(|x| x.try_animation_time())
}
}
impl<T: ExtractIndex> ExtractIndex for Option<T> {
fn try_index(&self) -> Option<usize> {
self.as_ref().and_then(|x| x.try_index())
@ -107,6 +116,11 @@ impl<T: ExtractTime + Sync> ExtractTime for Arc<T> {
(**self).try_time()
}
}
impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Arc<T> {
fn try_animation_time(&self) -> Option<f64> {
(**self).try_animation_time()
}
}
impl<T: ExtractIndex> ExtractIndex for Arc<T> {
fn try_index(&self) -> Option<usize> {
(**self).try_index()
@ -182,6 +196,11 @@ impl ExtractTime for OwnedContextImpl {
self.time
}
}
impl ExtractAnimationTime for OwnedContextImpl {
fn try_animation_time(&self) -> Option<f64> {
self.animation_time
}
}
impl ExtractIndex for OwnedContextImpl {
fn try_index(&self) -> Option<usize> {
self.index
@ -227,6 +246,7 @@ pub struct OwnedContextImpl {
// This could be converted into a single enum to save extra bytes
index: Option<usize>,
time: Option<f64>,
animation_time: Option<f64>,
}
impl Default for OwnedContextImpl {
@ -252,6 +272,7 @@ impl OwnedContextImpl {
let footprint = value.try_footprint().copied();
let index = value.try_index();
let time = value.try_time();
let frame_time = value.try_animation_time();
let parent = value.arc_clone();
OwnedContextImpl {
footprint,
@ -259,6 +280,7 @@ impl OwnedContextImpl {
parent,
index,
time,
animation_time: frame_time,
}
}
pub const fn empty() -> Self {
@ -268,6 +290,7 @@ impl OwnedContextImpl {
parent: None,
index: None,
time: None,
animation_time: None,
}
}
}
@ -280,6 +303,14 @@ impl OwnedContextImpl {
self.footprint = Some(footprint);
self
}
pub fn with_time(mut self, time: f64) -> Self {
self.time = Some(time);
self
}
pub fn with_animation_time(mut self, animation_time: f64) -> Self {
self.animation_time = Some(animation_time);
self
}
pub fn into_context(self) -> Option<Arc<Self>> {
Some(Arc::new(self))
}

View file

@ -296,31 +296,31 @@ async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElemen
stack
}
// TODO: Once we have nicely working spreadsheet tables, test this and make it nicely user-facing and move it from "Debug" to "General"
#[node_macro::node(category("Debug"))]
async fn concatenate<T: Clone>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
ImageFrameTable<Color>,
TextureFrameTable,
)]
from: Instances<T>,
#[expose]
#[implementations(
GraphicGroupTable,
VectorDataTable,
ImageFrameTable<Color>,
TextureFrameTable,
)]
mut to: Instances<T>,
) -> Instances<T> {
for instance in from.instances() {
to.push_instance(instance);
}
to
}
// // TODO: Once we have nicely working spreadsheet tables, test this and make it nicely user-facing and move it from "Debug" to "General"
// #[node_macro::node(category("Debug"))]
// async fn concatenate<T: Clone>(
// _: impl Ctx,
// #[implementations(
// GraphicGroupTable,
// VectorDataTable,
// ImageFrameTable<Color>,
// TextureFrameTable,
// )]
// from: Instances<T>,
// #[expose]
// #[implementations(
// GraphicGroupTable,
// VectorDataTable,
// ImageFrameTable<Color>,
// TextureFrameTable,
// )]
// mut to: Instances<T>,
// ) -> Instances<T> {
// for instance in from.instances() {
// to.push_instance(instance);
// }
// to
// }
#[node_macro::node(category("Debug"))]
async fn to_element<Data: Into<GraphicElement> + 'n>(

View file

@ -13,6 +13,7 @@ pub use crate as graphene_core;
#[cfg(feature = "reflections")]
pub use ctor;
pub mod animation;
pub mod consts;
pub mod context;
pub mod generic;

View file

@ -10,12 +10,35 @@ fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bo
value
}
#[node_macro::node(category("Debug"), skip_impl)]
#[node_macro::node(category("Text"))]
fn to_string<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
format!("{:?}", value)
}
#[node_macro::node(category("Debug"))]
#[node_macro::node(category("Text"))]
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, #[implementations(String)] second: String) -> String {
first.clone() + &second
}
#[node_macro::node(category("Text"))]
fn string_replace(_: impl Ctx, #[implementations(String)] string: String, from: String, to: String) -> String {
string.replace(&from, &to)
}
#[node_macro::node(category("Text"))]
fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f64, end: f64) -> String {
let start = if start < 0. { string.len() - start.abs() as usize } else { start as usize };
let end = if end <= 0. { string.len() - end.abs() as usize } else { end as usize };
let n = end.saturating_sub(start);
string.char_indices().skip(start).take(n).map(|(_, c)| c).collect()
}
#[node_macro::node(category("Text"))]
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize {
string.len()
}
#[node_macro::node(category("Text"))]
async fn switch<T, C: Send + 'n + Clone>(
#[implementations(Context)] ctx: C,
condition: bool,

View file

@ -412,6 +412,12 @@ fn blend_mode_value(_: impl Ctx, _primary: (), blend_mode: BlendMode) -> BlendMo
blend_mode
}
/// Constructs a string value which may be set to any plain text.
#[node_macro::node(category("Value"))]
fn string_value(_: impl Ctx, _primary: (), string: String) -> String {
string
}
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[cfg(feature = "std")]
#[node_macro::node(category("Debug"))]

View file

@ -3,6 +3,8 @@ use crate::vector::{HandleId, VectorData, VectorDataTable};
use bezier_rs::Subpath;
use glam::DVec2;
use super::misc::AsU64;
trait CornerRadius {
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable;
}
@ -70,30 +72,32 @@ fn rectangle<T: CornerRadius>(
}
#[node_macro::node(category("Vector: Shape"))]
fn regular_polygon(
fn regular_polygon<T: AsU64>(
_: impl Ctx,
_primary: (),
#[default(6)]
#[min(3.)]
sides: u32,
#[implementations(u32, u64, f64)]
sides: T,
#[default(50)] radius: f64,
) -> VectorDataTable {
let points = sides.into();
let points = sides.as_u64();
let radius: f64 = radius * 2.;
VectorDataTable::new(VectorData::from_subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius)))
}
#[node_macro::node(category("Vector: Shape"))]
fn star(
fn star<T: AsU64>(
_: impl Ctx,
_primary: (),
#[default(5)]
#[min(2.)]
sides: u32,
#[implementations(u32, u64, f64)]
sides: T,
#[default(50)] radius: f64,
#[default(25)] inner_radius: f64,
) -> VectorDataTable {
let points = sides.into();
let points = sides.as_u64();
let diameter: f64 = radius * 2.;
let inner_diameter = inner_radius * 2.;

View file

@ -47,3 +47,41 @@ impl core::fmt::Display for BooleanOperation {
}
}
}
pub trait AsU64 {
fn as_u64(&self) -> u64;
}
impl AsU64 for u32 {
fn as_u64(&self) -> u64 {
*self as u64
}
}
impl AsU64 for u64 {
fn as_u64(&self) -> u64 {
*self
}
}
impl AsU64 for f64 {
fn as_u64(&self) -> u64 {
*self as u64
}
}
pub trait AsI64 {
fn as_i64(&self) -> i64;
}
impl AsI64 for u32 {
fn as_i64(&self) -> i64 {
*self as i64
}
}
impl AsI64 for u64 {
fn as_i64(&self) -> i64 {
*self as i64
}
}
impl AsI64 for f64 {
fn as_i64(&self) -> i64 {
*self as i64
}
}

View file

@ -182,6 +182,7 @@ tagged_value! {
NodePath(Vec<NodeId>),
VecDVec2(Vec<DVec2>),
RedGreenBlue(graphene_core::raster::RedGreenBlue),
RealTimeMode(graphene_core::animation::RealTimeMode),
RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha),
NoiseType(graphene_core::raster::NoiseType),
FractalType(graphene_core::raster::FractalType),

View file

@ -559,12 +559,10 @@ impl core::fmt::Debug for GraphErrorType {
let inputs = inputs.replace("Option<Arc<OwnedContextImpl>>", "Context");
write!(
f,
"This node isn't compatible with the com-\n\
bination of types for the data it is given:\n\
"This node isn't compatible with the combination of types for the data it is given:\n\
{inputs}\n\
\n\
Each invalid input should be replaced by\n\
data with one of these supported types:\n\
Each invalid input should be replaced by data with one of these supported types:\n\
{}",
errors.join("\n")
)

View file

@ -235,7 +235,11 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
) -> RenderOutput {
let footprint = render_config.viewport;
let ctx = OwnedContextImpl::default().with_footprint(footprint).into_context();
let ctx = OwnedContextImpl::default()
.with_footprint(footprint)
.with_time(render_config.time.time)
.with_animation_time(render_config.time.animation_time.as_secs_f64())
.into_context();
ctx.footprint();
let RenderConfig { hide_artboards, for_export, .. } = render_config;