mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
WIP: Use List<Graphic> to render Color & Gradient
This commit is contained in:
@@ -2393,6 +2393,7 @@ impl DocumentMessageHandler {
|
||||
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.
|
||||
// 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());
|
||||
|
||||
// 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::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 {
|
||||
|
||||
@@ -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""#),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user