Wrap opacity/blend_mode in alpha_blending struct for graphic elements

This commit is contained in:
Keavon Chambers 2023-12-08 20:15:37 -08:00
parent 10f2fa92e5
commit e459e599b4
15 changed files with 111 additions and 96 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -577,11 +577,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
responses.add(DocumentMessage::StartTransaction);
let image_frame = ImageFrame {
image,
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
};
let image_frame = ImageFrame { image, ..Default::default() };
use crate::messages::tool::common_functionality::graph_modification_utils;
let layer = graph_modification_utils::new_image_layer(image_frame, generate_uuid(), self.new_layer_parent(), responses);

View File

@ -13,38 +13,72 @@ use glam::{DAffine2, DVec2, IVec2, UVec2};
pub mod renderer;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlphaBlending {
pub opacity: f32,
pub blend_mode: BlendMode,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl core::hash::Hash for AlphaBlending {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.blend_mode.hash(state);
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
blend_mode: BlendMode::Normal,
}
}
}
/// A list of [`GraphicElement`]s
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<GraphicElement>,
pub opacity: f32,
pub blend_mode: BlendMode,
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
}
impl core::hash::Hash for GraphicGroup {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.transform.to_cols_array().iter().for_each(|element| element.to_bits().hash(state));
self.elements.hash(state);
self.opacity.to_bits().hash(state);
self.transform.to_cols_array().iter().for_each(|element| element.to_bits().hash(state))
self.alpha_blending.hash(state);
}
}
/// Internal data for a [`GraphicElement`]. Can be [`VectorData`], [`ImageFrame`], text, or a nested [`GraphicGroup`]
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
/// Can be another recursively nested [`GraphicGroup`], [`VectorData`], an [`ImageFrame`], text (not yet implemented), or an [`Artboard`].
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GraphicElement {
VectorShape(Box<VectorData>),
ImageFrame(ImageFrame<Color>),
Text(String),
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroup),
/// A vector shape, equivalent to the SVG <path> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
VectorData(Box<VectorData>),
/// A bitmap image with a finite position and extent, equivalent to the SVG <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
ImageFrame(ImageFrame<Color>),
// TODO: Switch from `String` to a proper formatted typography type
/// Text, equivalent to the SVG <text> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text
/// (Not yet implemented.)
Text(String),
/// The bounds for displaying a page of contained content
Artboard(Artboard),
}
// TODO: Can this be removed? It doesn't necessarily make that much sense to have a default when, instead, the entire GraphicElement just shouldn't exist if there's no specific content to assign it.
impl Default for GraphicElement {
fn default() -> Self {
Self::VectorShape(Box::new(VectorData::empty()))
Self::VectorData(Box::new(VectorData::empty()))
}
}
@ -131,7 +165,7 @@ impl From<ImageFrame<Color>> for GraphicElement {
}
impl From<VectorData> for GraphicElement {
fn from(vector_data: VectorData) -> Self {
GraphicElement::VectorShape(Box::new(vector_data))
GraphicElement::VectorData(Box::new(vector_data))
}
}
impl From<GraphicGroup> for GraphicElement {
@ -173,9 +207,8 @@ where
fn from(value: T) -> Self {
Self {
elements: (vec![value.into()]),
opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
}
}
}
@ -183,9 +216,8 @@ where
impl GraphicGroup {
pub const EMPTY: Self = Self {
elements: Vec::new(),
opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::new(),
};
pub fn to_usvg_tree(&self, resolution: UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
@ -214,7 +246,7 @@ impl GraphicElement {
}
match self {
GraphicElement::VectorShape(vector_data) => {
GraphicElement::VectorData(vector_data) => {
use usvg::tiny_skia_path::PathBuilder;
let mut builder = PathBuilder::new();

View File

@ -223,12 +223,12 @@ impl GraphicElementRendered for GraphicGroup {
|attributes| {
attributes.push("transform", format_transform_matrix(self.transform));
if self.opacity < 1. {
attributes.push("opacity", self.opacity.to_string());
if self.alpha_blending.opacity < 1. {
attributes.push("opacity", self.alpha_blending.opacity.to_string());
}
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
},
|render| {
@ -275,12 +275,12 @@ impl GraphicElementRendered for VectorData {
.render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
attributes.push_val(fill_and_stroke);
if self.style.opacity < 1. {
attributes.push("opacity", self.style.opacity.to_string());
if self.alpha_blending.opacity < 1. {
attributes.push("opacity", self.alpha_blending.opacity.to_string());
}
if self.style.blend_mode != BlendMode::default() {
attributes.push("style", self.style.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
});
}
@ -426,8 +426,8 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid));
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
});
render.image_data.push((uuid, self.image.clone()))
@ -449,8 +449,8 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", base64_string);
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
});
}
@ -493,7 +493,7 @@ impl GraphicElementRendered for ImageFrame<Color> {
impl GraphicElementRendered for GraphicElement {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
GraphicElement::VectorShape(vector_data) => vector_data.render_svg(render, render_params),
GraphicElement::VectorData(vector_data) => vector_data.render_svg(render, render_params),
GraphicElement::ImageFrame(image_frame) => image_frame.render_svg(render, render_params),
GraphicElement::Text(_) => todo!("Render a text GraphicElement"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params),
@ -503,7 +503,7 @@ impl GraphicElementRendered for GraphicElement {
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
match self {
GraphicElement::VectorShape(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform),
GraphicElement::VectorData(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform),
GraphicElement::ImageFrame(image_frame) => image_frame.bounding_box(transform),
GraphicElement::Text(_) => todo!("Bounds of a text GraphicElement"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform),
@ -513,7 +513,7 @@ impl GraphicElementRendered for GraphicElement {
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
match self {
GraphicElement::VectorShape(vector_data) => vector_data.add_click_targets(click_targets),
GraphicElement::VectorData(vector_data) => vector_data.add_click_targets(click_targets),
GraphicElement::ImageFrame(image_frame) => image_frame.add_click_targets(click_targets),
GraphicElement::Text(_) => todo!("click target for text GraphicElement"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets),
@ -523,7 +523,7 @@ impl GraphicElementRendered for GraphicElement {
fn to_usvg_node(&self) -> usvg::Node {
match self {
GraphicElement::VectorShape(vector_data) => vector_data.to_usvg_node(),
GraphicElement::VectorData(vector_data) => vector_data.to_usvg_node(),
GraphicElement::ImageFrame(image_frame) => image_frame.to_usvg_node(),
GraphicElement::Text(text) => text.to_usvg_node(),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.to_usvg_node(),

View File

@ -912,14 +912,14 @@ fn opacity_node(color: Color, opacity_multiplier: f32) -> Color {
#[node_macro::node_impl(OpacityNode)]
fn opacity_node(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData {
let opacity_multiplier = opacity_multiplier / 100.;
vector_data.style.opacity *= opacity_multiplier;
vector_data.alpha_blending.opacity *= opacity_multiplier;
vector_data
}
#[node_macro::node_impl(OpacityNode)]
fn opacity_node(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup {
let opacity_multiplier = opacity_multiplier / 100.;
graphic_group.opacity *= opacity_multiplier;
graphic_group.alpha_blending.opacity *= opacity_multiplier;
graphic_group
}
@ -930,19 +930,19 @@ pub struct BlendModeNode<BM> {
#[node_macro::node_fn(BlendModeNode)]
fn blend_mode_node(mut vector_data: VectorData, blend_mode: BlendMode) -> VectorData {
vector_data.style.blend_mode = blend_mode;
vector_data.alpha_blending.blend_mode = blend_mode;
vector_data
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut graphic_group: GraphicGroup, blend_mode: BlendMode) -> GraphicGroup {
graphic_group.blend_mode = blend_mode;
graphic_group.alpha_blending.blend_mode = blend_mode;
graphic_group
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut image_frame: ImageFrame<Color>, blend_mode: BlendMode) -> ImageFrame<Color> {
image_frame.blend_mode = blend_mode;
image_frame.alpha_blending.blend_mode = blend_mode;
image_frame
}

View File

@ -1,6 +1,6 @@
use super::discrete_srgb::float_to_srgb_u8;
use super::{Color, ImageSlice};
use crate::Node;
use crate::{AlphaBlending, Node};
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use dyn_any::StaticType;
@ -260,7 +260,7 @@ pub struct ImageFrame<P: Pixel> {
// positive going right and y axis positive going down, with the origin
// being an unspecified quantity.
pub transform: DAffine2,
pub blend_mode: BlendMode,
pub alpha_blending: AlphaBlending,
}
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
@ -312,7 +312,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self {
image: Image::empty(),
transform: DAffine2::ZERO,
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::new(),
}
}
@ -320,7 +320,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self {
image: Image::empty(),
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::new(),
}
}
@ -381,7 +381,7 @@ impl From<ImageFrame<Color>> for ImageFrame<SRGBA8> {
height: image.image.height,
},
transform: image.transform,
blend_mode: BlendMode::Normal,
alpha_blending: image.alpha_blending,
}
}
}
@ -396,7 +396,7 @@ impl From<ImageFrame<SRGBA8>> for ImageFrame<Color> {
height: image.image.height,
},
transform: image.transform,
blend_mode: BlendMode::Normal,
alpha_blending: image.alpha_blending,
}
}
}

View File

@ -73,7 +73,7 @@ impl TransformMut for GraphicGroup {
impl Transform for GraphicElement {
fn transform(&self) -> DAffine2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.transform(),
GraphicElement::VectorData(vector_shape) => vector_shape.transform(),
GraphicElement::ImageFrame(image_frame) => image_frame.transform(),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform(),
@ -82,7 +82,7 @@ impl Transform for GraphicElement {
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.local_pivot(pivot),
GraphicElement::VectorData(vector_shape) => vector_shape.local_pivot(pivot),
GraphicElement::ImageFrame(image_frame) => image_frame.local_pivot(pivot),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.local_pivot(pivot),
@ -91,7 +91,7 @@ impl Transform for GraphicElement {
}
fn decompose_scale(&self) -> DVec2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.decompose_scale(),
GraphicElement::VectorData(vector_shape) => vector_shape.decompose_scale(),
GraphicElement::ImageFrame(image_frame) => image_frame.decompose_scale(),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.decompose_scale(),
@ -102,7 +102,7 @@ impl Transform for GraphicElement {
impl TransformMut for GraphicElement {
fn transform_mut(&mut self) -> &mut DAffine2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.transform_mut(),
GraphicElement::VectorData(vector_shape) => vector_shape.transform_mut(),
GraphicElement::ImageFrame(image_frame) => image_frame.transform_mut(),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform_mut(),

View File

@ -1,7 +1,6 @@
//! Contains stylistic options for SVG elements.
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::raster::BlendMode;
use crate::Color;
use dyn_any::{DynAny, StaticType};
@ -410,27 +409,18 @@ impl Default for Stroke {
pub struct PathStyle {
stroke: Option<Stroke>,
fill: Fill,
pub opacity: f32,
pub blend_mode: BlendMode,
}
impl core::hash::Hash for PathStyle {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.stroke.hash(state);
self.fill.hash(state);
self.opacity.to_bits().hash(state);
self.blend_mode.hash(state);
}
}
impl PathStyle {
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
Self {
stroke,
fill,
opacity: 1.,
blend_mode: BlendMode::Normal,
}
Self { stroke, fill }
}
/// Get the current path's [Fill].

View File

@ -1,6 +1,6 @@
use super::style::{PathStyle, Stroke};
use crate::uuid::ManipulatorGroupId;
use crate::Color;
use crate::{uuid::ManipulatorGroupId, AlphaBlending};
use bezier_rs::ManipulatorGroup;
use dyn_any::{DynAny, StaticType};
@ -8,13 +8,14 @@ use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
/// [VectorData] is passed between nodes.
/// It contains a list of subpaths (that may be open or closed), a transform and some style information.
/// It contains a list of subpaths (that may be open or closed), a transform, and some style information.
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2,
pub style: PathStyle,
pub alpha_blending: AlphaBlending,
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
pub mirror_angle: Vec<ManipulatorGroupId>,
}
@ -24,6 +25,7 @@ impl core::hash::Hash for VectorData {
self.subpaths.hash(state);
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
self.style.hash(state);
self.alpha_blending.hash(state);
self.mirror_angle.hash(state);
}
}
@ -35,6 +37,7 @@ impl VectorData {
subpaths: Vec::new(),
transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::new(),
mirror_angle: Vec::new(),
}
}

View File

@ -356,7 +356,7 @@ async fn brush(image: ImageFrame<Color>, bounds: ImageFrame<Color>, strokes: Vec
let opaque_image = ImageFrame {
image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE),
transform: background_bounds,
blend_mode: BlendMode::Normal,
..Default::default()
};
let mut erase_restore_mask = opaque_image;
@ -410,11 +410,7 @@ mod test {
#[test]
fn test_translate_node() {
let image = Image::new(10, 10, Color::TRANSPARENT);
let mut image = ImageFrame {
image,
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
};
let mut image = ImageFrame { image, ..Default::default() };
image.translate(DVec2::new(1., 2.));
let translate_node = TranslateNode::new(ClonedNode::new(image));
let image = translate_node.eval(DVec2::new(1., 2.));

View File

@ -90,7 +90,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
height: image.image.height,
},
transform: image.transform,
blend_mode: image.blend_mode,
alpha_blending: image.alpha_blending,
};
// TODO: The cache should be based on the network topology not the node name
@ -142,7 +142,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
height: image.image.height,
},
transform: image.transform,
blend_mode: image.blend_mode,
alpha_blending: image.alpha_blending,
}
}
@ -588,6 +588,6 @@ async fn blend_gpu_image(foreground: ImageFrame<Color>, background: ImageFrame<C
height: background.image.height,
},
transform: background.transform,
blend_mode: background.blend_mode,
alpha_blending: background.alpha_blending,
}
}

View File

@ -8,7 +8,7 @@ use graphene_core::transform::{Footprint, Transform};
use crate::wasm_application_io::WasmEditorApi;
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::value::CopiedNode;
use graphene_core::{Color, Node};
use graphene_core::{AlphaBlending, Color, Node};
use std::collections::HashMap;
use std::fmt::Debug;
@ -115,7 +115,7 @@ fn sample(footprint: Footprint, image_frame: ImageFrame<Color>) -> ImageFrame<Co
ImageFrame {
image,
transform: new_transform,
blend_mode: image_frame.blend_mode,
alpha_blending: image_frame.alpha_blending,
}
}
@ -309,7 +309,7 @@ where
let mut new_background = ImageFrame {
image: new_background,
transform: transfrom,
blend_mode: background.blend_mode,
alpha_blending: background.alpha_blending,
};
new_background = blend_image(background, new_background, map_fn);
@ -422,7 +422,7 @@ fn extend_image_to_bounds_node(image: ImageFrame<Color>, bounds: DAffine2) -> Im
ImageFrame {
image: new_img,
transform: new_texture_to_layer_space,
blend_mode: image.blend_mode,
alpha_blending: image.alpha_blending,
}
}
@ -457,8 +457,11 @@ fn empty_image<_P: Pixel>(transform: DAffine2, color: _P) -> ImageFrame<_P> {
let image = Image::new(width, height, color);
let blend_mode = BlendMode::Normal;
ImageFrame { image, transform, blend_mode }
ImageFrame {
image,
transform,
alpha_blending: AlphaBlending::default(),
}
}
macro_rules! generate_imaginate_node {
@ -495,7 +498,7 @@ macro_rules! generate_imaginate_node {
use std::hash::Hasher;
let mut hasher = rustc_hash::FxHasher::default();
frame.image.hash(&mut hasher);
let hash =hasher.finish();
let hash = hasher.finish();
Box::pin(async move {
let controller: std::pin::Pin<Box<dyn std::future::Future<Output = ImaginateController>>> = controller;
@ -505,16 +508,12 @@ macro_rules! generate_imaginate_node {
let image = super::imaginate::imaginate(frame.image, editor_api, controller, $($val,)*).await;
self.cache.lock().unwrap().insert(hash, image.clone());
return ImageFrame {
image,
..frame
}
return ImageFrame { image, ..frame }
}
let image = self.cache.lock().unwrap().get(&hash).cloned().unwrap_or_default();
ImageFrame {
image,
..frame
}
ImageFrame { image, ..frame }
})
}
}
@ -549,7 +548,7 @@ fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> graphene_cor
graphene_core::raster::ImageFrame {
image,
transform,
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::default(),
}
}
@ -576,7 +575,7 @@ fn pixel_noise(width: u32, height: u32, seed: u32, noise_type: NoiseType) -> gra
ImageFrame::<Color> {
image,
transform: DAffine2::from_scale(DVec2::new(width as f64, height as f64)),
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::default(),
}
}
@ -621,7 +620,7 @@ fn mandelbrot_node(footprint: Footprint) -> ImageFrame<Color> {
ImageFrame {
image: Image { width, height, data },
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
blend_mode: BlendMode::Normal,
..Default::default()
}
}

View File

@ -279,8 +279,7 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
width: image.width(),
height: image.height(),
},
transform: glam::DAffine2::IDENTITY,
blend_mode: graphene_core::raster::BlendMode::Normal,
..Default::default()
};
image
}

View File

@ -312,7 +312,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
let empty_image = ImageFrame {
image: Image::new(bounds.x, bounds.y, Color::BLACK),
transform,
blend_mode: BlendMode::Normal,
..Default::default()
};
let final_image = ClonedNode::new(empty_image).then(complete_node);
let final_image = FutureWrapperNode::new(final_image);