Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::application_io::PlatformEditorApi;
use crate::proto::{Any as DAny, FutureAny};
use brush_nodes::brush_stroke::BrushStroke;
use core_types::color::SRGBA8;
use core_types::animation::AnimationCurve;
use core_types::list::List;
use core_types::transform::Footprint;
use core_types::uuid::NodeId;
Expand Down Expand Up @@ -397,6 +398,7 @@ tagged_value! {
VectorModification(Box<VectorModification>),
ImageData(Image<Color>),
Resource(graphene_application_io::ResourceHash),
AnimationCurve(AnimationCurve),
// ==========
// ENUM TYPES
// ==========
Expand Down
4 changes: 4 additions & 0 deletions node-graph/interpreted-executor/src/node_registry.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use core_types::animation::AnimationCurve;
use dyn_any::StaticType;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::application_io::PlatformEditorApi;
Expand Down Expand Up @@ -167,6 +168,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AnimationCurve]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]),
Expand Down Expand Up @@ -194,6 +196,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AnimationCurve, Context => graphene_std::ContextFeatures]),
#[cfg(target_family = "wasm")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]),
// ==========
Expand Down Expand Up @@ -232,6 +235,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AnimationCurve]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
Expand Down
202 changes: 202 additions & 0 deletions node-graph/libraries/core-types/src/animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//! Animation Curve implementation based off of Blender's fcurves.
//!

use dyn_any::DynAny;

use glam::DVec2;
use graphene_hash::CacheHash;
use kurbo::{CubicBez, ParamCurve, Point};

// Every keyframe defines a left handle point for any bezier easings to the left,
// and info defining the behavior to the right hand side of the keyframe
#[derive(Debug, Clone, Copy, PartialEq, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Keyframe {
/// If None, defaults to knot in the case of a bezier keyframe to the left.
pub left_handle: Option<DVec2>,
pub knot: DVec2,
pub interp_behavior: InterpolationBehavior,
}
impl Keyframe {
pub fn new_linear(knot: DVec2, left_handle: Option<DVec2>) -> Self {
Self {
left_handle,
knot,
interp_behavior: InterpolationBehavior::Linear,
}
}
pub fn new_constant(knot: DVec2, left_handle: Option<DVec2>) -> Self {
Self {
left_handle,
knot,
interp_behavior: InterpolationBehavior::Constant,
}
}
pub fn new_bezier(knot: DVec2, left_handle: Option<DVec2>, right_handle: DVec2) -> Self {
Self {
left_handle,
knot,
interp_behavior: InterpolationBehavior::Bezier { right_handle },
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum InterpolationBehavior {
Bezier { right_handle: DVec2 },
Constant,
Linear,
}

#[derive(Default, Debug, Clone, PartialEq, DynAny, CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnimationCurve {
keyframes: Vec<Keyframe>, // not public to maintain sorted order
}

impl AnimationCurve {
pub fn new() -> Self {
Self { keyframes: Vec::new() }
}

pub fn evaluate(&self, time: f64) -> f64 {
if self.keyframes.is_empty() || !time.is_finite() {
return 0.0;
}

// keyframes should (hopefully) have finite, real coordinates
let index = self.keyframes.binary_search_by(|kf| kf.knot.x.partial_cmp(&time).unwrap_or(std::cmp::Ordering::Equal));

// We are on a keyframe, use its knot
if let Ok(idx) = index {
return self.keyframes[idx].knot.y;
}

let index = index.unwrap_err();

if index == 0 {
return 0.0;
} else if index == self.keyframes.len() {
// unwrap is safe because of the non-empty guard at the top
return self.keyframes.last().unwrap().knot.y;
}

let segment_start = &self.keyframes[index - 1];
let segment_end = &self.keyframes[index];

match segment_start.interp_behavior {
InterpolationBehavior::Bezier { right_handle } => {
let to_point = |vec: DVec2| Point::new(vec.x, vec.y);

let curve = CubicBez::new(
to_point(segment_start.knot),
to_point(right_handle),
segment_end.left_handle.map(|end| to_point(end)).unwrap_or_else(|| to_point(segment_end.knot)),
to_point(segment_end.knot),
);

// Find the value of t where curve.x == time to find the value
//TODO: find proper values for epsilon and k1. The docs suggest 0.2 for k1 but epsilon should be tested with several values
let t = kurbo::common::solve_itp(|t| curve.eval(t).x - time, 0.0, 1.0, 0.00001, 1, 0.2, segment_start.knot.x - time, segment_end.knot.x - time);

curve.eval(t).y
}
InterpolationBehavior::Constant => segment_start.knot.y,
InterpolationBehavior::Linear => {
let start = segment_start.knot.y;
let end = segment_end.knot.y;
let i = (time - segment_start.knot.x) / (segment_end.knot.x - segment_start.knot.x);

start + (end - start) * i
}
}
}

pub fn keyframes(&self) -> &[Keyframe] {
&self.keyframes
}

pub fn push_keyframe(&mut self, keyframe: Keyframe) {
self.keyframes.push(keyframe);
self.keyframes.sort_by(|lhs, rhs| lhs.knot.x.partial_cmp(&rhs.knot.x).unwrap_or(std::cmp::Ordering::Equal));
}
pub fn remove_keyframe(&mut self, idx: usize) -> Option<Keyframe> {
if idx >= self.keyframes.len() {
return None;
}
Some(self.keyframes.remove(idx))
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn out_of_bounds() {
let empty_curve = AnimationCurve::new();
assert_eq!(empty_curve.evaluate(10.0), 0.0);

let mut single_kf = AnimationCurve::new();
single_kf.push_keyframe(Keyframe {
left_handle: None,
knot: DVec2::new(1.0, 10.0),
interp_behavior: InterpolationBehavior::Constant,
});
assert_eq!(single_kf.evaluate(0.0), 0.0);
assert_eq!(single_kf.evaluate(2.0), 10.0);
}

#[test]
pub fn bezier_segment() {
let mut anim_curve = AnimationCurve::new();
anim_curve.push_keyframe(Keyframe {
left_handle: None,
knot: DVec2::new(0.0, 0.0),
interp_behavior: InterpolationBehavior::Bezier { right_handle: DVec2::new(0.5, 0.0) },
});
anim_curve.push_keyframe(Keyframe {
left_handle: Some(DVec2::new(0.5, 1.0)),
knot: DVec2::new(1.0, 1.0),
interp_behavior: InterpolationBehavior::Constant,
});

assert_eq!(anim_curve.evaluate(0.5), 0.5);
assert!(anim_curve.evaluate(0.25) - 0.104 < 0.01);
assert!(anim_curve.evaluate(0.75) - 0.896 < 0.01);
}

#[test]
pub fn simple_segments() {
let mut anim_curve = AnimationCurve::new();
anim_curve.push_keyframe(Keyframe {
left_handle: None,
knot: DVec2::new(0.0, 0.0),
interp_behavior: InterpolationBehavior::Linear,
});
anim_curve.push_keyframe(Keyframe {
left_handle: None,
knot: DVec2::new(1.0, 1.0),
interp_behavior: InterpolationBehavior::Constant,
});
anim_curve.push_keyframe(Keyframe {
left_handle: None,
knot: DVec2::new(2.0, 0.0),
interp_behavior: InterpolationBehavior::Constant,
});
anim_curve.push_keyframe(Keyframe {
left_handle: None,
knot: DVec2::new(3.0, 1.0),
interp_behavior: InterpolationBehavior::Constant,
});

assert_eq!(anim_curve.evaluate(0.5), 0.5);
assert_eq!(anim_curve.evaluate(0.25), 0.25);
assert_eq!(anim_curve.evaluate(0.75), 0.75);

assert_eq!(anim_curve.evaluate(2.5), 0.0);
}

#[test]
pub fn constant_segment() {}
}
1 change: 1 addition & 0 deletions node-graph/libraries/core-types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
extern crate log;

pub mod animation;
pub mod bounds;
pub mod consts;
pub mod context;
Expand Down
17 changes: 17 additions & 0 deletions node-graph/nodes/gcore/src/animation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use core_types::animation::{AnimationCurve, Keyframe};
use core_types::list::List;
use core_types::transform::Footprint;
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
Expand Down Expand Up @@ -27,6 +28,22 @@ pub enum AnimationTimeMode {
FrameNumber,
}

/// Evaluate the value of an animation curve with the given time
#[node_macro::node(category("Animation"))]
fn eval_curve(_: impl Ctx, curve: AnimationCurve, time: f64) -> f64 {
curve.evaluate(time)
}

/// Contstructs a new AnimationCurves value with default curve
#[node_macro::node(category("Value"))]
fn animation_curve_value(_: impl Ctx, _primary: ()) -> AnimationCurve {
let mut curve = AnimationCurve::new();
curve.push_keyframe(Keyframe::new_linear(DVec2::new(0.0, 0.0), None));
curve.push_keyframe(Keyframe::new_constant(DVec2::new(1.0, 360.0), None));

curve
}

/// Produces a chosen representation of the current real time and date (in UTC) based on the system clock.
#[node_macro::node(category("Animation"))]
fn real_time(
Expand Down
2 changes: 2 additions & 0 deletions node-graph/nodes/gcore/src/context_modification.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use core::f64;
use core_types::animation::AnimationCurve;
use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll};
use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
use core_types::transform::Footprint;
Expand Down Expand Up @@ -40,6 +41,7 @@ async fn context_modification<T>(
Context -> AttributeDyn,
Context -> AttributeValueDyn,
Context -> ListDyn,
Context -> AnimationCurve,
)]
value: impl Node<Context<'static>, Output = T>,
/// The parts of the context to keep when evaluating the input value. All other parts are nullified.
Expand Down