Skip to content

Commit 72f1047

Browse files
mTvare60HyperCubeKeavon
authored
Display images in the SVG viewport renderer via canvases instead of base64 PNGs (#2903)
* add: move images as rendered canvases to node_graph_executor * add: added the frontend message * fix: bytemuck stuff * fix: canvas element breaking * fix: width issues * fix: remove the old message * npm: run lint-fix * fix * works finally * fix transforms * Fix self closing tag * fix: reuse id * fix: have it working with repeat instance * cargo: fmt * fix * Avoid "canvas" prefix to IDs * fix * fix: vello issue from 6111440 * fix: gpu stuff * fix: vello bbox * Code review --------- Co-authored-by: hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 45bd031 commit 72f1047

File tree

17 files changed

+279
-116
lines changed

17 files changed

+279
-116
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ num_enum = { workspace = true }
4646
usvg = { workspace = true }
4747
once_cell = { workspace = true }
4848
web-sys = { workspace = true }
49+
bytemuck = { workspace = true }
4950

5051
# Required dependencies
5152
spin = "0.9.8"

editor/src/messages/frontend/frontend_message.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP
88
use crate::messages::prelude::*;
99
use crate::messages::tool::utility_types::HintData;
1010
use graph_craft::document::NodeId;
11+
use graphene_std::raster::Image;
1112
use graphene_std::raster::color::Color;
1213
use graphene_std::text::Font;
1314

@@ -179,6 +180,9 @@ pub enum FrontendMessage {
179180
UpdateDocumentArtwork {
180181
svg: String,
181182
},
183+
UpdateImageData {
184+
image_data: Vec<(u64, Image<Color>)>,
185+
},
182186
UpdateDocumentBarLayout {
183187
#[serde(rename = "layoutTarget")]
184188
layout_target: LayoutTarget,

editor/src/node_graph_executor.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ impl NodeGraphExecutor {
213213

214214
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
215215
let TaggedValue::RenderOutput(RenderOutput {
216-
data: graphene_std::wasm_application_io::RenderOutputType::Svg(svg),
216+
data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. },
217217
..
218218
}) = node_graph_output
219219
else {
@@ -350,15 +350,16 @@ impl NodeGraphExecutor {
350350
match node_graph_output {
351351
TaggedValue::RenderOutput(render_output) => {
352352
match render_output.data {
353-
graphene_std::wasm_application_io::RenderOutputType::Svg(svg) => {
353+
graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => {
354354
// Send to frontend
355+
responses.add(FrontendMessage::UpdateImageData { image_data });
355356
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
356357
}
357358
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
358359
let matrix = format_transform_matrix(frame.transform);
359-
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) };
360+
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
360361
let svg = format!(
361-
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>"#,
362+
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
362363
frame.resolution.x, frame.resolution.y, frame.surface_id.0
363364
);
364365
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });

frontend/src/components/panels/Document.svelte

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,25 @@
192192
193193
const placeholders = window.document.querySelectorAll("[data-viewport] [data-canvas-placeholder]");
194194
// Replace the placeholders with the actual canvas elements
195-
placeholders.forEach((placeholder) => {
195+
Array.from(placeholders).forEach((placeholder) => {
196196
const canvasName = placeholder.getAttribute("data-canvas-placeholder");
197197
if (!canvasName) return;
198198
// Get the canvas element from the global storage
199199
// eslint-disable-next-line @typescript-eslint/no-explicit-any
200-
const canvas = (window as any).imageCanvases[canvasName];
200+
let canvas = (window as any).imageCanvases[canvasName];
201+
202+
if (canvasName !== "0" && canvas.parentElement) {
203+
var newCanvas = window.document.createElement("canvas");
204+
var context = newCanvas.getContext("2d");
205+
206+
newCanvas.width = canvas.width;
207+
newCanvas.height = canvas.height;
208+
209+
context?.drawImage(canvas, 0, 0);
210+
211+
canvas = newCanvas;
212+
}
213+
201214
placeholder.replaceWith(canvas);
202215
});
203216
}

frontend/wasm/src/editor_api.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,27 @@ use editor::messages::portfolio::utility_types::Platform;
1616
use editor::messages::prelude::*;
1717
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
1818
use graph_craft::document::NodeId;
19+
use graphene_std::raster::Image;
1920
use graphene_std::raster::color::Color;
21+
use js_sys::{Object, Reflect};
2022
use serde::Serialize;
2123
use serde_wasm_bindgen::{self, from_value};
2224
use std::cell::RefCell;
23-
use std::sync::atomic::Ordering;
25+
use std::sync::atomic::{AtomicU64, Ordering};
2426
use std::time::Duration;
27+
use wasm_bindgen::JsCast;
2528
use wasm_bindgen::prelude::*;
29+
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, window};
30+
31+
static IMAGE_DATA_HASH: AtomicU64 = AtomicU64::new(0);
32+
33+
fn calculate_hash<T: std::hash::Hash>(t: &T) -> u64 {
34+
use std::collections::hash_map::DefaultHasher;
35+
use std::hash::Hasher;
36+
let mut hasher = DefaultHasher::new();
37+
t.hash(&mut hasher);
38+
hasher.finish()
39+
}
2640

2741
/// Set the random seed used by the editor by calling this from JS upon initialization.
2842
/// This is necessary because WASM doesn't have a random number generator.
@@ -37,6 +51,75 @@ pub fn wasm_memory() -> JsValue {
3751
wasm_bindgen::memory()
3852
}
3953

54+
fn render_image_data_to_canvases(image_data: &[(u64, Image<Color>)]) {
55+
let window = match window() {
56+
Some(window) => window,
57+
None => {
58+
error!("Cannot render canvas: window object not found");
59+
return;
60+
}
61+
};
62+
let document = window.document().expect("window should have a document");
63+
let window_obj = Object::from(window);
64+
let image_canvases_key = JsValue::from_str("imageCanvases");
65+
66+
let canvases_obj = match Reflect::get(&window_obj, &image_canvases_key) {
67+
Ok(obj) if !obj.is_undefined() && !obj.is_null() => obj,
68+
_ => {
69+
let new_obj = Object::new();
70+
if Reflect::set(&window_obj, &image_canvases_key, &new_obj).is_err() {
71+
error!("Failed to create and set imageCanvases object on window");
72+
return;
73+
}
74+
new_obj.into()
75+
}
76+
};
77+
let canvases_obj = Object::from(canvases_obj);
78+
79+
for (placeholder_id, image) in image_data.iter() {
80+
let canvas_name = placeholder_id.to_string();
81+
let js_key = JsValue::from_str(&canvas_name);
82+
83+
if Reflect::has(&canvases_obj, &js_key).unwrap_or(false) || image.width == 0 || image.height == 0 {
84+
continue;
85+
}
86+
87+
let canvas: HtmlCanvasElement = document
88+
.create_element("canvas")
89+
.expect("Failed to create canvas element")
90+
.dyn_into::<HtmlCanvasElement>()
91+
.expect("Failed to cast element to HtmlCanvasElement");
92+
93+
canvas.set_width(image.width);
94+
canvas.set_height(image.height);
95+
96+
let context: CanvasRenderingContext2d = canvas
97+
.get_context("2d")
98+
.expect("Failed to get 2d context")
99+
.expect("2d context was not found")
100+
.dyn_into::<CanvasRenderingContext2d>()
101+
.expect("Failed to cast context to CanvasRenderingContext2d");
102+
let u8_data: Vec<u8> = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect();
103+
let clamped_u8_data = wasm_bindgen::Clamped(&u8_data[..]);
104+
match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) {
105+
Ok(image_data_obj) => {
106+
if context.put_image_data(&image_data_obj, 0., 0.).is_err() {
107+
error!("Failed to put image data on canvas for id: {placeholder_id}");
108+
}
109+
}
110+
Err(e) => {
111+
error!("Failed to create ImageData for id: {placeholder_id}: {e:?}");
112+
}
113+
}
114+
115+
let js_value = JsValue::from(canvas);
116+
117+
if Reflect::set(&canvases_obj, &js_key, &js_value).is_err() {
118+
error!("Failed to set canvas '{canvas_name}' on imageCanvases object");
119+
}
120+
}
121+
}
122+
40123
// ============================================================================
41124

42125
/// This struct is, via wasm-bindgen, used by JS to interact with the editor backend. It does this by calling functions, which are `impl`ed
@@ -88,6 +171,17 @@ impl EditorHandle {
88171

89172
// Sends a FrontendMessage to JavaScript
90173
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
174+
if let FrontendMessage::UpdateImageData { ref image_data } = message {
175+
let new_hash = calculate_hash(image_data);
176+
let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed);
177+
178+
if new_hash != prev_hash {
179+
render_image_data_to_canvases(image_data.as_slice());
180+
IMAGE_DATA_HASH.store(new_hash, Ordering::Relaxed);
181+
}
182+
return;
183+
}
184+
91185
if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message {
92186
message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() };
93187
}

node-graph/gcore/src/blending.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ impl AlphaBlending {
5656
clip: if t < 0.5 { self.clip } else { other.clip },
5757
}
5858
}
59+
60+
pub fn opacity(&self, mask: bool) -> f32 {
61+
self.opacity * if mask { 1. } else { self.fill }
62+
}
5963
}
6064

6165
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

node-graph/gcore/src/raster.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
use crate::GraphicGroupTable;
2-
pub use crate::color::*;
3-
use crate::raster_types::{CPU, RasterDataTable};
4-
use crate::vector::VectorDataTable;
5-
use std::fmt::Debug;
6-
7-
#[cfg(target_arch = "spirv")]
8-
use spirv_std::num_traits::float::Float;
9-
101
/// as to not yet rename all references
112
pub mod color {
123
pub use super::*;
134
}
145

156
pub mod image;
167

17-
pub use self::image::Image;
8+
pub use self::image::{Image, TransformImage};
9+
use crate::GraphicGroupTable;
10+
pub use crate::color::*;
11+
use crate::raster_types::{CPU, RasterDataTable};
12+
use crate::vector::VectorDataTable;
13+
#[cfg(target_arch = "spirv")]
14+
use spirv_std::num_traits::float::Float;
15+
use std::fmt::Debug;
1816

1917
pub trait Bitmap {
2018
type Pixel: Pixel;

node-graph/gcore/src/raster/image.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ pub struct Image<P: Pixel> {
5050
// TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1).
5151
}
5252

53+
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
54+
pub struct TransformImage(pub DAffine2);
55+
56+
impl Hash for TransformImage {
57+
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
58+
}
59+
5360
impl<P: Pixel + Debug> Debug for Image<P> {
5461
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5562
let length = self.data.len();

node-graph/gcore/src/vector/vector_nodes.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,6 @@ where
417417
// Create and add mirrored instance
418418
for mut instance in instance.instance_iter() {
419419
instance.transform = reflected_transform * instance.transform;
420-
instance.source_node_id = None;
421420
result_table.push(instance);
422421
}
423422

@@ -448,6 +447,7 @@ async fn round_corners(
448447
.map(|source| {
449448
let source_transform = *source.transform;
450449
let source_transform_inverse = source_transform.inverse();
450+
let source_node_id = source.source_node_id;
451451
let source = source.instance;
452452

453453
let upstream_graphic_group = source.upstream_graphic_group.clone();
@@ -542,7 +542,7 @@ async fn round_corners(
542542
instance: result,
543543
transform: source_transform,
544544
alpha_blending: Default::default(),
545-
source_node_id: None,
545+
source_node_id: *source_node_id,
546546
}
547547
})
548548
.collect()
@@ -645,7 +645,6 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
645645
// Add this to the table and reset the transform since we've applied it directly to the points
646646
vector_data_instance.instance = result;
647647
vector_data_instance.transform = DAffine2::IDENTITY;
648-
vector_data_instance.source_node_id = None;
649648
vector_data_instance
650649
})
651650
.collect()
@@ -917,7 +916,6 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
917916
result.style.set_stroke_transform(DAffine2::IDENTITY);
918917

919918
vector_data_instance.instance = result;
920-
vector_data_instance.source_node_id = None;
921919
vector_data_instance
922920
})
923921
.collect()
@@ -1013,7 +1011,6 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
10131011
}
10141012

10151013
vector_data_instance.instance = result;
1016-
vector_data_instance.source_node_id = None;
10171014
vector_data_instance
10181015
})
10191016
.collect()
@@ -1068,7 +1065,6 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
10681065
}
10691066

10701067
vector_data_instance.instance = result;
1071-
vector_data_instance.source_node_id = None;
10721068
vector_data_instance
10731069
})
10741070
.collect()

0 commit comments

Comments
 (0)