mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
parent
b98711dbdb
commit
44694ff8d6
31 changed files with 428 additions and 62 deletions
64
node-graph/gcore/src/animation.rs
Normal file
64
node-graph/gcore/src/animation.rs
Normal 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
|
||||
// }
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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.;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue