Random Seed takes 0..100000, After Effects' recorded valid range (and what
the other effects' Random Seed already takes). The CPU and GPU hash it the
same way with wrapping u32 arithmetic, so any seed costs the same.
Default and slider are unchanged.
Scale Height and Scale Width take -30000..30000 %, After Effects' recorded
valid range in the same units. The CPU and GPU share the matrix
(transform_matrix_px) and the buffer growth, which is capped at 16 Mpx
(transform_grow; the CPU test already renders 100000 %), so larger scales
stay bounded.
Default and slider are unchanged.
Starting and Ending Thickness take 0..4000, After Effects' recorded valid
range in the same units (pixels). The CPU and GPU renderers draw the beam
per pixel from its distance to the segment, within the layer's buffer, so
a thicker beam costs the same.
Default and slider are unchanged.
Scatter Amount takes 0..1000, After Effects' recorded valid range in the
same units (pixels); the slider stays 0..127. The CPU and GPU renderers move
each pixel by one random offset of up to the amount, so a larger amount
costs the same; samples outside the layer are transparent.
Default and slider are unchanged.
Feather takes 0..32000, After Effects' recorded valid range in the same
units. The CPU and GPU renderers evaluate the feather per pixel in closed
form (a Gaussian of the distance to the wedge, radial_wipe_alpha), so a
wide feather costs the same and stays finite.
Default and slider are unchanged.
Width takes 1..32000 and Feather 0..32000, After Effects' recorded valid
ranges in the same units (pixels). The work stays bounded: the renderers
floor the width at 1 buffer pixel, sum at most 40 Gaussian terms per
pixel, and above 8 widths the feather is the even veil
(venetian_cover; the GPU hands that case to the CPU, as before).
Default and slider are unchanged.
Border takes 0..2000, After Effects' recorded valid range in the same units
(pixels). The CPU and GPU renderers compute the border per pixel in closed
form, so a wider border costs the same; one wider than the cells covers
them.
Default and slider are unchanged.
Hue Transform takes -1800..1800 degrees, After Effects' recorded valid range
in the same units. The CPU and GPU renderers wrap the hue (rem_euclid /
fract_euclid), so five turns render like the hue they end on, at the same
cost.
Default and slider are unchanged.
Hue, Lightness and Saturation go up to 1000 % and Grain Size down to 0 (it
keeps its wider maximum, 100), After Effects' recorded valid ranges in the
same units. Noise HLS Auto shares the controls and widens with them.
Defaults and sliders are unchanged.
The renderers need no change: hue wraps, lightness and saturation clamp per
pixel after the noise is added, and Grain Size is floored at 0.1 px on the
CPU and the GPU, so the new values cost the same and stay finite.
The amounts were written as "percent" in keyframe text because their range
was 0..100, and Grain Size would become "percent" at 0..100; UNITS rows keep
the units they had.
Blend goes up to 10000 (it keeps its minimum of 1, below After Effects' 5)
and Jitter up to 500 %, After Effects' recorded valid ranges in the same
units. Defaults and sliders are unchanged.
The CPU and GPU renderers capped Jitter's dither at 100 %, so higher values
would be stored but render as 100. Both now take it from one function,
four_color_jitter, which keeps the dither linear up to 500 %. Blend 10000
is a finite softening (Blend squared) at the same cost.
Jitter was written as "percent" in keyframe text because its range was
0..100; a UNITS row keeps that unit.
Input/Output Black and White of every channel take -10000..10000 and Gamma
goes down to 0 (it keeps its wider maximum, 10), After Effects' recorded
valid ranges in the same units (the .aep mapping is Conv::Plain). Defaults
and the 0..1 / 0.1..3 sliders are unchanged.
Gamma 0 renders as the existing 0.01 floor, an exponent of 100, so an HDR
value a few times Input White would overflow f32 and an infinite curve
times an empty output range is NaN. The curve is now bounded at +-1e6 on
the CPU (Lv::apply) and the GPU (fxt_lv), so the output stays finite over
the whole range; values below the bound are unchanged.
The engine regression (tests/effect_ranges.rs) sets both ends of each row
through prop.set, checks undo/redo, save and reopen, and that a project
saved with the narrower ranges opens with the new ones and its values.
A test walks every effect parameter the .aep table maps to a recorded After
Effects definition (fx_defs) and checks that the effect's hard range
(ParamUi::Slider min..max, what prop.set and the value editors clamp to)
covers After Effects' valid range, converted to EffectCraft's units the way
the importer converts values. Conv::number_scale is that conversion, now
shared by the importer and the test.
The parameters still narrower are allow-listed by issue (#3460-#3492); the
test fails on a new narrower parameter and on a row that is no longer
needed, so the list shrinks as the issues are fixed.
It arrived as a plain number, so Effect Controls neither dragged it over its
slider range nor clamped an edit to its valid range, as After Effects does.
A definition of parameters that are not kept, shared by many instances, was
walked again for each one without spending the budget; long popup item
names were kept per instance with each item counted once.
A pseudo effect's match name (Pseudo/0.25) was its group's match id, so a
match path to its parameters (keyframe paste, pick whip) was cut apart at
'/' and '.' and found nothing. The effect id stays the match name.
A skipped instance was reported through the capped detail list, so past 20 effect losses it was only counted. It now goes through the per-effect grouping under a key of its own, so it is named like any effect without a counterpart and is not merged into a kept instance's line.
A parameter past the budget was skipped and the next one tried, so the
rest of the effect, and every later effect without a counterpart, still
read its shared definitions and built popup items. The first parameter
that does not fit now ends the budget; a later effect without a
counterpart is reported on its own line without reading its definitions.
The docs now say that a placeholder's layer parameters report their keys
and expression.
Such an item is shown on the document, which the size does not confirm; it is now reported in unsupported_items, when the opti stores the document's size.
An item as big as its layer's bounds was taken for Retain Layer Sizes. When
the layer is also as big as the document but not at its corner, a
document-framed item has the same size, so it may show shifted. The opti
record holds the document's size at bytes 32 and 36 (1000, 1000 in every
record of the issue's project, the .psd's size); with it, that case is
reported. The layer still shows at its own bounds.
popup_key lowered only ASCII letters, so an item starting with a capital
outside ASCII (French "Écran") kept it and never matched the same word in
lower case. After Effects stores popup items in its UI language, and
#3432 starts matching such names through popup_alias, keyed by
popup_key. Use Unicode lower case; ASCII keys are unchanged.
* .aep import: recognize French names for Levels' Channel and Clip popups
After Effects stores a popup's items in the UI's own language. A project saved
by a French After Effects writes "RVB", "Activé" and "Désactivé pour la
couleur 32 bpc" for Levels' Channel and Clip to Output Black / White.
EffectCraft's own popup options are English only, so these never matched by
name and the parameter fell back to its default, reported as "has no
counterpart".
Add popup_alias, a small table of confirmed After Effects labels in other
languages, used as a fallback in popup_mapping after the exact name match
fails. A synthetic test reproduces the three losses from a real file and
checks they are gone, with the same values as the English original.
* Cover Levels' French Red, Green and Blue popup items too
popup_alias only mapped "rvb" (RGB), so a French Levels with Red, Green or
Blue selected still lost that selection the same way. Add "rouge", "vert"
and "bleu" (from the same real pdnm list, confirmed against it); "Alpha" is
already the same word in both languages. A direct test on popup_alias covers
every item of the Channel popup it claims to know.
---------
Co-authored-by: sunkidda <sunkidda@users.noreply.github.com>
A popup of more than 1024 items kept only the first 1024 without a word, and
a valid selection past them was reported as damaged data. The cut is now
reported, and such a selection takes the first item and says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Footage made from one layer of a layered .psd showed the whole document and
was named after the file. Its opti (tag 8BPS) stores the layer: the record
index (BE i32 at 14, -1 for the merged image), the bounds (LE i32 at 78-93)
and the name (UTF-8 at 344). The item now shows that layer as EffectCraft's
own Photoshop import does (at its own size with Retain Layer Sizes), named
<layer>/<file>. A layer without pixels (an adjustment layer) keeps the merged
image and is reported.
Third-party plug-ins and pseudo effects (expression controls) were dropped on
import, so expressions reading them broke. Such an effect now arrives as an
effect group whose effect id is its After Effects match name: it renders
nothing, like a missing plug-in, and is listed by Find Missing Effects. It
holds the effect's parameters flat, named as in After Effects, with their
values, keys and expressions in the units After Effects' expressions read.
It is still reported (once per match name, "kept without rendering") and
counted in imported.placeholderEffects. The parameters and popup items kept
are bounded per import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Orientation with an expression was reported as "not decoded" and lost its
value and keys too. The expression is stored in the otst's tdbs like on any
property, so it now arrives with the stored value or keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shape Direction, blend mode, composite order and the gradient highlight
values were reported when their record was in the default state (tdsb 3),
which holds After Effects' default (docs/aep-format.md 3.4), so a plain
guide rectangle reported "shape direction not imported". They are now read
only from written records, keys or an expression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prompt showed 3 detail lines per category under a bare count, which read
as the project's total. The headline now says how many occurrences are left
out or changed, every line the report keeps shows, and "… and N more" counts
the rest. The import report records that number as `unlisted` (an unmapped
effect's line covers all its instances); unmapped effects are listed most
used first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An effect past the limit was still added to the example lines when they had room,
so a report could name up to 120. Past the limit an effect is only counted.
The import report keeps only the first 20 example lines per category, and each
unmapped effect instance was one line in the Effects category. With other losses
in that category, an effect used on many layers could be counted without being
named. Each effect with no counterpart now gets one line, with how many instances
there were ('... and 68 more like it'), placed before the other examples. The
category's count is unchanged.
* #3099: placed .ai pages fill the size After Effects stored; null anchors default to the corner
Footage of an .aep keeps the size After Effects stored for a placed PDF / Illustrator / EPS
file (the ArtBox, rounded up), and masks and anchors are measured against it. The page was
drawn as a PDF viewer frames it (CropBox, /Rotate) at its own size, scaled by the width ratio
at full resolution and stretched to the stored aspect below 3/4: masks too large, art off
centre, squeezed at half resolution.
- effectcraft-pdf: Framing::Placed / parse_page_framed (ArtBox clipped to the CropBox,
/Rotate ignored); boxes with a non-finite corner or span are not used
- Footage::placed_page (set by the .aep importer, serialized only when true): the page is
framed so and filled into the stored size (effectcraft_render::vector_fit), decoded,
continuously rasterised and in Create Shapes from Vector Layer; Reload keeps the size;
the frame cache keys it by size and framing
- .aep import/export: a null's absent Anchor Point is [0, 0], as After Effects reads it
- fixture ai-mask-3099.aep (own synthetic files, scrubbed, profiles stubbed) and tests
against After Effects 2026's read-back
* #3099: placed-page framing only for .ai and .pdf, EPS as before; doc comments
After Effects' sizing was measured with PDFs named .ai only; an EPS from an .aep keeps its
bounding box at its own size (listed as not measured in docs/gaps.md and docs/aep-import.md).
Doc comments on the touched functions.
* #3099: shapes from a placed page keep its frame (track matte)
Create Shapes from Vector Layer converted every path of a placed page, so bleed, art past the
artboard or the page's background showed where the footage's raster hides them. The layers it
makes from a placed page now take a hidden "<layer> Frame" shape layer (the footage's
width x height) as their Alpha track matte: drawn at the comp's resolution, its edge is the
raster's on shape and image layers alike. The command returns the frame layer's id too.
import_extra returned as soon as the arbitrary data was missing, so an
EXtractoR or IDentifier value record whose aRbs/aRbp payload is gone was
silently imported with the default channels; the payload may have chosen
other ones (Z). As for Lumetri's data parameters, a stored record without its
payload now adds a DamagedData loss; a parameter with no record stays the
silent default. The test now asserts the report.
After Effects' Channel Info kind 2 "(copy)" was imported as R, G, B or A.
EffectCraft's EXtractoR resolves those names through the footage's aux planes
first, and an OpenEXR keeps its own R/G/B/A planes, so "(copy)" read the
original file plane and dropped what an earlier effect did to the channel.
"(copy)" is now a reserved EXtractoR channel name (EXTRACTOR_COPY) that the
CPU and GPU renderers never look up in the aux planes: it is the input's own
channel of the same position. The importer writes it for kind 2.
Render regression: a modified input buffer with same-named R/G/B/A aux planes
of other values; GPU parity case with (copy) on an aux scene with R/G/B.
Refs #3170 (the Color Control part).
Some files have no project-level effect definition list (EfdG); it was seen in
17 projects, all from inside After Effects-made Motion Graphics templates. In
those, one instance per effect type keeps the full parameter definitions
(parT) and every other instance of the type has an empty parT. The others
were reported "without readable parameter definitions" and dropped (129
Color Controls of one template). The definition index now falls back to the
parT of the first instance of the same match name that has its own
(DefinitionSource::OtherInstance). Values still come from each instance's own
records; a value record that borrowed definitions do not name is reported
instead of vanishing.
The "Color ... holds an unreadable value" report came from 0.21.1, which read
only written records (tdsb 1). Reading default-state records (tdsb 3, #3126)
probably fixes it, but that is not confirmed for Color Control; a synthetic
test now pins a default-state colour record.
* AEP import: read image sequences without a filename prefix and with Force alphabetical order (#3174)
After Effects leaves out an empty filename prefix, so only one Utf8 (the
suffix, such as `.png`) follows the alias. The single part is now placed by
the first file name stored in `opti`: a name starting with it has it as the
prefix, one ending with it as the suffix; without that evidence a part
starting with `.` is the suffix and anything else is not read.
A sequence imported with Force alphabetical order stores its files in
`LIST StVc` (`StVS` with the count as a little-endian u32, then one Utf8 per
file name) instead of prefix and suffix; that list now replaces the
numbering. A list with a name that is not a plain file name (a separator, a
colon, `.` or `..`) is not read, since the names are joined to the folder.
A folder alias whose naming still cannot be read is reported as an import
loss instead of silently linking the folder. docs/aep-format.md describes
the empty prefix and StVc/StVS.
* AEP import: a listed sequence file name with a control character such as NUL is not read (#3174)
A NUL or another control character in a Force alphabetical order file name
now makes the whole list unreadable, like a separator or a dot directory, so
the import reports the naming instead of linking files that cannot exist.
* AEP import: a sequence file list without a readable count is not read (#3174)
After Effects writes the StVS count before the names. A StVc list without
StVS, or with one shorter than its u32, is now reported as unreadable naming
instead of taking every listed name.
* Doc comments for the functions this PR touches