diff --git a/crates/effects/src/ocio/effects.rs b/crates/effects/src/ocio/effects.rs index 0eb7bd330..e426e22fd 100644 --- a/crates/effects/src/ocio/effects.rs +++ b/crates/effects/src/ocio/effects.rs @@ -360,7 +360,8 @@ pub(super) fn profile_converter(ctx: &EffectCtx, mut b: Buf) -> Buf { // ---- Color Stabilizer -/// Mean straight colour of a square (half-size `r` layer pixels) around layer point `pt`. +/// Mean straight colour of a square (half-size `r` layer pixels, half the Sample Size) around +/// layer point `pt`. fn sample_mean(b: &Buf, pt: [f64; 2], r: f64) -> [f64; 3] { let (cx, cy) = b.to_px(pt); let rr = (r * b.scale).max(0.5); @@ -410,7 +411,10 @@ fn stabilizer_samples(ctx: &EffectCtx, b: &Buf) -> Option<([[f64; 3]; 3], [[f64; let host = ctx.env.host?; let t_ref = ctx.params.f("referenceFrame"); let reference = host.self_at(t_ref, ctx.env.effect_index)?; - let r = ctx.params.f("sampleSize").max(0.5); + // Sample Size is the full width of the sampled square in layer pixels: After Effects 2026 + // reacts to a bright square 7 px from the point at Sample Size 15 but not at 12, at any + // layer size (#3125), so the half-width is half of it. + let r = (ctx.params.f("sampleSize") / 2.0).max(0.5); let pts = STABILIZER_POINTS.map(|id| ctx.params.v2(id)); let ref_params = host.params_at(t_ref); let ref_pts = match &ref_params { diff --git a/crates/effects/src/ocio/tests.rs b/crates/effects/src/ocio/tests.rs index fe93dd25a..92f832168 100644 --- a/crates/effects/src/ocio/tests.rs +++ b/crates/effects/src/ocio/tests.rs @@ -396,3 +396,55 @@ fn color_stabilizer_samples_the_reference_at_the_points_reference_positions() { assert!((o(x) - (a + b * v(x))).abs() < 1e-3, "x {x}"); } } + +/// After Effects 2026's Sample Size sample (#3125): a horizontal ramp, Stabilize Levels with +/// the Black Point at 25 % of the width, mid-height, and a 6 px white square appearing `d` px +/// to the right of it (near edge `d − 3`). The card at `square: None` is the reference. +fn sample_size_card(w: u32, h: u32, square: Option) -> Image { + let mut img = Image::new(w, h); + let (bx, by) = (w as f64 * 0.25, h as f64 * 0.5); + for y in 0..h { + for x in 0..w { + let (cx, cy) = (x as f64 + 0.5, y as f64 + 0.5); + let mut v = (cx / w as f64) as f32; + if square.is_some_and(|d| (cx - (bx + d)).abs() < 3.0 && (cy - by).abs() < 3.0) { + v = 1.0; + } + img.set(x, y, [v, v, v, 1.0]); + } + } + img +} + +/// #3125: Sample Size is the full width of the sampled area in layer pixels, at any layer size. +/// After Effects reacts to the square at near edge 7 px from Sample Size 15 (not at 12), at +/// 17 px from 35 (not at 30), and never to one at 37 px up to 50. EffectCraft used Sample Size +/// as the half-width and reacted at 12 and 30. +#[test] +fn color_stabilizer_sample_size_is_the_full_width_in_pixels() { + let reacts = |w: u32, h: u32, d: f64, size: f64| { + let host = RefHost(sample_size_card(w, h, None)); + let env = EffectEnv { host: Some(&host), ..Default::default() }; + let (bx, by) = (w as f64 * 0.25, h as f64 * 0.5); + let vals = [ + ("stabilize", Value::Enum(1)), + ("blackPoint", Value::Vec2([bx, by])), + ("whitePoint", Value::Vec2([w as f64 * 0.75, by])), + ("sampleSize", num(size)), + ]; + let input = sample_size_card(w, h, Some(d)); + let out = run_fx("ec.color.colorstabilizer", &vals, input.clone(), 0.0, env); + // The change on the right half of the layer, far from the square. + let (x, y) = (w as i64 * 7 / 8, h as i64 / 4); + (out.img.get(x, y)[0] - input.get(x, y)[0]).abs() > 1e-4 + }; + for (w, h) in [(320, 180), (640, 360)] { + assert!(reacts(w, h, 10.0, 15.0), "{w}x{h}: d 10 at 15"); + assert!(!reacts(w, h, 10.0, 12.0), "{w}x{h}: d 10 at 12"); + assert!(reacts(w, h, 20.0, 35.0), "{w}x{h}: d 20 at 35"); + assert!(!reacts(w, h, 20.0, 30.0), "{w}x{h}: d 20 at 30"); + for size in [1.0, 5.0, 12.0, 25.0, 40.0, 50.0] { + assert!(!reacts(w, h, 40.0, size), "{w}x{h}: d 40 at {size}"); + } + } +} diff --git a/docs/aep-import.md b/docs/aep-import.md index ed3ab57f5..c1aee6bac 100644 --- a/docs/aep-import.md +++ b/docs/aep-import.md @@ -179,7 +179,7 @@ parameter gets the file's value: | `ADBE Easy Levels2` 1 Channel, 2 Histogram data, 8 / 9 Clip To Output Black / White | `channel`; `inBlack`, `inWhite`, `gamma`, `outBlack`, `outWhite` from the data; `clipToOutputBlack`, `clipToOutputWhite` | popups by name (After Effects' items On, Off, Off for 32 bpc Color are in another order here); the RGB values are the `f32` of the arbitrary data; the per-channel values are reported when they are not at their defaults (not sampled) | 511-516 | | Batch 6 (903 defaults, 904 / 905 set values): `ADBE Brightness & Contrast 2`, `ADBE Turbulent Displace`, `ADBE Fractal Noise`, `ADBE Laser` (Beam), `CC Radial Fast Blur`, `ADBE Noise2`, `ADBE Pro Levels2` (Levels (Individual Controls)), `ADBE Corner Pin`, `ADBE Grid`, `ADBE Sharpen`, `ADBE Linear Color Key2`, `ADBE Radial Wipe`, `ADBE Remove Color Matting`, `CC Bend It`, `CC Power Pin`, `ADBE 4ColorGradient`, `ADBE Change Color`, `ADBE Extract`, `ADBE Noise HLS2`, `ADBE Scatter`, `CC Light Sweep`, `ADBE Compound Blur`, `ADBE Displacement Map`, `ADBE Set Matte3`, `ADBE Gaussian Blur` (legacy), `ADBE OFMotionBlur` (Pixel Motion Blur), `ADBE Layer Control` | the EffectCraft effect of the same name, every parameter EffectCraft has | sliders as stored (each checked against its valid maximum); Beam Length, Time and Softness and CC Power Pin Perspective are 0..1 fractions, percentages here; points as layer pixels; popups by item name (Set Matte's `Red Channel` ... is `Red` ...); twirl-down group markers and the Levels histogram carry nothing; **layer parameters** (below) | 903, 904, 905; 220, 320, 321 | | `Cryptomatte` (the Cryptomatte plug-in) 1 Settings data, 2 Output, 3 Selection Mode | `ec.3d.cryptomatte` `layerName`, `selection`; `display`, `matteOnly` | the layer name and the selection text from the data, verbatim (`CryptomatteData`; Layer Name picks the footage's Cryptomatte layer of exactly that name, the Layer popup is the fallback; the `` selection tokens are EffectCraft's selection grammar); Output Colors -> Display Colors, Matted RGBA -> Display Matte, Matte Only -> Matte Only on (Display Matte); **Matted Colors** (the ID colours keyed by the selection) has no counterpart: imported as Display Matte (the same matte over the layer's own colours) and reported; no Output record is the plug-in's default Colors (also for a borrowed definition; Colors too for an Output that is keyed, has an expression or cannot be read, each reported); Selection Mode (the plug-in's picking switch) is reported when on, also in the default-state record After Effects writes for it; keys or expressions on any of them are reported; unreadable data is damaged data (no layer, no selection). **Render-compared** with After Effects (1400): with a chosen layer and a picked object (the two preset compositions, Output Colors) EffectCraft's matte covers exactly the pixels After Effects draws as selected (6 012 fully covered for the box on CryptoObject, 10 962 for box and disc on CryptoMaterial, the same counts; on the 190 edge pixels of the latter the coverage implied by After Effects' frame is EffectCraft's within 0.0002); After Effects' Colors draws the selection as `c * (1 - m) + m` (a fully selected pixel white), EffectCraft's Colors as `c * (1 - m / 2) + m / 2`, and the ID colours themselves differ, so Colors is not compared per pixel. With the fresh data (empty selection, every matte 0): Matted RGBA matches (transparent); Matte Only differs in alpha (After Effects: the matte in RGB, opaque; EffectCraft: the matte in RGBA, so the same over black and as a luma matte, not as an alpha matte); Matted Colors is transparent in After Effects and in the imported Matte; Colors is a preview of the IDs and not compared per pixel (After Effects' colours are near black and, with no layer chosen, show the footage's first Cryptomatte layer, CryptoMaterial in 1400, where EffectCraft's Layer popup starts at CryptoObject); what Selection Mode draws is not reproduced | aep-format 3.7 (88 instances of one project; 1400); test `import::cryptomatte` | -| Importer block 2026-11-05 (`1100-quickwins.aep`: per effect defaults, set values, minimum and maximum, every popup item, a keyed and an expression parameter): `ADBE HDR ToneMap` (HDR Highlight Compression) 1 Amount; `ADBE Echo` 1 Echo Time, 2 Number Of Echoes, 3 Starting Intensity, 4 Decay, 5 Echo Operator; `ADBE Linear Wipe` 1 Transition Completion, 2 Wipe Angle, 3 Feather; `ADBE Offset` 1 Shift Center To, 2 Blend With Original; `ADBE Deflicker` (Color Stabilizer) 1 Stabilize, 2-4 Black / Mid / White Point, 5 Sample Size | `ec.utility.hdrcompression` `amount`; `ec.time.echo` `echoTime`, `numberOfEchoes`, `startingIntensity`, `decay`, `echoOperator`; `ec.transition.linearwipe` `completion`, `angle`, `feather`; `ec.distort.offset` `shift`, `blend`; `ec.color.colorstabilizer` `stabilize`, `blackPoint`, `midPoint`, `whitePoint`, `sampleSize` | percent (float, valid 0..100, default 100); seconds (float, valid ±30 000), integer (valid 30 000), 0..1 (float), Decay as stored (float, valid 0..100), popup by name: EffectCraft's Echo uses at most 255 echoes and a Decay up to 1, a value or key beyond that is imported as stored and **reported**; percent (fixed, 100), degrees, pixels (fixed, 32 000); point in layer pixels, Blend 0..1 x 100 = percent; popup by name (**fresh default Levels**), points in layer pixels, Sample Size as stored (fixed, valid 50, default 3; its unit, pixels here, is not verified). Color Stabilizer's reference frame (Set Frame) is not a parameter in the file and is not imported (EffectCraft's Reference Frame stays 0 s) | `tests/it/import/effects_quickwins.rs`; render comparison below | +| Importer block 2026-11-05 (`1100-quickwins.aep`: per effect defaults, set values, minimum and maximum, every popup item, a keyed and an expression parameter): `ADBE HDR ToneMap` (HDR Highlight Compression) 1 Amount; `ADBE Echo` 1 Echo Time, 2 Number Of Echoes, 3 Starting Intensity, 4 Decay, 5 Echo Operator; `ADBE Linear Wipe` 1 Transition Completion, 2 Wipe Angle, 3 Feather; `ADBE Offset` 1 Shift Center To, 2 Blend With Original; `ADBE Deflicker` (Color Stabilizer) 1 Stabilize, 2-4 Black / Mid / White Point, 5 Sample Size | `ec.utility.hdrcompression` `amount`; `ec.time.echo` `echoTime`, `numberOfEchoes`, `startingIntensity`, `decay`, `echoOperator`; `ec.transition.linearwipe` `completion`, `angle`, `feather`; `ec.distort.offset` `shift`, `blend`; `ec.color.colorstabilizer` `stabilize`, `blackPoint`, `midPoint`, `whitePoint`, `sampleSize` | percent (float, valid 0..100, default 100); seconds (float, valid ±30 000), integer (valid 30 000), 0..1 (float), Decay as stored (float, valid 0..100), popup by name: EffectCraft's Echo uses at most 255 echoes and a Decay up to 1, a value or key beyond that is imported as stored and **reported**; percent (fixed, 100), degrees, pixels (fixed, 32 000); point in layer pixels, Blend 0..1 x 100 = percent; popup by name (**fresh default Levels**), points in layer pixels, Sample Size as stored (fixed, valid 50, default 3; the full width of the sampled square in layer pixels, measured in After Effects 2026, #3125). Color Stabilizer's reference frame (Set Frame) is not a parameter in the file and is not imported (EffectCraft's Reference Frame stays 0 s) | `tests/it/import/effects_quickwins.rs`; render comparison below | | `ADBE Slider Control`, `ADBE Angle Control`, `ADBE Checkbox Control`, `ADBE Color Control`, `ADBE Point Control` (parameter 1) | `ec.control.slider` `slider`, `ec.control.angle` `angle`, `ec.control.checkbox` `checkbox`, `ec.control.color` `color`, `ec.control.point` `point` | float (valid max 1 000 000) as stored; degrees; checkbox; colour; point in layer pixels | 071, 073, 217-219, 221, 304, 310 | - **Nothing to import:** the implicit input-layer parameter (index 0), the Swap Colors buttons