Add Per-Corner Roundness to Rectangle Path: a checkbox (off by default) and four Pixels properties (top-left, top-right, bottom-right, bottom-left). New rectangles initialise the four values from their Roundness, so ticking the box keeps the shape; the radii are animatable and expression-driven.\n\nrect_radii() builds the path clockwise from the top-left with kappa quarter-circle arcs. Overlapping radii scale down like CSS border-radius; negative and non-finite radii are square; rect() delegates to it, so uniform roundness clamps to half the shorter side as before.\n\nLottie rc has one r: four equal static radii export as that value; mixed or animated radii export roundness 0 with a warning. Import puts r into the four per-corner values so ticking the box on an imported rectangle keeps the shape.\n\nTests: path geometry and clamping, rendered per-corner pixels, Lottie equal/mixed/checkbox-off export.
Wiggle Paths had Size, Detail, Points, Wiggles/Second, Correlation,
Temporal Phase and Random Seed, but no Spatial Phase, which After
Effects lists among its properties (ADBE Vector Spatial Phase). Spatial
Phase now moves each point's own displacement along its subpath: 360
degrees is once around, and a fraction of a point is interpolated
between neighbors. At 0 degrees a path draws as before.
A project saved without the property gets it at 0 degrees, after
Temporal Phase, when it opens. Scripts can read and set every Wiggle
Paths property by its match name.
A shape layer's Contents > Add menu had every After Effects path operation
except Wiggle Transform. It is now a shape item (layer.addShapeItem kind
wiggler, "ADBE Vector Filter - Wiggler" for scripts) with Wiggles/Second,
Correlation, Temporal Phase, Spatial Phase, Random Seed and a Transform
group whose Anchor Point, Position, Scale and Rotation set how far each
transformation wiggles.
The values come from the Wiggly selector's noise, with each Repeater copy
as one unit: the copies of a Repeater above the operation, in its group or
a nested one, wiggle independently as far as Correlation allows, and the
copies of a Repeater below it wiggle the same way. A paint above it whose
paths all move by one matrix moves its gradient and stroke scale with them.
The layer cache keys a layer with a Wiggle Transform by time.
After Effects gives Repeater copy i the opacity Start + (End - Start) * i / Copies,
so the last copy stops one step short of End Opacity. EffectCraft reached End
Opacity at the last copy (i / (Copies - 1)). Measured in AE 2026 (case 709: 3
copies, End Opacity 40): copies at 100 / 80 / 60 % (EffectCraft 100 / 70 / 40);
the overlap of the two copies 92 % (EffectCraft 82 %). Now within 2/255 at the
sampled pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
draw_bounds padded every stroke by width × miter limit / 2 on each side, joins or
not. A straight 1080 px wide wipe used as a track matte became a 4326×6366 layer
buffer (~440 MB as RGBA f32), made anew every frame while its trim animates, then
cached on the CPU and uploaded to the GPU. With 12 frames exported in parallel a
16 GB Mac ran out of memory and froze.
Now the miter limit applies only to miter-joined paths with a join (two or more
segments, or closed); square caps reach √2 half widths, other strokes one; Taper
and Wave keep the widest padding. Rendered pixels are unchanged.
A full-res frame of the affected ad: 1.7 GB → 0.7 GB peak.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Merge Paths item appended to a shape layer's Contents cleared every draw
collected so far, including fills and strokes inside groups above it, so
the layer's shapes vanished from the render. AE's rule is per group: only
paint items in the merge's own group above it are dropped. Draws now
record which contents level created them; the merge drops only its own
group's paint, and the group-bubbled draws keep rendering.
Adds a regression test (Merge Shapes and Add modes) that renders nothing
without the fix.
render: the debug-only quantisation check uses debug_assert! instead of panic!. gpu: the
test-only panic on a wgpu validation error is cfg(test) with a narrow allow, and release
builds only log. Test-only unreachable!() calls become panic!(). Denies the panic lints in both
crates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shapes (SHP-2): Stroke Taper (pixels/percent start/end length, width, ease) and
Wave (amount, pixels/cycles, wavelength, phase) rendered as a variable-width
outline; Dash 2/Gap 2/Dash 3/Gap 3 (shape.dashes.add/remove, timeline +/-
buttons); radial gradient Highlight Length/Angle as a focal gradient.
Masks (MSK-1): mask feather points on the Mask Path (mask.featherPoint.*,
Mask Feather tool add/drag), per-point signed radius and tension, rendered
from the signed distance to the path; carried through vertex edits.
Motion blur (LYR-10): Adaptive Sample Limit comp setting; 2D layers take one
sample per pixel of shutter travel between Samples Per Frame and the limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>