mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:42:42 +00:00
Effects: each copy of a Transform effect draws the raster effects listed before it
A path's Drop Shadow, glow, blur, Feather or pixel filter listed before a Transform effect that makes copies was drawn once under all the copies, at the object's own distances. Each copy now carries those raster effects, scaled by the copy's mean scale, with a Drop Shadow's offset turned, reflected and scaled with the copy. Raster effects listed after the Transform effect still paint once around all the copies. Copies that stop at the effect budget keep their effects, and a Transform effect without copies draws as before. The canvas, the object's bounds, export, baking and Expand Appearance use the same art. Type, images, symbol instances, groups, a fill's or stroke's own copying Transform effect, and paths with Rasterize or an SVG filter before the copying effect keep one set of raster effects.
This commit is contained in:
@@ -81,8 +81,8 @@ pub use pixel::{PIXEL_EFFECTS, PixelFx, PixelSpace};
|
||||
pub use raster::{RASTERIZE, RasterFx, Rasterize, color_param, follow_items, item_effects_in, outset, raster_effects, without_geometry};
|
||||
pub use reshape::{
|
||||
SYMBOL_ART_MAX_NODES, expand_outlined, follows_image_pixels, gives_image_shape, has_gradient_copies, has_instance_row_raster, has_item_geometry,
|
||||
has_painted_effects, has_transform_art, has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text,
|
||||
reshape,
|
||||
has_painted_effects, has_transform_art, has_transform_copies, has_transform_options, image_paint, needs_outline, outer_effects, outline_art,
|
||||
outline_text, reshape,
|
||||
};
|
||||
pub use revolve::{
|
||||
CLASSIC_REVOLVE, REVOLVE, has_revolve, is_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in,
|
||||
|
||||
@@ -107,11 +107,44 @@ fn reaches_appearance(e: &Effect) -> bool {
|
||||
}
|
||||
|
||||
/// Is `n` a path or compound path that its Transform effects make into art of separate objects
|
||||
/// ([`has_gradient_copies`], [`has_layered_copies`], [`has_transform_options`]), or whose
|
||||
/// live-effect plug-ins paint what they return ([`has_painted_effects`])? It is drawn, baked and
|
||||
/// expanded through that art ([`reshape`]).
|
||||
/// ([`has_gradient_copies`], [`has_layered_copies`], [`has_transform_options`], [`copied_raster_at`]),
|
||||
/// or whose live-effect plug-ins paint what they return ([`has_painted_effects`])? It is drawn,
|
||||
/// baked and expanded through that art ([`reshape`]).
|
||||
pub fn has_transform_art(n: &Node) -> bool {
|
||||
has_gradient_copies(n) || has_layered_copies(n) || has_transform_options(n) || has_painted_effects(n)
|
||||
has_gradient_copies(n) || has_layered_copies(n) || has_transform_options(n) || has_painted_effects(n) || copied_raster_at(n).is_some()
|
||||
}
|
||||
|
||||
/// Where the own effects of `n`, a path or compound path, split so that each copy of a Transform
|
||||
/// effect carries the raster effects listed before that effect ([`copied_raster_art`]): the index
|
||||
/// of the last visible Transform effect that makes copies, when a visible raster effect that
|
||||
/// follows a Transform effect ([`crate::raster::follows`]) is listed before it and every other
|
||||
/// visible effect before it is a geometry effect or such a raster effect. `None` otherwise, and for
|
||||
/// a path with a linked opacity mask, a 3D effect, Crop Marks, Vector Halftone, a live-effect
|
||||
/// plug-in that paints what it returns, or a fill or stroke that paints with geometry effects of
|
||||
/// its own.
|
||||
pub(crate) fn copied_raster_at(n: &Node) -> Option<usize> {
|
||||
if !matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. }) {
|
||||
return None;
|
||||
}
|
||||
let effects = &n.appearance.effects;
|
||||
// Checked first: this runs for every object drawn.
|
||||
if !effects.iter().any(|e| e.visible && crate::raster::follows(&e.id)) || !effects.iter().any(|e| e.visible && e.id == "distort.transform") {
|
||||
return None;
|
||||
}
|
||||
let t = effects.iter().rposition(|e| {
|
||||
e.visible && e.id == "distort.transform" && crate::distort::transform_map(Rect::ZERO, &merged_params(&e.id, &e.params), 0).1 > 0
|
||||
})?;
|
||||
let before = effects.get(..t)?;
|
||||
let carried = before.iter().any(|e| e.visible && crate::raster::follows(&e.id))
|
||||
&& before.iter().all(|e| !e.visible || is_geometry(&e.id) || crate::raster::follows(&e.id));
|
||||
let excluded = n.extras().mask.as_ref().is_some_and(|m| m.linked)
|
||||
|| crate::has_solid(n)
|
||||
|| crate::has_revolve(n)
|
||||
|| crate::has_crop_marks(n)
|
||||
|| crate::addons::halftone::has_halftone(n)
|
||||
|| has_painted_effects(n)
|
||||
|| n.appearance.items.iter().any(|i| crate::paints(i) && crate::has_geometry(i.effects()));
|
||||
(carried && !excluded).then_some(t)
|
||||
}
|
||||
|
||||
/// Is `e` a visible live-effect plug-in whose manifest declares `paintOutput`: its id's plug-in id,
|
||||
@@ -787,6 +820,9 @@ fn as_art_opts(
|
||||
nodes: &SymbolNodes,
|
||||
apart: bool,
|
||||
) -> Option<Node> {
|
||||
if let Some(t) = copied_raster_at(n) {
|
||||
return Some(dressed_keeping_effects(copied_raster_art(paint_doc, n, t, apart), n));
|
||||
}
|
||||
let fx = geometry_effects(n);
|
||||
let mut art = match &n.kind {
|
||||
NodeKind::Image(im) => {
|
||||
@@ -872,6 +908,89 @@ fn dressed(mut art: Node, n: &Node) -> Node {
|
||||
art
|
||||
}
|
||||
|
||||
/// [`dressed`] that keeps the effects of `art`.
|
||||
fn dressed_keeping_effects(mut art: Node, n: &Node) -> Node {
|
||||
let effects = std::mem::take(&mut art.appearance.effects);
|
||||
let mut art = dressed(art, n);
|
||||
art.appearance.effects = effects;
|
||||
art
|
||||
}
|
||||
|
||||
/// The art of path or compound path `n` whose own effects split at `t` ([`copied_raster_at`]): the
|
||||
/// art the effects before the Transform effect at `t` make of `n`, with their raster effects, then
|
||||
/// copied by that Transform effect, each copy an object of its own carrying those raster effects
|
||||
/// moved, turned, reflected and scaled with it ([`follow_art`]), and then reshaped by the geometry
|
||||
/// effects after it. The art keeps `n`'s id and carries the raster effects from `t` on, without
|
||||
/// `n`'s name, visibility, transparency and opacity mask. The copies stop where [`copy_art`]'s do,
|
||||
/// each with its raster effects. With `apart`, the Transform effects before `t` make each copy an
|
||||
/// object of its own ([`copy_leaf`]).
|
||||
fn copied_raster_art(doc: Option<&Document>, n: &Node, t: usize, apart: bool) -> Node {
|
||||
let (before, rest) = n.appearance.effects.split_at(t);
|
||||
let mut source = n.clone();
|
||||
source.appearance.effects = before.to_vec();
|
||||
let piece = match copied_raster_at(&source) {
|
||||
Some(at) => copied_raster_art(doc, &source, at, apart),
|
||||
None => {
|
||||
let mut art = one_piece(&source);
|
||||
let carry = reshape_leaves_apart(&mut art, &geometry_effects(&source), &source.appearance.effects, Budget::EFFECTS, apart, doc);
|
||||
if apart {
|
||||
hoist_copies(&mut art);
|
||||
}
|
||||
transform_items(&mut art, doc);
|
||||
let mut ran = source.appearance.effects.clone();
|
||||
if carry.skipped() {
|
||||
carry.hide_skipped(&mut ran);
|
||||
}
|
||||
art.appearance.effects = crate::raster::without_geometry(&ran);
|
||||
art
|
||||
}
|
||||
};
|
||||
let mut art = Node::group(n.id, vec![Arc::new(piece)]);
|
||||
if let (Some(e), Some(bounds)) = (rest.first(), art.geometric_bounds()) {
|
||||
let params = merged_params(&e.id, &e.params);
|
||||
let (m, copies) = crate::distort::transform_map(bounds, ¶ms, n.id.0);
|
||||
let options = crate::distort::TransformOptions::of(¶ms);
|
||||
// The raster effects follow the copies below; Scale Strokes & Effects scales the others.
|
||||
let mut scaling = copy_scaling(options);
|
||||
scaling.effects = options.strokes.then_some(scale_unfollowed as fn(&mut Effect, f64));
|
||||
let max = Budget::EFFECTS.at_least(art_cost(&art));
|
||||
copy_art(&mut art, m, copies, scaling, max);
|
||||
if let Some(children) = art.children_mut() {
|
||||
// The original is last, and each copy above it is mapped once more by `m`.
|
||||
let step = m.with_translation(vectorcraft_geom::Vec2::ZERO);
|
||||
let mut a = Affine::IDENTITY;
|
||||
let none = HashSet::new();
|
||||
for (k, c) in children.iter_mut().rev().enumerate() {
|
||||
if k > 0 {
|
||||
a *= step;
|
||||
follow_art(Arc::make_mut(c), a, &none, Some(&none), None, 0);
|
||||
}
|
||||
// Each copy is the piece in a group of its own: the piece takes its place.
|
||||
if let Some([one]) = c.children().map(Vec::as_slice) {
|
||||
*c = one.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let later: Vec<&Effect> = rest.iter().skip(1).filter(|e| e.visible && is_geometry(&e.id)).collect();
|
||||
let carry = reshape_leaves_apart(&mut art, &later, &n.appearance.effects, Budget::EFFECTS, apart, doc);
|
||||
let mut ran = n.appearance.effects.clone();
|
||||
if carry.skipped() {
|
||||
carry.hide_skipped(&mut ran);
|
||||
}
|
||||
art.appearance.effects = crate::raster::without_geometry(ran.get(t..).unwrap_or_default());
|
||||
art
|
||||
}
|
||||
|
||||
/// The effects of `n` that paint around its whole art: all of its own, or from the Transform effect
|
||||
/// on for a path whose copies each carry the raster effects listed before it ([`copied_raster_at`]).
|
||||
pub fn outer_effects(n: &Node) -> &[Effect] {
|
||||
match copied_raster_at(n) {
|
||||
Some(t) => n.appearance.effects.get(t..).unwrap_or_default(),
|
||||
None => &n.appearance.effects,
|
||||
}
|
||||
}
|
||||
|
||||
/// A group with `n`'s id holding `n` without its transparency, opacity mask and effects: the one
|
||||
/// piece that `n`'s own geometry effects reshape.
|
||||
fn one_piece(n: &Node) -> Node {
|
||||
@@ -899,6 +1018,9 @@ pub(crate) fn own_effects_art(doc: Option<&Document>, n: &Node) -> Option<Node>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(t) = copied_raster_at(n) {
|
||||
return Some(dressed_keeping_effects(copied_raster_art(doc, n, t, false), n));
|
||||
}
|
||||
if n.appearance.items.iter().any(|i| crate::paints(i) && crate::has_geometry(i.effects())) {
|
||||
// One element: its pieces never knock each other out.
|
||||
return crate::bake_type_appearance(doc, n).map(|art| Node { knockout: vectorcraft_doc::Knockout::Off, ..art });
|
||||
|
||||
@@ -584,8 +584,9 @@ fn image(id: u64, effects: Vec<Effect>) -> Node {
|
||||
/// A group's Transform effect transforms, with Scale Strokes & Effects off or on, the raster
|
||||
/// effects that a member's own Transform effect with copies holds back: they keep their value
|
||||
/// through the member's effect and transform with the group's. That holds for a shadow listed
|
||||
/// before the member's copying effect, one listed after it, a fill's shadow under it, the shadow
|
||||
/// of a member of a nested group with such an effect, and an image's shadow.
|
||||
/// before the member's copying effect (which each copy carries), one listed after it, a fill's
|
||||
/// shadow under it, the shadow of a member of a nested group with such an effect, and an image's
|
||||
/// shadow.
|
||||
#[test]
|
||||
fn a_groups_transform_moves_the_shadows_its_members_copies_hold_back() {
|
||||
let copies = || transform(json!({"moveH": 60, "copies": 1}));
|
||||
@@ -605,7 +606,8 @@ fn a_groups_transform_moves_the_shadows_its_members_copies_hold_back() {
|
||||
let art = crate::evaluate_container(None, &g).unwrap();
|
||||
for id in [71, 72, 73, 75, 76] {
|
||||
let drawn = shadows_in(&node_in(&art, id));
|
||||
assert!(near(&drawn, &[want]), "Scale Strokes & Effects {strokes}: {id} {drawn:?}");
|
||||
let wanted = if id == 71 { vec![want, want] } else { vec![want] };
|
||||
assert!(near(&drawn, &wanted), "Scale Strokes & Effects {strokes}: {id} {drawn:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,3 +843,93 @@ fn copying_and_object_disabled_transforms_keep_their_existing_shadow_rules() {
|
||||
assert!(close(drawn_shadow(&effects), (10.0, 5.0, 4.0)), "{t:?}: {:?}", drawn_shadow(&effects));
|
||||
}
|
||||
}
|
||||
|
||||
/// The blur of every Drop Shadow drawn on `n` and the nodes under it, in the order of
|
||||
/// [`shadows_in`].
|
||||
fn shadow_blurs_in(n: &Node) -> Vec<f64> {
|
||||
let mut out = vec![];
|
||||
n.walk(&mut |m| {
|
||||
let lists = std::iter::once(&m.appearance.effects).chain(m.appearance.items.iter().map(|i| i.effects()));
|
||||
for list in lists {
|
||||
for r in raster_effects(list) {
|
||||
if let RasterFx::DropShadow { blur, .. } = r {
|
||||
out.push(blur);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// How many paths `n` holds.
|
||||
fn paths_in(n: &Node) -> usize {
|
||||
let mut count = 0;
|
||||
n.walk(&mut |m| count += usize::from(matches!(m.kind, vectorcraft_doc::NodeKind::Path { .. })));
|
||||
count
|
||||
}
|
||||
|
||||
/// A Transform effect that makes copies gives each copy the shadow listed before it, moved, turned,
|
||||
/// reflected and scaled with that copy, the last copy lowest: drawn, expanded and baked. The object
|
||||
/// itself keeps the shadow as it is, and paints none around all of them. With Scale Strokes &
|
||||
/// Effects on, the shadow is not scaled a second time.
|
||||
#[test]
|
||||
fn each_copy_of_a_transform_effect_carries_the_shadow_listed_before_it() {
|
||||
let cases = [
|
||||
(json!({"moveH": 100, "copies": 2}), vec![(10.0, 0.0), (10.0, 0.0), (10.0, 0.0)], vec![4.0, 4.0, 4.0]),
|
||||
// A turn of 90° counterclockwise on screen takes an offset to the right to one upward.
|
||||
(json!({"rotate": 90, "copies": 2}), vec![(-10.0, 0.0), (0.0, -10.0), (10.0, 0.0)], vec![4.0, 4.0, 4.0]),
|
||||
(json!({"reflectX": true, "moveH": 100, "copies": 1}), vec![(-10.0, 0.0), (10.0, 0.0)], vec![4.0, 4.0]),
|
||||
(json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "copies": 1}), vec![(20.0, 0.0), (10.0, 0.0)], vec![8.0, 4.0]),
|
||||
(json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "copies": 1, "strokes": true}), vec![(20.0, 0.0), (10.0, 0.0)], vec![8.0, 4.0]),
|
||||
(json!({"scaleH": 200, "scaleV": 200, "moveH": 100, "copies": 1, "random": true}), vec![(10.0, 0.0), (10.0, 0.0)], vec![4.0, 4.0]),
|
||||
];
|
||||
for (params, want, blurs) in cases {
|
||||
let n = square(91, vec![shadow(10.0, 0.0, 4.0), transform(params.clone())]);
|
||||
assert!(crate::has_transform_art(&n), "{params}");
|
||||
assert!(crate::outer_effects(&n).iter().all(|e| e.id != "stylize.dropShadow"), "{params}");
|
||||
let random = params["random"].as_bool().unwrap_or(false);
|
||||
for (how, art) in
|
||||
[("drawn", crate::reshape(None, &n, None).unwrap()), ("expanded", expand_outlined(None, &n, None).unwrap()), ("baked", baked(n.clone()))]
|
||||
{
|
||||
let drawn = shadows_in(&art);
|
||||
assert_eq!(paths_in(&art), want.len(), "{params}: {how} {art:?}");
|
||||
if random {
|
||||
// Random scales the copy by a share of the 200 %, and its shadow with it; the object
|
||||
// keeps its own.
|
||||
assert_eq!(drawn.len(), want.len(), "{params}: {how} {drawn:?}");
|
||||
assert!(near(&drawn[1..], &want[1..]), "{params}: {how} {drawn:?}");
|
||||
continue;
|
||||
}
|
||||
assert!(near(&drawn, &want), "{params}: {how} {drawn:?}");
|
||||
let b = shadow_blurs_in(&art);
|
||||
assert!(b.len() == blurs.len() && b.iter().zip(&blurs).all(|(x, y)| (x - y).abs() < 1e-9), "{params}: {how} blurs {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A shadow listed after a Transform effect that makes copies paints once around all of them, and
|
||||
/// one listed before it on each copy. A Transform effect that makes no copies, or has Transform
|
||||
/// Objects off, leaves the object's shadows on the object.
|
||||
#[test]
|
||||
fn a_shadow_after_a_copying_transform_paints_once_around_the_copies() {
|
||||
let n = square(92, vec![shadow(10.0, 0.0, 0.0), transform(json!({"rotate": 90, "copies": 1})), shadow(0.0, 5.0, 0.0)]);
|
||||
let art = crate::reshape(None, &n, None).unwrap();
|
||||
assert!(near(&shadows_in(&art), &[(0.0, 5.0), (0.0, -10.0), (10.0, 0.0)]), "{:?}", shadows_in(&art));
|
||||
assert_eq!(crate::outer_effects(&n).len(), 2);
|
||||
for params in [json!({"rotate": 90}), json!({"rotate": 90, "copies": 1, "objects": false})] {
|
||||
let n = square(93, vec![shadow(10.0, 0.0, 0.0), transform(params.clone())]);
|
||||
assert!(!crate::has_transform_art(&n), "{params}");
|
||||
assert_eq!(crate::outer_effects(&n).len(), 2, "{params}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies that stop at the effect budget each keep their shadow: no copy goes without one, and no
|
||||
/// shadow is left without its copy.
|
||||
#[test]
|
||||
fn copies_past_the_budget_each_keep_their_shadow() {
|
||||
let n = path_node(94, wavy(30_000), vec![shadow(1.0, 0.5, 0.0), transform(json!({"moveV": 50, "copies": 1000}))]);
|
||||
let art = crate::reshape(None, &n, None).unwrap();
|
||||
let paths = paths_in(&art);
|
||||
assert!(paths > 1 && paths < 1001, "{paths}");
|
||||
assert_eq!(shadows_in(&art).len(), paths);
|
||||
}
|
||||
|
||||
@@ -257,6 +257,27 @@ fn shadows_under_a_groups_or_an_objects_transform_expand_appearance_as_the_canva
|
||||
}
|
||||
}
|
||||
|
||||
/// Each copy of a Transform effect draws the drop shadow listed before the effect under itself,
|
||||
/// scaled with it, and Expand Appearance keeps those shadows: the square (20..40) has its shadow
|
||||
/// 20 pt to its right, and its copy scaled 200 % and moved 80 pt down (10..50 across, 90..130
|
||||
/// down) has its own 40 pt to its right.
|
||||
#[test]
|
||||
fn each_copys_shadow_expands_as_the_canvas_draws_it() {
|
||||
let mut s = session();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 20, "y": 20, "width": 20, "height": 20}));
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
let shadow = json!({"x": 20, "y": 0, "blur": 0, "opacity": 100, "mode": "normal", "color": "#00ff00"});
|
||||
run(&mut s, "effect.apply", json!({"effect": "stylize.dropShadow", "params": shadow}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.transform", "params": {"scaleH": 200, "scaleV": 200, "moveV": 80, "copies": 1}}));
|
||||
let before = render(&s);
|
||||
assert_eq!(before.pixel(70, 110), [0, 255, 0, 255], "the copy's shadow");
|
||||
run(&mut s, "effect.expandAppearance", json!({}));
|
||||
let after = render(&s);
|
||||
let diff = differing(&before, &after, 64);
|
||||
assert_eq!(diff, 0, "{diff} pixels differ");
|
||||
}
|
||||
|
||||
/// A group's Transform effect runs after its member's own Offset Path in Expand Appearance as on
|
||||
/// the canvas: the 100 pt square offset to 120 pt and scaled by half expands 60 pt wide.
|
||||
#[test]
|
||||
|
||||
@@ -67,6 +67,13 @@ pub(crate) fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
if let Some(art) = painted_art(symbols, n) {
|
||||
return art.visual_bounds_with(symbols).map(|r| with_raster_reach(symbols, n, r));
|
||||
}
|
||||
// Copies that each carry the raster effects listed before their Transform effect reach as far
|
||||
// as each copy's raster effects paint, and the raster effects after it spread them all.
|
||||
if effects::outer_effects(n).len() < n.appearance.effects.len()
|
||||
&& let Some(art) = effects::reshape(Some(symbols.document()), n, None)
|
||||
{
|
||||
return cull_bounds(symbols, &art);
|
||||
}
|
||||
let bp = node_bezpath(n)?;
|
||||
let ctx = GeomContext { doc: Some(symbols.document()), ..GeomContext::of(n) };
|
||||
let (g, carry) = effected_path_carrying(symbols.document(), n, &bp);
|
||||
|
||||
@@ -423,11 +423,12 @@ impl Renderer {
|
||||
let outer = self.made_again;
|
||||
self.made_again = outer || !kept;
|
||||
// With Crop Marks, the object inside the art paints its own raster effects; Vector
|
||||
// Halftone samples them with the object.
|
||||
// Halftone samples them with the object. The copies of a Transform effect that carry the
|
||||
// raster effects listed before it paint those themselves.
|
||||
let rfx = if effects::has_crop_marks(a) || effects::addons::halftone::has_halftone(a) {
|
||||
vec![]
|
||||
} else {
|
||||
effects::raster_effects(&a.appearance.effects)
|
||||
effects::raster_effects(effects::outer_effects(a))
|
||||
};
|
||||
match cull_bounds(f.symbols, &art) {
|
||||
Some(reach) if !f.opts.outline && !rfx.is_empty() => {
|
||||
|
||||
@@ -1219,3 +1219,26 @@ fn a_shadow_before_a_skipped_transform_reaches_as_far_as_it_is_drawn() {
|
||||
assert!(within(b), "{what}: painted {b:?} for art {art:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Each copy of a Transform effect draws the drop shadow listed before the effect under itself,
|
||||
/// scaled with it: the red square (10..30) and its copy scaled 200 % and moved 60 pt down (0..40
|
||||
/// across, 60..100 down) each have a green shadow to their right, 20 pt for the square and 40 pt
|
||||
/// for the copy. The bounds reach as far as the copy's shadow.
|
||||
#[test]
|
||||
fn each_copy_of_a_transform_effect_draws_its_own_shadow() {
|
||||
let shadow = fx("stylize.dropShadow", json!({"x": 20, "y": 0, "blur": 0, "opacity": 100, "mode": "normal", "color": "#00ff00"}));
|
||||
let copies = fx("distort.transform", json!({"scaleH": 200, "scaleV": 200, "moveV": 60, "copies": 1}));
|
||||
let red = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
let mut n = Node::path(NodeId(0), shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), red);
|
||||
n.appearance.effects = vec![shadow, copies];
|
||||
let d = doc_with(Document::new(100.0, 100.0), n);
|
||||
let img = render(&d);
|
||||
assert_eq!(img.pixel(20, 20), [255, 0, 0, 255], "the square");
|
||||
assert_eq!(img.pixel(45, 20), [0, 255, 0, 255], "the square's shadow");
|
||||
assert_eq!(img.pixel(20, 80), [255, 0, 0, 255], "the copy");
|
||||
assert_eq!(img.pixel(70, 80), [0, 255, 0, 255], "the copy's shadow, 40 pt to its right");
|
||||
assert_eq!(img.pixel(85, 80), [255, 255, 255, 255], "past the copy's shadow");
|
||||
let n = &d.layers[0].children().unwrap()[0];
|
||||
let b = crate::fx::visual_bounds(&d.symbol_boxes(), n).unwrap();
|
||||
assert!(b.x1 >= 80.0 - 1e-9, "{b:?}");
|
||||
}
|
||||
|
||||
@@ -616,6 +616,7 @@ since 2026-10-01.
|
||||
- **Selected Mirror Repeat handles:** a headless canvas regression creates the default mirror repeat and drags its Top bounding-box handle where the axis crosses it; the repeat grows vertically while its width stays fixed. Existing mirror-axis checks cover center movement and end-handle hit priority. The focused UI mirror tests pass. Scores and hours unchanged.
|
||||
- **Transform effect options (#885):** Effect › Distort & Transform › Transform… has visible Transform Objects, Transform Patterns and Scale Strokes & Effects checkboxes (`distort.transform` params `objects`, `patterns`, `strokes`; files without them draw as before). With Transform Objects off the geometry stays and makes no copies; Scale Strokes & Effects scales stroke weights, dashes and the fills' and strokes' effect distances by the mean scale, and Transform Patterns moves pattern tiles, each copy `k` by the transformation `k` times. Such objects draw, export and expand through the same per-copy art, so the canvas and Expand Appearance agree (engine `tests_transformfx`, effects `tests_transformfx`, the dialog test). A Transform effect in a fill's or stroke's own effects applies the same options to that fill or stroke: its weight and dashes and its pattern follow the transformation, and with copies each copy is an object of its own; the canvas, export, Expand Appearance and Use Preview Bounds use the same art (effects `tests_transformfx`). The effects listed after it in that fill's or stroke's list keep their own distances and apply to the transformed art and to each copy, as an object's later effects do after the object's Transform effect: a Drop Shadow after it keeps its offset, an Offset Path after it offsets by its own distance, and an SVG filter after it keeps its scale and the origin of its user space. With Transform Objects off, the effects before it keep their distances too. Before, Scale Strokes & Effects scaled a fill's or stroke's later effects by the mean scale, so existing documents with such effects draw differently. Effects apply in the order they are listed ([Appearance attributes](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/appearance-attributes-in-illustrator.html)); how the two options treat the effects after a fill's or stroke's own Transform effect is VectorCraft's own rule, following that order (effects `tests::rasterfollow::a_fills_own_transform_leaves_the_shadow_after_it`, `tests::rasterfollow::a_fills_own_transform_leaves_the_offset_after_it`, `tests::rasterfollow::a_fills_own_transform_leaves_the_svg_filter_after_it`, `tests::transformfx::a_fills_own_transform_with_scale_strokes_and_effects_leaves_its_later_effects`; engine `plugins::a_transform_after_a_painted_fill_effect_leaves_the_later_shadow_on_every_paint`). Public reference: [Scale objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-objects.html), [How to apply effects](https://helpx.adobe.com/illustrator/using/effects.html), [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html). Scores and hours unchanged.
|
||||
- **Raster effects before a Transform effect:** effects apply in the order they are listed ([Appearance attributes](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/appearance-attributes-in-illustrator.html)). A Drop Shadow, glow, blur, Feather or pixel filter listed before a Transform effect in the same list (an object's effects, or a fill's or stroke's own) now follows that Transform effect: its sizes scale by the mean scale, and a Drop Shadow's offset turns, reflects and scales with the art. VectorCraft applies this rule with Scale Strokes & Effects on or off. Before, such an effect was drawn at its own distances around the transformed art. The canvas, export, Expand Appearance, Object › Expand, Outline Stroke and the 3D effects use the same values (effects `tests::rasterfollow`; render `a_transform_after_a_drop_shadow_scales_the_shadow_with_the_art`; engine `objexpand::a_shadow_before_a_transform_expands_as_the_canvas_draws_it`, `outlinestroke::a_shadow_before_a_transform_outlines_as_the_canvas_draws_it`). Not yet: a Transform effect that makes copies or moves at random leaves the raster effects before it as they are; a Rasterize effect before a Transform effect keeps its pixel grid and room around the art; the angles and light directions of pixel filters do not turn or reflect with the art. Scores and hours unchanged.
|
||||
- **Raster effects before a Transform effect that makes copies:** the Transform effect can make copies with its transformation applied ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html)). Giving each copy the raster effects listed before that effect is VectorCraft's own rule. On a path or compound path, each copy now carries the Drop Shadow, glow, blur, Feather or pixel filter listed before the last Transform effect that makes copies, with its sizes scaled by that copy's mean scale and a Drop Shadow's offset turned, reflected and scaled with the copy; a raster effect listed after it paints once around all the copies. Before, one shadow was drawn under all the copies at the object's own distances. The canvas, the object's bounds, export and Expand Appearance draw the same copies, and copies that stop at the effect budget each keep their effects (effects `tests::rasterfollow::each_copy_of_a_transform_effect_carries_the_shadow_listed_before_it`, `tests::rasterfollow::a_shadow_after_a_copying_transform_paints_once_around_the_copies`, `tests::rasterfollow::copies_past_the_budget_each_keep_their_shadow`; render `objectfx::each_copy_of_a_transform_effect_draws_its_own_shadow`; engine `objexpand::each_copys_shadow_expands_as_the_canvas_draws_it`). A Transform effect without copies draws as before. Not yet: the copies of type, images, symbol instances and groups, and a fill's or stroke's own copying Transform effect, still share one set of raster effects, and so does a path with Rasterize, an SVG filter or another effect that is neither a geometry effect nor one of those raster effects before the copying effect. Scores and hours unchanged.
|
||||
- **Raster effects before a Transform effect past the effect budget:** VectorCraft leaves out a Transform effect whose result would pass its geometry budget of 2 million anchors or 8 million lines drawn, and the art stays where it was. The raster effects listed before that Transform effect now stay with the art too: a Drop Shadow keeps its own offset and blur, and the Transform effects after it that fit still move both. Before, the art stayed while the shadow scaled with the Transform effect it did not take. This is VectorCraft's own rule. It applies to a path's own effects and to its fills' and strokes' own, also when the path is in a group whose geometry effects run after its members' own, on the canvas and in its bounds, in export, Expand Appearance, Object › Expand and Outline Stroke, and a hidden shadow keeps its values once shown (effects `tests::rasterfollow::a_transform_past_the_line_budget_moves_the_art_with_its_shadow`, `tests::rasterfollow::only_transforms_that_ran_move_a_preceding_shadow`, `tests::rasterfollow::a_skipped_item_transform_keeps_its_shadow`, `tests::rasterfollow::the_shape_art_of_a_skipped_transform_keeps_its_shadows`, `tests::rasterfollow::a_members_skipped_transform_keeps_its_shadow_under_its_groups_effects`; render `objectfx::a_shadow_before_a_skipped_transform_reaches_as_far_as_it_is_drawn`, `fx::renderer::tests::a_path_paints_from_the_transforms_that_moved_it`; engine `objexpand::a_shadow_before_a_skipped_transform_expands_with_its_own_offset`). Not yet: the shadow of type, an image, a symbol instance or a group whose art a Transform effect leaves in place past the budget still follows that effect. Scores and hours unchanged.
|
||||
- **Raster effects inside an object or group with a Transform effect:** effects applied to a group affect everything in it ([Appearance attributes](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/appearance-attributes-in-illustrator.html)). How a Transform effect changes the raster effects inside a group or object is VectorCraft's own rule. A group's Transform effect that moves it once now transforms the raster effects of its members, of their fills and strokes, of nested groups and of the art of the linked opacity masks in it, after the members' own Transform effects. A raster effect that a member's own Transform effect with copies or random holds back keeps its value through that effect and still transforms with the group's. VectorCraft applies the same rule to the raster effects of an object's fills and strokes under the object's Transform effect. Sizes scale by the mean scale and a Drop Shadow's offset turns, reflects and scales, with Scale Strokes & Effects on or off. Before, they kept their values, or with Scale Strokes & Effects on scaled by the mean scale with the offset unturned. A group's or object's Transform effect that makes copies or moves at random leaves them as before. The canvas, the redraw region, export, Expand Appearance, Object › Expand and Outline Stroke use the same values (effects `tests::rasterfollow`, `group::tests::a_compound_shapes_transform_turns_its_fills_shadow`; render `a_transform_scales_the_shadows_of_fills_and_members_with_the_art`; engine `objexpand::a_fills_shadow_under_the_objects_transform_expands_as_the_canvas_draws_it`, `objexpand::shadows_under_a_groups_or_an_objects_transform_expand_appearance_as_the_canvas_draws_them`, `outlinestroke::a_strokes_shadow_under_the_objects_transform_outlines_as_the_canvas_draws_it`). Scores and hours unchanged.
|
||||
- **A group's geometry effects after its members' own:** effects applied to a group affect everything in it, and effects apply in the order they are listed ([Appearance attributes](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-graphic-styles/appearance-attributes-in-illustrator.html)). Running a group's effects after its members' own is VectorCraft's own rule. The geometry effects of a group or layer (the effects that reshape outlines, such as Distort & Transform, Offset Path and Warp) now reshape what its members draw: the geometry effects of each visible path and compound path in it and of their fills and strokes, and a nested group's own geometry and Pathfinder effects, run first, at any depth. A symbol instance's own geometry effects run after those of its symbol's members in the same way. Before, the group's effects reshaped the members' outlines and the members' own effects ran afterwards: a 100 pt square with its own 10 pt Offset Path in a group with a 50% Transform drew 70 pt wide, and it now draws 60 pt wide. Existing documents with such groups draw differently. The canvas, export, Expand Appearance and the art that Pathfinder operations and compound shapes take from such a group use the same order (effects `group::tests::a_members_own_offset_runs_before_its_groups_scale`, `group::tests::a_members_own_scale_runs_before_its_groups_offset`, `group::tests::a_member_of_a_plain_nested_group_takes_its_own_effects_first`, `group::tests::a_nested_groups_own_effects_run_before_its_groups`, `group::tests::a_nested_groups_pathfinder_runs_before_its_groups_effects`, `group::tests::a_members_fill_effects_run_before_its_groups`, `group::tests::shape_art_of_a_group_applies_its_members_effects_first`, `group::tests::a_symbol_instances_effects_run_after_its_symbols_members_own`, `group::tests::a_baked_group_applies_its_members_effects_first`, `group::tests::a_baked_group_that_keeps_its_type_applies_its_members_effects_first`, `tests::rasterfollow::a_groups_transform_applies_after_its_members_own`; engine `objexpand::a_groups_effects_after_its_members_own_expand_as_the_canvas_draws_them`). Not yet: the own geometry effects of images, symbol instances, paths with a linked opacity mask or a 3D effect, and nested groups with a 3D effect in the group still run after the group's. Scores and hours unchanged.
|
||||
|
||||
Reference in New Issue
Block a user