mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
Effects: a copying Transform effect after 3D holds raster effects back; pixel filter sizes follow
- A Transform effect that makes copies or moves at random, listed after an Extrude or Revolve effect, now keeps the raster effects before the 3D effect at their own values, as it does without the 3D effect. Before, the 3D art kept a Drop Shadow scaled by a Transform effect between them. - In a fill's or stroke's own effects, such a Transform effect also keeps the raster effects before it from following an earlier Transform effect with Transform Patterns on, and the Transform effects between them. - Colored Pencil's pencilWidth and Paint Daubs' brushSize are distances in document units, so a Transform effect after them and Scale Strokes & Effects scale them as they scale Palette Knife's strokeSize. The Paint Daubs params text names the unit.
This commit is contained in:
@@ -94,6 +94,8 @@ fn lengths_of(id: &str) -> Lengths {
|
||||
"stylize.glowingEdges" => always(&["edgeWidth", "smoothness"]),
|
||||
"brushStrokes.accentedEdges" => always(&["edgeWidth"]),
|
||||
"artistic.paletteKnife" => always(&["strokeSize"]),
|
||||
"artistic.coloredPencil" => always(&["pencilWidth"]),
|
||||
"artistic.paintDaubs" => always(&["brushSize"]),
|
||||
"brushStrokes.angledStrokes" | "brushStrokes.crosshatch" | "brushStrokes.inkOutlines" => always(&["strokeLength"]),
|
||||
"brushStrokes.spatter" => always(&["sprayRadius"]),
|
||||
"brushStrokes.sprayedStrokes" => always(&["strokeLength", "sprayRadius"]),
|
||||
@@ -387,7 +389,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"artistic.paintDaubs",
|
||||
"Paint Daubs…",
|
||||
ARTISTIC,
|
||||
"{brushSize: 1..50 (10; dab centres farther apart), sharpness: 0..40 (25; harder dab edges), brushType: \"simple\"|\"lightRough\"|\"darkRough\"|\"wideSharp\"|\"wideBlurry\"|\"sparkle\" (\"simple\")} repaints the object with thick round brush dabs in the local colours, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
|
||||
"{brushSize: 1..50 (10; spacing of the dab centers in document units),sharpness: 0..40 (25; harder dab edges), brushType: \"simple\"|\"lightRough\"|\"darkRough\"|\"wideSharp\"|\"wideBlurry\"|\"sparkle\" (\"simple\")} repaints the object with thick round brush dabs in the local colours, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
|
||||
json!({"brushSize": 10.0, "sharpness": 25.0, "brushType": "simple"}),
|
||||
),
|
||||
r(
|
||||
|
||||
@@ -1263,9 +1263,13 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Opt
|
||||
let params = merged_params(&fx[at].id, &fx[at].params);
|
||||
// Where each effect the piece keeps sits among the item's effects.
|
||||
let kept: Vec<usize> = (0..at).filter(|&k| !is_geometry(&fx[k].id)).chain(at + 1..fx.len()).collect();
|
||||
// A Transform effect after this one that makes copies or moves at random holds the raster
|
||||
// effects before this one back from every Transform effect ([`crate::raster::followed`]).
|
||||
let held_later = fx.get(at + 1..).unwrap_or_default().iter().any(crate::raster::holds_back);
|
||||
// The effects before the Transform effect that it keeps, each raster effect following the
|
||||
// geometry effects before it ([`crate::raster::without_geometry`]).
|
||||
let kept_before = crate::without_geometry(before);
|
||||
// geometry effects before it ([`crate::raster::without_geometry`]) unless held back.
|
||||
let kept_before: Vec<Effect> =
|
||||
if held_later { before.iter().filter(|e| !is_geometry(&e.id)).cloned().collect() } else { crate::without_geometry(before) };
|
||||
if let Some(plugin) = painted_plugin(&fx[at]) {
|
||||
let mut it = item.clone();
|
||||
let rest: Vec<Effect> = kept_before.iter().chain(&fx[at + 1..]).cloned().collect();
|
||||
@@ -1308,7 +1312,7 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Opt
|
||||
let (made, moved) = transform_leaf(&mut p, &g, map, copies, options, room);
|
||||
*used = used.plus(made);
|
||||
let from = crate::raster::held_back(before);
|
||||
if moved {
|
||||
if moved && !held_later {
|
||||
follow_piece(&mut p, kept_before.get(from..).unwrap_or_default(), from, &fx[at]);
|
||||
}
|
||||
transform_piece(&mut p, (ctx.seed, i, &kept), crate::raster::held_back(&fx[..=at]), limit, used, doc);
|
||||
@@ -1333,7 +1337,8 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Opt
|
||||
/// A live-effect plug-in that paints what it returns ([`painted_plugin`]) makes each path one path
|
||||
/// for each piece it returns ([`paint_piece`]). The raster effects before a Transform effect
|
||||
/// follow it ([`follow_piece`]), except the first `held` effects, which a Transform effect before
|
||||
/// the piece's effects that makes copies or moves at random holds back. The geometry effects then
|
||||
/// the piece's effects that makes copies or moves at random holds back, and the effects before
|
||||
/// such a Transform effect among the piece's effects. The geometry effects then
|
||||
/// leave the fills and strokes; their other effects stay. The paths keep to `max` together with
|
||||
/// the rest of the art (`used`), and the swatches and patterns painted pieces name must be in
|
||||
/// `doc`. `(seed, source, kept)`: the object's seed, the index in the object's appearance of the
|
||||
@@ -1346,7 +1351,14 @@ fn transform_piece(p: &mut Node, (seed, source, kept): (u64, usize, &[usize]), h
|
||||
count = count.max(item.effects().len());
|
||||
}
|
||||
});
|
||||
// The effects before a Transform effect among the piece's effects that makes copies or moves at
|
||||
// random are held back from the Transform effects before it too.
|
||||
let mut held = held;
|
||||
for_each_leaf(p, &mut |l| {
|
||||
if let Some(last) = l.appearance.items.first().and_then(|item| item.effects().iter().rposition(crate::raster::holds_back)) {
|
||||
held = held.max(last);
|
||||
}
|
||||
});
|
||||
for j in 0..count {
|
||||
let Some(bounds) = p.geometric_bounds() else { break };
|
||||
let from = held.min(j);
|
||||
@@ -1397,7 +1409,7 @@ fn transform_piece(p: &mut Node, (seed, source, kept): (u64, usize, &[usize]), h
|
||||
follow_piece(leaf, was, from, &e);
|
||||
});
|
||||
if holds {
|
||||
held = j;
|
||||
held = held.max(j);
|
||||
}
|
||||
}
|
||||
for_each_leaf(p, &mut |leaf| {
|
||||
|
||||
@@ -961,10 +961,17 @@ fn group_solid_art(n: &Node) -> Option<Node> {
|
||||
|
||||
/// `effects` without the 3D effect at `index` and the geometry effects before it, which shape the
|
||||
/// art it projects. Each raster effect before it follows the Transform effects among those
|
||||
/// ([`crate::without_geometry`]); the effects after it stay as they are.
|
||||
/// ([`crate::without_geometry`]), unless a Transform effect after it makes copies or moves at
|
||||
/// random ([`crate::raster::holds_back`]); the effects after it stay as they are.
|
||||
pub(crate) fn past_effect(effects: &[Effect], index: usize) -> Vec<Effect> {
|
||||
let (before, after) = effects.split_at(index.min(effects.len()));
|
||||
crate::without_geometry(before).into_iter().chain(after.iter().skip(1).cloned()).collect()
|
||||
let after = after.get(1..).unwrap_or_default();
|
||||
let kept: Vec<Effect> = if after.iter().any(crate::raster::holds_back) {
|
||||
before.iter().filter(|e| !is_geometry(&e.id)).cloned().collect()
|
||||
} else {
|
||||
crate::without_geometry(before)
|
||||
};
|
||||
kept.into_iter().chain(after.iter().cloned()).collect()
|
||||
}
|
||||
|
||||
/// One solid of a 3D object as a mesh, with the color it is painted.
|
||||
|
||||
@@ -264,6 +264,54 @@ fn glows_blurs_and_feathers_scale_with_the_transform_after_them() {
|
||||
assert_eq!(crate::outset(&shadowed, Rect::new(0.0, 0.0, 100.0, 100.0)), 20.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_filter_sizes_scale_with_the_transform_after_them() {
|
||||
let effects = [
|
||||
fx("artistic.coloredPencil", json!({"pencilWidth": 5})),
|
||||
fx("artistic.paintDaubs", json!({"brushSize": 10})),
|
||||
transform(json!({"scaleH": 200, "scaleV": 200})),
|
||||
];
|
||||
let drawn = raster_effects(&effects);
|
||||
assert!(matches!(drawn.first(), Some(RasterFx::Pixel(crate::PixelFx::ColoredPencil { width, .. })) if (width - 10.0).abs() < 1e-9), "{drawn:?}");
|
||||
assert!(matches!(drawn.get(1), Some(RasterFx::Pixel(crate::PixelFx::PaintDaubs { size, .. })) if (size - 20.0).abs() < 1e-9), "{drawn:?}");
|
||||
}
|
||||
|
||||
/// A Transform effect after a 3D effect that makes copies or moves at random holds back the shadow
|
||||
/// before the 3D effect, as it does without the 3D effect.
|
||||
#[test]
|
||||
fn a_copying_transform_after_3d_holds_the_shadow_back() {
|
||||
for later in [json!({"copies": 1}), json!({"scaleH": 150, "scaleV": 150, "random": true})] {
|
||||
let plain = [shadow(10.0, 0.0, 0.0), transform(json!({"scaleH": 200, "scaleV": 200})), transform(later.clone())];
|
||||
assert!(close(drawn_shadow(&plain), (10.0, 0.0, 0.0)), "{later}: {:?}", drawn_shadow(&plain));
|
||||
for three_d in [crate::EXTRUDE, crate::REVOLVE] {
|
||||
let art = |n: &Node| if three_d == crate::EXTRUDE { crate::solid_art(n) } else { crate::revolve_art(n) };
|
||||
let effects = vec![plain[0].clone(), plain[1].clone(), fx(three_d, json!({})), plain[2].clone()];
|
||||
let projected = art(&square(16, effects)).unwrap();
|
||||
let drawn = shadows_in(&projected);
|
||||
assert!(!drawn.is_empty() && near(&drawn, &vec![(10.0, 0.0); drawn.len()]), "{three_d} {later}: {drawn:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In a fill's own effects, a Transform effect that makes copies after a Transform effect with
|
||||
/// Transform Patterns on holds back the shadow before both, and before a Transform effect between
|
||||
/// them.
|
||||
#[test]
|
||||
fn a_fills_copying_transform_holds_back_the_shadow_before_an_earlier_transform() {
|
||||
let patterns = transform(json!({"scaleH": 200, "scaleV": 200, "patterns": true}));
|
||||
let copies = transform(json!({"copies": 1}));
|
||||
let lists = [
|
||||
vec![shadow(10.0, 0.0, 0.0), patterns.clone(), copies.clone()],
|
||||
vec![shadow(10.0, 0.0, 0.0), patterns, transform(json!({"rotate": 90})), copies],
|
||||
];
|
||||
for effects in lists {
|
||||
let n = fill_square(17, effects.clone());
|
||||
let art = crate::reshape(None, &n, None).unwrap();
|
||||
let drawn = shadows_in(&art);
|
||||
assert!(!drawn.is_empty() && near(&drawn, &vec![(10.0, 0.0); drawn.len()]), "{} effects: {drawn:?}", effects.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanded_and_exported_objects_keep_the_shadow_the_canvas_draws() {
|
||||
let effects = vec![shadow(10.0, 10.0, 4.0), transform(json!({"scaleH": 200, "scaleV": 200}))];
|
||||
|
||||
Reference in New Issue
Block a user