mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 01:14:29 -06: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:
@@ -32,6 +32,19 @@ pub(crate) fn effected_path(n: &Node, bp: &BezPath) -> BezPath {
|
||||
apply(&n.appearance.effects, bp, &GeomContext::of(n))
|
||||
}
|
||||
|
||||
/// [`apply`], and how gradients follow the effects ([`effects::Carry`]).
|
||||
pub(super) fn apply_carrying(effects: &[Effect], bp: &BezPath, ctx: &GeomContext) -> (BezPath, effects::Carry) {
|
||||
if !effects::has_geometry(effects) || bp.elements().is_empty() {
|
||||
return (bp.clone(), effects::Carry::default());
|
||||
}
|
||||
effects::apply_geometry_bez_carrying(effects, bp, bp.bounding_box(), ctx)
|
||||
}
|
||||
|
||||
/// [`effected_path`], and how `n`'s gradients follow its effects.
|
||||
pub(crate) fn effected_path_carrying(n: &Node, bp: &BezPath) -> (BezPath, effects::Carry) {
|
||||
apply_carrying(&n.appearance.effects, bp, &GeomContext::of(n))
|
||||
}
|
||||
|
||||
pub(super) fn item_effects(item: &AppearanceItem) -> &[Effect] {
|
||||
match item {
|
||||
AppearanceItem::Fill(f) => &f.effects,
|
||||
|
||||
@@ -360,6 +360,11 @@ pub(super) fn raster_bytes(pm: &vello_cpu::Pixmap) -> usize {
|
||||
pub(crate) struct FxGeom {
|
||||
pub(crate) node: Arc<Node>,
|
||||
pub(super) g: Arc<BezPath>,
|
||||
pub(super) items: Vec<Option<Arc<BezPath>>>,
|
||||
/// The node with its gradients moved along with its effects, when they move.
|
||||
pub(super) carried: Option<Arc<Node>>,
|
||||
pub(super) items: Vec<Option<ItemGeom>>,
|
||||
pub(crate) stamp: u64,
|
||||
}
|
||||
|
||||
/// One appearance item's own effected geometry, and how its gradient follows those effects.
|
||||
pub(super) type ItemGeom = (Arc<BezPath>, Arc<effects::Carry>);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::bounds::{apply, item_effects};
|
||||
use super::entries::{Content, DocInputs, Drawn, FX_RASTER_BUDGET, Inputs, KeptImage, Made, Reads, feather_outline};
|
||||
use super::bounds::{apply_carrying, item_effects};
|
||||
use super::entries::{Content, DocInputs, Drawn, FX_RASTER_BUDGET, Inputs, ItemGeom, KeptImage, Made, Reads, feather_outline};
|
||||
#[cfg(test)]
|
||||
use super::raster::RASTERIZED;
|
||||
use super::raster::{
|
||||
@@ -624,7 +624,7 @@ impl Renderer {
|
||||
g: &BezPath,
|
||||
rule: FillRule,
|
||||
gctx: &GeomContext,
|
||||
mut cache: Option<&mut Vec<Option<Arc<BezPath>>>>,
|
||||
mut cache: Option<&mut Vec<Option<ItemGeom>>>,
|
||||
) {
|
||||
let bounds = g.bounding_box();
|
||||
for (j, item) in n.appearance.items.iter().enumerate() {
|
||||
@@ -633,12 +633,23 @@ impl Renderer {
|
||||
}
|
||||
// The item's own geometry effects on `g`, from the cache when there is one.
|
||||
let slot = cache.as_deref_mut().and_then(|c| c.get_mut(j));
|
||||
let ig: Arc<BezPath> = match slot {
|
||||
Some(Some(ig)) => ig.clone(),
|
||||
Some(s) => s.insert(Arc::new(apply(item_effects(item), g, &gctx.item(item)))).clone(),
|
||||
None => Arc::new(apply(item_effects(item), g, &gctx.item(item))),
|
||||
let made = || {
|
||||
let (ig, carry) = apply_carrying(item_effects(item), g, &gctx.item(item));
|
||||
(Arc::new(ig), Arc::new(carry))
|
||||
};
|
||||
let (ig, carry): ItemGeom = match slot {
|
||||
Some(Some(e)) => e.clone(),
|
||||
Some(s) => s.insert(made()).clone(),
|
||||
None => made(),
|
||||
};
|
||||
let ig = &*ig;
|
||||
// The item with its gradient moved along with its own effects.
|
||||
let carried = (!carry.is_empty() && matches!(item.paint(), vectorcraft_color::Paint::Gradient(_))).then(|| {
|
||||
let mut ap = vectorcraft_doc::Appearance { items: vec![item.clone()], ..Default::default() };
|
||||
carry.apply(&mut ap);
|
||||
ap.items.pop()
|
||||
});
|
||||
let item = carried.as_ref().and_then(Option::as_ref).unwrap_or(item);
|
||||
let ib = if effects::has_geometry(item_effects(item)) && !ig.elements().is_empty() { ig.bounding_box() } else { bounds };
|
||||
let rfx = effects::raster_effects(item_effects(item));
|
||||
if rfx.is_empty() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::bounds::{cull_bounds, effected_path};
|
||||
use super::entries::{Content, Drawn, FX_RASTER_BUDGET, FxGeom, Inputs, Kept, Made, Reads, Shadow, ShadowEntry};
|
||||
use super::bounds::{cull_bounds, effected_path_carrying};
|
||||
use super::entries::{Content, Drawn, FX_RASTER_BUDGET, FxGeom, Inputs, ItemGeom, Kept, Made, Reads, Shadow, ShadowEntry};
|
||||
use super::predicates::{evaluates_live, is_container, rasterize_bounds, reads_outside, shifted};
|
||||
use super::raster::{
|
||||
DRAG_PIXEL_BUDGET, MAX_RASTERIZE, PIXEL_BUDGET, ViewCrop, content_changing, doc_raster_ppi, image_size, linear_key, pan_margin, pcolor,
|
||||
@@ -314,29 +314,39 @@ impl Renderer {
|
||||
let stamp = self.stamp;
|
||||
let e = match self.fx_geom.remove(&key) {
|
||||
Some(e) if Arc::ptr_eq(&e.node, &o) => e,
|
||||
_ => FxGeom { g: Arc::new(effected_path(n, bp)), items: vec![None; n.appearance.items.len()], node: o, stamp },
|
||||
_ => {
|
||||
let (g, carry) = effected_path_carrying(n, bp);
|
||||
FxGeom { g: Arc::new(g), carried: carried(n, &carry), items: vec![None; n.appearance.items.len()], node: o, stamp }
|
||||
}
|
||||
};
|
||||
(key, FxGeom { stamp, ..e })
|
||||
});
|
||||
let g = match &cached {
|
||||
Some((_, e)) => e.g.clone(),
|
||||
None => Arc::new(effected_path(n, bp)),
|
||||
let (g, carried) = match &cached {
|
||||
Some((_, e)) => (e.g.clone(), e.carried.clone()),
|
||||
None => {
|
||||
let (g, carry) = effected_path_carrying(n, bp);
|
||||
(Arc::new(g), carried(n, &carry))
|
||||
}
|
||||
};
|
||||
self.draw_shape_fx_geom(ctx, f, n, &g, rule, cached.as_mut().map(|(_, e)| &mut e.items));
|
||||
let paints = carried.as_deref().unwrap_or(n);
|
||||
self.draw_shape_fx_geom(ctx, f, n, paints, &g, rule, cached.as_mut().map(|(_, e)| &mut e.items));
|
||||
if let Some((key, e)) = cached {
|
||||
self.fx_geom.insert(key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::draw_shape_fx`] on the effected geometry `g`, with the items' cached geometry.
|
||||
/// [`Self::draw_shape_fx`] on the effected geometry `g`, with the items' cached geometry:
|
||||
/// `paints` is `n` with the fills and strokes it paints.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_shape_fx_geom(
|
||||
&mut self,
|
||||
ctx: &mut RenderContext,
|
||||
f: &Frame,
|
||||
n: &Node,
|
||||
paints: &Node,
|
||||
g: &BezPath,
|
||||
rule: FillRule,
|
||||
items: Option<&mut Vec<Option<Arc<BezPath>>>>,
|
||||
items: Option<&mut Vec<Option<ItemGeom>>>,
|
||||
) {
|
||||
if g.elements().is_empty() {
|
||||
return;
|
||||
@@ -345,7 +355,7 @@ impl Renderer {
|
||||
self.hairline(ctx, f, g, [0, 0, 0, 255]);
|
||||
return;
|
||||
}
|
||||
let gctx = GeomContext::of(n);
|
||||
let gctx = GeomContext::of(paints);
|
||||
let rfx = effects::raster_effects(&n.appearance.effects);
|
||||
// Document-space reach of the painted geometry (strokes, arrowheads…).
|
||||
let outset = n.appearance.outset();
|
||||
@@ -355,7 +365,7 @@ impl Renderer {
|
||||
let reach = bounds.map_or(reach, |b| reach.union(b));
|
||||
let content = Content { node: n, owner: None, cache: Some(0), reach, outline: Some((g, rule)), bounds, kept: None };
|
||||
let mut items = items;
|
||||
self.raster_fx(ctx, f, &content, &rfx, &mut |r, c, fr| r.paint_items(c, fr, n, g, rule, &gctx, items.as_deref_mut()));
|
||||
self.raster_fx(ctx, f, &content, &rfx, &mut |r, c, fr| r.paint_items(c, fr, paints, g, rule, &gctx, items.as_deref_mut()));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
|
||||
@@ -772,6 +782,16 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// `n` with the gradients of its fills and strokes moved along with its effects by `carry`, or
|
||||
/// `None` when they stay where they are.
|
||||
fn carried(n: &Node, carry: &effects::Carry) -> Option<Arc<Node>> {
|
||||
carry.moves(&n.appearance).then(|| {
|
||||
let mut m = n.clone();
|
||||
carry.apply(&mut m.appearance);
|
||||
Arc::new(m)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -811,6 +811,52 @@ fn a_path_with_its_own_transform_paints_each_copy_with_its_own_gradient() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paths_gradient_moves_with_its_transform_and_free_distort() {
|
||||
use vectorcraft_color::GradientGeom;
|
||||
let stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
|
||||
let ramp = GradientPaint::new(Gradient::new(GradientKind::Linear, stops));
|
||||
// Placed across the square from its left edge (red) to its right edge (blue).
|
||||
let geom = GradientGeom { start: Point::new(10.0, 50.0), end: Point::new(40.0, 50.0), aspect: 1.0, focal: None };
|
||||
let draw = |paint: &GradientPaint, e: Option<Effect>| {
|
||||
let square = shapes::rectangle(vectorcraft_geom::Rect::new(10.0, 35.0, 40.0, 65.0));
|
||||
let mut n = Node::path(NodeId(0), square, Appearance::basic(Paint::Gradient(Box::new(paint.clone())), Paint::None, 0.0));
|
||||
n.appearance.effects = e.into_iter().collect();
|
||||
render(&doc_with(Document::new(100.0, 100.0), n))
|
||||
};
|
||||
let placed = GradientPaint { geom: Some(geom), ..ramp.clone() };
|
||||
let rb = |img: &Rendered, x: u32, y: u32| {
|
||||
let p = img.pixel(x, y);
|
||||
(p[0], p[2])
|
||||
};
|
||||
let red = |(r, b): (u8, u8)| r > 200 && b < 60;
|
||||
let blue = |(r, b): (u8, u8)| b > 200 && r < 60;
|
||||
// Moved 40 pt right by Transform, and 30 pt (one width) right by a Free Distort that moves
|
||||
// every corner.
|
||||
let corners = json!({"corners": [[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]]});
|
||||
for e in [fx("distort.transform", json!({"moveH": 40})), fx("distort.freeDistort", corners)] {
|
||||
let img = draw(&placed, Some(e.clone()));
|
||||
let moved = if e.id == "distort.transform" { 40 } else { 30 };
|
||||
let (left, right) = (11 + moved, 38 + moved);
|
||||
assert!(red(rb(&img, left, 50)) && blue(rb(&img, right, 50)), "{}: {:?} {:?}", e.id, rb(&img, left, 50), rb(&img, right, 50));
|
||||
}
|
||||
// Turned a quarter: the gradient runs from top to bottom (or bottom to top).
|
||||
let turn = fx("distort.transform", json!({"rotate": 90}));
|
||||
let img = draw(&placed, Some(turn));
|
||||
let (top, bottom) = (rb(&img, 25, 36), rb(&img, 25, 63));
|
||||
assert!((red(top) && blue(bottom)) || (blue(top) && red(bottom)), "{top:?} {bottom:?}");
|
||||
assert_eq!(rb(&img, 12, 50), rb(&img, 38, 50), "the same color across");
|
||||
// Scaled to half its width about its center: the gradient spans the narrower square.
|
||||
let img = draw(&placed, Some(fx("distort.transform", json!({"scaleH": 50}))));
|
||||
assert!(red(rb(&img, 18, 50)) && blue(rb(&img, 31, 50)), "{:?} {:?}", rb(&img, 18, 50), rb(&img, 31, 50));
|
||||
// The fill's own Transform moves its gradient too.
|
||||
let square = shapes::rectangle(vectorcraft_geom::Rect::new(10.0, 35.0, 40.0, 65.0));
|
||||
let mut n = Node::path(NodeId(0), square, Appearance::basic(Paint::Gradient(Box::new(placed.clone())), Paint::None, 0.0));
|
||||
n.appearance.items[0].effects_mut().push(fx("distort.transform", json!({"moveH": 40})));
|
||||
let img = render(&doc_with(Document::new(100.0, 100.0), n));
|
||||
assert!(red(rb(&img, 51, 50)) && blue(rb(&img, 78, 50)), "{:?} {:?}", rb(&img, 51, 50), rb(&img, 78, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn darkness_subtracts_from_rgb_before_shadow_opacity() {
|
||||
for opacity in [50.0, 100.0] {
|
||||
|
||||
Reference in New Issue
Block a user