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

This commit is contained in:
YohYamasaki
2026-05-19 16:19:19 +09:00
parent 746d78364c
commit 1adeb4cedd
5 changed files with 116 additions and 32 deletions

View File

@@ -1,6 +1,8 @@
use std::borrow::Cow;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
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::render_complexity::RenderComplexity;
use core_types::uuid::NodeId;
@@ -357,7 +359,16 @@ impl Graphic {
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false };
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,
}
@@ -370,6 +381,15 @@ impl Graphic {
_ => 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 {

View File

@@ -12,6 +12,28 @@ use std::fmt::Write;
use vector_types::GradientStops;
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 {
type Output;
#[allow(clippy::too_many_arguments)]
@@ -24,6 +46,7 @@ pub trait RenderExt {
bounds: DAffine2,
transformed_bounds: DAffine2,
render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output;
}
@@ -39,12 +62,13 @@ impl RenderExt for List<Color> {
_bounds: DAffine2,
_transformed_bounds: DAffine2,
_render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output {
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. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
}
result
@@ -64,6 +88,7 @@ impl RenderExt for List<GradientStops> {
bounds: DAffine2,
transformed_bounds: DAffine2,
_render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output {
let mut stop = String::new();
@@ -136,7 +161,7 @@ impl RenderExt for List<GradientStops> {
impl RenderExt for Stroke {
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(
&self,
_svg_defs: &mut String,
@@ -146,9 +171,9 @@ impl RenderExt for Stroke {
_bounds: DAffine2,
_transformed_bounds: DAffine2,
render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output {
// 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() {
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);
// Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, SRGBA8::from(color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
let mut attributes = String::new();
if let Some(mut weight) = weight {
if stroke_align.is_some() && render_params.aligned_strokes {
weight *= 2.;
@@ -210,21 +232,23 @@ impl RenderExt for List<Graphic> {
bounds: DAffine2,
transformed_bounds: DAffine2,
render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output {
let fill_graphic = self.element(0);
let paint_attr = target.paint_attr();
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)) => {
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
format!(r##" fill="url(#{gradient_id})""##)
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, transformed_bounds, render_params, target);
format!(r##" {paint_attr}="url(#{gradient_id})""##)
}
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) => {
render_svg_fill_pattern(svg_defs, self, item_transform, bounds, render_params)
.map(|id| format!(r##" fill="url(#{id})""##))
.unwrap_or_else(|| r#" fill="none""#.to_string())
.map(|id| format!(r##" {paint_attr}="url(#{id})""##))
.unwrap_or_else(|| format!(r#" {paint_attr}="none""#))
}
None => r#" fill="none""#.to_string(),
None => format!(r#" {paint_attr}="none""#),
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::render_ext::RenderExt;
use crate::render_ext::{PaintTarget, RenderExt};
use crate::to_peniko::{BlendModeExt, ToPenikoColor};
use core_types::CacheHash;
use core_types::blending::BlendMode;
@@ -6,7 +6,7 @@ use core_types::bounds::BoundingBox;
use core_types::bounds::RenderBoundingBox;
use core_types::color::Color;
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::render_complexity::RenderComplexity;
use core_types::transform::Footprint;
@@ -18,7 +18,7 @@ use core_types::{
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
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::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath;
@@ -367,6 +367,7 @@ fn emit_svg_fill_path(
bounds_matrix,
transformed_bounds_matrix,
render_params,
PaintTarget::Fill,
)
})
.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));
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 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 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.override_paint_order = override_paint_order;
let stroke_attribute = vector
let stroke_shape_attribute = vector
.style
.stroke()
.map(|stroke| {
stroke.render(
defs,
item_transform,
element_transform,
applied_stroke_transform,
bounds_matrix,
transformed_bounds_matrix,
&render_params,
)
if stroke_paint_graphic_list.as_ref().and_then(|l| l.element(0)).is_some() {
stroke.render(
defs,
item_transform,
element_transform,
applied_stroke_transform,
bounds_matrix,
transformed_bounds_matrix,
&render_params,
PaintTarget::Stroke,
)
} else {
String::new()
}
})
.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 {
r#" fill="none""#.to_string()
} else {
@@ -1128,6 +1164,7 @@ impl Render for List<Vector> {
bounds_matrix,
transformed_bounds_matrix,
&render_params,
PaintTarget::Fill,
)
})
.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_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 {
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
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
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 opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;

View File

@@ -573,7 +573,7 @@ impl Stroke {
}
pub fn has_renderable_stroke(&self) -> bool {
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.)
self.weight > 0.
}
}