From 5b1e1cb2fb6efc432c68309576475288f0bc42ec Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 23 Mar 2026 02:20:54 +0100 Subject: [PATCH] Fix artboards not exporting with transparency using Vello (#3921) * Fix hide artboard for raster render mode * Desktop: Fix transparent viewport blending * Fix vello render using wrong color space conversion for background * Review --- desktop/src/render/composite_shader.wgsl | 2 + editor/src/node_graph_executor.rs | 9 +---- .../no-std-types/src/color/color_types.rs | 38 +++++++------------ .../libraries/rendering/src/renderer.rs | 13 ++++--- node-graph/libraries/wgpu-executor/src/lib.rs | 2 +- 5 files changed, 26 insertions(+), 38 deletions(-) diff --git a/desktop/src/render/composite_shader.wgsl b/desktop/src/render/composite_shader.wgsl index b018de12..c4aae035 100644 --- a/desktop/src/render/composite_shader.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -71,6 +71,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if (viewport_srgb.a < 0.001) { viewport_srgb = constants.background_color; + } else if (viewport_srgb.a < 0.999) { + viewport_srgb = blend(viewport_srgb, constants.background_color); } if (overlay_srgb.a < 0.001) { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index aa63da40..820a643a 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -440,8 +440,6 @@ impl NodeGraphExecutor { file_type, name, size, - #[cfg(feature = "gpu")] - transparent_background, artboard_name, artboard_count, .. @@ -491,12 +489,7 @@ impl NodeGraphExecutor { match file_type { FileType::Png => { - let result = if transparent_background { - image.write_to(&mut cursor, ImageFormat::Png) - } else { - let image: RgbImage = image.convert(); - image.write_to(&mut cursor, ImageFormat::Png) - }; + let result = image.write_to(&mut cursor, ImageFormat::Png); if let Err(err) = result { return Err(format!("Failed to encode PNG: {err}")); } diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index ebb654b3..abcb1e9e 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -879,42 +879,32 @@ impl Color { ) } + /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in gamma space. + #[inline(always)] + pub fn to_rgba8(&self) -> [u8; 4] { + [(self.red * 255.) as u8, (self.green * 255.) as u8, (self.blue * 255.) as u8, (self.alpha * 255.) as u8] + } + /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in linear space. - /// - /// # Examples - /// ``` - /// use core_types::color::Color; - /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); - /// // TODO: Add test - /// ``` #[inline(always)] pub fn to_rgba8_srgb(&self) -> [u8; 4] { - let gamma = self.to_gamma_srgb(); - [(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8] + self.to_gamma_srgb().to_rgba8() + } + + /// Return the all RGB components as a u8 slice, first component is red, followed by green, followed by blue. Use this if the [`Color`] is in gamma space. + #[inline(always)] + pub fn to_rgb8(&self) -> [u8; 3] { + [(self.red * 255.) as u8, (self.green * 255.) as u8, (self.blue * 255.) as u8] } /// Return the all RGB components as a u8 slice, first component is red, followed by green, followed by blue. Use this if the [`Color`] is in linear space. - /// - /// # Examples - /// ``` - /// use core_types::color::Color; - /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); - /// // TODO: Add test - /// ``` #[inline(always)] pub fn to_rgb8_srgb(&self) -> [u8; 3] { - let gamma = self.to_gamma_srgb(); - [(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8] + self.to_gamma_srgb().to_rgb8() } // https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ /// Convert a [Color] to a hue, saturation, lightness and alpha (all between 0 and 1) - /// - /// # Examples - /// ``` - /// use core_types::color::Color; - /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla(); - /// ``` pub fn to_hsla(&self) -> [f32; 4] { let min_channel = self.red.min(self.green).min(self.blue); let max_channel = self.red.max(self.green).max(self.blue); diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 47f4d295..7869cf68 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -522,18 +522,21 @@ impl Render for Artboard { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { use vello::peniko; - // Render background - let color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.background.a()]); let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()]; let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)); - scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), color, None, &rect); - scene.pop_layer(); + // Render background + if !render_params.hide_artboards { + let color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.background.a()]); + scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), color, None, &rect); + scene.pop_layer(); + } if self.clip { scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect); } + // Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here. let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2()); let mut render_params = render_params.clone(); diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index e9886923..011da195 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -129,7 +129,7 @@ impl WgpuExecutor { if let Some(target_texture) = output.as_mut() { target_texture.ensure_size(&self.context.device, size); - let [r, g, b, a] = background.unwrap_or(Color::TRANSPARENT).to_rgba8_srgb(); + let [r, g, b, a] = background.unwrap_or(Color::TRANSPARENT).to_rgba8(); let render_params = RenderParams { base_color: vello::peniko::Color::from_rgba8(r, g, b, a), width: size.x,