@hyperframes/core 0.8.70 → 0.8.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * `displacement-map` kernel — GLSL ES 3.00, capture `self`.
3
3
  *
4
- * **Scope (v1, recorded rather than implied):** every corpus instance points
5
- * its Displacement Map Layer at **the layer itself** (retro-wave `Main/Text`
6
- * → idx 5 = Text; `Logo Anim/Text Site` and `Logo Anim/Logo` likewise), so
7
- * `u_src` is both the source and the map and no second capture is needed. A
8
- * map that is a *different* layer is out of v1 — it needs a second source,
9
- * which is Phase 2b's `ref`-param shape; the exporter emits a structural
10
- * capability for it instead.
4
+ * **The map layer.** Every corpus instance points its Displacement Map Layer
5
+ * at **the layer itself** (retro-wave `Main/Text` → idx 5 = Text; `Logo Anim/
6
+ * Text Site` and `Logo Anim/Logo` likewise), so `u_src` is both the source and
7
+ * the map and no second capture is needed — that is what the `map` ref param
8
+ * being EMPTY means, and it stays the default. An external map layer is the
9
+ * same kernel reading `u_src2`: the exporter names that element in `map` and
10
+ * wraps it in its own `.hf-vfx-src` capture, and `u_hasSrc2` switches the
11
+ * sampler. The two inputs are read at the same `v_uv`, so an external map is
12
+ * scaled into the host's box (see `captureSource`), not placed in composition
13
+ * space.
11
14
  *
12
15
  * Per axis: `d = (channel(map) − 0.5) · 2 · max`, then sample `u_src` at
13
16
  * `uv + d/u_size` with **clamp-to-transparent** edges (Edge = Off, the only
@@ -1 +1 @@
1
- {"version":3,"file":"displacementMap.frag.d.ts","sourceRoot":"","sources":["../../src/vfx/displacementMap.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,0BAA0B;;;;CAI7B,CAAC;AAIX,eAAO,MAAM,qBAAqB,QA2CjC,CAAC"}
1
+ {"version":3,"file":"displacementMap.frag.d.ts","sourceRoot":"","sources":["../../src/vfx/displacementMap.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,0BAA0B;;;;CAI7B,CAAC;AAIX,eAAO,MAAM,qBAAqB,QA+CjC,CAAC"}
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * `displacement-map` kernel — GLSL ES 3.00, capture `self`.
3
3
  *
4
- * **Scope (v1, recorded rather than implied):** every corpus instance points
5
- * its Displacement Map Layer at **the layer itself** (retro-wave `Main/Text`
6
- * → idx 5 = Text; `Logo Anim/Text Site` and `Logo Anim/Logo` likewise), so
7
- * `u_src` is both the source and the map and no second capture is needed. A
8
- * map that is a *different* layer is out of v1 — it needs a second source,
9
- * which is Phase 2b's `ref`-param shape; the exporter emits a structural
10
- * capability for it instead.
4
+ * **The map layer.** Every corpus instance points its Displacement Map Layer
5
+ * at **the layer itself** (retro-wave `Main/Text` → idx 5 = Text; `Logo Anim/
6
+ * Text Site` and `Logo Anim/Logo` likewise), so `u_src` is both the source and
7
+ * the map and no second capture is needed — that is what the `map` ref param
8
+ * being EMPTY means, and it stays the default. An external map layer is the
9
+ * same kernel reading `u_src2`: the exporter names that element in `map` and
10
+ * wraps it in its own `.hf-vfx-src` capture, and `u_hasSrc2` switches the
11
+ * sampler. The two inputs are read at the same `v_uv`, so an external map is
12
+ * scaled into the host's box (see `captureSource`), not placed in composition
13
+ * space.
11
14
  *
12
15
  * Per axis: `d = (channel(map) − 0.5) · 2 · max`, then sample `u_src` at
13
16
  * `uv + d/u_size` with **clamp-to-transparent** edges (Edge = Off, the only
@@ -48,6 +51,10 @@ uniform vec2 u_size;
48
51
  uniform float u_t;
49
52
  uniform float u_fps;
50
53
  uniform sampler2D u_src;
54
+ uniform sampler2D u_src2;
55
+ // 1 when the map ref named another element, 0 when the map is the layer
56
+ // itself. Set by the runtime for every pass, from whether a ref resolved.
57
+ uniform float u_hasSrc2;
51
58
  uniform float u_useH;
52
59
  uniform float u_maxH;
53
60
  uniform float u_useV;
@@ -75,7 +82,7 @@ float hfMapChannel(vec4 premultiplied, float selector) {
75
82
  }
76
83
 
77
84
  void main() {
78
- vec4 mapTexel = texture(u_src, v_uv);
85
+ vec4 mapTexel = u_hasSrc2 > 0.5 ? texture(u_src2, v_uv) : texture(u_src, v_uv);
79
86
  float h = hfMapChannel(mapTexel, u_useH);
80
87
  float v = hfMapChannel(mapTexel, u_useV);
81
88
  vec2 d = vec2((h - 0.5) * 2.0 * u_maxH, (v - 0.5) * 2.0 * u_maxV);
@@ -1 +1 @@
1
- {"version":3,"file":"displacementMap.frag.js","sourceRoot":"","sources":["../../src/vfx/displacementMap.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,KAAK,EAAE,KAAK;IACZ,KAAK,EAAE,KAAK;IACZ,KAAK,EAAE,KAAK;CACJ,CAAC;AAEX,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,0BAA0B,CAAC;AAE3D,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CA+BQ,KAAK,KAAK,KAAK,KAAK,KAAK;;;;;;;;;;;;CAYrE,CAAC"}
1
+ {"version":3,"file":"displacementMap.frag.js","sourceRoot":"","sources":["../../src/vfx/displacementMap.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,KAAK,EAAE,KAAK;IACZ,KAAK,EAAE,KAAK;IACZ,KAAK,EAAE,KAAK;CACJ,CAAC;AAEX,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,0BAA0B,CAAC;AAE3D,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAmCQ,KAAK,KAAK,KAAK,KAAK,KAAK;;;;;;;;;;;;CAYrE,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `luma-matte` kernel — GLSL ES 3.00, capture `self` plus a `ref` second
3
+ * source.
4
+ *
5
+ * **What it ports.** Not a single After Effects effect: this is the runtime
6
+ * form of every matte the exporter cannot express as CSS — a track matte whose
7
+ * source layer is unsupported (`matte-source-unsupported:*`), a stencil or
8
+ * silhouette (`stencil-*`), and Set Matte. All of them are the same
9
+ * per-pixel operation, `out = src · coverage(matte)`, differing only in which
10
+ * element is the source and which is the matte. The exporter decides that by
11
+ * choosing which element the host wraps and which the `matte` ref names; a
12
+ * stencil is this kernel with the host's own content as the matte.
13
+ *
14
+ * **Coverage.** `src` arrives premultiplied, so scaling all four channels by a
15
+ * single coverage value is the whole operation — no unpremultiply/repremultiply
16
+ * round trip, and a coverage of 0 yields a genuinely empty pixel rather than a
17
+ * black one.
18
+ *
19
+ * - Alpha (1) / Alpha Inverted (2): the matte's alpha.
20
+ * - Luma (3) / Luma Inverted (4): the matte's luminance **composited over
21
+ * black**, which is what the premultiplied texel already holds — a matte that
22
+ * is 50 % grey at 50 % opacity reads as 0.25, matching After Effects, where a
23
+ * luma matte's transparent regions are black rather than ignored.
24
+ *
25
+ * Rec.601 weights, shared with `displacement-map`'s Luminance channel selector
26
+ * so the two agree on what "luminance" means.
27
+ *
28
+ * **Out of scope in v1, recorded rather than implied:** the matte is sampled at
29
+ * the host's own `v_uv`, so the runtime captures it into the host's box (see
30
+ * `captureSource`). After Effects places a matte layer in composition space
31
+ * with its own transform; a matte whose box differs from the layer's is
32
+ * therefore stretched here, not positioned. Every corpus matte is a
33
+ * full-frame sibling of its layer, where the two agree.
34
+ */
35
+ /** The `mode` enum, named once so the def, the shader and the reference agree. */
36
+ export declare const LUMA_MATTE_MODES: {
37
+ readonly alpha: 1;
38
+ readonly alphaInverted: 2;
39
+ readonly luma: 3;
40
+ readonly lumaInverted: 4;
41
+ };
42
+ export declare const LUMA_MATTE_FRAG: string;
43
+ //# sourceMappingURL=lumaMatte.frag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lumaMatte.frag.d.ts","sourceRoot":"","sources":["../../src/vfx/lumaMatte.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH,kFAAkF;AAClF,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAC;AAEX,eAAO,MAAM,eAAe,QA4B3B,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `luma-matte` kernel — GLSL ES 3.00, capture `self` plus a `ref` second
3
+ * source.
4
+ *
5
+ * **What it ports.** Not a single After Effects effect: this is the runtime
6
+ * form of every matte the exporter cannot express as CSS — a track matte whose
7
+ * source layer is unsupported (`matte-source-unsupported:*`), a stencil or
8
+ * silhouette (`stencil-*`), and Set Matte. All of them are the same
9
+ * per-pixel operation, `out = src · coverage(matte)`, differing only in which
10
+ * element is the source and which is the matte. The exporter decides that by
11
+ * choosing which element the host wraps and which the `matte` ref names; a
12
+ * stencil is this kernel with the host's own content as the matte.
13
+ *
14
+ * **Coverage.** `src` arrives premultiplied, so scaling all four channels by a
15
+ * single coverage value is the whole operation — no unpremultiply/repremultiply
16
+ * round trip, and a coverage of 0 yields a genuinely empty pixel rather than a
17
+ * black one.
18
+ *
19
+ * - Alpha (1) / Alpha Inverted (2): the matte's alpha.
20
+ * - Luma (3) / Luma Inverted (4): the matte's luminance **composited over
21
+ * black**, which is what the premultiplied texel already holds — a matte that
22
+ * is 50 % grey at 50 % opacity reads as 0.25, matching After Effects, where a
23
+ * luma matte's transparent regions are black rather than ignored.
24
+ *
25
+ * Rec.601 weights, shared with `displacement-map`'s Luminance channel selector
26
+ * so the two agree on what "luminance" means.
27
+ *
28
+ * **Out of scope in v1, recorded rather than implied:** the matte is sampled at
29
+ * the host's own `v_uv`, so the runtime captures it into the host's box (see
30
+ * `captureSource`). After Effects places a matte layer in composition space
31
+ * with its own transform; a matte whose box differs from the layer's is
32
+ * therefore stretched here, not positioned. Every corpus matte is a
33
+ * full-frame sibling of its layer, where the two agree.
34
+ */
35
+ import { DISPLACEMENT_MAP_CONSTANTS } from "./displacementMap.frag.js";
36
+ const { lumaR, lumaG, lumaB } = DISPLACEMENT_MAP_CONSTANTS;
37
+ /** The `mode` enum, named once so the def, the shader and the reference agree. */
38
+ export const LUMA_MATTE_MODES = {
39
+ alpha: 1,
40
+ alphaInverted: 2,
41
+ luma: 3,
42
+ lumaInverted: 4,
43
+ };
44
+ export const LUMA_MATTE_FRAG = `#version 300 es
45
+ precision highp float;
46
+
47
+ in vec2 v_uv;
48
+ out vec4 fragColor;
49
+
50
+ uniform vec2 u_size;
51
+ uniform float u_t;
52
+ uniform float u_fps;
53
+ uniform sampler2D u_src;
54
+ uniform sampler2D u_src2;
55
+ uniform float u_mode;
56
+
57
+ // The matte texel is premultiplied, so its rgb is already "over black" — the
58
+ // composite After Effects reads a luma matte through.
59
+ float hfCoverage(vec4 matte, float mode) {
60
+ if (mode < 1.5) return matte.a;
61
+ if (mode < 2.5) return 1.0 - matte.a;
62
+ float luma = dot(matte.rgb, vec3(${lumaR}, ${lumaG}, ${lumaB}));
63
+ if (mode < 3.5) return luma;
64
+ return 1.0 - luma;
65
+ }
66
+
67
+ void main() {
68
+ vec4 src = texture(u_src, v_uv);
69
+ vec4 matte = texture(u_src2, v_uv);
70
+ fragColor = src * clamp(hfCoverage(matte, u_mode), 0.0, 1.0);
71
+ }
72
+ `;
73
+ //# sourceMappingURL=lumaMatte.frag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lumaMatte.frag.js","sourceRoot":"","sources":["../../src/vfx/lumaMatte.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAEpE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,0BAA0B,CAAC;AAE3D,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,CAAC;IACR,aAAa,EAAE,CAAC;IAChB,IAAI,EAAE,CAAC;IACP,YAAY,EAAE,CAAC;CACP,CAAC;AAEX,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;qCAkBM,KAAK,KAAK,KAAK,KAAK,KAAK;;;;;;;;;;CAU7D,CAAC"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `noise` kernel — GLSL ES 3.00, capture `self`.
3
+ *
4
+ * A port of After Effects' `ADBE Noise`, decoded from `AEFX_Noise.metallib`
5
+ * (`docs/superpowers/specs/ae-metal-ir/AEFX_Noise.metallib.ll` in the exporter
6
+ * repo, helper `_Z5Noise8PixelRGBiifiiff`). The kernel is a pure integer hash of
7
+ * `(x, y, seed)` — no lookup table, no state between frames — so it ports
8
+ * exactly and stays seek-safe by construction.
9
+ *
10
+ * **What the IR says, read from the struct metadata rather than from prose.**
11
+ * `NoiseKernelValues` is `{ inDeviceFormat, inNoiseAmount, inUseColorNoise,
12
+ * inClipNoiseOutput, inRandomSeed_High, inRandomSeed_Low, inNoiseOffsetX,
13
+ * inNoiseOffsetY, … }`. So `ADBE Noise-0002` is **Use Color Noise**, a
14
+ * checkbox — not the "Noise Type (Uniform / Gaussian)" popup the deep dive
15
+ * guessed — and the two mystery seed floats are the halves of one 32-bit
16
+ * random seed. Both params are named here the way the metadata names them.
17
+ *
18
+ * Per channel:
19
+ *
20
+ * seed32 = (uint(seedHigh) << 16) | (uint(seedLow) & 0xFFFF)
21
+ * h = jenkinsMix(a, y, seed32) // Bob Jenkins' 1997 mix, verbatim
22
+ * t2 = h * 1103515245 + 12345
23
+ * t4 = t2 * 12996205 + 12345
24
+ * byte = ((t4 >> 16) & 0xFF) ^ ((t2 >> 9) & 0x7F80)
25
+ * noise = amount · (byte / 32704 − 0.5)
26
+ *
27
+ * with `a = x` for monochrome noise and `a = 3x + channel` (R, G, B) for colour
28
+ * noise — four separate hash blocks in the IR, three of them consecutive in
29
+ * `3x`. `32704` is the reciprocal constant `0x3F00002000000000` spelled as a
30
+ * fraction. The noise is ADDED to the buffer's colour and, when Clip Result
31
+ * Values is on, all four channels are clamped to [0, 1] — alpha is clamped but
32
+ * never noised.
33
+ *
34
+ * **Interim seed (plan Task 2.7, pending the Task 4.3 probe).** What the CPU
35
+ * puts in `inRandomSeed_High`/`inRandomSeed_Low` per frame is still unknown —
36
+ * it is the effect's Random Seed combined with something frame-derived, which
37
+ * is why the noise animates with no keyframed property. Until the probe pins
38
+ * it, this kernel seeds the low half with the frame index and leaves the high
39
+ * half at zero, so the pattern is deterministic per frame and different between
40
+ * frames. Switching to the decoded packing is a change to two lines here and
41
+ * two in the reference; nothing else depends on it, and the tests assert the
42
+ * byte distribution and determinism rather than After Effects parity.
43
+ *
44
+ * **Pixel space.** `x` and `y` are integer pixel coordinates with y measured
45
+ * from the TOP, which is how After Effects indexes the buffer — the shader's
46
+ * `v_uv` is y-up, so the row is flipped back here. `inNoiseOffsetX/Y` (the
47
+ * layer's origin inside the source buffer) are not modelled: a HyperFrames host
48
+ * always captures its own box, so its origin is (0, 0).
49
+ *
50
+ * **Premultiplication.** The noise is added to the captured texel as it is,
51
+ * which is premultiplied — the literal port, and After Effects' own buffers are
52
+ * premultiplied too. The consequence is worth stating: on a layer with
53
+ * transparency the noise lands in the transparent region as well, exactly as
54
+ * the effect does in After Effects.
55
+ *
56
+ * **`rgb ≤ a` is enforced regardless of `Clip Result Values`, and that is a
57
+ * deliberate DEPARTURE from AE, not a port of it.** A valid premultiplied
58
+ * colour always has `rgb ≤ a` component-wise (it is `straightRgb · a`); adding
59
+ * noise to `src.rgb` can push it above `src.a` on a near-transparent texel,
60
+ * and `clamp(out4, 0, 1)` alone does not fix that — it clamps each channel to
61
+ * [0, 1] independently, never rgb to a. After Effects can carry that
62
+ * transient state through its float pipeline because everything downstream of
63
+ * it is float too; every render target this runtime writes is 8-bit RGBA
64
+ * (`GL_ATTRS`, `makePingTarget`), and an invalid premultiplied texel read back
65
+ * later (e.g. straight-alpha `rgb / a` for display) reads as a blown-out
66
+ * bright fringe rather than the visible defect After Effects itself never
67
+ * produces. So the clamp to `a` runs unconditionally, on both the `Clip
68
+ * Result Values` on and off paths.
69
+ *
70
+ * **`Clip Result Values` off is otherwise inert here, and that is a real,
71
+ * currently permanent limitation, not a bug to fix in this file.** Every
72
+ * render target in this runtime — the visible `.hf-vfx-out` canvas AND the
73
+ * ping-pong intermediates a multi-node chain reads between passes — is 8-bit
74
+ * `UNSIGNED_BYTE` RGBA, and the GL spec clamps a float fragment output to
75
+ * [0, 1] on conversion to a normalized integer target regardless of what the
76
+ * shader computes. `u_clipping = 0` therefore cannot produce an unclamped
77
+ * value anywhere in this pipeline today; it would take a floating-point
78
+ * render target (a real infrastructure change, not a one-line fix) to give it
79
+ * one. Recorded rather than silently accepted: a reviewer who mutates this
80
+ * shader and finds `u_clipping` untestable is finding a known limitation, not
81
+ * a fresh bug.
82
+ */
83
+ /**
84
+ * The constants the reference has to agree with, named once. `byteScale` is
85
+ * `0x3F00002000000000` from the IR — (1 + 2⁻⁹) · 2⁻¹⁵, i.e. 1/32704.
86
+ */
87
+ export declare const NOISE_CONSTANTS: {
88
+ readonly lcgMulA: 1103515245;
89
+ readonly lcgMulB: 12996205;
90
+ readonly lcgAdd: 12345;
91
+ readonly highMask: 32640;
92
+ readonly lowMask: 255;
93
+ readonly byteScale: 32704;
94
+ };
95
+ export declare const NOISE_FRAG: string;
96
+ //# sourceMappingURL=noise.frag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noise.frag.d.ts","sourceRoot":"","sources":["../../src/vfx/noise.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;CAOlB,CAAC;AAIX,eAAO,MAAM,UAAU,QAwEtB,CAAC"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * `noise` kernel — GLSL ES 3.00, capture `self`.
3
+ *
4
+ * A port of After Effects' `ADBE Noise`, decoded from `AEFX_Noise.metallib`
5
+ * (`docs/superpowers/specs/ae-metal-ir/AEFX_Noise.metallib.ll` in the exporter
6
+ * repo, helper `_Z5Noise8PixelRGBiifiiff`). The kernel is a pure integer hash of
7
+ * `(x, y, seed)` — no lookup table, no state between frames — so it ports
8
+ * exactly and stays seek-safe by construction.
9
+ *
10
+ * **What the IR says, read from the struct metadata rather than from prose.**
11
+ * `NoiseKernelValues` is `{ inDeviceFormat, inNoiseAmount, inUseColorNoise,
12
+ * inClipNoiseOutput, inRandomSeed_High, inRandomSeed_Low, inNoiseOffsetX,
13
+ * inNoiseOffsetY, … }`. So `ADBE Noise-0002` is **Use Color Noise**, a
14
+ * checkbox — not the "Noise Type (Uniform / Gaussian)" popup the deep dive
15
+ * guessed — and the two mystery seed floats are the halves of one 32-bit
16
+ * random seed. Both params are named here the way the metadata names them.
17
+ *
18
+ * Per channel:
19
+ *
20
+ * seed32 = (uint(seedHigh) << 16) | (uint(seedLow) & 0xFFFF)
21
+ * h = jenkinsMix(a, y, seed32) // Bob Jenkins' 1997 mix, verbatim
22
+ * t2 = h * 1103515245 + 12345
23
+ * t4 = t2 * 12996205 + 12345
24
+ * byte = ((t4 >> 16) & 0xFF) ^ ((t2 >> 9) & 0x7F80)
25
+ * noise = amount · (byte / 32704 − 0.5)
26
+ *
27
+ * with `a = x` for monochrome noise and `a = 3x + channel` (R, G, B) for colour
28
+ * noise — four separate hash blocks in the IR, three of them consecutive in
29
+ * `3x`. `32704` is the reciprocal constant `0x3F00002000000000` spelled as a
30
+ * fraction. The noise is ADDED to the buffer's colour and, when Clip Result
31
+ * Values is on, all four channels are clamped to [0, 1] — alpha is clamped but
32
+ * never noised.
33
+ *
34
+ * **Interim seed (plan Task 2.7, pending the Task 4.3 probe).** What the CPU
35
+ * puts in `inRandomSeed_High`/`inRandomSeed_Low` per frame is still unknown —
36
+ * it is the effect's Random Seed combined with something frame-derived, which
37
+ * is why the noise animates with no keyframed property. Until the probe pins
38
+ * it, this kernel seeds the low half with the frame index and leaves the high
39
+ * half at zero, so the pattern is deterministic per frame and different between
40
+ * frames. Switching to the decoded packing is a change to two lines here and
41
+ * two in the reference; nothing else depends on it, and the tests assert the
42
+ * byte distribution and determinism rather than After Effects parity.
43
+ *
44
+ * **Pixel space.** `x` and `y` are integer pixel coordinates with y measured
45
+ * from the TOP, which is how After Effects indexes the buffer — the shader's
46
+ * `v_uv` is y-up, so the row is flipped back here. `inNoiseOffsetX/Y` (the
47
+ * layer's origin inside the source buffer) are not modelled: a HyperFrames host
48
+ * always captures its own box, so its origin is (0, 0).
49
+ *
50
+ * **Premultiplication.** The noise is added to the captured texel as it is,
51
+ * which is premultiplied — the literal port, and After Effects' own buffers are
52
+ * premultiplied too. The consequence is worth stating: on a layer with
53
+ * transparency the noise lands in the transparent region as well, exactly as
54
+ * the effect does in After Effects.
55
+ *
56
+ * **`rgb ≤ a` is enforced regardless of `Clip Result Values`, and that is a
57
+ * deliberate DEPARTURE from AE, not a port of it.** A valid premultiplied
58
+ * colour always has `rgb ≤ a` component-wise (it is `straightRgb · a`); adding
59
+ * noise to `src.rgb` can push it above `src.a` on a near-transparent texel,
60
+ * and `clamp(out4, 0, 1)` alone does not fix that — it clamps each channel to
61
+ * [0, 1] independently, never rgb to a. After Effects can carry that
62
+ * transient state through its float pipeline because everything downstream of
63
+ * it is float too; every render target this runtime writes is 8-bit RGBA
64
+ * (`GL_ATTRS`, `makePingTarget`), and an invalid premultiplied texel read back
65
+ * later (e.g. straight-alpha `rgb / a` for display) reads as a blown-out
66
+ * bright fringe rather than the visible defect After Effects itself never
67
+ * produces. So the clamp to `a` runs unconditionally, on both the `Clip
68
+ * Result Values` on and off paths.
69
+ *
70
+ * **`Clip Result Values` off is otherwise inert here, and that is a real,
71
+ * currently permanent limitation, not a bug to fix in this file.** Every
72
+ * render target in this runtime — the visible `.hf-vfx-out` canvas AND the
73
+ * ping-pong intermediates a multi-node chain reads between passes — is 8-bit
74
+ * `UNSIGNED_BYTE` RGBA, and the GL spec clamps a float fragment output to
75
+ * [0, 1] on conversion to a normalized integer target regardless of what the
76
+ * shader computes. `u_clipping = 0` therefore cannot produce an unclamped
77
+ * value anywhere in this pipeline today; it would take a floating-point
78
+ * render target (a real infrastructure change, not a one-line fix) to give it
79
+ * one. Recorded rather than silently accepted: a reviewer who mutates this
80
+ * shader and finds `u_clipping` untestable is finding a known limitation, not
81
+ * a fresh bug.
82
+ */
83
+ /**
84
+ * The constants the reference has to agree with, named once. `byteScale` is
85
+ * `0x3F00002000000000` from the IR — (1 + 2⁻⁹) · 2⁻¹⁵, i.e. 1/32704.
86
+ */
87
+ export const NOISE_CONSTANTS = {
88
+ lcgMulA: 1103515245,
89
+ lcgMulB: 12996205,
90
+ lcgAdd: 12345,
91
+ highMask: 0x7f80,
92
+ lowMask: 0xff,
93
+ byteScale: 32704,
94
+ };
95
+ const { lcgMulA, lcgMulB, lcgAdd, highMask, lowMask, byteScale } = NOISE_CONSTANTS;
96
+ export const NOISE_FRAG = `#version 300 es
97
+ precision highp float;
98
+ // NOT decoration: a fragment shader's default integer precision is mediump,
99
+ // and a mediump uint is only guaranteed 16 bits (ESSL 3.00 §4.5.2). Every step
100
+ // below — the Jenkins mix, both LCG rounds — is defined by 32-bit wraparound,
101
+ // so without this the hash is a different function on any implementation that
102
+ // honours the minimum. ANGLE and SwiftShader both use 32-bit ints, which is
103
+ // exactly why no test on this machine would catch it.
104
+ precision highp int;
105
+
106
+ in vec2 v_uv;
107
+ out vec4 fragColor;
108
+
109
+ uniform vec2 u_size;
110
+ uniform float u_t;
111
+ uniform float u_fps;
112
+ uniform sampler2D u_src;
113
+ uniform float u_amount;
114
+ uniform float u_useColorNoise;
115
+ uniform float u_clipping;
116
+
117
+ // Bob Jenkins' 1997 integer mix, the six-round shift/xor/subtract sequence the
118
+ // metallib runs verbatim. uint arithmetic wraps in GLSL ES 3.00, which is what
119
+ // makes this bit-identical to the C original.
120
+ uint hfJenkinsMix(uint a, uint b, uint c) {
121
+ a -= b; a -= c; a ^= (c >> 13u);
122
+ b -= c; b -= a; b ^= (a << 8u);
123
+ c -= a; c -= b; c ^= (b >> 13u);
124
+ a -= b; a -= c; a ^= (c >> 12u);
125
+ b -= c; b -= a; b ^= (a << 16u);
126
+ c -= a; c -= b; c ^= (b >> 5u);
127
+ a -= b; a -= c; a ^= (c >> 3u);
128
+ b -= c; b -= a; b ^= (a << 10u);
129
+ c -= a; c -= b; c ^= (b >> 15u);
130
+ return c;
131
+ }
132
+
133
+ // The hash's 15-bit output byte for one channel.
134
+ float hfNoiseByte(uint a, uint y, uint seed) {
135
+ uint h = hfJenkinsMix(a, y, seed);
136
+ uint t2 = h * ${lcgMulA}u + ${lcgAdd}u;
137
+ uint t4 = t2 * ${lcgMulB}u + ${lcgAdd}u;
138
+ return float(((t4 >> 16u) & ${lowMask}u) ^ ((t2 >> 9u) & ${highMask}u));
139
+ }
140
+
141
+ void main() {
142
+ vec4 src = texture(u_src, v_uv);
143
+ vec2 px = floor(v_uv * u_size);
144
+ uint x = uint(max(px.x, 0.0));
145
+ // After Effects indexes rows from the top; v_uv is y-up.
146
+ uint y = uint(max(u_size.y - 1.0 - px.y, 0.0));
147
+ float frame = floor(u_t * u_fps + 0.5);
148
+ uint seed = uint(max(frame, 0.0)) & 0xFFFFu;
149
+
150
+ float amount = u_amount * 0.01;
151
+ vec3 nz;
152
+ if (u_useColorNoise > 0.5) {
153
+ uint a = x * 3u;
154
+ nz = vec3(hfNoiseByte(a, y, seed), hfNoiseByte(a + 1u, y, seed), hfNoiseByte(a + 2u, y, seed));
155
+ } else {
156
+ nz = vec3(hfNoiseByte(x, y, seed));
157
+ }
158
+ nz = amount * (nz / ${byteScale}.0 - 0.5);
159
+
160
+ vec4 out4 = vec4(src.rgb + nz, src.a);
161
+ // A valid premultiplied colour needs rgb <= a component-wise; noise on a
162
+ // near-transparent texel can push rgb above it, which our fixed 8-bit RGBA
163
+ // storage cannot carry safely the way AE's float pipeline can (see the doc
164
+ // comment above). This runs before, and independently of, u_clipping.
165
+ out4.rgb = min(out4.rgb, out4.a);
166
+ fragColor = u_clipping > 0.5 ? clamp(out4, 0.0, 1.0) : out4;
167
+ }
168
+ `;
169
+ //# sourceMappingURL=noise.frag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noise.frag.js","sourceRoot":"","sources":["../../src/vfx/noise.frag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,OAAO,EAAE,UAAU;IACnB,OAAO,EAAE,QAAQ;IACjB,MAAM,EAAE,KAAK;IACb,QAAQ,EAAE,MAAM;IAChB,OAAO,EAAE,IAAI;IACb,SAAS,EAAE,KAAK;CACR,CAAC;AAEX,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,eAAe,CAAC;AAEnF,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwCR,OAAO,OAAO,MAAM;mBACnB,OAAO,OAAO,MAAM;gCACP,OAAO,sBAAsB,QAAQ;;;;;;;;;;;;;;;;;;;;wBAoB7C,SAAS;;;;;;;;;;CAUhC,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * CPU reference for the `luma-matte` kernel. Unlike `wave-warp`'s and
3
+ * `displacement-map`'s, which return the coordinate they sample, this one
4
+ * returns the colour: the kernel's whole decision is a per-pixel scale, and a
5
+ * caller with both texels can check it exactly.
6
+ *
7
+ * Both texels are **premultiplied**, matching the textures the runtime uploads.
8
+ */
9
+ import { type HfVfxParamValues } from "../../vfx";
10
+ export interface Rgba {
11
+ r: number;
12
+ g: number;
13
+ b: number;
14
+ a: number;
15
+ }
16
+ /** The shader's `hfCoverage`. */
17
+ export declare function lumaMatteCoverageRef(matte: Rgba, mode: number): number;
18
+ /**
19
+ * The colour the kernel paints. `t` is part of the call shape every kernel
20
+ * reference shares; a matte is time-invariant — its animation arrives as
21
+ * movement in the matte element itself.
22
+ */
23
+ export declare function lumaMatteSampleRef(src: Rgba, matte: Rgba, t: number, params: HfVfxParamValues): Rgba;
24
+ //# sourceMappingURL=lumaMatte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lumaMatte.d.ts","sourceRoot":"","sources":["../../../src/vfx/refs/lumaMatte.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAMtE,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,iCAAiC;AACjC,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,EACT,KAAK,EAAE,IAAI,EACX,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,gBAAgB,GACvB,IAAI,CAKN"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CPU reference for the `luma-matte` kernel. Unlike `wave-warp`'s and
3
+ * `displacement-map`'s, which return the coordinate they sample, this one
4
+ * returns the colour: the kernel's whole decision is a per-pixel scale, and a
5
+ * caller with both texels can check it exactly.
6
+ *
7
+ * Both texels are **premultiplied**, matching the textures the runtime uploads.
8
+ */
9
+ import { normalizeVfxParams } from "../../vfx.js";
10
+ import { DISPLACEMENT_MAP_CONSTANTS } from "../displacementMap.frag.js";
11
+ import { LUMA_MATTE_MODES } from "../lumaMatte.frag.js";
12
+ const { lumaR, lumaG, lumaB } = DISPLACEMENT_MAP_CONSTANTS;
13
+ /** The shader's `hfCoverage`. */
14
+ export function lumaMatteCoverageRef(matte, mode) {
15
+ if (mode < LUMA_MATTE_MODES.alphaInverted)
16
+ return matte.a;
17
+ if (mode < LUMA_MATTE_MODES.luma)
18
+ return 1 - matte.a;
19
+ const luma = matte.r * lumaR + matte.g * lumaG + matte.b * lumaB;
20
+ return mode < LUMA_MATTE_MODES.lumaInverted ? luma : 1 - luma;
21
+ }
22
+ /**
23
+ * The colour the kernel paints. `t` is part of the call shape every kernel
24
+ * reference shares; a matte is time-invariant — its animation arrives as
25
+ * movement in the matte element itself.
26
+ */
27
+ export function lumaMatteSampleRef(src, matte, t, params) {
28
+ void t;
29
+ const n = normalizeVfxParams("luma-matte", params);
30
+ const coverage = Math.min(1, Math.max(0, lumaMatteCoverageRef(matte, Number(n["mode"]))));
31
+ return { r: src.r * coverage, g: src.g * coverage, b: src.b * coverage, a: src.a * coverage };
32
+ }
33
+ //# sourceMappingURL=lumaMatte.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lumaMatte.js","sourceRoot":"","sources":["../../../src/vfx/refs/lumaMatte.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAyB,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,0BAA0B,CAAC;AAS3D,iCAAiC;AACjC,MAAM,UAAU,oBAAoB,CAAC,KAAW,EAAE,IAAY;IAC5D,IAAI,IAAI,GAAG,gBAAgB,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC;IAC1D,IAAI,IAAI,GAAG,gBAAgB,CAAC,IAAI;QAAE,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC;IACjE,OAAO,IAAI,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAS,EACT,KAAW,EACX,CAAS,EACT,MAAwB;IAExB,KAAK,CAAC,CAAC;IACP,MAAM,CAAC,GAAG,kBAAkB,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1F,OAAO,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC;AAChG,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CPU reference for the `noise` kernel — the same integer hash the shader
3
+ * runs, in 32-bit wrapping arithmetic. JavaScript's `>>> 0` and `Math.imul`
4
+ * reproduce C's `unsigned int` overflow exactly, which is the property the
5
+ * whole port rests on.
6
+ *
7
+ * Returns the noise OFFSET applied to a pixel, not the colour: the offset is
8
+ * the entirety of what the kernel computes, and the add-and-clip around it is
9
+ * two lines a caller can apply itself.
10
+ */
11
+ import { type HfVfxParamValues } from "../../vfx";
12
+ /** The hash's 15-bit output byte for one channel index `a`. */
13
+ export declare function noiseByteRef(a: number, y: number, seed: number): number;
14
+ /**
15
+ * The 32-bit seed the kernel hashes with. Interim, pending the Task 4.3 probe:
16
+ * the frame index goes in the low half and the high half is zero. The packing
17
+ * itself is the metallib's (`inRandomSeed_High` << 16 | `inRandomSeed_Low`), so
18
+ * the probe's answer replaces the arguments, not the shape.
19
+ */
20
+ export declare function noiseSeedRef(t: number, fps: number): number;
21
+ /**
22
+ * The per-channel noise offset the kernel adds at pixel `(x, y)` — `y` counted
23
+ * from the TOP of the layer, as After Effects indexes its buffer.
24
+ */
25
+ export declare function noiseOffsetRef(p: {
26
+ x: number;
27
+ y: number;
28
+ }, t: number, fps: number, params: HfVfxParamValues): {
29
+ r: number;
30
+ g: number;
31
+ b: number;
32
+ };
33
+ //# sourceMappingURL=noise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noise.d.ts","sourceRoot":"","sources":["../../../src/vfx/refs/noise.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAsBtE,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3B,CAAC,EAAE,MAAM,EACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,gBAAgB,GACvB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAerC"}