248 Commits
Author SHA1 Message Date
aiongg b7b75ed37a style: cargo fmt on the moved files 2026-10-26 21:54:30 +00:00
aiongg 5cb742591c refactor(effects): split ocio.rs into ocio/*.rs 2026-10-26 21:49:12 +00:00
aiongg 13fe6e5c03 refactor(effects): split sim.rs into sim/*.rs 2026-10-26 21:49:12 +00:00
aiongg 67ef9ad93c refactor(effects): split sim2.rs into sim2/*.rs 2026-10-26 21:49:12 +00:00
aiongg b05467e47f refactor(effects): split generate3.rs into generate3/*.rs 2026-10-26 21:49:12 +00:00
aiongg 873b519033 refactor(effects): move src/sim3.rs tests to src/sim3/tests.rs 2026-10-26 21:49:11 +00:00
aiongg 60126c4c97 refactor(effects): move src/sim2.rs tests to src/sim2/tests.rs 2026-10-26 21:49:11 +00:00
aiongg 111767f196 refactor(effects): move src/sim.rs tests to src/sim/tests.rs 2026-10-26 21:49:11 +00:00
aiongg 93ba0d34cb refactor(effects): move src/ocio_config.rs tests to src/ocio_config/tests.rs 2026-10-26 21:49:11 +00:00
aiongg ee489aae54 refactor(effects): move src/ocio.rs tests to src/ocio/tests.rs 2026-10-26 21:49:11 +00:00
aiongg 07c669adb9 refactor(effects): move src/generate3.rs tests to src/generate3/tests.rs 2026-10-26 21:49:10 +00:00
aiongg b4f32d3398 refactor(effects): move src/generate2.rs tests to src/generate2/tests.rs 2026-10-26 21:49:10 +00:00
aiongg 6f8b4406ba refactor(effects): move src/distort3.rs tests to src/distort3/tests.rs 2026-10-26 21:49:10 +00:00
aiongg fa7fe244a5 refactor(effects): move src/color_fx.rs tests to src/color_fx/tests.rs 2026-10-26 21:49:10 +00:00
aiongg cb97c5fbfe refactor(effects): move src/color2.rs tests to src/color2/tests.rs 2026-10-26 21:49:10 +00:00
aiongg 0e9ae5a49f refactor(effects): move src/channel3d.rs tests to src/channel3d/tests.rs 2026-10-26 21:49:10 +00:00
aiongg fe2469ee97 refactor(effects): move src/puppet/mod.rs to src/puppet.rs 2026-10-26 21:49:10 +00:00
skylar taylor-barrick 69b9ce2bd1 M0.37: reconcile shared docking and qualify migration follow-up 2026-10-26 06:47:07 +01:00
Dylan M. TaylorandClaude Sonnet 5.5 c07604563e sonar: leave the duplicated cache blocks in object_matte and roto alone (new-code duplication gate)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 08:45:39 +00:00
Dylan M. TaylorandClaude Sonnet 5.5 4e9fa51f12 sonar: method paths instead of closures (rust:S1612)
Poison recovery, OsStr::to_str and char predicates only; S4325 casts are
f64 to f32 and stay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 08:17:16 +00:00
aiongg 981d3116d1 Merge #2040: Fractal Noise: resolve the Blending Mode once per layer, not per pixel (@arnav-hiwarkar) 2026-10-24 01:21:39 +00:00
aiongg a622e6bc5d Merge #1604: Cryptomatte: skip the ID colours unless Colors shows them, key the layer in place, and cache each rank's last ID colour (@MatejKozlovsky) 2026-10-24 01:21:39 +00:00
aiongg 3cf11de7c2 Merge #1810: Shift Channels: After Effects' Take … From menu order, so 10 is Full Off (#1797) (@Anubhav-Rai) 2026-10-24 01:21:11 +00:00
aiongg 65a199bbd7 Merge #1968: Make Gaussian Blur and Fast Box Blur continuous at small values (#1939) (@MarekSpodBiedry) 2026-10-24 01:20:43 +00:00
aiongg 6d0dc7ac57 Merge #2206: effects: CC Radial Fast Blur grows its buffer so streaks pass the layer edge (#2095) (@mercuryVM) 2026-10-24 01:20:43 +00:00
aiongg 017d08bbd0 Merge #2027: Fix #1708: calibrate CC Blobbylize from the AE oracle (@ki2pixel) 2026-10-24 01:20:20 +00:00
aiongg bae27ebbe7 Merge #1841: Cryptomatte: pick the ID under a point into the Selection (effect.pickCryptomatte) (@MatejKozlovsky) 2026-10-23 23:33:10 +00:00
aiongg d74fe2d51e Merge #2003: Motion Tile's Output size works on adjustment layers (#1727) (@ded-achtung) 2026-10-23 23:22:30 +00:00
aiongg d17c2e86cd Merge #1846: Make Fractal Noise evolution progress linearly (@Silvertree2010) 2026-10-23 23:21:37 +00:00
aiongg a78c7a585d Merge #1872: Color effects and keys render as in After Effects (Tint, Tritone, Black & White, Exposure, Levels, Brightness & Contrast, Curves, Luma Key, Extract, 4-Color Gradient, Fill) (@vangel76) 2026-10-23 23:21:37 +00:00
Gabriel Luís da Silva 9199fc20b3 effects: keep CC Radial Fast Blur's pad within 4096 including the extra pixel (#2095) 2026-10-23 17:35:00 -03:00
Gabriel Luís da Silva 6d87dd6217 effects: CC Radial Fast Blur grows its buffer so streaks pass the layer edge (#2095)
Content r from Center smears out to r / (1 - s), so the buffer is padded by
the farthest corner's reach (capped at 4096, skipped on adjustment layers),
on the CPU and GPU paths alike.
2026-10-23 17:34:59 -03:00
aiongg 0173abde8c CI: dev-profile gate, one test binary per crate, nextest, line tables only (#2235)
* Dev profile: line tables only for workspace crates, no debug info for dependencies

* xtask ci: dev-profile gate, nextest, tests without examples, wasm --keep-going

* One integration-test binary per crate

* xtask ci: run effectcraft-gpu's shared-device tests in one cargo test process

* xtask ci: run every stage and report all failures

* xtask ci: name the stage type (clippy type_complexity)

* xtask ci: run every test pass and report all failures

* cargo fmt
2026-10-23 12:59:51 -04:00
Dylan M. Taylor f6ef1a7906 Use chrono instead of hand-rolled UTC conversion (#2023)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil_from_days / days_from_civil copies (log
timestamp, ISO metadata dates, Align Video to Data timestamp parsing, the
Numbers effect's date, the Home and Render Queue date columns, and the
benchmark report date) with chrono as a workspace dependency. Output formats
are unchanged and pinned by the existing tests. AGENTS.md now says time goes
through chrono.

Follow-up to storytold/cadcraft#1785

* Keep 5-digit years unprefixed; derive Numbers weekday from the same date

chrono's %Y writes a '+' before years past 9999, where the old code wrote
{y:04}. timestamp, iso_date, format_utc, utc_stamp and xtask's utc now write
the year with {:04} again and let chrono format the rest, so a far-future
clock keeps the plain zero-padded year.

date_from_days took its weekday from the raw input modulo while the
year/month/day fell back to the epoch for day counts outside chrono's range,
so an unrepresentable count printed a fallback date paired with the input's
weekday. The weekday now comes from the same resolved date, so both agree.

Tests cover the year-10000 timestamp and the out-of-range Numbers date.

* Keep the whole date in format_ago for five-digit years

format_ago cut format_utc to 10 bytes, so a year past 9999 lost its last
digit (10000-01-0). Take the date up to the space instead. Tested.

* Keep Numbers dates exact past chrono's range

date_from_days converted the day count by building a Unix timestamp and
calling DateTime::from_timestamp, which falls back to the epoch (and so
reads 1970-01-01, a Thursday) once the date leaves chrono's NaiveDate
range near year ±262143. Numbers' `value` slider is a plain scalar with
no range guard, so a keyframed count reaches that: Long Date of
100_000_000 showed "Thursday, January 1, 1970" instead of the real
"Monday, September 13, 275790" -- exactly the plausible unrelated-date
fallback the review rejects, and the derived weekday matched the stand-in
date, not the input.

Route it through the 400-year Gregorian cycle like pdfcraft 9d4cb3ca:
chrono converts the day within one cycle (num_days_from_ce of the rebased
rest, always in range) and the whole cycles are added back as years, in
i128 so i64::MIN/MAX can't overflow. chrono stays the only calendar
logic. The weekday still comes from the resolved date, so it always
agrees with it.

Tested: date_from_days_past_chrono_range pins the pre-chrono dates and
weekdays at/over chrono's bound on both sides of the 2000-01-01 epoch
(and that the extremes don't overflow); numbers_formats adds the same
through the public numbers_text entry (Long/Short Date). cargo test
-p effectcraft-effects, fmt --check and clippy -D warnings all clean.

* time: preserve out-of-range years instead of the epoch

`DateTime::from_timestamp(..).unwrap_or_default()` changed the existing output:
inputs the old hand-rolled civil-from-days converted (e.g. `10_000_000_000_000`
s → 318857-05-20 17:46:40) collapsed to the epoch. Restore the old conversion
domain with a cycle-based helper: reduce by whole 400-year Gregorian cycles
(146_097 days), convert the in-range remainder with chrono, and carry the year
back as i128 (saturating to the output type). Only the year leaves chrono's
range; month, day and time of day stay exact. chrono remains the only calendar
logic (NaiveDate::from_num_days_from_ce_opt); no new features.

Sites (UTC):
- crates/engine logging.rs `timestamp` (u64 ms): far-future clock keeps its year.
  10e12 s → "318857-05-20T17:46:40.000Z", u64::MAX ms → "584556019-04-03T14:25:51.615Z"
  (was "1970-01-01...").
- crates/engine media_browser.rs `iso_date` (u64): 10e12 s → "318857-05-20T17:46:40Z",
  u64::MAX → "584554051223-11-09T07:00:15Z".
- crates/ui-egui panels/home.rs `format_utc` (i64): 10e12 → "318857-05-20 17:46",
  i64::MIN → "-292277022657-01-27 08:29".
- crates/ui-egui panels/render_queue.rs `utc_stamp` (u64): 10e12 → "318857-05-20
  17:46:40", u64::MAX → "584554051223-11-09 07:00:15".
- xtask bench.rs `utc` (u64): 10e12 → "318857-05-20 17:46 UTC", u64::MAX →
  "584554051223-11-09 07:00 UTC"; added a test.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-23 11:29:09 -04:00
Dylan M. Taylor 09b9ebca34 roto: bound frozen matte decoding and serialization (#2194)
* roto: bound frozen matte decoding and serialization

* roto: address review (fallible RLE growth, pre-check plane bytes, saturate 32-bit limits)
2026-10-23 08:59:06 -04:00
arnav-hiwarkarandClaude Opus 5.5 ca88af2b5f Fractal Noise: resolve the Blending Mode once per layer, not per pixel
`crates/effects/src/noise3.rs` only:

- a `blend_mode(u32) -> Option<BlendMode>` helper, which the CPU and GPU paths now share;
- the CPU `render` resolves the mode once and passes `Option<BlendMode>` to the per-pixel `composite`.

The semantics are unchanged: None means the noise alone, and an unknown index means Normal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:34:45 +05:30
Camille Moine 374c3bf6b9 fix(effects): clear Cut Away 100 opaque edges
The gradient antialias could leave a half-covered pixel where an opaque source reaches h = 1. Add an endpoint guard on CPU and GPU and cover an opaque edge in the regression test.
2026-10-21 14:46:21 +02:00
Camille Moine 6e85c762c1 test(effects): pin the AE defaults of CC Blobbylize 2026-10-21 13:33:53 +02:00
Camille Moine 89d954a10b fix(effects): calibrate CC Blobbylize to the AE oracle
Use the measured Softness kernel (sigma = 0.42 * Softness), threshold the blurred height with a ~1 px antialias, and allow Cut Away 100 to empty the matte. Split the source-matte displacement from the height-field RGB refraction: the matte uses a z=1 bevel normal fitted to the T2 step edges, while RGB follows the measured z=height refraction. Align the documented AE defaults and add CPU regression tests for the cut threshold, Cut Away 100, and the inward matte warp. Update the GPU kernel and parameters to match, keeping CPU/GPU parity.
2026-10-21 13:06:40 +02:00
dedandClaude Opus 5.5 915ba28a9d Motion Tile's Output size works on adjustment layers (#1727)
On an adjustment layer Motion Tile kept the comp-sized buffer whatever the
Output Width / Height, and the adjustment draw dropped any stack result whose
size differed from the canvas. After Effects grows (or crops) the comp
rectangle there as on any layer, so a Transform after it brings the extra
tiles into view, as in the frames attached to #1727 and #1409.

- motion_tile_frame: one output frame for the CPU and GPU effect; on an
  adjustment it grows about the comp rectangle on whole pixels, keeps the
  buffer at 100 % and past 16 384 pixels a side.
- adjustment_crop: the CPU and GPU adjustment draws put a stack result whose
  frame changed back onto the canvas instead of dropping it (Enc::crop on
  the GPU).

Tests: the #1727 After Effects frames (910 x 340 % and 90 x 340 % then
Scale 12 %), tiles past the frame and cropping on CPU, GPU vs CPU for
adjustment and full-frame layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:35:47 +03:00
MarekSpodBiedry 3878fdf0ba Make Gaussian Blur and Fast Box Blur continuous at small values (#1939) 2026-10-20 18:49:19 +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
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 84ffc8453e Fill: Opacity is the filled layer's opacity, as After Effects
Without a Fill Mask, After Effects turns the layer into the fill colour and
applies Opacity to its alpha; nothing of the original shows through. AE 2026
renders Fill (0, 0.5, 1) at Opacity 50 % on a Gradient Ramp over a red solid
as premultiplied (0, 0.251, 0.502) at alpha 0.502 at every pixel; EffectCraft
mixed the fill with the ramp (mean difference 95.5/255), now within 1/255,
CPU and GPU. Invert fills the transparent part the same way. With a Fill
Mask the mix with the original is unchanged (not measured). Lottie's fill
effect (colour, alpha x opacity) now plays the same in EffectCraft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:16:12 +02:00
vangel76andClaude Opus 5.5 d7854b84a3 4-Color Gradient: weights 1 / (d² + Blend²), as After Effects
After Effects weighs each of the four points by its inverse squared
distance softened by Blend squared, 1 / (d^2 + Blend^2) in pixels: fitted to
AE 2026 renders of the default gradient at Blend 10, 100 and 300 and case
904 E16 at Blend 50 (power 2.00 at every Blend, softening 0 / 2367 / 9817 /
90127 = about Blend^2; mean error 0.27/255, max 0.9). EffectCraft used
1 / (d^2 / (100 Blend))^1.5, in which Blend cancels out: E16 was 34/255 off
on 99 % of the grid, now 0 grid points off, max 1/255, CPU and GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:16:02 +02:00
vangel76andClaude Opus 5.5 75271bfe6c Luma Key and Extract: Rec. 601 luminance, as After Effects
After Effects 2026 keys Luma Key and Extract (Channel Luminance) on Rec. 601
weights of the working-space values: Key Out Darker / Extract Black Point at
65, 166 and 24 on pure red, green and blue keep red (601: 76, 709: 54) and
blue (29 / 18) and key out green (150 / 182) - Rec. 709, which EffectCraft
used, decides all three the other way. Case 904 E18 (Extract 30 / 220 /
softness 10 on grey and colour steps): 513 of 5184 grid points off (pure blue
kept at 231 in AE, keyed out in EC), now 0, max 0. CPU and GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:16:01 +02:00
vangel76andClaude Opus 5.5 39bd0efac2 Luma Key: Key Out Brighter and Darker cut hard at Threshold, as After Effects
After Effects' Luma Key removes everything darker (or brighter) than Threshold
and ignores Tolerance for those two key types; Tolerance only sets the band of
Key Out Similar / Dissimilar. EffectCraft ramped the matte over Tolerance, so
case 719 (a 50 % grey, Key Out Darker, Threshold 140, Tolerance 20) kept about
80 % of the layer where AE keys it out completely.

Measured in AE 2026 on a vertical grey ramp (cases 812-814): Darker keys up to
level 139.9 and keeps 140.1 (EffectCraft now the same row); Similar keys levels
120.3-160.0; with Edge Thin 1 / Edge Feather 2.5 the matte rises over rows
591-596 (EffectCraft within 0.2 of AE's alpha there). CPU and GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:15:02 +02:00
vangel76andClaude Opus 5.5 dc9f15ada1 Brightness & Contrast: negative settings invert the positive curves, as After Effects
In After Effects 2026 the non-legacy curve of Brightness -40 is the inverse
of Brightness +40 (within 2.6/255 on a full black-to-white ramp) and Contrast
-50 the inverse of Contrast +50. EffectCraft fitted the negative brightness
with its own power (2^(1.32 |b|) on v), 20/255 off AE at -40; the negatives
now use the reciprocal exponents of the positive curves: Brightness -40 max
6.2/255 (was 19.8), Contrast -50 4.1 (was 7.0); positives unchanged (40:
5.5, 80: 7.8, Contrast 50: 2.7, 90: 5.0; B20 C30: 6.6, which confirms
brightness before contrast). The #681 oracle samples still pass. CPU and GPU.
The positive curves' remaining 5-8/255 needs a better curve family (a
two-point spline fits each capture within 1.6/255), left open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:15:02 +02:00
vangel76andClaude Opus 5.5 515cc3d5f7 Levels: channel controls before RGB, "Off for 32 bpc Color" unclipped in 32 bpc, as After Effects
After Effects 2026 in a 32 bpc project, Levels (Individual Controls) with RGB
(0.1, 0.9, gamma 1.5, 0.05, 0.95), Red Gamma 0.8, Green Input White 0.8 and
Blue Output Black 0.2 on grey steps 0..255 (case 904 E07): every step matches
"each channel's controls, then the RGB controls" with nothing clipped
(values below Input Black end below Output Black, above Input White above
Output White). EffectCraft ran RGB first and clipped "Off for 32 bpc Color"
(it could not see the bit depth): up to 27/255 off on 95 % of the grid; now
0 grid points off, max 1/255, CPU and GPU.

EffectEnv gains float_depth (the renderer sets it for 32 bpc projects), so
"Off for 32 bpc Color" clips in 8 / 16 bpc projects as before. Unclipped
values below 0 take the gamma mirrored through 0. Plain Levels keeps its
order (its channel controls were not measured); its clipping follows the same
rule. EffectCraft's own projects default to 8 bpc and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:14:42 +02:00
vangel76andClaude Opus 5.5 617b0a578d Exposure: After Effects parameter ranges
All twelve sliders (Master, Red, Green, Blue) take After Effects'
ranges: Exposure valid -100..100, slider -4..4, 2 decimals (was -20..20,
slider -5..5); Offset -2..2, slider -0.5..0.5, 4 decimals (unchanged);
Gamma Correction valid and slider 0.1..10, 2 decimals (was 0.01..9.99,
slider 0.1..3).

Every value inside the old Exposure and Offset ranges stays valid. Saved
values are not clamped on load, so a gamma below 0.1 from an older
project or a Photoshop import (0.01..9.99) is kept and still renders
(the renderer's 0.01 floor is unchanged); only new edits clamp to 0.1.
Test of the ranges and of an old gamma surviving the load refresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:14:26 +02:00