Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ impl NodeNetworkInterface {
let layer_output = NodeInput::node(layer.to_node(), 0);

match post_node_input {
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
NodeInput::Value { .. } | NodeInput::Timeline { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
// First child in the stack: wire layer output to the post_node input
self.set_input_for_import(&post_node, layer_output, network_path);
}
Expand Down Expand Up @@ -855,7 +855,7 @@ impl NodeNetworkInterface {
if !inserting_into_stack {
match post_node_input {
// Create a new stack
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
NodeInput::Value { .. } | NodeInput::Timeline { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
self.create_wire(&OutputConnector::primary_output(layer.to_node()), &post_node, network_path);

let final_layer_position = after_move_post_layer_position + IVec2::new(-LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP);
Expand All @@ -881,7 +881,7 @@ impl NodeNetworkInterface {
} else {
match post_node_input {
// Move to the bottom of the stack
NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
NodeInput::Value { .. } | NodeInput::Timeline { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => {
let offset = after_move_post_layer_position - previous_layer_position + IVec2::new(0, STACK_VERTICAL_GAP + height_above_layer);
self.shift_absolute_node_position(&layer.to_node(), offset, network_path);
self.create_wire(&OutputConnector::primary_output(layer.to_node()), &post_node, network_path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ impl NodeNetworkInterface {
}

NodeInput::Value { tagged_value, .. } => TypeSource::TaggedValue(tagged_value.ty()),
NodeInput::Timeline { .. } => TypeSource::TaggedValue(concrete!(f64)),
NodeInput::Import { import_index, .. } => {
// Get the input type of the encapsulating node input
let Some((encapsulating_node, encapsulating_path)) = network_path.split_last() else {
Expand Down
54 changes: 54 additions & 0 deletions node-graph/graph-craft/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ pub enum NodeInput {
tagged_value: MemoHash<TaggedValue>,
exposed: bool,
},
/// A reference to an [`AnimationCurve`](core_types::animation::AnimationCurve) on the timeline.
/// Gets converted into an AnimationCurve node during graph compilation.
Timeline {
curve_id: u64,
},

// TODO: Remove import_type and get type from parent node input
/// Input that is provided by the import from the parent network to this document node network.
Expand Down Expand Up @@ -286,6 +291,7 @@ impl NodeInput {
match self {
NodeInput::Node { .. } => true,
NodeInput::Value { exposed, .. } => *exposed,
NodeInput::Timeline { .. } => false,
NodeInput::Import { .. } => true,
NodeInput::Inline(_) => false,
NodeInput::Scope(_) => false,
Expand All @@ -297,6 +303,7 @@ impl NodeInput {
match self {
NodeInput::Node { .. } => unreachable!("ty() called on NodeInput::Node"),
NodeInput::Value { tagged_value, .. } => tagged_value.ty(),
NodeInput::Timeline { .. } => concrete!(f64),
// Stored import types are normalized to their structural form once at document migration
NodeInput::Import { import_type, .. } => import_type.clone(),
NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"),
Expand Down Expand Up @@ -944,6 +951,14 @@ impl NodeNetwork {
return;
};

Self::replace_timeline_inputs_with_nodes(
&mut inner_network.exports,
&mut inner_network.nodes,
node.original_location.path.as_ref().unwrap_or(&vec![]),
gen_id,
map_ids,
id,
);
// Replace value and reflection imports with value nodes, added inside nested network
Self::replace_value_inputs_with_nodes(
&mut inner_network.exports,
Expand Down Expand Up @@ -994,6 +1009,7 @@ impl NodeNetwork {
*import_index = parent_input_index;
}
NodeInput::Value { .. } => unreachable!("Value inputs should have been replaced with value nodes"),
NodeInput::Timeline { .. } => unreachable!("Value inputs should have been replaced with animation curve nodes"),
NodeInput::Inline(_) => (),
NodeInput::Scope(_) => unreachable!("Scope inputs should have been resolved by resolve_scope_inputs_recursive before flattening"),
NodeInput::Reflection(_) => unreachable!("Reflection inputs should have been replaced with value nodes"),
Expand Down Expand Up @@ -1032,6 +1048,44 @@ impl NodeNetwork {
}
}

fn replace_timeline_inputs_with_nodes(
inputs: &mut [NodeInput],
collection: &mut FxHashMap<NodeId, DocumentNode>,
path: &[NodeId],
gen_id: impl Fn() -> NodeId + Copy,
map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy,
id: NodeId,
) {
for input in inputs {
let NodeInput::Timeline { curve_id } = *input else { continue };

let curve_node_id = gen_id();
let merged_node_id = map_ids(id, curve_node_id);
let mut original_location = OriginalLocation {
path: Some(path.to_vec()),
dependants: vec![vec![id]],
..Default::default()
};
if let Some(path) = &mut original_location.path {
path.push(curve_node_id);
}

collection.insert(
merged_node_id,
DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::U64(curve_id), false)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_core::animation::animation_curve::IDENTIFIER),
original_location,
..Default::default()
},
);
*input = NodeInput::Node {
node_id: merged_node_id,
output_index: 0,
};
}
}

#[inline(never)]
fn replace_value_inputs_with_nodes(
inputs: &mut [NodeInput],
Expand Down
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 @@ -6,6 +6,7 @@ use brush_nodes::{BrushCache, Stroke};
use core_types::color::SRGBA8;
use core_types::list::{Item, List, NodeIdPath};
use core_types::transfer_curve::TransferCurve;
use core_types::animation::AnimationCurve;
use core_types::transform::Footprint;
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor};
use dyn_any::DynAny;
Expand Down Expand Up @@ -544,6 +545,7 @@ tagged_value! {
LegacyOptionalDAffine2(Option<DAffine2>),
#[serde(alias = "FillGradient")]
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
AnimationCurve(AnimationCurve),
// ==========
// ENUM TYPES
// ==========
Expand Down
6 changes: 6 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};
use graph_craft::application_io::PlatformEditorApi;
Expand Down Expand Up @@ -91,6 +92,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<RenderOutput>, Context => Item<graphene_std::ContextFeatures>]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>, Context => Item<graphene_std::ContextFeatures>]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => Item<graphene_std::ContextFeatures>]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AnimationCurve, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AnimationCurve]),

#[cfg(target_family = "wasm")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<CanvasHandle>, Context => Item<graphene_std::ContextFeatures>]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item<graphene_std::ContextFeatures>]),
Expand Down Expand Up @@ -145,6 +149,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<CanvasHandle>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<RenderOutput>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&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 => List<graphene_std::brush::Stroke>]),
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
Loading