diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 1b5a5063ae..a7eb895f54 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -1,5 +1,5 @@ -use crate::renderer::{RenderParams, format_transform_matrix}; -use core_types::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; +use crate::renderer::{RenderParams, black_or_white_for_best_contrast, format_transform_matrix}; +use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT; use core_types::uuid::generate_uuid; use glam::DAffine2; use graphic_types::vector_types::gradient::{Gradient, GradientType}; @@ -167,9 +167,12 @@ impl RenderExt for PathStyle { match render_mode { RenderMode::Outline => { let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); - let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT); + + let outline_color = black_or_white_for_best_contrast(render_params.artboard_background); + let mut outline_stroke = Stroke::new(Some(outline_color), LAYER_OUTLINE_STROKE_WEIGHT); // Outline strokes should be non-scaling by default outline_stroke.non_scaling = true; + let stroke_attribute = outline_stroke.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); format!("{fill_attribute}{stroke_attribute}") } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index f8f87964bc..74372383ac 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -182,6 +182,7 @@ pub struct RenderParams { pub alignment_parent_transform: Option, pub aligned_strokes: bool, pub override_paint_order: bool, + pub artboard_background: Option, } impl Hash for RenderParams { @@ -198,6 +199,7 @@ impl Hash for RenderParams { } self.aligned_strokes.hash(state); self.override_paint_order.hash(state); + self.artboard_background.hash(state); } } @@ -235,6 +237,26 @@ fn max_scale(transform: DAffine2) -> f64 { (sx + sy).sqrt() } +pub fn black_or_white_for_best_contrast(background: Option) -> Color { + let Some(bg) = background else { return core_types::consts::LAYER_OUTLINE_STROKE_COLOR }; + + let alpha = bg.a(); + + // Un-premultiply, then convert to gamma sRGB + let srgb = if alpha > f32::EPSILON { + Color::from_rgbaf32_unchecked(bg.r() / alpha, bg.g() / alpha, bg.b() / alpha, alpha).to_gamma_srgb() + } else { + Color::TRANSPARENT + }; + + // Composite over black in sRGB space, then convert back to linear for luminance + let composited = Color::from_rgbaf32_unchecked(srgb.r() * alpha, srgb.g() * alpha, srgb.b() * alpha, 1.).to_linear_srgb(); + + let threshold = (1.05 * 0.05f32).sqrt() - 0.05; + + if composited.luminance_srgb() > threshold { Color::BLACK } else { Color::WHITE } +} + pub fn to_transform(transform: DAffine2) -> usvg::Transform { let cols = transform.to_cols_array(); usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32) @@ -441,7 +463,9 @@ impl Render for Artboard { }, // Artwork content |render| { - self.content.render_svg(render, render_params); + let mut render_params = render_params.clone(); + render_params.artboard_background = Some(self.background); + self.content.render_svg(render, &render_params); }, ); } @@ -463,7 +487,9 @@ impl Render for Artboard { } // 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()); - self.content.render_to_vello(scene, child_transform, context, render_params); + let mut render_params = render_params.clone(); + render_params.artboard_background = Some(self.background); + self.content.render_to_vello(scene, child_transform, context, &render_params); if self.clip { scene.pop_layer(); } @@ -882,7 +908,7 @@ impl Render for Table { } fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { - use core_types::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; + use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT; use graphic_types::vector_types::vector::style::{GradientType, StrokeCap, StrokeJoin}; use vello::kurbo::{Cap, Join}; use vello::peniko; @@ -1066,12 +1092,9 @@ impl Render for Table { dash_pattern: Default::default(), dash_offset: 0., }; - let outline_color = peniko::Color::new([ - LAYER_OUTLINE_STROKE_COLOR.r(), - LAYER_OUTLINE_STROKE_COLOR.g(), - LAYER_OUTLINE_STROKE_COLOR.b(), - LAYER_OUTLINE_STROKE_COLOR.a(), - ]); + + let outline_color = black_or_white_for_best_contrast(render_params.artboard_background); + let outline_color = peniko::Color::new([outline_color.r(), outline_color.g(), outline_color.b(), outline_color.a()]); scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path); }