Commit Graph
3793 Commits
Author SHA1 Message Date
dedandClaude Opus 5.5 49ec37ec3f Disk cache: an edit that leaves the footage alone keeps its salt (#1926)
Session::bump took the disk cache's footage salt at every edit, which reads the size and time of
every footage file, each frame of an image sequence included. With three 5000-frame sequences a
caret move or a typed character in a text field took ~0.6 s on the UI thread (9 ms with the disk
cache off). The salt is now taken again only when the footage items changed, or when a project
is opened, footage is reloaded or the disk cache attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:40:24 +03:00
dedandClaude Opus 5.5 a330110476 G5: a styled layer rendered by two workers at once shares its body and passes (#1812)
Review: styled() kept the buffers it rendered instead of the ones LayerCache::insert returned, so
without a blending-space conversion concurrent misses still returned separate body and pass
buffers (and the GPU uploaded each).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:12:22 +03:00
tylerjenningsw 8ace9aac7f Clipboard: keyframe text starts the layer with a Layer line (#1908) 2026-10-20 09:10:29 -04:00
tylerjenningsw 47093d997a Clipboard: keyframe text has the source's pixel aspect (#1908) 2026-10-20 08:52:55 -04:00
tylerjenningsw 22ddf7bb99 Clipboard: keyframe text for a 2D layer writes the Z it shows (#1908) 2026-10-20 08:51:09 -04:00
tylerjenningsw 6b77e6a1a7 Clipboard: keyframe text leaves out Transform properties the layer doesn't use (#1908) 2026-10-20 08:49:33 -04:00
tylerjenningsw 9313784304 Clipboard: Orientation and Rotation headers as After Effects writes them (#1908) 2026-10-20 08:47:51 -04:00
Camille Moine 14453386ac Viewer: keep hidden AV layers' controls visible (#1769) 2026-10-20 14:46:05 +02:00
Camille Moine c7eb48dc3a Layer: reveal layer sources in the Project panel (#1809) 2026-10-20 13:59:01 +02:00
Camille Moine 8d887fd6da Layer: replace a layer's source with Alt-drag and Ctrl+Alt+/ (#991, #813)
Alt/Option-drag a Project item onto a Timeline layer, or press Ctrl+Alt+/ (Cmd+Opt+/) with layers and an item selected, to swap the layer's source. The layer keeps its name, timing, transforms, keyframes, expressions, effects, masks, switches, parenting and track matte; a source with audio adds Audio properties when missing. Precompositions can't be nested inside themselves.

AVLayer.replaceSource now runs the same engine command and closes the replaceSource part of #813.
2026-10-20 12:18:46 +02:00
dedandClaude Opus 5.5 9c5494fd29 G5: a layer rendered by two workers at once is shared from the cache (#1812)
Preview workers that start together miss the layer cache together, and each kept its own copy
of every layer; on the GPU each copy was uploaded and packed into its own Classic 3D atlas, which
ran the device out of memory on a cold start. LayerCache::insert now keeps the first buffer of a
key and returns it, and the renderer goes on with the returned buffer, so the workers share one.
Renders don't wait for each other: on the shared frame pool a blocking wait could end up beneath
the work it waits for (tried, it stalled playback for the wait's timeout).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:31:25 +03:00
dedandClaude Opus 5.5 ac5799bd26 G5: the playing prefetch window starts at the first uncached frame, ahead × 3 wide
Looking up to the whole RAM preview for uncached frames let the workers spread over the preview
while the playhead waited. On the GPU each worker then built a Classic 3D atlas for its own set
of layers, and on a cold start of the #1812 project the device ran out of memory far more often
(first pass 1-56 % of real time instead of 63-81 %). The window now keeps its old width but
starts at the first frame ahead that isn't cached, so a loop or a restart still renders on at
once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:43:30 +03:00
dedandClaude Opus 5.5 4cf6580b19 G5: a reduced still's cache key includes its nominal size (#1812)
Review: two footage items of one file with different nominal sizes could share a sized entry,
though one is resampled and the other kept as decoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:42:22 +03:00
tylerjenningsw 40d5466cc5 Clipboard: Copy puts keyframe text on the system clipboard (#1908)
Edit > Copy with keys or Transform properties selected puts the After Effects keyframe text on the system clipboard, for other applications such as AE2Blend, in place of the 'EffectCraft: N keyframes' note. Ctrl+V in EffectCraft still pastes from its own clipboard.
2026-10-19 22:49:45 -04:00
tylerjenningsw 598f0d51c1 Clipboard: copied keyframes as After Effects keyframe text (#1908)
Copying keys or unanimated properties of one layer records them as After Effects keyframe text: its Transform properties, one row per key with the frame in comp time (start offset and stretch applied) or a frameless row for a static value. EffectCraft's own keyframe clipboard is unchanged.
2026-10-19 22:49:41 -04:00
dedandClaude Opus 5.5 e14cdd6009 Layer cache: animated masks with mask motion blur key the layer time
Review: mask motion blur draws an animated mask at the shutter's other times, but the layer key
hashed its value at this time only. Two frames with the same mask value (a hold before a move)
shared a cached layer, with the wrong blur. This applied to solids, text and shapes already, and
to stills since this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:21:53 +03:00
dedandClaude Opus 5.5 ea1f2fe984 G5: a still decoded larger than its nominal size is resampled like the renderer does (#1812)
Review: still_at_size returned a smart object's pixels unresampled when they differed from the
footage's nominal size. It now follows footage_buf: pixels larger than nominal are resampled to
the requested size, smaller ones are left for the renderer to scale up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:21:14 +03:00
SBaksa f41aa134a8 Text Range Selector test: change Units through the Timeline popup (#1814)
The Index-units test now opens the Units popup in the Timeline and picks
Index, asserts the stored Units value, then reads the painted Start, End
and Offset, so it also fails when the Units control doesn't update the
session.
2026-10-20 12:09:33 +11:00
dedandClaude Opus 5.5 c33d5c20df G5: a still asked for at its own size is the unsized frame (#1812)
Review: frame_at_size at a still's native size made a second cache key, so the pool decoded
and kept a second full-size copy of a frame thumbnail() or frame() already had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:01:07 +03:00
dedandClaude Opus 5.5 ff20f8ec41 G5: a playing preview prefetches past the frames it already cached
The playback prefetch only looked ahead*3 frames (96 with 16 workers) for uncached frames.
After a loop or a restart from the start those were all cached, so the workers stood idle until
the playhead came within that window of the end of the cached run, then the preview stalled.
A playing preview now looks as far ahead as the RAM preview holds (and the range spans) for
the next uncached frames, still queuing at most ahead at a time; eviction already spares the
frames the playhead reaches first (#595).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:43:35 +03:00
dedandClaude Opus 5.5 fd1250d224 G5: still footage layers are kept in the layer cache across times (#1812)
Footage layers had no layer cache key, so a still made a new layer buffer every frame on every
preview worker, and the GPU compositor uploaded it again each time (its upload cache is keyed
by buffer). Stills (without a proxy) are now keyed like text and shape layers, without the
source time; movies and sequences are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:43:33 +03:00
dedandClaude Opus 5.5 57326dc6a7 G5: stills at a reduced render size are resampled once and kept at that size (#1812)
The media pool honoured the requested size only for movies; stills came back
at full size and the renderer resampled them on every frame. A 21120 px wide
plate at Quarter was re-filtered each frame, and four of them (1.85 GB as f32)
did not fit the 1 GiB pool, so they were also decoded again and again.

Stills are now resampled once with effectcraft_raster::resample (the same
call the renderer made, so pixels are unchanged) and the pool keeps the
reduced copy under the sized key.

#1812 project at Quarter, CLI bench: 410 ms -> 33 ms a frame (26 ms with the
layer cache); desktop playback 2% -> 60-99% of real time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:43:33 +03:00
vangel76andClaude Sonnet 5.5 7c8ea2713d Review: Delete in the render queue does nothing while a menu is open
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 01:41:23 +02:00
vangel76andClaude Sonnet 5.5 67ccf44fba Review: behavior preset test also covers redo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 01:38:45 +02:00
TimotheusG f667b6e11e fix(script): retire streams after failed mode transitions 2026-10-19 19:19:20 -04:00
TimotheusG c4505d34de test(render): document automatic backend fixtures 2026-10-19 18:47:51 -04:00
TimotheusG 4a84b4da08 docs(script): explain socket dispatch and regression cases 2026-10-19 18:26:34 -04:00
TimotheusG e551cbe2ca test(render): use controlled costs for Auto backend checks 2026-10-19 18:26:34 -04:00
Egon 5000 ce928a66da Merge remote-tracking branch 'origin/main' into ui/shape-group-elided-contents
# Conflicts:
#	docs/ui-parity.md
2026-10-19 23:00:22 +02:00
Egon 5000 89eaac0489 Merge remote-tracking branch 'origin/main' into menu-bar-native-behaviour
# Conflicts:
#	crates/ui-egui/src/panels/timeline.rs
2026-10-19 22:59:07 +02:00
Wes CampbellandClaude Opus 5.5 a3d5e55bc7 EPS: Illustrator EPS files draw their artwork (#955)
Illustrator EPS imported as an empty layer. Three gaps in the PostScript
interpreter, each against the PostScript Language Reference:

- An operator stored under another name (`/mo /moveto load def`, as
  Illustrator's prolog does for every path and paint operator) pushed the
  operator's name instead of running it, so nothing was drawn.
- Device queries that return values (currentpagedevice, currentscreen,
  currentflat, currenthalftone, status, dictstack, ...) pushed nothing,
  shifting the stack under the procset's guards. They now push a plain
  device's defaults; setscreen pops its three operands.
- `currentfile 0 (eod) /SubFileDecode filter` now skips to the end-of-data
  string, so an embedded XMP packet isn't run as PostScript.

The regression test uses synthetic PostScript only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:33:59 -05:00
TimotheusG d6afa87641 fix(text): isolate CJK fallback language and cache entries (#1141) 2026-10-19 15:30:35 -04:00
Camille Moine 45f54a4221 effects: fix the grain-size drift from the crop halo
The crop kept 3 px of halo, which makes the blur exact inside a box, but the
lag-1 correlation also reads the right neighbour's high-pass residual — one
column outside the box, still polluted by the crop edge when another box
covers it (adjacent boxes are the norm with a step of size/2). The size
estimate drifted by ~5e-5 in the parity test; a 4 px halo removes it.

The parity test now holds std to 1e-6 and size to 1e-7 (was 5 % for size),
and the boxed statistics still cost 1.8 / 3.1 ms at 1080p / 4K.
2026-10-19 21:22:03 +02:00
Camille Moine a05d97a0e9 effects: speed up Match Grain's CPU analysis
The sample-box search rescanned every candidate box and the grain statistics
blurred the whole image three times per layer, although Match Grain only reads
the 80 sample boxes (~6.5k pixels). On a 4K frame that analysis was ~90 % of
the effect.

- measure the statistics from small crops around each box (3 px halo, the
  Gaussian's support), deduplicating overlapping boxes so the sums match the
  previous masked version;
- search the candidate box rows in parallel (the sort is a total order, so the
  chosen boxes are unchanged);
- compute the "own" boxes and stats only when Compensate for Existing Noise is
  on, and clone the frame for Preview composition only in Preview mode;
- run Plane::transpose/map_rows on the calling thread for small planes: the
  rayon dispatch dominated per-box crops (240 blurs of ~15x15).

Release, Ryzen Threadripper 1920X, noisy source / flat target, defaults, best
of 4 (1080p / 4K):
- grain statistics: 151/463 ms -> 1.2/2.8 ms
- sample boxes (source): 43/179 ms -> 16/71 ms
- end to end: 266/1416 ms -> 99/465 ms (Compensate 50 %: 392/1830 -> 104/482)

Tests: the boxed statistics match the masked ones (overlapping and corner
boxes, alpha holes), plus the existing Match Grain/Remove Grain suites.
2026-10-19 20:44:32 +02:00
NetterSilverandClaude Haiku 5.5 eca2c32793 Fix hsv_to_rgb returning magenta for tiny negative hues
Found while testing crates/color/src/lib.rs:143-158

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 20:31:18 +02:00
NetterSilverandClaude Haiku 5.5 81548edff9 Resolve SVG font-size percentages against the parent font size
Found while testing crates/svg/src/lib.rs:624

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 20:22:36 +02:00
vangel76andClaude Opus 5.5 daedbd5ca1 Review: short "d" falls back to the font ascent; tracking unit comment; Lottie hunk out
- first_ascent: a "d" shorter than 0.3 x the font ascent (a symbol or icon font) no
  longer puts box text's first baseline almost at the box top; the font ascent is used,
  as before the "d" rule (CodeRabbit on #1875). Test:
  a_short_d_falls_back_to_the_font_ascent.
- CharXf::track_before / track_after are pixels, not 1/1000 em (CodeRabbit).
- The Lottie export of style-run tracking is back to what it was: the pixel conversion
  was not checked against a player, so it is left for a separate, verified change.
- The Subtract clamp in Mode::combine is intended for every animator property: the
  selectors combine into one selection per character for the whole animator, as in
  After Effects; the read-back observed it through Tracking (now said in the code).
- Per-character 3D rotation notes that #1876 adds Mat4::rotation_xyz for the same order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:10:21 +02:00
TimotheusG 0a78eb2870 fix(script): bound and cancel socket writes (#1248) 2026-10-19 12:56:33 -04:00
vangel76andClaude Opus 5.5 83c76f266f Review: Glow's WGSL literals tied to GLOW_GAIN / GLOW_KNEE; parity summary consistent
kernels.wgsl repeats Glow's bright-pass gain 8.0 and knee 0.0611 as literals; they now
name effects::GLOW_GAIN / GLOW_KNEE (exported) and the test
glow_constants_match_the_cpu (no GPU needed) fails if they drift. docs/effects-parity.md:
the summary row no longer says no effect is compared with After Effects while the
Blur & Sharpen row reports Radial Blur's comparison (CodeRabbit on #1873), and the
Radial Blur changelog row says CPU only (it has no GPU path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:49:00 +02:00
vangel76andClaude Opus 5.5 e559a7ce9a Review: tie the colour kernels' literals to the CPU constants; visible GPU skip; B&C wording
WGSL cannot read Rust constants, so the Rec. 601 luma (Tint, Tritone, Luma Key /
Extract), Black & White's tint weights and Exposure's 2.2 power stay literals in the
kernels; they now name their Rust source (effects::LUMA_601, BW_TINT_LUMA,
EXPOSURE_LINEAR_GAMMA, newly exported) and the test shader_constants_match_the_cpu
(no GPU needed) fails if a literal drifts. gpu_tint_defaults_match_after_effects_greys
says "skipped: no GPU adapter" when it skips (EFFECTCRAFT_REQUIRE_GPU=1 still makes it
fail). docs/effects-parity.md: Brightness & Contrast is close to, not the same as,
After Effects (up to 6.2/255), and the summary row lists what is measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:38:41 +02:00
NetterSilverandClaude Haiku 5.5 e244d64235 Reject non-finite numbers in CSS colour parsing
Found while testing crates/svg/src/color.rs:182-185

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 18:25:20 +02:00
vangel76andClaude Opus 5.5 8b715fc227 Masks: coarsen the feather grid instead of dropping its padding on huge layers
Above MAX_GRID_PX the padded feather grid fell back to the unpadded buffer, so a
feathered Add mask just outside a very large layer (e.g. 8192 x 8192, feather 40)
covered nothing and the whole layer vanished (CodeRabbit on #1874). The grid is now
coarsened by 2, 4, ... until it fits; radii and padding scale with it, so the feather
of an edge outside the layer still fades into it. The doc comment on the Linear falloff
no longer says it is unchanged: wide linear ramps now go through the coarse grid.
Regression tests: a_grid_too_big_is_coarsened_and_keeps_its_padding,
a_coarsened_grid_still_feathers_a_mask_outside_the_buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:18:58 +02:00
vangel76andClaude Opus 5.5 d9205fa5f4 Curves: a natural cubic spline through the points, as After Effects
After Effects' Curves effect interpolates its points with a natural cubic
spline (zero curvature at the ends), clamped to 0..1: AE 2026 renders of an
RGB curve (0,0) (125,199) (255,255) and a Red curve (0,0) (128,44)
(255,255) (cases 962 / 963, ROUND4) give exactly the natural spline's values
on every grey step (0, 30, 59, 115, 164, 202, 227, 242, 250, 255 and 0, 0, 1,
6, 19, 44, 83, 135, 194, 255). EffectCraft used a monotone (Fritsch-Carlson)
spline: 34 / 67 / 125 / 171 ... on the RGB curve, up to 10/255 off (ROUND4
measured up to 6 levels on real projects); now within 1/255, CPU and GPU (the
same Hermite tables carry the spline's knot slopes). Lumetri's curves and
Glow's Arbitrary Map keep the monotone spline (not measured).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:54:31 +02:00
vangel76andClaude Opus 5.5 8b64074ce3 Text: per-character 3D rotation turns Z first, as a 3D layer and After Effects
With per-character 3D on, After Effects turns each character by Rx * Ry * Rz
(Z first), as it turns a 3D layer's X / Y / Z Rotation (case 931, AE 2026:
"H" at 400 px with animator X / Y / Z Rotation 40 / 30 / 20). EffectCraft
composed Rz * Ry * Rx, so the glyph faced another way: 23 of 5184 grid
points off (max 255), now 3 at the anti-aliased edge (mean 0.015/255). The
same order fix-orientation uses for layers; texts with one rotation axis
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:41:16 +02:00
vangel76andClaude Opus 5.5 8ea876914d Text animators: Tracking Amount in pixels, Subtract selectors stop at 0, as After Effects
A text animator's Tracking Amount is in pixels in After Effects 2026, not in
1/1000 em like the Character panel's tracking: Amount 30 on every character
adds 30 px between neighbours (half before, half after each) at 80 px and at
40 px. And a Subtract range selector over characters the first selector
leaves out keeps them at 0 instead of selecting them negatively. With both,
the EXPORTER's t8 layer D (Tracking 30 on characters 2-4 by index, Subtract
over the last 20 %) puts every glyph where AE does (it ended 88 px short,
1553 vs 1641; now 1641).

The "Tracking In" text preset animates 40 -> 0 px (was 400 -> 0 thousandths
of an em, about the same at 100 px text); Lottie export writes a style run's
tracking (1/1000 em) as pixels for its tracking animator, which Lottie
players add in pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:41:15 +02:00
vangel76andClaude Opus 5.5 83b134cdc8 Text: right-to-left point text with a justified paragraph sits flush right, as After Effects
After Effects 2026 lays out right-to-left point text whose paragraph carries a
justified code (justify last left / centre / right, justify all) flush at
its start, the right, like right-aligned text (EXPORTER read-back t7: point
rows with codes 3-6 at -152..-8 from the anchor). EffectCraft placed it by
the last-line side (left / centre / left). Left-to-right point text and box
text are unchanged; AE's slightly wider right-to-left box wrapping (1-2 px
narrower word gaps) stays open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:41:15 +02:00
vangel76andClaude Opus 5.5 8bc57bc7ad Text: Justify All balances the lines with the every-line composer, as After Effects
With Justify All After Effects stretches the paragraph's last line too, and
its every-line composer breaks the paragraph so that line is not a lone
word: a 400 px box that the other justifications break into 6 / 6 / 1 words
breaks 4 / 4 / 4 (AE 2026, EXPORTER read-back t6). EffectCraft's composer
left the last line's slack out of its cost, so "twelve" stood alone on a
stretched line; it now counts that slack for Justify All and every word
starts within 1 px of AE's (what remains in that box is the known box-text
baseline offset). Other justifications and the single-line composer are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:41:03 +02:00
vangel76andClaude Opus 5.5 a7255f7608 Text: box text's first baseline is the height of "d" below the box top, as After Effects
After Effects puts the first baseline of paragraph (box) text the height of the
font's lowercase "d" (in whole thousandths of an em) below the box top, at the
text's nominal size; superscript keeps the line there and only raises its glyphs.
EffectCraft used the font ascent of the drawn size plus the superscript rise.
Measured in AE 2026 (case 818, box at y -200): Times New Roman 100 px -130.50
(EffectCraft -110.9), 50 px -165.25, superscript -163.80 (EffectCraft drew it
15.8 px lower), Arial 100 px -128.40, superscript -161.70; now within 0.15 px.
517 (four wrapped lines, Times New Roman superscript): the line bands now land
on AE's rows exactly (336-388, 434-486, 532-584, 643-682; were 16 px lower).

A face without a "d" outline, and vertical type (not measured), keep the font
ascent. The paragraph-box test now
expects the caret (full ascent) to start a little above the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:41:03 +02:00
vangel76andClaude Opus 5.5 d57b73d4cb Shape rectangles start at the top-right, as After Effects, so Trim Paths cut the same segment
After Effects draws a Rectangle Path clockwise from its top-right corner, or
with round corners from the end of the top-right arc on the right edge;
Trim Paths measures from there (AE 2026, case 930: 400 x 300 rectangles,
Trim 0-25 % and 70 / 30). EffectCraft built the path with kurbo, starting at
the top-left corner, so trimmed rectangles drew another segment: case 930
was 86 of 5184 grid points off (max 255), now 5 at stroke edges. Start > End
was already drawn swapped, as in AE. Convert to Bezier now gives AE's vertex
order (top-right first); the engine test follows. Lottie rectangles share
the path and now start where lottie players start them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:33:44 +02:00
vangel76andClaude Opus 5.5 710ffd9f7e Shapes: Repeater copy opacity steps 1 / Copies toward End Opacity, as After Effects
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>
2026-10-19 16:33:44 +02:00