mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 13:33:19 +00:00
Effects: placed gradients move with Transform and Free Distort effects
A gradient placed with the Gradient tool stayed where it was when the object's Transform or Free Distort effect moved, turned or scaled the outline, so the shape slid across its own gradient. The Transform effect now maps placed gradients by its own transform, and each of its copies takes them as many times as it takes the transform. Under Free Distort, each path's placed gradients move and scale from the path's bounds before the effect to its bounds after it. This change does not alter gradients fitted to the object. The canvas, Expand Appearance and export, Pathfinder and compound shapes, groups, a fill's or stroke's own effects, and Object › Expand of a gradient fill apply the same rule. In a group, the own fills and strokes of the group, of the groups inside it and of a symbol's art follow it too, and images and symbol instances that a group's Transform effect moves take the placed gradients of their own fills and strokes along. A group whose path keeps its shape because the effect would pass the geometry budget keeps its own gradients in place. This changes how existing documents draw: a gradient placed on a path, or on an image's or symbol instance's own fill, inside a group with a Transform effect now moves with it. Before, only the gradients of type moved (#2214). How gradients follow each effect is VectorCraft's own rule. The Distort & Transform help page describes what the effects do to the object: https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html
This commit is contained in:
@@ -407,8 +407,9 @@ fn expand_fills(d: &mut Document, l: &Node, to: GradientTo) -> Option<Node> {
|
||||
let mut expanded = false;
|
||||
for item in &l.appearance.items {
|
||||
// Items that paint nothing go; a gradient that can't be expanded stays.
|
||||
let art = gradient_fill(item).and_then(|(f, g)| {
|
||||
let (fl, path, bounds) = fill_shape(l, item, &path, bounds)?;
|
||||
let art = gradient_fill(item).and_then(|_| {
|
||||
let (fl, path, bounds, moved) = fill_shape(l, item, &path, bounds)?;
|
||||
let (f, g) = gradient_fill(&moved)?;
|
||||
gradient_art(d, &fl, &path, bounds, f, g, to)
|
||||
});
|
||||
let Some(art) = art else {
|
||||
@@ -470,29 +471,39 @@ fn gradient_art(d: &mut Document, l: &Node, path: &PathData, bounds: Rect, f: &F
|
||||
/// path (a plain path then), and every geometry effect of its own removed. `None` when it has no
|
||||
/// visible geometry effect.
|
||||
fn reshaped(l: &Node) -> Option<Node> {
|
||||
use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry, is_geometry};
|
||||
use vectorcraft_render::effects::{GeomContext, apply_geometry_carrying, has_geometry, is_geometry};
|
||||
if !has_geometry(&l.appearance.effects) {
|
||||
return None;
|
||||
}
|
||||
let (path, _) = node_path(l)?;
|
||||
let g = apply_geometry_with(&l.appearance.effects, &path, path.bounds()?, &GeomContext::of(l));
|
||||
let (g, carry) = apply_geometry_carrying(&l.appearance.effects, &path, path.bounds()?, &GeomContext::of(l));
|
||||
let mut m = with_path(l, g);
|
||||
// Its gradients move with the outline, as the canvas paints them.
|
||||
if carry.moves(&m.appearance) {
|
||||
carry.apply(&mut m.appearance);
|
||||
}
|
||||
m.appearance.effects.retain(|e| !is_geometry(&e.id));
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Path `l` (outline `path`, box `bounds`) as its fill `item` paints it on the canvas, with its
|
||||
/// outline and box: when the fill has visible geometry effects, `l` with the outline they make of
|
||||
/// `path` as a plain path, and that outline's box.
|
||||
fn fill_shape(l: &Node, item: &AppearanceItem, path: &PathData, bounds: Rect) -> Option<(Node, PathData, Rect)> {
|
||||
use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry};
|
||||
/// outline, box and fill: when the fill has visible geometry effects, `l` with the outline they
|
||||
/// make of `path` as a plain path, that outline's box, and the fill with its gradient moved along
|
||||
/// with the outline.
|
||||
fn fill_shape(l: &Node, item: &AppearanceItem, path: &PathData, bounds: Rect) -> Option<(Node, PathData, Rect, AppearanceItem)> {
|
||||
use vectorcraft_render::effects::{GeomContext, apply_geometry_carrying, has_geometry};
|
||||
if !has_geometry(item.effects()) {
|
||||
return Some((l.clone(), path.clone(), bounds));
|
||||
return Some((l.clone(), path.clone(), bounds, item.clone()));
|
||||
}
|
||||
// In the fill's context, as drawing runs them: a plug-in effect is told which fill it is on.
|
||||
let outline = apply_geometry_with(item.effects(), path, bounds, &GeomContext::of(l).item(item));
|
||||
let (outline, carry) = apply_geometry_carrying(item.effects(), path, bounds, &GeomContext::of(l).item(item));
|
||||
let b = outline.bounds()?;
|
||||
Some((with_path(l, outline.clone()), outline, b))
|
||||
let mut one = Appearance { items: vec![item.clone()], ..Default::default() };
|
||||
if carry.moves(&one) {
|
||||
carry.apply(&mut one);
|
||||
}
|
||||
let moved = one.items.pop().unwrap_or_else(|| item.clone());
|
||||
Some((with_path(l, outline.clone()), outline, b, moved))
|
||||
}
|
||||
|
||||
/// Path or compound path `l` with `path` as its path, a plain path.
|
||||
@@ -809,7 +820,7 @@ fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, &
|
||||
later -= usize::from(fill.is_some());
|
||||
let room = if later > 0 { left.saturating_sub(l.count() + 1) } else { left };
|
||||
let tiles = fill.filter(|_| !full).and_then(|(f, def, xf)| {
|
||||
let (fl, _, bounds) = fill_shape(l, item, &path, bounds)?;
|
||||
let (fl, _, bounds, _) = fill_shape(l, item, &path, bounds)?;
|
||||
let Some(t) = pattern_tiles(d, &def, xf, bounds, room) else {
|
||||
full = true;
|
||||
return None;
|
||||
|
||||
@@ -133,6 +133,37 @@ fn white_black() -> Value {
|
||||
json!({"kind": "linear", "stops": [{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placed_gradient_moved_by_a_transform_or_free_distort_expands_as_the_canvas_draws_it() {
|
||||
// The object's own effect, and the fill's (the first appearance item).
|
||||
let slant = json!({"corners": [[0.25, 0.0], [1.25, 0.0], [1.0, 1.0], [0.0, 1.0]]});
|
||||
for (effect, params, item) in [
|
||||
("distort.transform", json!({"rotate": 90}), json!(null)),
|
||||
("distort.transform", json!({"rotate": 30, "scaleH": 60}), json!(null)),
|
||||
("distort.transform", json!({"rotate": 90}), json!(0)),
|
||||
("distort.freeDistort", slant.clone(), json!(null)),
|
||||
("distort.freeDistort", slant, json!(0)),
|
||||
] {
|
||||
let mut s = session();
|
||||
let id = gradient_rect(&mut s, Rect::new(40.0, 70.0, 160.0, 130.0), white_black(), false);
|
||||
// Placed from a quarter of the way across to the right edge.
|
||||
run(&mut s, "paint.setGradientGeom", json!({"start": [70, 100], "end": [160, 100], "stroke": false}));
|
||||
let placed = s.doc().unwrap().doc.node(id).and_then(|n| match n.appearance.fill_paint() {
|
||||
vectorcraft_color::Paint::Gradient(g) => g.geom,
|
||||
_ => None,
|
||||
});
|
||||
assert!(placed.is_some(), "the fill's gradient is placed");
|
||||
run(&mut s, "effect.apply", json!({"effect": effect, "params": params, "ids": [id.0], "item": item}));
|
||||
let before = render(&s);
|
||||
// Fill alone: the gradient becomes strips inside the outline the effect makes.
|
||||
run(&mut s, "object.expand", json!({"object": false, "stroke": false, "steps": 64}));
|
||||
let after = render(&s);
|
||||
// The gradient moves with the rectangle on the canvas, and the strips follow it.
|
||||
let diff = mean_diff(&before, &after);
|
||||
assert!(diff < 1.0, "{effect} {params} on item {item}: mean difference {diff}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_strips_match_the_gradient_samples() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user