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

@ -13,6 +13,7 @@ pub struct Dispatcher {
#[derive(Debug, Default)]
pub struct DispatcherMessageHandlers {
animation_message_handler: AnimationMessageHandler,
broadcast_message_handler: BroadcastMessageHandler,
debug_message_handler: DebugMessageHandler,
dialog_message_handler: DialogMessageHandler,
@ -50,12 +51,9 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)),
MessageDiscriminant::InputPreprocessor(InputPreprocessorMessageDiscriminant::FrameTimeAdvance),
];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame))];
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw"];
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
impl Dispatcher {
pub fn new() -> Self {
@ -177,6 +175,9 @@ impl Dispatcher {
// Finish loading persistent data from the browser database
queue.add(FrontendMessage::TriggerLoadRestAutoSaveDocuments);
}
Message::Animation(message) => {
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
}
Message::Batched(messages) => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
}
@ -232,6 +233,7 @@ impl Dispatcher {
let preferences = &self.message_handlers.preferences_message_handler;
let current_tool = &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type;
let message_logging_verbosity = self.message_handlers.debug_message_handler.message_logging_verbosity;
let timing_information = self.message_handlers.animation_message_handler.timing_information();
self.message_handlers.portfolio_message_handler.process_message(
message,
@ -241,6 +243,7 @@ impl Dispatcher {
preferences,
current_tool,
message_logging_verbosity,
timing_information,
},
);
}
@ -283,6 +286,7 @@ impl Dispatcher {
// TODO: Reduce the number of heap allocations
let mut list = Vec::new();
list.extend(self.message_handlers.dialog_message_handler.actions());
list.extend(self.message_handlers.animation_message_handler.actions());
list.extend(self.message_handlers.input_preprocessor_message_handler.actions());
list.extend(self.message_handlers.key_mapping_message_handler.actions());
list.extend(self.message_handlers.debug_message_handler.actions());

View file

@ -0,0 +1,17 @@
use crate::messages::prelude::*;
use super::animation_message_handler::AnimationTimeMode;
#[impl_message(Message, Animation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AnimationMessage {
ToggleLivePreview,
EnableLivePreview,
DisableLivePreview,
ResetAnimation,
SetFrameIndex(f64),
SetTime(f64),
UpdateTime,
IncrementFrameCounter,
SetAnimationTimeMode(AnimationTimeMode),
}

View file

@ -0,0 +1,84 @@
use std::time::Duration;
use crate::messages::prelude::*;
use super::TimingInformation;
#[derive(PartialEq, Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum AnimationTimeMode {
#[default]
TimeBased,
FrameBased,
}
#[derive(Debug, Default)]
pub struct AnimationMessageHandler {
live_preview: bool,
timestamp: f64,
frame_index: f64,
animation_start: Option<f64>,
fps: f64,
animation_time_mode: AnimationTimeMode,
}
impl AnimationMessageHandler {
pub(crate) fn timing_information(&self) -> TimingInformation {
let animation_time = self.timestamp - self.animation_start.unwrap_or(self.timestamp);
let animation_time = match self.animation_time_mode {
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 }
}
}
impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
fn process_message(&mut self, message: AnimationMessage, responses: &mut VecDeque<Message>, _data: ()) {
match message {
AnimationMessage::ToggleLivePreview => {
if self.animation_start.is_none() {
self.animation_start = Some(self.timestamp);
}
self.live_preview = !self.live_preview
}
AnimationMessage::EnableLivePreview => {
if self.animation_start.is_none() {
self.animation_start = Some(self.timestamp);
}
self.live_preview = true
}
AnimationMessage::DisableLivePreview => self.live_preview = false,
AnimationMessage::SetFrameIndex(frame) => {
self.frame_index = frame;
log::debug!("set frame index to {}", frame);
responses.add(PortfolioMessage::SubmitActiveGraphRender)
}
AnimationMessage::SetTime(time) => {
self.timestamp = time;
responses.add(AnimationMessage::UpdateTime);
}
AnimationMessage::IncrementFrameCounter => {
if self.live_preview {
self.frame_index += 1.;
responses.add(AnimationMessage::UpdateTime);
}
}
AnimationMessage::UpdateTime => {
if self.live_preview {
responses.add(PortfolioMessage::SubmitActiveGraphRender)
}
}
AnimationMessage::ResetAnimation => {
self.frame_index = 0.;
self.animation_start = None;
responses.add(PortfolioMessage::SubmitActiveGraphRender)
}
AnimationMessage::SetAnimationTimeMode(animation_time_mode) => self.animation_time_mode = animation_time_mode,
}
}
advertise_actions!(AnimationMessageDiscriminant;
ToggleLivePreview,
SetFrameIndex,
ResetAnimation,
);
}

View file

@ -0,0 +1,9 @@
mod animation_message;
mod animation_message_handler;
#[doc(inline)]
pub use animation_message::{AnimationMessage, AnimationMessageDiscriminant};
#[doc(inline)]
pub use animation_message_handler::AnimationMessageHandler;
pub use graphene_core::application_io::TimingInformation;

View file

@ -430,6 +430,9 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Digit0); modifiers=[Alt], action_dispatch=DebugMessage::MessageOff),
entry!(KeyDown(Digit1); modifiers=[Alt], action_dispatch=DebugMessage::MessageNames),
entry!(KeyDown(Digit2); modifiers=[Alt], action_dispatch=DebugMessage::MessageContents),
// AnimationMessage
entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview),
entry!(KeyDown(ArrowLeft); modifiers=[Control], action_dispatch=AnimationMessage::ResetAnimation),
];
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move) = mappings;

View file

@ -1,7 +1,6 @@
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ViewportBounds};
use crate::messages::prelude::*;
use core::time::Duration;
#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -13,6 +12,6 @@ pub enum InputPreprocessorMessage {
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
FrameTimeAdvance { timestamp: Duration },
CurrentTime { timestamp: u64 },
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
}

View file

@ -12,6 +12,7 @@ pub struct InputPreprocessorMessageData {
#[derive(Debug, Default)]
pub struct InputPreprocessorMessageHandler {
pub frame_time: FrameTimeInfo,
pub time: u64,
pub keyboard: KeyStates,
pub mouse: MouseState,
pub viewport_bounds: ViewportBounds,
@ -93,8 +94,9 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageData> for
self.translate_mouse_event(mouse_state, false, responses);
}
InputPreprocessorMessage::FrameTimeAdvance { timestamp } => {
self.frame_time.advance_timestamp(timestamp);
InputPreprocessorMessage::CurrentTime { timestamp } => {
responses.add(AnimationMessage::SetTime(timestamp as f64));
self.time = timestamp;
}
InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);

View file

@ -10,6 +10,8 @@ pub enum Message {
StartBuffer,
EndBuffer(graphene_std::renderer::RenderMetadata),
#[child]
Animation(AnimationMessage),
#[child]
Broadcast(BroadcastMessage),
#[child]

View file

@ -1,5 +1,6 @@
//! The root-level messages forming the first layer of the message system architecture.
pub mod animation;
pub mod broadcast;
pub mod debug;
pub mod dialog;

View file

@ -2086,7 +2086,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNodeDefinition {
identifier: "Text",
category: "Vector",
category: "Text",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_std::text::TextNode"),

View file

@ -20,6 +20,7 @@ use graphene_core::raster::{
use graphene_core::text::Font;
use graphene_core::vector::misc::CentroidType;
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
use graphene_std::animation::RealTimeMode;
use graphene_std::application_io::TextureFrameTable;
use graphene_std::transform::Footprint;
use graphene_std::vector::VectorDataTable;
@ -165,6 +166,7 @@ pub(crate) fn property_from_type(
last.clone()
}
Some(x) if x == TypeId::of::<BlendMode>() => blend_mode(document_node, node_id, index, name, true),
Some(x) if x == TypeId::of::<RealTimeMode>() => real_time_mode(document_node, node_id, index, name, true),
Some(x) if x == TypeId::of::<RedGreenBlue>() => color_channel(document_node, node_id, index, name, true),
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => rgba_channel(document_node, node_id, index, name, true),
Some(x) if x == TypeId::of::<NoiseType>() => noise_type(document_node, node_id, index, name, true),
@ -778,6 +780,40 @@ pub fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize
LayoutGroup::Row { widgets }.with_tooltip("Color Channel")
}
pub fn real_time_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return LayoutGroup::Row { widgets: vec![] };
};
if let Some(&TaggedValue::RealTimeMode(mode)) = input.as_non_exposed_value() {
let calculation_modes = [
RealTimeMode::Utc,
RealTimeMode::Year,
RealTimeMode::Hour,
RealTimeMode::Minute,
RealTimeMode::Second,
RealTimeMode::Millisecond,
];
let mut entries = Vec::with_capacity(calculation_modes.len());
for method in calculation_modes {
entries.push(
MenuListEntry::new(format!("{method:?}"))
.label(method.to_string())
.on_update(update_value(move |_| TaggedValue::RealTimeMode(method), node_id, index))
.on_commit(commit_value),
);
}
let entries = vec![entries];
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(),
]);
}
LayoutGroup::Row { widgets }.with_tooltip("Real Time Mode")
}
pub fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
let Some(input) = document_node.inputs.get(index) else {

View file

@ -115,6 +115,7 @@ pub enum PortfolioMessage {
bounds: ExportBounds,
transparent_background: bool,
},
SubmitActiveGraphRender,
SubmitGraphRender {
document_id: DocumentId,
ignore_hash: bool,

View file

@ -4,6 +4,7 @@ use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid;
use crate::consts::DEFAULT_DOCUMENT_NAME;
use crate::messages::animation::TimingInformation;
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::FrontendDocumentDetails;
@ -32,6 +33,7 @@ pub struct PortfolioMessageData<'a> {
pub preferences: &'a PreferencesMessageHandler,
pub current_tool: &'a ToolType,
pub message_logging_verbosity: MessageLoggingVerbosity,
pub timing_information: TimingInformation,
}
#[derive(Debug, Default)]
@ -56,6 +58,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
preferences,
current_tool,
message_logging_verbosity,
timing_information,
} = data;
match message {
@ -306,6 +309,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
let _ = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
ipp.viewport_bounds.size().as_uvec2(),
timing_information,
inspect_node,
true,
);
@ -1072,11 +1076,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
});
}
}
PortfolioMessage::SubmitActiveGraphRender => {
if let Some(document_id) = self.active_document_id {
responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
}
}
PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => {
let inspect_node = self.inspect_node_id();
let result = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
ipp.viewport_bounds.size().as_uvec2(),
timing_information,
inspect_node,
ignore_hash,
);

View file

@ -2,6 +2,7 @@
pub use crate::utility_traits::{ActionList, AsMessage, MessageHandler, ToDiscriminant, TransitiveChild};
// Message, MessageData, MessageDiscriminant, MessageHandler
pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler};
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageData, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};

View file

@ -57,7 +57,9 @@ where
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad {
let (text, font, typesetting) = get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool");
let Some((text, font, typesetting)) = get_text(layer, &document.network_interface) else {
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
};
let buzz_face = font_cache.get(font).map(|data| load_face(data));
let far = graphene_core::text::bounding_box(text, buzz_face.as_ref(), typesetting, false);

View file

@ -1,4 +1,5 @@
use crate::consts::FILE_SAVE_SUFFIX;
use crate::messages::animation::TimingInformation;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::prelude::*;
use glam::{DAffine2, DVec2, UVec2};
@ -572,13 +573,14 @@ impl NodeGraphExecutor {
}
/// Adds an evaluate request for whatever current network is cached.
pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2) -> Result<(), String> {
pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, time: TimingInformation) -> Result<(), String> {
let render_config = RenderConfig {
viewport: Footprint {
transform: document.metadata().document_to_viewport,
resolution: viewport_resolution,
..Default::default()
},
time,
#[cfg(any(feature = "resvg", feature = "vello"))]
export_format: graphene_core::application_io::ExportFormat::Canvas,
#[cfg(not(any(feature = "resvg", feature = "vello")))]
@ -596,9 +598,16 @@ impl NodeGraphExecutor {
}
/// Evaluates a node graph, computing the entire graph
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, inspect_node: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
pub fn submit_node_graph_evaluation(
&mut self,
document: &mut DocumentMessageHandler,
viewport_resolution: UVec2,
time: TimingInformation,
inspect_node: Option<NodeId>,
ignore_hash: bool,
) -> Result<(), String> {
self.update_node_graph(document, inspect_node, ignore_hash)?;
self.submit_current_node_graph_evaluation(document, viewport_resolution)?;
self.submit_current_node_graph_evaluation(document, viewport_resolution, time)?;
Ok(())
}
@ -623,6 +632,7 @@ impl NodeGraphExecutor {
resolution: (size * export_config.scale_factor).as_uvec2(),
..Default::default()
},
time: Default::default(),
export_format: graphene_core::application_io::ExportFormat::Svg,
view_mode: document.view_mode,
hide_artboards: export_config.transparent_background,

View file

@ -46,7 +46,7 @@ impl EditorTestUtils {
let viewport_resolution = glam::UVec2::ONE;
exector
.submit_current_node_graph_evaluation(document, viewport_resolution)
.submit_current_node_graph_evaluation(document, viewport_resolution, Default::default())
.expect("submit_current_node_graph_evaluation failed");
runtime.run().await;

View file

@ -138,19 +138,18 @@ impl EditorHandle {
let f = std::rc::Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::new(move |timestamp| {
*g.borrow_mut() = Some(Closure::new(move |_timestamp| {
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
editor_and_handle(|editor, handle| {
let micros: f64 = timestamp * 1000.;
let timestamp = Duration::from_micros(micros.round() as u64);
for message in editor.handle_message(InputPreprocessorMessage::FrameTimeAdvance { timestamp }) {
for message in editor.handle_message(InputPreprocessorMessage::CurrentTime {
timestamp: js_sys::Date::now() as u64,
}) {
handle.send_frontend_message_to_js(message);
}
for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
for message in editor.handle_message(AnimationMessage::IncrementFrameCounter) {
handle.send_frontend_message_to_js(message);
}
});
@ -826,7 +825,13 @@ impl EditorHandle {
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
portfolio
.executor
.submit_node_graph_evaluation(portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(), glam::UVec2::ONE, None, true)
.submit_node_graph_evaluation(
portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(),
glam::UVec2::ONE,
Default::default(),
None,
true,
)
.unwrap();
editor::node_graph_executor::run_node_graph().await;

View file

@ -295,6 +295,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
/// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex.
pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self {
let sides = sides.max(3);
let angle_increment = std::f64::consts::TAU / (sides as f64);
let anchor_positions = (0..sides).map(|i| {
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
@ -306,6 +307,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
/// Constructs a star polygon (n-star). See [new_regular_polygon], but with interspersed vertices at an `inner_radius`.
pub fn new_star_polygon(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> Self {
let sides = sides.max(2);
let angle_increment = 0.5 * std::f64::consts::TAU / (sides as f64);
let anchor_positions = (0..sides * 2).map(|i| {
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;

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;