WIP: Use List<Graphic> to render Color & Gradient

This commit is contained in:
YohYamasaki
2026-05-20 11:17:28 +09:00
parent 746d78364c
commit 1adeb4cedd
5 changed files with 116 additions and 32 deletions
@@ -2393,6 +2393,7 @@ impl DocumentMessageHandler {
let has_fill = !matches!(style.fill, Fill::None); let has_fill = !matches!(style.fill, Fill::None);
// `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color. // `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color.
// So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke. // So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke.
// FIXME: Consider if we need to check ATTR_STROKE_PAINT_GRAPHIC
let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()); let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke());
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip. // No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
@@ -1,6 +1,8 @@
use std::borrow::Cow;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List}; use core_types::list::{ATTR_STROKE_PAINT_GRAPHIC, Item, List};
use core_types::ops::ListConvert; use core_types::ops::ListConvert;
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
@@ -357,7 +359,16 @@ impl Graphic {
Graphic::Vector(vector) => (0..vector.len()).all(|index| { Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false }; let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
opacity > 1. - f64::EPSILON && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) let stroke_paint_graphic_list = vector
.attribute::<List<Graphic>>(ATTR_STROKE_PAINT_GRAPHIC, index)
.filter(|list| !list.is_empty())
.map(Cow::Borrowed)
.or_else(|| color_to_graphic_list(element.style.stroke().and_then(|s| s.color())).map(Cow::Owned));
let stroke_paint_graphic = stroke_paint_graphic_list.as_ref().and_then(|l| l.element(0));
opacity > 1. - f64::EPSILON
&& element.style.fill().is_opaque()
&& (element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) || stroke_paint_graphic.is_none_or(|graphic| graphic.is_fully_transparent()))
}), }),
_ => false, _ => false,
} }
@@ -370,6 +381,15 @@ impl Graphic {
_ => false, _ => false,
} }
} }
pub fn is_fully_transparent(&self) -> bool {
match self {
Self::Color(list) => list.element(0).is_some_and(|c| c.a() == 0.),
Self::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
// FIXME: Write recursive check for other types
_ => false,
}
}
} }
impl BoundingBox for Graphic { impl BoundingBox for Graphic {
@@ -12,6 +12,28 @@ use std::fmt::Write;
use vector_types::GradientStops; use vector_types::GradientStops;
use vector_types::gradient::GradientSpreadMethod; use vector_types::gradient::GradientSpreadMethod;
#[derive(Copy, Clone)]
pub enum PaintTarget {
Fill,
Stroke,
}
impl PaintTarget {
fn paint_attr(self) -> &'static str {
match self {
Self::Fill => "fill",
Self::Stroke => "stroke",
}
}
fn opacity_attr(self) -> &'static str {
match self {
Self::Fill => "fill-opacity",
Self::Stroke => "stroke-opacity",
}
}
}
pub trait RenderExt { pub trait RenderExt {
type Output; type Output;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -24,6 +46,7 @@ pub trait RenderExt {
bounds: DAffine2, bounds: DAffine2,
transformed_bounds: DAffine2, transformed_bounds: DAffine2,
render_params: &RenderParams, render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output; ) -> Self::Output;
} }
@@ -39,12 +62,13 @@ impl RenderExt for List<Color> {
_bounds: DAffine2, _bounds: DAffine2,
_transformed_bounds: DAffine2, _transformed_bounds: DAffine2,
_render_params: &RenderParams, _render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output { ) -> Self::Output {
let Some(color) = self.element(0) else { return r#" fill="none""#.to_string() }; let Some(color) = self.element(0) else { return r#" fill="none""#.to_string() };
let mut result = format!(r##" fill="#{}""##, SRGBA8::from(*color).to_rgb_hex()); let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex());
if color.a() < 1. { if color.a() < 1. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.); let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
} }
result result
@@ -64,6 +88,7 @@ impl RenderExt for List<GradientStops> {
bounds: DAffine2, bounds: DAffine2,
transformed_bounds: DAffine2, transformed_bounds: DAffine2,
_render_params: &RenderParams, _render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output { ) -> Self::Output {
let mut stop = String::new(); let mut stop = String::new();
@@ -136,7 +161,7 @@ impl RenderExt for List<GradientStops> {
impl RenderExt for Stroke { impl RenderExt for Stroke {
type Output = String; type Output = String;
/// Provide the SVG attributes for the stroke. /// Provide the shape-related SVG attributes for the stroke. The paint-related attributes for the stroke are generated from `List<Graphic>.render` with `PaintTarget::Stroke`.
fn render( fn render(
&self, &self,
_svg_defs: &mut String, _svg_defs: &mut String,
@@ -146,9 +171,9 @@ impl RenderExt for Stroke {
_bounds: DAffine2, _bounds: DAffine2,
_transformed_bounds: DAffine2, _transformed_bounds: DAffine2,
render_params: &RenderParams, render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output { ) -> Self::Output {
// Don't render a stroke at all if it would be invisible // Don't render a stroke at all if it would be invisible
let Some(color) = self.color else { return String::new() };
if !self.has_renderable_stroke() { if !self.has_renderable_stroke() {
return String::new(); return String::new();
} }
@@ -166,10 +191,7 @@ impl RenderExt for Stroke {
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow); let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow);
// Render the needed stroke attributes // Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, SRGBA8::from(color).to_rgb_hex()); let mut attributes = String::new();
if color.a() < 1. {
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
if let Some(mut weight) = weight { if let Some(mut weight) = weight {
if stroke_align.is_some() && render_params.aligned_strokes { if stroke_align.is_some() && render_params.aligned_strokes {
weight *= 2.; weight *= 2.;
@@ -210,21 +232,23 @@ impl RenderExt for List<Graphic> {
bounds: DAffine2, bounds: DAffine2,
transformed_bounds: DAffine2, transformed_bounds: DAffine2,
render_params: &RenderParams, render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output { ) -> Self::Output {
let fill_graphic = self.element(0); let fill_graphic = self.element(0);
let paint_attr = target.paint_attr();
match fill_graphic { match fill_graphic {
Some(Graphic::Color(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params), Some(Graphic::Color(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params, target),
Some(Graphic::Gradient(gradient_list)) => { Some(Graphic::Gradient(gradient_list)) => {
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params); let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params, target);
format!(r##" fill="url(#{gradient_id})""##) format!(r##" {paint_attr}="url(#{gradient_id})""##)
} }
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) => { Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) => {
render_svg_fill_pattern(svg_defs, self, item_transform, bounds, render_params) render_svg_fill_pattern(svg_defs, self, item_transform, bounds, render_params)
.map(|id| format!(r##" fill="url(#{id})""##)) .map(|id| format!(r##" {paint_attr}="url(#{id})""##))
.unwrap_or_else(|| r#" fill="none""#.to_string()) .unwrap_or_else(|| format!(r#" {paint_attr}="none""#))
} }
None => r#" fill="none""#.to_string(), None => format!(r#" {paint_attr}="none""#),
} }
} }
} }
+54 -15
View File
@@ -1,4 +1,4 @@
use crate::render_ext::RenderExt; use crate::render_ext::{PaintTarget, RenderExt};
use crate::to_peniko::{BlendModeExt, ToPenikoColor}; use crate::to_peniko::{BlendModeExt, ToPenikoColor};
use core_types::CacheHash; use core_types::CacheHash;
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
@@ -6,7 +6,7 @@ use core_types::bounds::BoundingBox;
use core_types::bounds::RenderBoundingBox; use core_types::bounds::RenderBoundingBox;
use core_types::color::Color; use core_types::color::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::list::{ATTR_FILL_GRAPHIC, Item, List}; use core_types::list::{ATTR_FILL_GRAPHIC, ATTR_STROKE_PAINT_GRAPHIC, Item, List};
use core_types::math::quad::Quad; use core_types::math::quad::Quad;
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::transform::Footprint; use core_types::transform::Footprint;
@@ -18,7 +18,7 @@ use core_types::{
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_hash::CacheHashWrapper; use graphene_hash::CacheHashWrapper;
use graphic_types::graphic::fill_to_graphic_list; use graphic_types::graphic::{color_to_graphic_list, fill_to_graphic_list};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::subpath::Subpath;
@@ -367,6 +367,7 @@ fn emit_svg_fill_path(
bounds_matrix, bounds_matrix,
transformed_bounds_matrix, transformed_bounds_matrix,
render_params, render_params,
PaintTarget::Fill,
) )
}) })
.unwrap_or_else(|| r#" fill="none""#.to_string()); .unwrap_or_else(|| r#" fill="none""#.to_string());
@@ -1004,8 +1005,17 @@ impl Render for List<Vector> {
.or_else(|| fill_to_graphic_list(vector.style.fill()).map(Cow::Owned)); .or_else(|| fill_to_graphic_list(vector.style.fill()).map(Cow::Owned));
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0)); let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));
let stroke_paint_graphic_list = self
.attribute::<List<Graphic>>(ATTR_STROKE_PAINT_GRAPHIC, index)
.filter(|list| !list.is_empty())
.map(Cow::Borrowed)
.or_else(|| color_to_graphic_list(vector.style.stroke().and_then(|s| s.color())).map(Cow::Owned));
let stroke_paint_graphic = stroke_paint_graphic_list.as_ref().and_then(|l| l.element(0));
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()); let can_draw_aligned_stroke = path_is_closed
&& vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
&& stroke_paint_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.is_opaque()) || mask_type == MaskType::Clip); let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.is_opaque()) || mask_type == MaskType::Clip);
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
@@ -1098,22 +1108,48 @@ impl Render for List<Vector> {
render_params.aligned_strokes = can_draw_aligned_stroke; render_params.aligned_strokes = can_draw_aligned_stroke;
render_params.override_paint_order = override_paint_order; render_params.override_paint_order = override_paint_order;
let stroke_attribute = vector let stroke_shape_attribute = vector
.style .style
.stroke() .stroke()
.map(|stroke| { .map(|stroke| {
stroke.render( if stroke_paint_graphic_list.as_ref().and_then(|l| l.element(0)).is_some() {
defs, stroke.render(
item_transform, defs,
element_transform, item_transform,
applied_stroke_transform, element_transform,
bounds_matrix, applied_stroke_transform,
transformed_bounds_matrix, bounds_matrix,
&render_params, transformed_bounds_matrix,
) &render_params,
PaintTarget::Stroke,
)
} else {
String::new()
}
}) })
.unwrap_or_default(); .unwrap_or_default();
// Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback
let stroke_paint_attribute = if vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke()) {
stroke_paint_graphic_list
.as_deref()
.map(|list| {
list.render(
defs,
item_transform,
element_transform,
applied_stroke_transform,
bounds_matrix,
transformed_bounds_matrix,
&render_params,
PaintTarget::Stroke,
)
})
.unwrap_or_else(|| r#" stroke="none""#.to_string())
} else {
String::new()
};
let fill_attribute = if needs_separate_alignment_fill || use_face_fill { let fill_attribute = if needs_separate_alignment_fill || use_face_fill {
r#" fill="none""#.to_string() r#" fill="none""#.to_string()
} else { } else {
@@ -1128,6 +1164,7 @@ impl Render for List<Vector> {
bounds_matrix, bounds_matrix,
transformed_bounds_matrix, transformed_bounds_matrix,
&render_params, &render_params,
PaintTarget::Fill,
) )
}) })
.unwrap_or_else(|| r#" fill="none""#.to_string()) .unwrap_or_else(|| r#" fill="none""#.to_string())
@@ -1138,7 +1175,8 @@ impl Render for List<Vector> {
attributes.push(mask_type.to_attribute(), selector); attributes.push(mask_type.to_attribute(), selector);
} }
attributes.push_val(fill_attribute); attributes.push_val(fill_attribute);
attributes.push_val(stroke_attribute); attributes.push_val(stroke_shape_attribute);
attributes.push_val(stroke_paint_attribute);
if vector.is_branching() && !use_face_fill { if vector.is_branching() && !use_face_fill {
attributes.push("fill-rule", "evenodd"); attributes.push("fill-rule", "evenodd");
@@ -1218,6 +1256,7 @@ impl Render for List<Vector> {
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since // Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down. // the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
let stroke = element.style.stroke(); let stroke = element.style.stroke();
// FIXME: Need to add Graphic.is_fully_transparent check
let can_draw_aligned_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed()); let can_draw_aligned_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
@@ -573,7 +573,7 @@ impl Stroke {
} }
pub fn has_renderable_stroke(&self) -> bool { pub fn has_renderable_stroke(&self) -> bool {
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.) self.weight > 0.
} }
} }