mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-31 18:27:20 +00:00
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>
This commit is contained in:
parent
45bd031a36
commit
72f1047a27
17 changed files with 280 additions and 117 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1837,6 +1837,7 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bezier-rs",
|
"bezier-rs",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
|
"bytemuck",
|
||||||
"derivative",
|
"derivative",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|
|
@ -46,6 +46,7 @@ num_enum = { workspace = true }
|
||||||
usvg = { workspace = true }
|
usvg = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
web-sys = { workspace = true }
|
web-sys = { workspace = true }
|
||||||
|
bytemuck = { workspace = true }
|
||||||
|
|
||||||
# Required dependencies
|
# Required dependencies
|
||||||
spin = "0.9.8"
|
spin = "0.9.8"
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::utility_types::HintData;
|
use crate::messages::tool::utility_types::HintData;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
use graphene_std::text::Font;
|
use graphene_std::text::Font;
|
||||||
|
|
||||||
|
@ -179,6 +180,9 @@ pub enum FrontendMessage {
|
||||||
UpdateDocumentArtwork {
|
UpdateDocumentArtwork {
|
||||||
svg: String,
|
svg: String,
|
||||||
},
|
},
|
||||||
|
UpdateImageData {
|
||||||
|
image_data: Vec<(u64, Image<Color>)>,
|
||||||
|
},
|
||||||
UpdateDocumentBarLayout {
|
UpdateDocumentBarLayout {
|
||||||
#[serde(rename = "layoutTarget")]
|
#[serde(rename = "layoutTarget")]
|
||||||
layout_target: LayoutTarget,
|
layout_target: LayoutTarget,
|
||||||
|
|
|
@ -213,7 +213,7 @@ impl NodeGraphExecutor {
|
||||||
|
|
||||||
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||||
let TaggedValue::RenderOutput(RenderOutput {
|
let TaggedValue::RenderOutput(RenderOutput {
|
||||||
data: graphene_std::wasm_application_io::RenderOutputType::Svg(svg),
|
data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. },
|
||||||
..
|
..
|
||||||
}) = node_graph_output
|
}) = node_graph_output
|
||||||
else {
|
else {
|
||||||
|
@ -350,15 +350,16 @@ impl NodeGraphExecutor {
|
||||||
match node_graph_output {
|
match node_graph_output {
|
||||||
TaggedValue::RenderOutput(render_output) => {
|
TaggedValue::RenderOutput(render_output) => {
|
||||||
match render_output.data {
|
match render_output.data {
|
||||||
graphene_std::wasm_application_io::RenderOutputType::Svg(svg) => {
|
graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => {
|
||||||
// Send to frontend
|
// Send to frontend
|
||||||
|
responses.add(FrontendMessage::UpdateImageData { image_data });
|
||||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||||
}
|
}
|
||||||
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
|
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
|
||||||
let matrix = format_transform_matrix(frame.transform);
|
let matrix = format_transform_matrix(frame.transform);
|
||||||
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) };
|
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
|
||||||
let svg = format!(
|
let svg = format!(
|
||||||
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>"#,
|
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
|
||||||
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
||||||
);
|
);
|
||||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||||
|
|
|
@ -192,12 +192,25 @@
|
||||||
|
|
||||||
const placeholders = window.document.querySelectorAll("[data-viewport] [data-canvas-placeholder]");
|
const placeholders = window.document.querySelectorAll("[data-viewport] [data-canvas-placeholder]");
|
||||||
// Replace the placeholders with the actual canvas elements
|
// Replace the placeholders with the actual canvas elements
|
||||||
placeholders.forEach((placeholder) => {
|
Array.from(placeholders).forEach((placeholder) => {
|
||||||
const canvasName = placeholder.getAttribute("data-canvas-placeholder");
|
const canvasName = placeholder.getAttribute("data-canvas-placeholder");
|
||||||
if (!canvasName) return;
|
if (!canvasName) return;
|
||||||
// Get the canvas element from the global storage
|
// Get the canvas element from the global storage
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const canvas = (window as any).imageCanvases[canvasName];
|
let canvas = (window as any).imageCanvases[canvasName];
|
||||||
|
|
||||||
|
if (canvasName !== "0" && canvas.parentElement) {
|
||||||
|
var newCanvas = window.document.createElement("canvas");
|
||||||
|
var context = newCanvas.getContext("2d");
|
||||||
|
|
||||||
|
newCanvas.width = canvas.width;
|
||||||
|
newCanvas.height = canvas.height;
|
||||||
|
|
||||||
|
context?.drawImage(canvas, 0, 0);
|
||||||
|
|
||||||
|
canvas = newCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
placeholder.replaceWith(canvas);
|
placeholder.replaceWith(canvas);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,27 @@ use editor::messages::portfolio::utility_types::Platform;
|
||||||
use editor::messages::prelude::*;
|
use editor::messages::prelude::*;
|
||||||
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
|
use js_sys::{Object, Reflect};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_wasm_bindgen::{self, from_value};
|
use serde_wasm_bindgen::{self, from_value};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, window};
|
||||||
|
|
||||||
|
static IMAGE_DATA_HASH: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
fn calculate_hash<T: std::hash::Hash>(t: &T) -> u64 {
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::Hasher;
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
t.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the random seed used by the editor by calling this from JS upon initialization.
|
/// Set the random seed used by the editor by calling this from JS upon initialization.
|
||||||
/// This is necessary because WASM doesn't have a random number generator.
|
/// This is necessary because WASM doesn't have a random number generator.
|
||||||
|
@ -37,6 +51,75 @@ pub fn wasm_memory() -> JsValue {
|
||||||
wasm_bindgen::memory()
|
wasm_bindgen::memory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_image_data_to_canvases(image_data: &[(u64, Image<Color>)]) {
|
||||||
|
let window = match window() {
|
||||||
|
Some(window) => window,
|
||||||
|
None => {
|
||||||
|
error!("Cannot render canvas: window object not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let document = window.document().expect("window should have a document");
|
||||||
|
let window_obj = Object::from(window);
|
||||||
|
let image_canvases_key = JsValue::from_str("imageCanvases");
|
||||||
|
|
||||||
|
let canvases_obj = match Reflect::get(&window_obj, &image_canvases_key) {
|
||||||
|
Ok(obj) if !obj.is_undefined() && !obj.is_null() => obj,
|
||||||
|
_ => {
|
||||||
|
let new_obj = Object::new();
|
||||||
|
if Reflect::set(&window_obj, &image_canvases_key, &new_obj).is_err() {
|
||||||
|
error!("Failed to create and set imageCanvases object on window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new_obj.into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let canvases_obj = Object::from(canvases_obj);
|
||||||
|
|
||||||
|
for (placeholder_id, image) in image_data.iter() {
|
||||||
|
let canvas_name = placeholder_id.to_string();
|
||||||
|
let js_key = JsValue::from_str(&canvas_name);
|
||||||
|
|
||||||
|
if Reflect::has(&canvases_obj, &js_key).unwrap_or(false) || image.width == 0 || image.height == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas: HtmlCanvasElement = document
|
||||||
|
.create_element("canvas")
|
||||||
|
.expect("Failed to create canvas element")
|
||||||
|
.dyn_into::<HtmlCanvasElement>()
|
||||||
|
.expect("Failed to cast element to HtmlCanvasElement");
|
||||||
|
|
||||||
|
canvas.set_width(image.width);
|
||||||
|
canvas.set_height(image.height);
|
||||||
|
|
||||||
|
let context: CanvasRenderingContext2d = canvas
|
||||||
|
.get_context("2d")
|
||||||
|
.expect("Failed to get 2d context")
|
||||||
|
.expect("2d context was not found")
|
||||||
|
.dyn_into::<CanvasRenderingContext2d>()
|
||||||
|
.expect("Failed to cast context to CanvasRenderingContext2d");
|
||||||
|
let u8_data: Vec<u8> = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect();
|
||||||
|
let clamped_u8_data = wasm_bindgen::Clamped(&u8_data[..]);
|
||||||
|
match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) {
|
||||||
|
Ok(image_data_obj) => {
|
||||||
|
if context.put_image_data(&image_data_obj, 0., 0.).is_err() {
|
||||||
|
error!("Failed to put image data on canvas for id: {placeholder_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create ImageData for id: {placeholder_id}: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let js_value = JsValue::from(canvas);
|
||||||
|
|
||||||
|
if Reflect::set(&canvases_obj, &js_key, &js_value).is_err() {
|
||||||
|
error!("Failed to set canvas '{canvas_name}' on imageCanvases object");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 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
|
/// 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 {
|
||||||
|
|
||||||
// Sends a FrontendMessage to JavaScript
|
// Sends a FrontendMessage to JavaScript
|
||||||
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
|
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
|
||||||
|
if let FrontendMessage::UpdateImageData { ref image_data } = message {
|
||||||
|
let new_hash = calculate_hash(image_data);
|
||||||
|
let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
if new_hash != prev_hash {
|
||||||
|
render_image_data_to_canvases(image_data.as_slice());
|
||||||
|
IMAGE_DATA_HASH.store(new_hash, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message {
|
if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message {
|
||||||
message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() };
|
message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() };
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,10 @@ impl AlphaBlending {
|
||||||
clip: if t < 0.5 { self.clip } else { other.clip },
|
clip: if t < 0.5 { self.clip } else { other.clip },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn opacity(&self, mask: bool) -> f32 {
|
||||||
|
self.opacity * if mask { 1. } else { self.fill }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
use crate::GraphicGroupTable;
|
|
||||||
pub use crate::color::*;
|
|
||||||
use crate::raster_types::{CPU, RasterDataTable};
|
|
||||||
use crate::vector::VectorDataTable;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
#[cfg(target_arch = "spirv")]
|
|
||||||
use spirv_std::num_traits::float::Float;
|
|
||||||
|
|
||||||
/// as to not yet rename all references
|
/// as to not yet rename all references
|
||||||
pub mod color {
|
pub mod color {
|
||||||
pub use super::*;
|
pub use super::*;
|
||||||
|
@ -14,7 +5,14 @@ pub mod color {
|
||||||
|
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
|
||||||
pub use self::image::Image;
|
pub use self::image::{Image, TransformImage};
|
||||||
|
use crate::GraphicGroupTable;
|
||||||
|
pub use crate::color::*;
|
||||||
|
use crate::raster_types::{CPU, RasterDataTable};
|
||||||
|
use crate::vector::VectorDataTable;
|
||||||
|
#[cfg(target_arch = "spirv")]
|
||||||
|
use spirv_std::num_traits::float::Float;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
pub trait Bitmap {
|
pub trait Bitmap {
|
||||||
type Pixel: Pixel;
|
type Pixel: Pixel;
|
||||||
|
|
|
@ -50,6 +50,13 @@ pub struct Image<P: Pixel> {
|
||||||
// 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).
|
// 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).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||||
|
pub struct TransformImage(pub DAffine2);
|
||||||
|
|
||||||
|
impl Hash for TransformImage {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
|
||||||
|
}
|
||||||
|
|
||||||
impl<P: Pixel + Debug> Debug for Image<P> {
|
impl<P: Pixel + Debug> Debug for Image<P> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let length = self.data.len();
|
let length = self.data.len();
|
||||||
|
|
|
@ -417,7 +417,6 @@ where
|
||||||
// Create and add mirrored instance
|
// Create and add mirrored instance
|
||||||
for mut instance in instance.instance_iter() {
|
for mut instance in instance.instance_iter() {
|
||||||
instance.transform = reflected_transform * instance.transform;
|
instance.transform = reflected_transform * instance.transform;
|
||||||
instance.source_node_id = None;
|
|
||||||
result_table.push(instance);
|
result_table.push(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,6 +447,7 @@ async fn round_corners(
|
||||||
.map(|source| {
|
.map(|source| {
|
||||||
let source_transform = *source.transform;
|
let source_transform = *source.transform;
|
||||||
let source_transform_inverse = source_transform.inverse();
|
let source_transform_inverse = source_transform.inverse();
|
||||||
|
let source_node_id = source.source_node_id;
|
||||||
let source = source.instance;
|
let source = source.instance;
|
||||||
|
|
||||||
let upstream_graphic_group = source.upstream_graphic_group.clone();
|
let upstream_graphic_group = source.upstream_graphic_group.clone();
|
||||||
|
@ -542,7 +542,7 @@ async fn round_corners(
|
||||||
instance: result,
|
instance: result,
|
||||||
transform: source_transform,
|
transform: source_transform,
|
||||||
alpha_blending: Default::default(),
|
alpha_blending: Default::default(),
|
||||||
source_node_id: None,
|
source_node_id: *source_node_id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -645,7 +645,6 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
||||||
// Add this to the table and reset the transform since we've applied it directly to the points
|
// Add this to the table and reset the transform since we've applied it directly to the points
|
||||||
vector_data_instance.instance = result;
|
vector_data_instance.instance = result;
|
||||||
vector_data_instance.transform = DAffine2::IDENTITY;
|
vector_data_instance.transform = DAffine2::IDENTITY;
|
||||||
vector_data_instance.source_node_id = None;
|
|
||||||
vector_data_instance
|
vector_data_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -917,7 +916,6 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
|
||||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||||
|
|
||||||
vector_data_instance.instance = result;
|
vector_data_instance.instance = result;
|
||||||
vector_data_instance.source_node_id = None;
|
|
||||||
vector_data_instance
|
vector_data_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -1013,7 +1011,6 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
|
||||||
}
|
}
|
||||||
|
|
||||||
vector_data_instance.instance = result;
|
vector_data_instance.instance = result;
|
||||||
vector_data_instance.source_node_id = None;
|
|
||||||
vector_data_instance
|
vector_data_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -1068,7 +1065,6 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
|
||||||
}
|
}
|
||||||
|
|
||||||
vector_data_instance.instance = result;
|
vector_data_instance.instance = result;
|
||||||
vector_data_instance.source_node_id = None;
|
|
||||||
vector_data_instance
|
vector_data_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -7,6 +7,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||||
use graphene_application_io::SurfaceFrame;
|
use graphene_application_io::SurfaceFrame;
|
||||||
use graphene_brush::brush_cache::BrushCache;
|
use graphene_brush::brush_cache::BrushCache;
|
||||||
use graphene_brush::brush_stroke::BrushStroke;
|
use graphene_brush::brush_stroke::BrushStroke;
|
||||||
|
use graphene_core::raster::Image;
|
||||||
use graphene_core::raster_types::CPU;
|
use graphene_core::raster_types::CPU;
|
||||||
use graphene_core::transform::ReferencePoint;
|
use graphene_core::transform::ReferencePoint;
|
||||||
use graphene_core::uuid::NodeId;
|
use graphene_core::uuid::NodeId;
|
||||||
|
@ -424,10 +425,10 @@ pub struct RenderOutput {
|
||||||
pub metadata: RenderMetadata,
|
pub metadata: RenderMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, Hash, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum RenderOutputType {
|
pub enum RenderOutputType {
|
||||||
CanvasFrame(SurfaceFrame),
|
CanvasFrame(SurfaceFrame),
|
||||||
Svg(String),
|
Svg { svg: String, image_data: Vec<(u64, Image<Color>)> },
|
||||||
Image(Vec<u8>),
|
Image(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture, S
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use js_sys::{Object, Reflect};
|
use js_sys::{Object, Reflect};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::hash::Hash;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
|
@ -39,7 +40,7 @@ impl Drop for WindowWrapper {
|
||||||
let wrapper = || {
|
let wrapper = || {
|
||||||
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
||||||
// Convert key and value to JsValue
|
// Convert key and value to JsValue
|
||||||
let js_key = JsValue::from_str(format!("canvas{}", self.window.window_id).as_str());
|
let js_key = JsValue::from_str(self.window.window_id.to_string().as_str());
|
||||||
|
|
||||||
// Use Reflect API to set property
|
// Use Reflect API to set property
|
||||||
Reflect::delete_property(&canvases.into(), &js_key)?;
|
Reflect::delete_property(&canvases.into(), &js_key)?;
|
||||||
|
@ -200,7 +201,7 @@ impl ApplicationIo for WasmApplicationIo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert key and value to JsValue
|
// Convert key and value to JsValue
|
||||||
let js_key = JsValue::from_str(format!("canvas{}", id).as_str());
|
let js_key = JsValue::from_str(id.to_string().as_str());
|
||||||
let js_value = JsValue::from(canvas.clone());
|
let js_value = JsValue::from(canvas.clone());
|
||||||
|
|
||||||
let canvases = Object::from(canvases.unwrap());
|
let canvases = Object::from(canvases.unwrap());
|
||||||
|
@ -253,7 +254,7 @@ impl ApplicationIo for WasmApplicationIo {
|
||||||
let wrapper = || {
|
let wrapper = || {
|
||||||
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
||||||
// Convert key and value to JsValue
|
// Convert key and value to JsValue
|
||||||
let js_key = JsValue::from_str(format!("canvas{}", surface_id.0).as_str());
|
let js_key = JsValue::from_str(surface_id.0.to_string().as_str());
|
||||||
|
|
||||||
// Use Reflect API to set property
|
// Use Reflect API to set property
|
||||||
Reflect::delete_property(&canvases.into(), &js_key)?;
|
Reflect::delete_property(&canvases.into(), &js_key)?;
|
||||||
|
|
|
@ -31,7 +31,6 @@ async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percen
|
||||||
};
|
};
|
||||||
|
|
||||||
image_frame_instance.instance = Raster::new_cpu(dehazed_image);
|
image_frame_instance.instance = Raster::new_cpu(dehazed_image);
|
||||||
image_frame_instance.source_node_id = None;
|
|
||||||
image_frame_instance
|
image_frame_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -36,7 +36,6 @@ async fn blur(
|
||||||
};
|
};
|
||||||
|
|
||||||
image_instance.instance = blurred_image;
|
image_instance.instance = blurred_image;
|
||||||
image_instance.source_node_id = None;
|
|
||||||
image_instance
|
image_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -88,7 +88,6 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Rast
|
||||||
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
||||||
|
|
||||||
image_frame_instance.transform = new_transform;
|
image_frame_instance.transform = new_transform;
|
||||||
image_frame_instance.source_node_id = None;
|
|
||||||
image_frame_instance.instance = Raster::new_cpu(image);
|
image_frame_instance.instance = Raster::new_cpu(image);
|
||||||
Some(image_frame_instance)
|
Some(image_frame_instance)
|
||||||
})
|
})
|
||||||
|
@ -121,9 +120,10 @@ pub fn combine_channels(
|
||||||
let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
||||||
|
|
||||||
// Get this instance's transform and alpha blending mode from the first non-empty channel
|
// Get this instance's transform and alpha blending mode from the first non-empty channel
|
||||||
let Some((transform, alpha_blending)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending)) else {
|
let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha]
|
||||||
return None;
|
.iter()
|
||||||
};
|
.find_map(|i| i.as_ref())
|
||||||
|
.map(|i| (i.transform, i.alpha_blending, i.source_node_id))?;
|
||||||
|
|
||||||
// Get the common width and height of the channels, which must have equal dimensions
|
// Get the common width and height of the channels, which must have equal dimensions
|
||||||
let channel_dimensions = [
|
let channel_dimensions = [
|
||||||
|
@ -140,9 +140,7 @@ pub fn combine_channels(
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let Some(&(width, height)) = channel_dimensions.iter().flatten().next() else {
|
let &(width, height) = channel_dimensions.iter().flatten().next()?;
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a new image for this instance output
|
// Create a new image for this instance output
|
||||||
let mut image = Image::new(width, height, Color::TRANSPARENT);
|
let mut image = Image::new(width, height, Color::TRANSPARENT);
|
||||||
|
@ -179,7 +177,7 @@ pub fn combine_channels(
|
||||||
instance: Raster::new_cpu(image),
|
instance: Raster::new_cpu(image),
|
||||||
transform,
|
transform,
|
||||||
alpha_blending,
|
alpha_blending,
|
||||||
source_node_id: None,
|
source_node_id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -276,7 +274,6 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds:
|
||||||
|
|
||||||
image_instance.instance = Raster::new_cpu(new_image);
|
image_instance.instance = Raster::new_cpu(new_image);
|
||||||
image_instance.transform = new_texture_to_layer_space;
|
image_instance.transform = new_texture_to_layer_space;
|
||||||
image_instance.source_node_id = None;
|
|
||||||
image_instance
|
image_instance
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -30,35 +30,6 @@ async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<W
|
||||||
Arc::new(editor.application_io.as_ref().unwrap().create_window())
|
Arc::new(editor.application_io.as_ref().unwrap().create_window())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix and reenable in order to get the 'Draw Canvas' node working again.
|
|
||||||
// #[cfg(target_arch = "wasm32")]
|
|
||||||
// use wasm_bindgen::Clamped;
|
|
||||||
//
|
|
||||||
// #[node_macro::node(category("Debug: GPU"))]
|
|
||||||
// #[cfg(target_arch = "wasm32")]
|
|
||||||
// async fn draw_image_frame(
|
|
||||||
// _: impl Ctx,
|
|
||||||
// image: RasterDataTable<graphene_core::raster::SRGBA8>,
|
|
||||||
// surface_handle: Arc<WasmSurfaceHandle>,
|
|
||||||
// ) -> graphene_core::application_io::SurfaceHandleFrame<HtmlCanvasElement> {
|
|
||||||
// let image = image.instance_ref_iter().next().unwrap().instance;
|
|
||||||
// let image_data = image.image.data;
|
|
||||||
// let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice()));
|
|
||||||
// if image.image.width > 0 && image.image.height > 0 {
|
|
||||||
// let canvas = &surface_handle.surface;
|
|
||||||
// canvas.set_width(image.image.width);
|
|
||||||
// canvas.set_height(image.image.height);
|
|
||||||
// // TODO: replace "2d" with "bitmaprenderer" once we switch to ImageBitmap (lives on gpu) from RasterData (lives on cpu)
|
|
||||||
// let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
|
|
||||||
// let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.image.width, image.image.height).expect("Failed to construct RasterData");
|
|
||||||
// context.put_image_data(&image_data, 0., 0.).unwrap();
|
|
||||||
// }
|
|
||||||
// graphene_core::application_io::SurfaceHandleFrame {
|
|
||||||
// surface_handle,
|
|
||||||
// transform: image.transform,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[node_macro::node(category("Web Request"))]
|
#[node_macro::node(category("Web Request"))]
|
||||||
async fn get_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, discard_result: bool) -> String {
|
async fn get_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, discard_result: bool) -> String {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
@ -187,7 +158,10 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
|
||||||
|
|
||||||
render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
|
render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
|
||||||
|
|
||||||
RenderOutputType::Svg(render.svg.to_svg_string())
|
RenderOutputType::Svg {
|
||||||
|
svg: render.svg.to_svg_string(),
|
||||||
|
image_data: render.image_data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vello")]
|
#[cfg(feature = "vello")]
|
||||||
|
|
|
@ -20,6 +20,7 @@ use graphene_core::{Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTa
|
||||||
use num_traits::Zero;
|
use num_traits::Zero;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
#[cfg(feature = "vello")]
|
#[cfg(feature = "vello")]
|
||||||
|
@ -175,6 +176,10 @@ impl RenderParams {
|
||||||
let alignment_parent_transform = Some(transform);
|
let alignment_parent_transform = Some(transform);
|
||||||
Self { alignment_parent_transform, ..*self }
|
Self { alignment_parent_transform, ..*self }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_canvas(&self) -> bool {
|
||||||
|
!self.for_export && !self.thumbnail && !self.for_mask
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_transform_matrix(transform: DAffine2) -> String {
|
pub fn format_transform_matrix(transform: DAffine2) -> String {
|
||||||
|
@ -243,8 +248,7 @@ impl GraphicElementRendered for GraphicGroupTable {
|
||||||
attributes.push("transform", matrix);
|
attributes.push("transform", matrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
let opacity = instance.alpha_blending.opacity(render_params.for_mask);
|
||||||
let opacity = instance.alpha_blending.opacity * factor;
|
|
||||||
if opacity < 1. {
|
if opacity < 1. {
|
||||||
attributes.push("opacity", opacity.to_string());
|
attributes.push("opacity", opacity.to_string());
|
||||||
}
|
}
|
||||||
|
@ -299,8 +303,7 @@ impl GraphicElementRendered for GraphicGroupTable {
|
||||||
};
|
};
|
||||||
let mut bounds = None;
|
let mut bounds = None;
|
||||||
|
|
||||||
let factor = if render_params.for_mask { 1. } else { alpha_blending.fill };
|
let opacity = instance.alpha_blending.opacity(render_params.for_mask);
|
||||||
let opacity = alpha_blending.opacity * factor;
|
|
||||||
if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
|
if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
|
||||||
bounds = self
|
bounds = self
|
||||||
.instance_ref_iter()
|
.instance_ref_iter()
|
||||||
|
@ -503,8 +506,7 @@ impl GraphicElementRendered for VectorDataTable {
|
||||||
}
|
}
|
||||||
attributes.push_val(fill_and_stroke);
|
attributes.push_val(fill_and_stroke);
|
||||||
|
|
||||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
let opacity = instance.alpha_blending.opacity(render_params.for_mask);
|
||||||
let opacity = instance.alpha_blending.opacity * factor;
|
|
||||||
if opacity < 1. {
|
if opacity < 1. {
|
||||||
attributes.push("opacity", opacity.to_string());
|
attributes.push("opacity", opacity.to_string());
|
||||||
}
|
}
|
||||||
|
@ -545,10 +547,13 @@ impl GraphicElementRendered for VectorDataTable {
|
||||||
_ => instance.alpha_blending.blend_mode.to_peniko(),
|
_ => instance.alpha_blending.blend_mode.to_peniko(),
|
||||||
};
|
};
|
||||||
let mut layer = false;
|
let mut layer = false;
|
||||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
|
||||||
let opacity = instance.alpha_blending.opacity * factor;
|
let opacity = instance.alpha_blending.opacity(render_params.for_mask);
|
||||||
if opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
|
if opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||||
layer = true;
|
layer = true;
|
||||||
|
let weight = instance.instance.style.stroke().unwrap().weight;
|
||||||
|
let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant());
|
||||||
|
let layer_bounds = quad.bounding_box();
|
||||||
scene.push_layer(
|
scene.push_layer(
|
||||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
||||||
opacity,
|
opacity,
|
||||||
|
@ -945,56 +950,122 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
for instance in self.instance_ref_iter() {
|
for instance in self.instance_ref_iter() {
|
||||||
let transform = *instance.transform;
|
let transform = *instance.transform;
|
||||||
|
|
||||||
let image = &instance.instance;
|
let image = &instance.instance;
|
||||||
|
|
||||||
if image.data.is_empty() {
|
if image.data.is_empty() {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
|
if render_params.to_canvas() {
|
||||||
use base64::Engine;
|
let id = instance.source_node_id.map(|x| x.0).unwrap_or_else(|| {
|
||||||
|
let mut state = DefaultHasher::new();
|
||||||
|
image.data().hash(&mut state);
|
||||||
|
state.finish()
|
||||||
|
});
|
||||||
|
if !render.image_data.iter().any(|(old_id, _)| *old_id == id) {
|
||||||
|
render.image_data.push((id, image.data().clone()));
|
||||||
|
}
|
||||||
|
render.parent_tag(
|
||||||
|
"foreignObject",
|
||||||
|
|attributes| {
|
||||||
|
let mut transform_values = transform.to_scale_angle_translation();
|
||||||
|
let size = DVec2::new(image.width as f64, image.height as f64);
|
||||||
|
transform_values.0 /= size;
|
||||||
|
|
||||||
let output = image.to_png();
|
let matrix = DAffine2::from_scale_angle_translation(transform_values.0, transform_values.1, transform_values.2);
|
||||||
let preamble = "data:image/png;base64,";
|
let matrix = format_transform_matrix(matrix);
|
||||||
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
|
if !matrix.is_empty() {
|
||||||
base64_string.push_str(preamble);
|
attributes.push("transform", matrix);
|
||||||
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
|
}
|
||||||
base64_string
|
|
||||||
});
|
attributes.push("width", size.x.to_string());
|
||||||
render.leaf_tag("image", |attributes| {
|
attributes.push("height", size.y.to_string());
|
||||||
attributes.push("width", 1.to_string());
|
|
||||||
attributes.push("height", 1.to_string());
|
let opacity = instance.alpha_blending.opacity(render_params.for_mask);
|
||||||
attributes.push("preserveAspectRatio", "none");
|
if opacity < 1. {
|
||||||
attributes.push("href", base64_string);
|
attributes.push("opacity", opacity.to_string());
|
||||||
let matrix = format_transform_matrix(transform);
|
}
|
||||||
if !matrix.is_empty() {
|
|
||||||
attributes.push("transform", matrix);
|
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||||
}
|
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
||||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
}
|
||||||
let opacity = instance.alpha_blending.opacity * factor;
|
},
|
||||||
if opacity < 1. {
|
|render| {
|
||||||
attributes.push("opacity", opacity.to_string());
|
render.leaf_tag(
|
||||||
}
|
"img", // Must be a self-closing (void element) tag, so we can't use `div` or `span`, for example
|
||||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
|attributes| {
|
||||||
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
attributes.push("data-canvas-placeholder", id.to_string());
|
||||||
}
|
},
|
||||||
});
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
|
let output = image.to_png();
|
||||||
|
let preamble = "data:image/png;base64,";
|
||||||
|
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
|
||||||
|
base64_string.push_str(preamble);
|
||||||
|
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
|
||||||
|
base64_string
|
||||||
|
});
|
||||||
|
|
||||||
|
render.leaf_tag("image", |attributes| {
|
||||||
|
attributes.push("width", "1");
|
||||||
|
attributes.push("height", "1");
|
||||||
|
attributes.push("preserveAspectRatio", "none");
|
||||||
|
attributes.push("href", base64_string);
|
||||||
|
let matrix = format_transform_matrix(transform);
|
||||||
|
if !matrix.is_empty() {
|
||||||
|
attributes.push("transform", matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
let opacity = instance.alpha_blending.opacity(render_params.for_mask);
|
||||||
|
if opacity < 1. {
|
||||||
|
attributes.push("opacity", opacity.to_string());
|
||||||
|
}
|
||||||
|
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||||
|
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vello")]
|
#[cfg(feature = "vello")]
|
||||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, _render_params: &RenderParams) {
|
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
|
||||||
use vello::peniko;
|
use vello::peniko;
|
||||||
|
|
||||||
for instance in self.instance_ref_iter() {
|
for instance in self.instance_ref_iter() {
|
||||||
let image = &instance.instance;
|
let image = &instance.instance;
|
||||||
if image.data.is_empty() {
|
if image.data.is_empty() {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
let image = peniko::Image::new(image.to_flat_u8().0.into(), peniko::ImageFormat::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat);
|
|
||||||
let transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
|
|
||||||
|
|
||||||
scene.draw_image(&image, kurbo::Affine::new(transform.to_cols_array()));
|
let alpha_blending = *instance.alpha_blending;
|
||||||
|
let blend_mode = alpha_blending.blend_mode.to_peniko();
|
||||||
|
|
||||||
|
let opacity = alpha_blending.opacity(render_params.for_mask);
|
||||||
|
let mut layer = false;
|
||||||
|
|
||||||
|
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
|
||||||
|
if let Some(bounds) = self.bounding_box(transform, false) {
|
||||||
|
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
|
||||||
|
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||||
|
scene.push_layer(blending, opacity, kurbo::Affine::IDENTITY, &rect);
|
||||||
|
layer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = peniko::Image::new(image.to_flat_u8().0.into(), peniko::ImageFormat::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat);
|
||||||
|
let image_transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
|
||||||
|
|
||||||
|
scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array()));
|
||||||
|
|
||||||
|
if layer {
|
||||||
|
scene.pop_layer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1029,12 +1100,14 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
|
||||||
|
|
||||||
for instance in self.instance_ref_iter() {
|
for instance in self.instance_ref_iter() {
|
||||||
let blend_mode = *instance.alpha_blending;
|
let blend_mode = *instance.alpha_blending;
|
||||||
let layer = blend_mode != Default::default();
|
let mut layer = false;
|
||||||
if layer {
|
if blend_mode != Default::default() {
|
||||||
let Some(bounds) = self.bounding_box(transform, true) else { return };
|
if let Some(bounds) = self.bounding_box(transform, true) {
|
||||||
let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver);
|
let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver);
|
||||||
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||||
scene.push_layer(blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect);
|
scene.push_layer(blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect);
|
||||||
|
layer = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let image = peniko::Image::new(
|
let image = peniko::Image::new(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue