mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
@@ -131,6 +131,14 @@ pub trait FxTarget {
|
||||
buf
|
||||
});
|
||||
}
|
||||
/// Give the buffer the pixel-grid phase the layer rectangle's cover would
|
||||
/// ([`effectcraft_effects::Buf::align`]): grows left/top by less than 64 pixels.
|
||||
fn align(&mut self, size: [f64; 2]) {
|
||||
self.cpu(&mut |mut buf| {
|
||||
buf.align(size);
|
||||
buf
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The CPU effect target (GPU chains go through [`Accelerator::effects`]: upload, readback).
|
||||
|
||||
@@ -458,7 +458,7 @@ fn time_dependent(layer: &Layer, effects: bool) -> bool {
|
||||
if matches!(layer.source, LayerSource::Shape)
|
||||
&& let Some(c) = layer.props.sub("contents")
|
||||
{
|
||||
return group_has(c, "wiggle");
|
||||
return group_has(c, "wiggle") || group_has(c, "wiggler");
|
||||
}
|
||||
// Wiggly selectors move with time; Expression selectors evaluate per character (their
|
||||
// property value alone doesn't capture the result).
|
||||
|
||||
@@ -566,14 +566,26 @@ impl<'a> Renderer<'a> {
|
||||
let Some(fx) = layer.effects() else { return };
|
||||
let lt = layer.layer_time(ctx.time);
|
||||
let layer_size = ctx.effect_bounds(layer).0;
|
||||
// #659: an authored layer's buffer spans only its painted content while the stack works
|
||||
// on the layer's whole region (`layer_size`): give the buffer the room a sized source
|
||||
// already has, so tiling or displacement can place pixels outside the content instead
|
||||
// of losing them at its edge. Only when something will actually run, and not for an
|
||||
// adjustment layer, whose buffer is the comp below and already spans it.
|
||||
if !adjustment
|
||||
&& fx.groups().any(|g| g.enabled && matches!(&g.kind, GroupKind::Effect { effect } if effectcraft_effects::find(effect).is_some_and(|s| !effectcraft_effects::audio_fx::is_audio_effect(s.id)))) {
|
||||
target.cover(layer_size);
|
||||
// #659 / #1205: an authored layer's buffer spans only its painted content. Effects that
|
||||
// place pixels outside it grow the buffer themselves, by exactly what they need
|
||||
// (`layer_cover_pad`: CC Tiler, CC Smear, CC Mosaic sharp) instead of a blanket cover of
|
||||
// the whole layer rectangle on every stack — which paid a comp-sized texture per layer,
|
||||
// per frame in flight, for glow and blur that never use the extra room. The buffer still
|
||||
// takes the cover's pixel-grid phase: a downscaled layer resamples along that grid.
|
||||
// Effects not known to stay inside their own margins (Twirl, Bulge, Transform, ...) still
|
||||
// get the cover: they move content past the painted bounds without growing the buffer.
|
||||
let video = || {
|
||||
fx.groups().filter(|g| g.enabled).filter_map(|g| match &g.kind {
|
||||
GroupKind::Effect { effect } => effectcraft_effects::find(effect).filter(|s| !effectcraft_effects::audio_fx::is_audio_effect(s.id)),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
if !adjustment && video().next().is_some() {
|
||||
if video().all(|s| s.skips_cover()) {
|
||||
target.align(layer_size);
|
||||
} else {
|
||||
target.cover(layer_size);
|
||||
}
|
||||
}
|
||||
let mut mask_shapes = masks::shapes(ctx, layer);
|
||||
if origin != [0.0; 2] {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! with the After Effects stacking semantics:
|
||||
//!
|
||||
//! - path operators (Trim Paths, Round Corners, Offset Paths, Pucker & Bloat, Twist, Zig Zag,
|
||||
//! Wiggle Paths, Merge Paths, Repeater) act on all paths above them in the same group, including
|
||||
//! the paths of nested groups;
|
||||
//! Wiggle Paths, Wiggle Transform, Merge Paths, Repeater) act on all paths above them in the
|
||||
//! same group, including the paths of nested groups;
|
||||
//! - paint items (fills, strokes, gradient fills/strokes) paint the paths above them, *as modified
|
||||
//! by every operator in the stack* — a Trim Paths below a Stroke still trims it;
|
||||
//! - items higher in the list draw on top;
|
||||
@@ -17,7 +17,9 @@ use effectcraft_keyframe::{Gradient, Value};
|
||||
use effectcraft_path::{BezPath, Cap, FillRule, Join, StrokeStyle, ops, varstroke};
|
||||
use effectcraft_project::{Layer, Node, PropGroup};
|
||||
use effectcraft_raster::{Image, Mask};
|
||||
use effectcraft_text::selectors::{self, Wiggly};
|
||||
use rayon::prelude::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::eval::EvalCtx;
|
||||
|
||||
@@ -185,9 +187,75 @@ fn used_slots(live: &[usize], draws: &[Draw]) -> Vec<usize> {
|
||||
v
|
||||
}
|
||||
|
||||
/// Wiggle Transform: moves the live slots by a random offset of the Transform group's Anchor
|
||||
/// Point, Position, Scale and Rotation (the most each one wiggles), changing over time. Its
|
||||
/// Wiggles/Second, Correlation, Temporal Phase, Spatial Phase and Random Seed work as the Wiggly
|
||||
/// Selector's do, with each Repeater copy (`copy_of`) as one unit: the copies of a Repeater above
|
||||
/// the operation move on their own as far as Correlation allows, and the copies of a Repeater
|
||||
/// below it move as one. The four transformations of one operation move together.
|
||||
fn wiggle_transform(ctx: &EvalCtx, layer: &Layer, g: &PropGroup, arena: &mut [BezPath], live: &[usize], copy_of: &HashMap<usize, usize>, draws: &mut [Draw]) {
|
||||
let Some(tr) = g.sub("transform") else { return };
|
||||
let w = Wiggly {
|
||||
max: 1.0,
|
||||
min: -1.0,
|
||||
wiggles_per_second: ctx.f(layer, g, "speed", 2.0),
|
||||
correlation: ctx.f(layer, g, "correlation", 50.0) / 100.0,
|
||||
temporal_phase: ctx.f(layer, g, "phase", 0.0),
|
||||
spatial_phase: ctx.f(layer, g, "spatialPhase", 0.0),
|
||||
lock_dimensions: false,
|
||||
seed: ctx.f(layer, g, "seed", 0.0).round() as i64 as u64,
|
||||
};
|
||||
let anchor = ctx.v2(layer, tr, "anchor", [0.0; 2]);
|
||||
let pos = ctx.v2(layer, tr, "position", [0.0; 2]);
|
||||
let scale = ctx.v2(layer, tr, "scale", [0.0; 2]);
|
||||
let rot = ctx.f(layer, tr, "rotation", 0.0);
|
||||
let t = layer.layer_time(ctx.time).seconds();
|
||||
let matrix = |k: usize| {
|
||||
let (x, y) = (selectors::wiggly_value(&w, t, k, 0), selectors::wiggly_value(&w, t, k, 1));
|
||||
Mat3::translate(vec2(pos[0] * x, pos[1] * y))
|
||||
* Mat3::rotate_deg(rot * x)
|
||||
* Mat3::scale(vec2(1.0 + scale[0] * x / 100.0, 1.0 + scale[1] * y / 100.0))
|
||||
* Mat3::translate(vec2(-anchor[0] * x, -anchor[1] * y))
|
||||
};
|
||||
let mut by_copy: HashMap<usize, Mat3> = HashMap::new();
|
||||
let mut done: HashSet<usize> = HashSet::new();
|
||||
for &s in live {
|
||||
if !done.insert(s) {
|
||||
continue;
|
||||
}
|
||||
let k = copy_of.get(&s).copied().unwrap_or(0);
|
||||
let m = *by_copy.entry(k).or_insert_with(|| matrix(k));
|
||||
arena[s] = mat_path(&arena[s], &m);
|
||||
}
|
||||
// A paint above the operation whose paths all moved by one matrix moves its gradient and
|
||||
// stroke scale with them, as a group transform does. A paint over copies that moved by
|
||||
// different matrices stays one paint (its fill rule and opacity apply to all of them at once)
|
||||
// and keeps its gradient and stroke scale.
|
||||
for d in draws {
|
||||
let mut ms = d.slots.iter().map(|s| done.contains(s).then(|| by_copy.get(©_of.get(s).copied().unwrap_or(0)).copied()).flatten());
|
||||
let Some(Some(m)) = ms.next() else { continue };
|
||||
if ms.all(|x| x == Some(m)) {
|
||||
d.xf = m * d.xf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the draws of a contents group and the path slots live at its end (in group space).
|
||||
/// `arena` holds every path slot; operators rewrite slots in place.
|
||||
fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<BezPath>) -> (Vec<Draw>, Vec<usize>) {
|
||||
collect_in(ctx, layer, contents, arena, &mut Copies::default())
|
||||
}
|
||||
|
||||
/// The Repeater copy each path slot belongs to, for Wiggle Transform: `of` maps a slot a Repeater
|
||||
/// made to its copy's id (slots no Repeater made are copy 0). Ids are unique across the layer's
|
||||
/// groups, so copies made in a nested group stay apart in the groups around it.
|
||||
#[derive(Default)]
|
||||
struct Copies {
|
||||
of: HashMap<usize, usize>,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
fn collect_in(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<BezPath>, copy_ids: &mut Copies) -> (Vec<Draw>, Vec<usize>) {
|
||||
let mut draws: Vec<Draw> = Vec::new();
|
||||
let mut live: Vec<usize> = Vec::new();
|
||||
let opacity = |g: &PropGroup| (ctx.f(layer, g, "opacity", 100.0) / 100.0) as f32;
|
||||
@@ -330,6 +398,7 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
let t = layer.layer_time(ctx.time).seconds();
|
||||
apply(arena, &live, |p| ops::wiggle(p, &w, t));
|
||||
}
|
||||
"wiggler" => wiggle_transform(ctx, layer, g, arena, &live, ©_ids.of, &mut draws),
|
||||
"merge" => {
|
||||
let mode = ops::MergeMode::from_index(ctx.e(layer, g, "mode"));
|
||||
let input: Vec<BezPath> = live.iter().map(|&i| arena[i].clone()).collect();
|
||||
@@ -342,7 +411,7 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
}
|
||||
"group" => {
|
||||
let Some(inner) = g.sub("contents") else { continue };
|
||||
let (cd, cl) = collect(ctx, layer, inner, arena);
|
||||
let (cd, cl) = collect_in(ctx, layer, inner, arena, copy_ids);
|
||||
let (m, op) = g.sub("transform").map(|t| group_matrix(ctx, layer, t)).unwrap_or((Mat3::IDENTITY, 1.0));
|
||||
for i in used_slots(&cl, &cd) {
|
||||
arena[i] = mat_path(&arena[i], &m);
|
||||
@@ -379,12 +448,20 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec<B
|
||||
// Each copy gets its own transformed slots, so operators below the repeater act on
|
||||
// the repeated geometry (copy order: original first).
|
||||
let mut copy_maps: Vec<std::collections::HashMap<usize, usize>> = Vec::with_capacity(n);
|
||||
// Each copy of each earlier copy (a second Repeater) gets an id of its own.
|
||||
let mut ids: HashMap<(usize, usize), usize> = HashMap::new();
|
||||
for i in 0..n {
|
||||
let m = step(i as f64 + offset);
|
||||
let mut map = std::collections::HashMap::new();
|
||||
for &s in &base_slots {
|
||||
arena.push(mat_path(&arena[s], &m));
|
||||
map.insert(s, arena.len() - 1);
|
||||
let base = copy_ids.of.get(&s).copied().unwrap_or(0);
|
||||
let id = *ids.entry((i, base)).or_insert_with(|| {
|
||||
copy_ids.next += 1;
|
||||
copy_ids.next
|
||||
});
|
||||
copy_ids.of.insert(arena.len() - 1, id);
|
||||
}
|
||||
live.extend(base_live.iter().map(|s| map[s]));
|
||||
copy_maps.push(map);
|
||||
|
||||
@@ -310,6 +310,202 @@ fn wiggle_paths_vary_over_time_deterministically() {
|
||||
assert_ne!(a.data, c.data);
|
||||
}
|
||||
|
||||
/// Alpha-weighted centroid of the pixels in columns `x0..x1`.
|
||||
fn centroid(img: &Image, x0: i64, x1: i64) -> [f32; 2] {
|
||||
let (mut sx, mut sy, mut sa) = (0.0, 0.0, 0.0);
|
||||
for x in x0..x1 {
|
||||
for y in 0..img.height as i64 {
|
||||
let a = alpha(img, x, y);
|
||||
sx += a * x as f32;
|
||||
sy += a * y as f32;
|
||||
sa += a;
|
||||
}
|
||||
}
|
||||
[sx / sa, sy / sa]
|
||||
}
|
||||
|
||||
fn wiggle_transform(ids: &mut Ids, params: &[(&str, Value)]) -> PropGroup {
|
||||
op(ids, "wiggler", params)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiggle_transform_moves_paths_over_time() {
|
||||
let run = |position: [f64; 2], t: f64| {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let w = wiggle_transform(ids, &[("transform/position", Value::Vec2(position))]);
|
||||
vec![build::shape_rect(ids, [20.0, 20.0], [0.0, 0.0], 0.0), w, build::shape_fill(ids, RED)]
|
||||
});
|
||||
render_frame(&p, cid, Tick::from_seconds_f64(t), 1.0)
|
||||
};
|
||||
// The defaults wiggle nothing: the square stays centered on pixel 99.5, 49.5.
|
||||
let rest = [99.5, 49.5];
|
||||
assert_eq!(centroid(&run([0.0, 0.0], 0.37), 0, 200), rest);
|
||||
let a = run([15.0, 15.0], 0.37);
|
||||
assert_eq!(a.data, run([15.0, 15.0], 0.37).data, "the same time gives the same frame");
|
||||
let b = run([15.0, 15.0], 1.21);
|
||||
let (ca, cb) = (centroid(&a, 0, 200), centroid(&b, 0, 200));
|
||||
assert!(ca != rest && ca != cb, "{ca:?} {cb:?}");
|
||||
for c in [ca, cb] {
|
||||
assert!((c[0] - rest[0]).abs() <= 15.01 && (c[1] - rest[1]).abs() <= 15.01, "{c:?}");
|
||||
}
|
||||
// A translation keeps the square's area.
|
||||
assert!((coverage(&a) - 400.0).abs() < 1.0, "{}", coverage(&a));
|
||||
// The layer cache keys a layer with a Wiggle Transform by time.
|
||||
let (p, cid) = shape_comp(|ids| vec![build::shape_rect(ids, [20.0, 20.0], [0.0, 0.0], 0.0), wiggle_transform(ids, &[]), build::shape_fill(ids, RED)]);
|
||||
let comp = p.comp(cid).unwrap();
|
||||
let key = |t: f64| crate::cache::layer_key(&crate::eval::EvalCtx::new(&p, cid, comp, Tick::from_seconds_f64(t)), &comp.layers[0], 1.0, false, false);
|
||||
assert_ne!(key(0.37), key(1.21));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiggle_transform_scales_and_rotates() {
|
||||
let run = |params: &[(&str, Value)]| {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let w = wiggle_transform(ids, params);
|
||||
vec![build::shape_rect(ids, [40.0, 40.0], [0.0, 0.0], 0.0), w, build::shape_fill(ids, RED)]
|
||||
});
|
||||
render_frame(&p, cid, Tick::from_seconds_f64(0.37), 1.0)
|
||||
};
|
||||
let scaled = coverage(&run(&[("transform/scale", Value::Vec2([50.0, 50.0]))]));
|
||||
assert!((scaled - 1600.0).abs() > 20.0, "{scaled}");
|
||||
let rotated = run(&[("transform/rotation", Value::Scalar(45.0))]);
|
||||
assert!((coverage(&rotated) - 1600.0).abs() < 2.0, "{}", coverage(&rotated));
|
||||
assert!(alpha(&rotated, 81, 31) < 0.5 || alpha(&rotated, 119, 31) < 0.5, "a corner turned away");
|
||||
}
|
||||
|
||||
/// A gradient above a Wiggle Transform moves with the paths it paints, as under a group transform.
|
||||
#[test]
|
||||
fn wiggle_transform_moves_the_gradient_with_the_path() {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let g = Gradient { colors: vec![(0.0, [0.0, 0.0, 0.0, 1.0]), (1.0, [1.0, 1.0, 1.0, 1.0])], opacities: vec![(0.0, 1.0), (1.0, 1.0)] };
|
||||
vec![
|
||||
build::shape_rect(ids, [40.0, 40.0], [0.0, 0.0], 0.0),
|
||||
build::shape_gradient_fill(ids, false, [-20.0, 0.0], [20.0, 0.0], g),
|
||||
wiggle_transform(ids, &[("transform/position", Value::Vec2([60.0, 0.0]))]),
|
||||
]
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.37), 1.0);
|
||||
let cols: Vec<i64> = (0..200).filter(|&x| alpha(&img, x, 50) > 0.99).collect();
|
||||
let (l, r) = (cols[0], cols[cols.len() - 1]);
|
||||
assert!((l - 80).abs() > 3, "the square moved: {l}");
|
||||
let (cl, cr) = (img.get(l, 50), img.get(r, 50));
|
||||
assert!(cl[0] < 0.1 && cr[0] > 0.9, "the ramp runs edge to edge: {cl:?} {cr:?}");
|
||||
}
|
||||
|
||||
/// A gradient that paints every copy of a Repeater moves with the copies when they wiggle alike
|
||||
/// (Correlation 100).
|
||||
#[test]
|
||||
fn wiggle_transform_moves_a_gradient_shared_by_repeater_copies() {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let g = Gradient { colors: vec![(0.0, [0.0, 0.0, 0.0, 1.0]), (1.0, [1.0, 1.0, 1.0, 1.0])], opacities: vec![(0.0, 1.0), (1.0, 1.0)] };
|
||||
vec![
|
||||
build::shape_rect(ids, [20.0, 20.0], [-60.0, 0.0], 0.0),
|
||||
build::shape_repeater(ids, 3.0, [60.0, 0.0]),
|
||||
build::shape_gradient_fill(ids, false, [0.0, -10.0], [0.0, 10.0], g),
|
||||
wiggle_transform(ids, &[("correlation", Value::Scalar(100.0)), ("transform/position", Value::Vec2([0.0, 15.0]))]),
|
||||
]
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.9), 1.0);
|
||||
for cx in [40, 100, 160] {
|
||||
let rows: Vec<i64> = (0..100).filter(|&y| alpha(&img, cx, y) > 0.99).collect();
|
||||
let (t, b) = (rows[0], rows[rows.len() - 1]);
|
||||
assert!((t - 40).abs() > 3, "copy at {cx} moved: {t}");
|
||||
let (ct, cb) = (img.get(cx, t), img.get(cx, b));
|
||||
assert!(ct[0] < 0.1 && cb[0] > 0.9, "copy at {cx}: {ct:?} {cb:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient over copies that wiggle on their own keeps its place: every copy shows the ramp's
|
||||
/// middle at the group's origin row.
|
||||
#[test]
|
||||
fn wiggle_transform_keeps_a_gradient_over_copies_wiggling_apart() {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let g = Gradient { colors: vec![(0.0, [0.0, 0.0, 0.0, 1.0]), (1.0, [1.0, 1.0, 1.0, 1.0])], opacities: vec![(0.0, 1.0), (1.0, 1.0)] };
|
||||
vec![
|
||||
build::shape_rect(ids, [20.0, 40.0], [-60.0, 0.0], 0.0),
|
||||
build::shape_repeater(ids, 3.0, [60.0, 0.0]),
|
||||
build::shape_gradient_fill(ids, false, [0.0, -20.0], [0.0, 20.0], g),
|
||||
wiggle_transform(ids, &[("correlation", Value::Scalar(0.0)), ("transform/position", Value::Vec2([0.0, 15.0]))]),
|
||||
]
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.9), 1.0);
|
||||
for cx in [40, 100, 160] {
|
||||
let c = img.get(cx, 50);
|
||||
assert!(c[3] > 0.99 && (c[0] - 0.5).abs() < 0.05, "copy at {cx}: {c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies wiggling on their own under one 50 % fill stay one paint: where they overlap, the fill
|
||||
/// covers them once.
|
||||
#[test]
|
||||
fn wiggle_transform_keeps_one_fill_over_overlapping_copies() {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let mut fill = build::shape_fill(ids, RED);
|
||||
set(&mut fill, "opacity", Value::Scalar(50.0));
|
||||
vec![
|
||||
build::shape_rect(ids, [40.0, 40.0], [-20.0, 0.0], 0.0),
|
||||
build::shape_repeater(ids, 3.0, [20.0, 0.0]),
|
||||
fill,
|
||||
wiggle_transform(ids, &[("correlation", Value::Scalar(0.0)), ("transform/position", Value::Vec2([0.0, 15.0]))]),
|
||||
]
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.9), 1.0);
|
||||
let top = img.data.iter().map(|px| px[3]).fold(0.0f32, f32::max);
|
||||
assert!((top - 0.5).abs() < 0.01, "{top}");
|
||||
}
|
||||
|
||||
/// A long stack of Repeaters, each cleared by a zero-copy Repeater, still renders the last Repeater's copies.
|
||||
#[test]
|
||||
fn long_repeater_stack_with_cleared_copies_renders() {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let mut items = vec![];
|
||||
for _ in 0..70 {
|
||||
items.push(build::shape_rect(ids, [20.0, 20.0], [0.0, 0.0], 0.0));
|
||||
items.push(build::shape_repeater(ids, 3.0, [10.0, 0.0]));
|
||||
items.push(build::shape_repeater(ids, 0.0, [10.0, 0.0]));
|
||||
}
|
||||
items.push(build::shape_rect(ids, [20.0, 20.0], [0.0, 0.0], 0.0));
|
||||
items.push(build::shape_repeater(ids, 3.0, [30.0, 0.0]));
|
||||
items.push(wiggle_transform(ids, &[("correlation", Value::Scalar(0.0)), ("transform/position", Value::Vec2([0.0, 15.0]))]));
|
||||
items.push(build::shape_fill(ids, RED));
|
||||
items
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.37), 1.0);
|
||||
assert!((coverage(&img) - 1200.0).abs() < 2.0, "{}", coverage(&img));
|
||||
}
|
||||
|
||||
/// Copies a Repeater makes above a Wiggle Transform wiggle on their own (Correlation 0) or as one
|
||||
/// (Correlation 100); with the Wiggle Transform above the Repeater every copy wiggles the same.
|
||||
#[test]
|
||||
fn wiggle_transform_and_repeater_order() {
|
||||
let offsets = |wiggle_first: bool, correlation: f64| {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let rect = build::shape_rect(ids, [20.0, 20.0], [-60.0, 0.0], 0.0);
|
||||
let rep = build::shape_repeater(ids, 3.0, [60.0, 0.0]);
|
||||
let w = wiggle_transform(ids, &[("correlation", Value::Scalar(correlation)), ("transform/position", Value::Vec2([0.0, 15.0]))]);
|
||||
let fill = build::shape_fill(ids, RED);
|
||||
if wiggle_first { vec![rect, w, rep, fill] } else { vec![rect, rep, w, fill] }
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.37), 1.0);
|
||||
[(0, 70), (70, 130), (130, 200)].map(|(x0, x1)| centroid(&img, x0, x1)[1] - 50.0)
|
||||
};
|
||||
let own = offsets(false, 0.0);
|
||||
assert!((own[0] - own[1]).abs() > 0.5 || (own[1] - own[2]).abs() > 0.5, "copies wiggle on their own: {own:?}");
|
||||
// A Repeater inside a group above the Wiggle Transform: its copies still wiggle on their own.
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
let inner = vec![build::shape_rect(ids, [20.0, 20.0], [-60.0, 0.0], 0.0), build::shape_repeater(ids, 3.0, [60.0, 0.0]), build::shape_fill(ids, RED)];
|
||||
let group = build::shape_group(ids, "Repeated", inner);
|
||||
vec![group, wiggle_transform(ids, &[("correlation", Value::Scalar(0.0)), ("transform/position", Value::Vec2([0.0, 15.0]))])]
|
||||
});
|
||||
let img = render_frame(&p, cid, Tick::from_seconds_f64(0.37), 1.0);
|
||||
let nested = [(0, 70), (70, 130), (130, 200)].map(|(x0, x1)| centroid(&img, x0, x1)[1] - 50.0);
|
||||
assert!((nested[0] - nested[1]).abs() > 0.5 || (nested[1] - nested[2]).abs() > 0.5, "nested copies: {nested:?}");
|
||||
for same in [offsets(false, 100.0), offsets(true, 0.0)] {
|
||||
assert!(same[0].abs() > 0.5, "{same:?}");
|
||||
assert!((same[0] - same[1]).abs() < 0.05 && (same[1] - same[2]).abs() < 0.05, "copies wiggle as one: {same:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zigzag_and_twist_render() {
|
||||
let (p, cid) = shape_comp(|ids| {
|
||||
|
||||
@@ -510,6 +510,7 @@ pub struct PlacedGlyph {
|
||||
pub fill: [f32; 4],
|
||||
pub stroke: [f32; 4],
|
||||
pub stroke_width: f64,
|
||||
pub join: effectcraft_path::Join,
|
||||
/// Faux Bold (or synthetic bold for a family without a bold face): how far the outline grows
|
||||
/// on every side, in character space (0 when not bold).
|
||||
pub bold: f64,
|
||||
@@ -838,6 +839,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option<TextGeom> {
|
||||
fill,
|
||||
stroke,
|
||||
stroke_width: (base_stroke_w + x.stroke_width).max(0.0),
|
||||
join: path_join(doc.line_join),
|
||||
bold: if g.synth_bold { FAUX_BOLD_RADIUS * g.size } else { 0.0 },
|
||||
apply_fill: st.apply_fill,
|
||||
char_index: g.char_index,
|
||||
@@ -853,6 +855,15 @@ pub fn glyph_paths(ctx: &EvalCtx, layer: &Layer) -> Vec<(BezPath, CharXf)> {
|
||||
text_geom(ctx, layer).map(|t| t.glyphs.iter().map(|g| (g.path(), g.xf)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn path_join(j: effectcraft_keyframe::LineJoin) -> effectcraft_path::Join {
|
||||
use effectcraft_keyframe::LineJoin;
|
||||
match j {
|
||||
LineJoin::Miter => effectcraft_path::Join::Miter,
|
||||
LineJoin::Round => effectcraft_path::Join::Round,
|
||||
LineJoin::Bevel => effectcraft_path::Join::Bevel,
|
||||
}
|
||||
}
|
||||
|
||||
/// One paint pass of a glyph: fill (false) or stroke (true).
|
||||
type Pass = (usize, bool);
|
||||
|
||||
@@ -919,7 +930,7 @@ fn raster_pass(g: &PlacedGlyph, stroke: bool, paths: &[BezPath], m: &Mat3, rect:
|
||||
}
|
||||
outer
|
||||
} else if stroke {
|
||||
let st = StrokeStyle { width: g.stroke_width, join: effectcraft_path::Join::Round, ..Default::default() };
|
||||
let st = StrokeStyle { width: g.stroke_width, join: g.join, ..Default::default() };
|
||||
effectcraft_path::stroke_coverage(paths, &st, &mm, w, h)
|
||||
} else {
|
||||
grown_coverage(paths, &mm, w, h, bold)
|
||||
|
||||
@@ -598,6 +598,7 @@ fn touching_glyphs_join_without_a_seam() {
|
||||
fill,
|
||||
stroke: [0.0; 4],
|
||||
stroke_width: 0.0,
|
||||
join: effectcraft_path::Join::Miter,
|
||||
bold: 0.0,
|
||||
apply_fill: true,
|
||||
char_index: 0,
|
||||
@@ -668,6 +669,7 @@ fn faux_bold_under_a_non_uniform_scale() {
|
||||
fill: [1.0; 4],
|
||||
stroke: [0.0; 4],
|
||||
stroke_width: 0.0,
|
||||
join: effectcraft_path::Join::Miter,
|
||||
bold: 2.0,
|
||||
apply_fill: true,
|
||||
char_index: 0,
|
||||
|
||||
@@ -572,7 +572,7 @@ pub fn extruded_meshes(ctx: &EvalCtx, layer: &Layer, p: &model::extrude::Extrude
|
||||
groups.push((vec![g.local.clone()], c, g.m, usize::from(!stroke_on_top), g.xf.surface_colors(c)));
|
||||
}
|
||||
if g.stroke_width > 0.0 && g.stroke[3] * op > 0.0 {
|
||||
let st = effectcraft_path::StrokeStyle { width: g.stroke_width, join: effectcraft_path::Join::Round, ..Default::default() };
|
||||
let st = effectcraft_path::StrokeStyle { width: g.stroke_width, join: g.join, ..Default::default() };
|
||||
if let Some(region) = crate::shapes::stroke_region(std::slice::from_ref(&g.local), &st) {
|
||||
let c = [g.stroke[0], g.stroke[1], g.stroke[2], g.stroke[3] * op];
|
||||
groups.push((vec![region], c, g.m, usize::from(stroke_on_top), g.xf.surface_colors(c)));
|
||||
|
||||
Reference in New Issue
Block a user