mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Transition gradients from bbox-relative to document-absolute space (#4241)
* Use userSpaceOnUse to render gradient on SVG * Allow non-uniform transform to radial gradient on vello * Position gradients absolutely instead of by bbox * Fix SVG import by removing multiplication of bound's inverse transform * Represent sheared linear gradients via their equivalent gradient line * Migration * Fix migration for radial (elliptical) gradients * Fix boolean operation gradients --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -25,8 +25,8 @@ pub use graphene_hash;
|
||||
pub use graphene_hash::CacheHash;
|
||||
pub use list::{
|
||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
||||
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL,
|
||||
ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
||||
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME,
|
||||
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
|
||||
};
|
||||
pub use memo::MemoHash;
|
||||
pub use no_std_types::AsU32;
|
||||
|
||||
@@ -58,6 +58,10 @@ pub const ATTR_CLIP: &str = "clip";
|
||||
pub const ATTR_SPREAD_METHOD: &str = "spread_method";
|
||||
/// Gradient's `GradientType` (`Linear` or `Radial`).
|
||||
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// `bool` runtime marker (never serialized) flagging a gradient that came from the legacy bounding-box-relative `Fill::Gradient`,
|
||||
/// so the renderer reproduces the pre-#4241 positioning instead of the new absolute path.
|
||||
pub const ATTR_GRADIENT_LEGACY: &str = "gradient_legacy";
|
||||
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
|
||||
pub const ATTR_FILL: &str = "fill";
|
||||
/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.
|
||||
|
||||
@@ -4,7 +4,7 @@ use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
|
||||
use core_types::ops::{FromAnchorPosition, ListConvert};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
@@ -193,15 +193,26 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
||||
|
||||
/// Converts a `Fill` enum into the `List<Graphic>` representation used as paint storage.
|
||||
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
|
||||
pub fn fill_to_graphic_list(fill: &Fill) -> Option<List<Graphic>> {
|
||||
pub fn fill_to_graphic_list(fill: &Fill, bounding_box_transform: DAffine2) -> Option<List<Graphic>> {
|
||||
match fill {
|
||||
Fill::None => None,
|
||||
Fill::Solid(color) => Some(List::new_from_element((*color).into())),
|
||||
Fill::Gradient(gradient) => {
|
||||
let gradient_transform = gradient.transform * gradient.to_transform();
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Absolute gradients carry their effective frame into the new pipeline; legacy bounding-box gradients bake the bbox
|
||||
// in and flag the legacy render path until the deferred migration converts them.
|
||||
let (transform, legacy) = if gradient.absolute {
|
||||
(gradient_transform, false)
|
||||
} else {
|
||||
(bounding_box_transform * gradient_transform, true)
|
||||
};
|
||||
let gradient_item = Item::new_from_element(gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, gradient.to_transform())
|
||||
.with_attribute(ATTR_TRANSFORM, transform)
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type)
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method);
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method)
|
||||
.with_attribute(ATTR_GRADIENT_LEGACY, legacy);
|
||||
let gradient_list = List::new_from_item(gradient_item);
|
||||
|
||||
Some(List::new_from_element(Graphic::Gradient(gradient_list)))
|
||||
@@ -246,7 +257,9 @@ pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool
|
||||
pub fn fill_graphic_list_at(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
|
||||
graphic_list_at(list, index, ATTR_FILL).or_else(|| {
|
||||
let vector = list.element(index)?;
|
||||
fill_to_graphic_list(vector.style.fill()).map(Cow::Owned)
|
||||
let bounds = vector.nonzero_bounding_box();
|
||||
let bounding_box_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
fill_to_graphic_list(vector.style.fill(), bounding_box_transform).map(Cow::Owned)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::renderer::{RenderParams, format_transform_matrix, transform_is_invertible};
|
||||
use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible};
|
||||
use crate::{Render, RenderSvgSegmentList, SvgRender};
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::list::List;
|
||||
@@ -45,7 +45,6 @@ pub trait RenderExt {
|
||||
element_transform: DAffine2,
|
||||
stroke_transform: DAffine2,
|
||||
bounds: DAffine2,
|
||||
transformed_bounds: DAffine2,
|
||||
render_params: &RenderParams,
|
||||
target: PaintTarget,
|
||||
) -> Self::Output;
|
||||
@@ -61,7 +60,6 @@ impl RenderExt for List<Color> {
|
||||
_element_transform: DAffine2,
|
||||
_stroke_transform: DAffine2,
|
||||
_bounds: DAffine2,
|
||||
_transformed_bounds: DAffine2,
|
||||
_render_params: &RenderParams,
|
||||
target: PaintTarget,
|
||||
) -> Self::Output {
|
||||
@@ -85,11 +83,10 @@ impl RenderExt for List<GradientStops> {
|
||||
fn render(
|
||||
&self,
|
||||
svg_defs: &mut String,
|
||||
_item_transform: DAffine2,
|
||||
item_transform: DAffine2,
|
||||
element_transform: DAffine2,
|
||||
stroke_transform: DAffine2,
|
||||
bounds: DAffine2,
|
||||
transformed_bounds: DAffine2,
|
||||
_stroke_transform: DAffine2,
|
||||
_bounds: DAffine2,
|
||||
_render_params: &RenderParams,
|
||||
_target: PaintTarget,
|
||||
) -> Self::Output {
|
||||
@@ -97,7 +94,7 @@ impl RenderExt for List<GradientStops> {
|
||||
|
||||
let Some(stops) = self.element(0) else { return 0 };
|
||||
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
||||
let gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
|
||||
|
||||
for (position, color, original_midpoint) in stops.interpolated_samples() {
|
||||
@@ -115,16 +112,17 @@ impl RenderExt for List<GradientStops> {
|
||||
stop.push_str(" />")
|
||||
}
|
||||
|
||||
let transform_points = element_transform * stroke_transform * bounds * gradient_transform;
|
||||
let start = transform_points.transform_point2(DVec2::ZERO);
|
||||
let end = transform_points.transform_point2(DVec2::X);
|
||||
|
||||
let gradient_transform = if transform_is_invertible(transformed_bounds) {
|
||||
transformed_bounds.inverse()
|
||||
// Need to cancel out the element's transform as it is already applied to the path itself.
|
||||
let element_transform_inverse = if transform_is_invertible(element_transform) {
|
||||
element_transform.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
|
||||
DAffine2::IDENTITY
|
||||
};
|
||||
let gradient_transform = format_transform_matrix(gradient_transform);
|
||||
|
||||
let document_transform = item_transform * local_gradient_transform;
|
||||
|
||||
let placement = gradient_placement(document_transform, gradient_type);
|
||||
let gradient_transform = format_transform_matrix(element_transform_inverse * placement);
|
||||
let gradient_transform = if gradient_transform.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
@@ -143,16 +141,15 @@ impl RenderExt for List<GradientStops> {
|
||||
GradientType::Linear => {
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{spread_method}{gradient_transform}>{}</linearGradient>"#,
|
||||
gradient_id, start.x, start.y, end.x, end.y, stop
|
||||
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
|
||||
gradient_id, stop
|
||||
);
|
||||
}
|
||||
GradientType::Radial => {
|
||||
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{spread_method}{gradient_transform}>{}</radialGradient>"#,
|
||||
gradient_id, start.x, start.y, radius, stop
|
||||
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
|
||||
gradient_id, stop
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -172,7 +169,6 @@ impl RenderExt for Stroke {
|
||||
_element_transform: DAffine2,
|
||||
_stroke_transform: DAffine2,
|
||||
_bounds: DAffine2,
|
||||
_transformed_bounds: DAffine2,
|
||||
render_params: &RenderParams,
|
||||
_target: PaintTarget,
|
||||
) -> Self::Output {
|
||||
@@ -233,7 +229,6 @@ impl RenderExt for List<Graphic> {
|
||||
element_transform: DAffine2,
|
||||
stroke_transform: DAffine2,
|
||||
bounds: DAffine2,
|
||||
transformed_bounds: DAffine2,
|
||||
render_params: &RenderParams,
|
||||
target: PaintTarget,
|
||||
) -> Self::Output {
|
||||
@@ -241,9 +236,9 @@ impl RenderExt for List<Graphic> {
|
||||
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, target),
|
||||
Some(Graphic::Color(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, 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, target);
|
||||
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target);
|
||||
format!(r##" {paint_attr}="url(#{gradient_id})""##)
|
||||
}
|
||||
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) => {
|
||||
|
||||
@@ -14,11 +14,11 @@ use core_types::transform::Footprint;
|
||||
use core_types::uuid::{NodeId, generate_uuid};
|
||||
use core_types::{
|
||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
|
||||
ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD,
|
||||
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||
ATTR_FONT_SIZE, ATTR_GRADIENT_LEGACY, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL,
|
||||
ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
|
||||
};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_hash::CacheHashWrapper;
|
||||
use graphene_resource::Resource;
|
||||
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, stroke_graphic_list_at};
|
||||
@@ -356,7 +356,6 @@ fn emit_svg_fill_path(
|
||||
element_transform: DAffine2,
|
||||
applied_stroke_transform: DAffine2,
|
||||
bounds_matrix: DAffine2,
|
||||
transformed_bounds_matrix: DAffine2,
|
||||
render_params: &RenderParams,
|
||||
) {
|
||||
render.leaf_tag("path", |attributes| {
|
||||
@@ -367,18 +366,7 @@ fn emit_svg_fill_path(
|
||||
}
|
||||
let defs = &mut attributes.0.svg_defs;
|
||||
let fill_attribute = fill_graphic_list
|
||||
.map(|list| {
|
||||
list.render(
|
||||
defs,
|
||||
item_transform,
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
bounds_matrix,
|
||||
transformed_bounds_matrix,
|
||||
render_params,
|
||||
PaintTarget::Fill,
|
||||
)
|
||||
})
|
||||
.map(|list| list.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, render_params, PaintTarget::Fill))
|
||||
.unwrap_or_else(|| r#" fill="none""#.to_string());
|
||||
attributes.push_val(fill_attribute);
|
||||
});
|
||||
@@ -389,12 +377,33 @@ pub(crate) fn transform_is_invertible(transform: DAffine2) -> bool {
|
||||
transform.matrix2.determinant().recip().is_finite()
|
||||
}
|
||||
|
||||
fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_vector: &Vector, parent_transform: &DAffine2, multiplied_transform: &DAffine2) -> Option<peniko::Brush> {
|
||||
/// Maps a gradient's `transform` into the frame handed to the renderer: radial keeps the full matrix (so a
|
||||
/// non-uniform transform makes an ellipse), while linear is reduced to the equivalent non-sheared gradient line (the
|
||||
/// axis projected onto the band normal) so the iso-color bands keep following a sheared transform, which Vello can
|
||||
/// represent since it stores only two endpoints.
|
||||
pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientType) -> DAffine2 {
|
||||
match gradient_type {
|
||||
GradientType::Radial => transform,
|
||||
GradientType::Linear => {
|
||||
let axis = transform.matrix2.x_axis;
|
||||
let band_normal = transform.matrix2.y_axis.perp();
|
||||
let line = if band_normal.length_squared() > 0. { axis.project_onto(band_normal) } else { axis };
|
||||
DAffine2 {
|
||||
matrix2: DMat2::from_cols(line, line.perp()),
|
||||
translation: transform.translation,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_transform: &DAffine2, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
||||
let stops = gradient_list.element(0)?;
|
||||
|
||||
let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
||||
let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
let spread_method: GradientSpreadMethod = gradient_list.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
let legacy_bounding_box: bool = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_LEGACY, 0);
|
||||
|
||||
let mut peniko_stops = peniko::ColorStops::new();
|
||||
for (position, color, _) in stops.interpolated_samples() {
|
||||
@@ -404,18 +413,20 @@ fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_vect
|
||||
});
|
||||
}
|
||||
|
||||
let bounds = parent_vector.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
||||
let inverse_parent_transform = if transform_is_invertible(*parent_transform) {
|
||||
parent_transform.inverse()
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Legacy bounding-box gradients reproduce the pre-#4241 renderer: literal endpoints in the layer's own space, with the
|
||||
// parent transform applied as the brush so its shear bends the bands. New gradients use the unit gradient placed by the desheared frame.
|
||||
let (start, end, gradient_to_device) = if legacy_bounding_box {
|
||||
let inverse_parent_transform = if transform_is_invertible(*parent_transform) {
|
||||
parent_transform.inverse()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let mod_points = inverse_parent_transform * *multiplied_transform * gradient_transform;
|
||||
(mod_points.transform_point2(DVec2::ZERO), mod_points.transform_point2(DVec2::X), *parent_transform)
|
||||
} else {
|
||||
Default::default()
|
||||
(DVec2::ZERO, DVec2::X, gradient_placement(multiplied_transform * gradient_transform, gradient_type))
|
||||
};
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform * gradient_transform;
|
||||
|
||||
let start = mod_points.transform_point2(DVec2::ZERO);
|
||||
let end = mod_points.transform_point2(DVec2::X);
|
||||
|
||||
let brush = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind: match gradient_type {
|
||||
@@ -424,16 +435,13 @@ fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_vect
|
||||
end: to_point(end),
|
||||
}
|
||||
.into(),
|
||||
GradientType::Radial => {
|
||||
let radius = start.distance(end);
|
||||
peniko::RadialGradientPosition {
|
||||
start_center: to_point(start),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
.into()
|
||||
GradientType::Radial => peniko::RadialGradientPosition {
|
||||
start_center: to_point(start),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(start),
|
||||
end_radius: start.distance(end) as f32,
|
||||
}
|
||||
.into(),
|
||||
},
|
||||
extend: match spread_method {
|
||||
GradientSpreadMethod::Pad => peniko::Extend::Pad,
|
||||
@@ -445,7 +453,7 @@ fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, parent_vect
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Some(brush)
|
||||
Some((brush, gradient_to_device))
|
||||
}
|
||||
|
||||
// TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes.
|
||||
@@ -1084,7 +1092,6 @@ impl Render for List<Vector> {
|
||||
let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY).unwrap_or(layer_bounds);
|
||||
|
||||
let bounds_matrix = DAffine2::from_scale_angle_translation(layer_bounds[1] - layer_bounds[0], 0., layer_bounds[0]);
|
||||
let transformed_bounds_matrix = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
let stroke_bounds_matrix = DAffine2::from_scale_angle_translation(stroke_layer_bounds[1] - stroke_layer_bounds[0], 0., stroke_layer_bounds[0]);
|
||||
|
||||
let mut path = String::new();
|
||||
@@ -1126,7 +1133,6 @@ impl Render for List<Vector> {
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
bounds_matrix,
|
||||
transformed_bounds_matrix,
|
||||
render_params,
|
||||
);
|
||||
}
|
||||
@@ -1158,7 +1164,6 @@ impl Render for List<Vector> {
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
bounds_matrix,
|
||||
transformed_bounds_matrix,
|
||||
render_params,
|
||||
);
|
||||
}
|
||||
@@ -1207,16 +1212,7 @@ impl Render for List<Vector> {
|
||||
.stroke()
|
||||
.map(|stroke| {
|
||||
if stroke_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,
|
||||
)
|
||||
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
@@ -1235,16 +1231,7 @@ impl Render for List<Vector> {
|
||||
Some(Graphic::Color(_)) | Some(Graphic::Gradient(_)) => bounds_matrix,
|
||||
_ => stroke_bounds_matrix,
|
||||
};
|
||||
list.render(
|
||||
defs,
|
||||
item_transform,
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
paint_bounds,
|
||||
transformed_bounds_matrix,
|
||||
&render_params,
|
||||
PaintTarget::Stroke,
|
||||
)
|
||||
list.render(defs, item_transform, element_transform, applied_stroke_transform, paint_bounds, &render_params, PaintTarget::Stroke)
|
||||
})
|
||||
.unwrap_or_else(|| r#" stroke="none""#.to_string())
|
||||
} else {
|
||||
@@ -1256,18 +1243,7 @@ impl Render for List<Vector> {
|
||||
} else {
|
||||
fill_graphic_list
|
||||
.as_deref()
|
||||
.map(|list| {
|
||||
list.render(
|
||||
defs,
|
||||
item_transform,
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
bounds_matrix,
|
||||
transformed_bounds_matrix,
|
||||
&render_params,
|
||||
PaintTarget::Fill,
|
||||
)
|
||||
})
|
||||
.map(|list| list.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Fill))
|
||||
.unwrap_or_else(|| r#" fill="none""#.to_string())
|
||||
};
|
||||
|
||||
@@ -1303,7 +1279,6 @@ impl Render for List<Vector> {
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
bounds_matrix,
|
||||
transformed_bounds_matrix,
|
||||
render_params,
|
||||
);
|
||||
}
|
||||
@@ -1397,7 +1372,7 @@ impl Render for List<Vector> {
|
||||
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
|
||||
}
|
||||
Graphic::Gradient(list) => {
|
||||
let Some(brush) = create_peniko_gradient_brush(list, element, &parent_transform, &multiplied_transform) else {
|
||||
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &parent_transform, &multiplied_transform) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1406,7 +1381,7 @@ impl Render for List<Vector> {
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * gradient_to_device).to_cols_array());
|
||||
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), path);
|
||||
}
|
||||
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
|
||||
@@ -1479,7 +1454,7 @@ impl Render for List<Vector> {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path);
|
||||
}
|
||||
Graphic::Gradient(list) => {
|
||||
let Some(brush) = create_peniko_gradient_brush(list, element, &parent_transform, &multiplied_transform) else {
|
||||
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(list, &parent_transform, &multiplied_transform) else {
|
||||
continue;
|
||||
};
|
||||
let inverse_element_transform = if transform_is_invertible(element_transform) {
|
||||
@@ -1487,7 +1462,7 @@ impl Render for List<Vector> {
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * gradient_to_device).to_cols_array());
|
||||
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), &path);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use core_types::Color;
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
||||
@@ -484,6 +484,18 @@ pub struct Gradient {
|
||||
pub end: DVec2,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub spread_method: GradientSpreadMethod,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Whether `start`/`end` are absolute (layer-space) rather than in the legacy [0,1] bounding-box space.
|
||||
/// Documents predating the gradient migration deserialize this as `false`; the deferred migration converts
|
||||
/// them and sets it `true`. Once all documents are migrated, the legacy rendering path can be removed.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub absolute: bool,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// An extra frame adjustment composed onto the `start`/`end` frame (`transform * to_transform()`), letting the gradient
|
||||
/// describe shapes the endpoint pair cannot, such as an elliptical radial. It defaults to identity, so existing documents
|
||||
/// (which only have `start`/`end`) are unaffected; the migration stores a non-identity value only where it's needed.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Default for Gradient {
|
||||
@@ -494,6 +506,9 @@ impl Default for Gradient {
|
||||
start: DVec2::new(0., 0.5),
|
||||
end: DVec2::new(1., 0.5),
|
||||
spread_method: GradientSpreadMethod::Pad,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,6 +527,41 @@ impl std::fmt::Display for Gradient {
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
|
||||
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
|
||||
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
|
||||
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> Gradient {
|
||||
let start = bounding_box.transform_point2(self.start);
|
||||
let end = bounding_box.transform_point2(self.end);
|
||||
let direction = end - start;
|
||||
|
||||
// The legacy radial drew as a circle in the layer's own space; bake the adjustment that, composed with the
|
||||
// endpoint frame, makes the new pipeline reproduce that circle through the (possibly non-uniform) layer transform.
|
||||
let radial_invertible =
|
||||
self.gradient_type == GradientType::Radial && layer_transform.is_finite() && layer_transform.matrix2.determinant().recip().is_finite() && direction.length_squared() > 1e-20;
|
||||
let transform = if radial_invertible {
|
||||
let radius = (layer_transform.matrix2 * direction).length();
|
||||
let circle = DAffine2 {
|
||||
matrix2: DMat2::from_diagonal(DVec2::splat(radius)),
|
||||
translation: layer_transform.transform_point2(start),
|
||||
};
|
||||
let base = DAffine2::from_cols(direction, direction.perp(), start);
|
||||
(layer_transform.inverse() * circle) * base.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY
|
||||
};
|
||||
|
||||
Gradient {
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self {
|
||||
let stops = GradientStops::new([
|
||||
@@ -533,6 +583,9 @@ impl Gradient {
|
||||
stops,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,6 +607,9 @@ impl Gradient {
|
||||
stops,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: self.absolute,
|
||||
transform: if time < 0.5 { self.transform } else { other.transform },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,9 @@ impl From<List<GradientStops>> for Fill {
|
||||
spread_method,
|
||||
start: transform.transform_point2(DVec2::ZERO),
|
||||
end: transform.transform_point2(DVec2::X),
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -668,6 +671,10 @@ impl PathStyle {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
pub fn fill_mut(&mut self) -> &mut Fill {
|
||||
&mut self.fill
|
||||
}
|
||||
|
||||
/// Get the current path's [Stroke].
|
||||
///
|
||||
/// # Example
|
||||
|
||||
Reference in New Issue
Block a user