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
|
@ -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());
|
||||
|
|
17
editor/src/messages/animation/animation_message.rs
Normal file
17
editor/src/messages/animation/animation_message.rs
Normal 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),
|
||||
}
|
84
editor/src/messages/animation/animation_message_handler.rs
Normal file
84
editor/src/messages/animation/animation_message_handler.rs
Normal 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,
|
||||
);
|
||||
}
|
9
editor/src/messages/animation/mod.rs
Normal file
9
editor/src/messages/animation/mod.rs
Normal 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;
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -10,6 +10,8 @@ pub enum Message {
|
|||
StartBuffer,
|
||||
EndBuffer(graphene_std::renderer::RenderMetadata),
|
||||
|
||||
#[child]
|
||||
Animation(AnimationMessage),
|
||||
#[child]
|
||||
Broadcast(BroadcastMessage),
|
||||
#[child]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -115,6 +115,7 @@ pub enum PortfolioMessage {
|
|||
bounds: ExportBounds,
|
||||
transparent_background: bool,
|
||||
},
|
||||
SubmitActiveGraphRender,
|
||||
SubmitGraphRender {
|
||||
document_id: DocumentId,
|
||||
ignore_hash: bool,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
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