Correctly apply transforms to vector data and strokes (#1977)

* Fix adding a layer to a transformed group

* Fix assorted transform issues

* Default stroke transform

* Fix bench

* Transform gradient

* Gradient fix

* Add gradient reversal buttons to Fill node in the Properties panel

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
James Lindsay 2024-09-15 23:26:11 +01:00 committed by GitHub
parent 2fa8773092
commit dd4a97b09f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 152 additions and 59 deletions

View file

@ -244,7 +244,15 @@ async fn construct_layer<Data: Into<GraphicElement> + Send>(
) -> GraphicGroup {
let graphic_element = self.graphic_element.eval(footprint).await;
let mut stack = self.stack.eval(footprint).await;
stack.push(graphic_element.into());
let mut element: GraphicElement = graphic_element.into();
if stack.transform.matrix2.determinant() != 0. {
*element.transform_mut() = stack.transform.inverse() * element.transform();
} else {
stack.clear();
stack.transform = DAffine2::IDENTITY;
}
stack.push(element);
stack
}

View file

@ -360,20 +360,27 @@ impl GraphicElementRendered for GraphicGroup {
impl GraphicElementRendered for VectorData {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let multiplied_transform = render.transform * self.transform;
let set_stroke_transform = self.style.stroke().map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
let applied_stroke_transform = set_stroke_transform.unwrap_or(self.transform);
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
let layer_bounds = self.bounding_box().unwrap_or_default();
let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default();
let transformed_bounds = self.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
let mut path = String::new();
for subpath in self.stroke_bezier_paths() {
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
let _ = subpath.subpath_to_svg(&mut path, applied_stroke_transform);
}
render.leaf_tag("path", |attributes| {
attributes.push("d", path);
let matrix = format_transform_matrix(element_transform);
attributes.push("transform", matrix);
let defs = &mut attributes.0.svg_defs;
let fill_and_stroke = self
.style
.render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
.render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds);
attributes.push_val(fill_and_stroke);
if self.alpha_blending.opacity < 1. {
@ -411,31 +418,31 @@ impl GraphicElementRendered for VectorData {
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) {
use crate::vector::style::GradientType;
use vello::peniko;
let transformed_bounds = GraphicElementRendered::bounding_box(self, transform).unwrap_or_default();
let mut layer = false;
let stroke_transform = self.style.stroke().map_or(DAffine2::IDENTITY, |stroke| stroke.transform);
let path_transform = (transform * self.transform) * stroke_transform.inverse();
let transformed_bounds = GraphicElementRendered::bounding_box(self, path_transform).unwrap_or_default();
if self.alpha_blending.opacity < 1. || self.alpha_blending.blend_mode != BlendMode::default() {
layer = true;
scene.push_layer(
peniko::BlendMode::new(self.alpha_blending.blend_mode.into(), peniko::Compose::SrcOver),
self.alpha_blending.opacity,
kurbo::Affine::IDENTITY,
kurbo::Affine::new((path_transform).to_cols_array()),
&kurbo::Rect::new(transformed_bounds[0].x, transformed_bounds[0].y, transformed_bounds[1].x, transformed_bounds[1].y),
);
}
let kurbo_transform = kurbo::Affine::new(transform.to_cols_array());
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
let mut path = kurbo::BezPath::new();
for subpath in self.stroke_bezier_paths() {
subpath.to_vello_path(self.transform, &mut path);
subpath.to_vello_path(stroke_transform, &mut path);
}
match self.style.fill() {
Fill::Solid(color) => {
let fill = peniko::Brush::Solid(peniko::Color::rgba(color.r() as f64, color.g() as f64, color.b() as f64, color.a() as f64));
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(path_transform.to_cols_array()), &fill, None, &path);
}
Fill::Gradient(gradient) => {
let mut stops = peniko::ColorStops::new();
@ -472,7 +479,7 @@ impl GraphicElementRendered for VectorData {
stops,
..Default::default()
});
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(path_transform.to_cols_array()), &fill, None, &path);
}
Fill::None => (),
};
@ -504,7 +511,7 @@ impl GraphicElementRendered for VectorData {
dash_offset: stroke.dash_offset,
};
if stroke.width > 0. {
scene.stroke(&stroke, kurbo_transform, color, None, &path);
scene.stroke(&stroke, kurbo::Affine::new(path_transform.to_cols_array()), color, None, &path);
}
}
if layer {
@ -596,7 +603,8 @@ impl GraphicElementRendered for Artboard {
// Render background
let color = peniko::Color::rgba(self.background.r() as f64, self.background.g() as f64, self.background.b() as f64, self.background.a() as f64);
let rect = kurbo::Rect::new(self.location.x as f64, self.location.y as f64, self.dimensions.x as f64, self.dimensions.y as f64);
let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()];
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
@ -606,7 +614,8 @@ impl GraphicElementRendered for Artboard {
if self.clip {
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
}
self.graphic_group.render_to_vello(scene, transform, context);
let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform;
self.graphic_group.render_to_vello(scene, child_transform, context);
if self.clip {
scene.pop_layer();
}
@ -614,8 +623,10 @@ impl GraphicElementRendered for Artboard {
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
subpath.apply_transform(self.graphic_group.transform.inverse());
click_targets.push(ClickTarget::new(subpath, 0.));
if self.graphic_group.transform.matrix2.determinant() != 0. {
subpath.apply_transform(self.graphic_group.transform.inverse());
click_targets.push(ClickTarget::new(subpath, 0.));
}
}
fn contains_artboard(&self) -> bool {

View file

@ -145,10 +145,9 @@ impl Gradient {
}
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
let updated_transform = multiplied_transform * bound_transform;
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
let mut stop = String::new();
for (position, color) in self.stops.0.iter() {
@ -168,7 +167,7 @@ impl Gradient {
} else {
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
};
let mod_points = updated_transform;
let mod_points = element_transform * stroke_transform * bound_transform;
let start = mod_points.transform_point2(self.start);
let end = mod_points.transform_point2(self.end);
@ -301,7 +300,7 @@ impl Fill {
}
/// Renders the fill, adding necessary defs through mutating the first argument.
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
match self {
Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => {
@ -312,7 +311,7 @@ impl Fill {
result
}
Self::Gradient(gradient) => {
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{gradient_id}')""##)
}
}
@ -450,6 +449,10 @@ impl Display for LineJoin {
}
}
fn daffine2_identity() -> DAffine2 {
DAffine2::IDENTITY
}
#[repr(C)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Stroke {
@ -462,6 +465,8 @@ pub struct Stroke {
pub line_cap: LineCap,
pub line_join: LineJoin,
pub line_join_miter_limit: f64,
#[serde(default = "daffine2_identity")]
pub transform: DAffine2,
}
impl core::hash::Hash for Stroke {
@ -487,6 +492,7 @@ impl Stroke {
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
line_join_miter_limit: 4.,
transform: DAffine2::IDENTITY,
}
}
@ -499,6 +505,10 @@ impl Stroke {
line_cap: if time < 0.5 { self.line_cap } else { other.line_cap },
line_join: if time < 0.5 { self.line_join } else { other.line_join },
line_join_miter_limit: self.line_join_miter_limit + (other.line_join_miter_limit - self.line_join_miter_limit) * time,
transform: DAffine2::from_mat2_translation(
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
self.transform.translation * time + other.transform.translation * (1. - time),
),
}
}
@ -635,6 +645,7 @@ impl Default for Stroke {
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
line_join_miter_limit: 4.,
transform: DAffine2::IDENTITY,
}
}
}
@ -787,15 +798,15 @@ impl PathStyle {
}
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
match view_mode {
ViewMode::Outline => {
let fill_attribute = Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
let stroke_attribute = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render();
format!("{fill_attribute}{stroke_attribute}")
}
_ => {
let fill_attribute = self.fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
format!("{fill_attribute}{stroke_attribute}")
}

View file

@ -156,6 +156,7 @@ fn set_vector_data_stroke(
line_cap,
line_join,
line_join_miter_limit: miter_limit,
transform: vector_data.transform,
});
vector_data
}