refactor(engine,producer): adopt requestPaint contract, retire autoAlpha rewrite (#2021)

* refactor(engine,producer): adopt requestPaint contract, retire autoAlpha rewrite

crbug 529829538 was closed "working as intended": the html-in-canvas API's
contract is mutate -> canvas.requestPaint() -> await the canvas paint event ->
drawElementImage, which refreshes the subtree's paint records including
compositor-applied properties. Verified on the pinned 151 floor and 152
canary: root opacity, root filter, nested group opacity, and child transforms
(incl. will-change-promoted) all capture exactly; the root element's own
TRANSFORM is the one property still never baked.

- Paint invalidation: all three paint-wait sites (serial capture, worker
  produce, batch produce) now call canvas.requestPaint() when available and
  fall back to the __hf_de_tick sentinel background toggle on builds without
  it. The 250ms unsynchronized-draw safety net is unchanged.
- Root-opacity ratio correction REMOVED (all three draw sites + base-opacity
  recording at injection). Since 151 the paint wait bakes current root opacity
  into the snapshot as pixel alpha, so the ratio correction DOUBLE-APPLIED
  animated root fades: a root-fade A/B tripped the runtime self-verify at
  30.1dB (frame 24, ~0.92 expected vs ~0.85 rendered). Post-removal the same
  comp self-verifies at inf and matches the screenshot render at PSNR=inf.
  The root TRANSFORM correction stays — verified still required.
- autoAlpha rewrite machinery DELETED: the opt-in opacity->autoAlpha tween
  rewrite (default-off since the retraction fix; measured ~28dB damage on
  comps whose fades it touched), its flush-time transparent-target hiding,
  the __HF_FAST_CAPTURE_AUTOALPHA__ flag plumbing, and the deferral-time
  retract/re-assert dance. The stub keeps tween-target tracking (3D
  projection + at-risk scans depend on it).

Validation: canary suite 7/7 with PSNRs identical to baseline (58.30 /
43.13 / 54.15 dB); root-fade A/B PSNR=inf vs screenshot; engine suite 905
passed (1 pre-existing color-grading failure); tsc/oxlint/oxfmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(engine,producer): review fixes — gate opacity correction by paint mechanism

Max code-review findings on the requestPaint adoption:

- Root-opacity ratio correction RESTORED, gated per frame on how the paint
  was produced: it applies on BeginFrame (sync=false) captures and on builds
  without canvas.requestPaint() — the two paths where the snapshot holds the
  root's load-time opacity — and is skipped only on requestPaint-driven
  paints, where the snapshot bakes the current opacity and the ratio
  double-applies (the proven 30.1dB root-fade failure). Base opacity is
  recorded at injection again.
- Invalidation extracted to a page-scope helper (__hfDeInvalidate, installed
  by injectDrawElementCanvas) shared by all three paint-wait sites: sentinel
  toggle ALWAYS (a paint is guaranteed even if requestPaint elides one on a
  clean subtree) + requestPaint() in a try/catch (a throwing implementation
  degrades to sentinel-only instead of rejecting the capture). Returns
  whether requestPaint ran, feeding the opacity-correction gate. Also removes
  the triplicated inline block and its three anonymous `as T` casts.
- HF_FAST_CAPTURE_AUTOALPHA now logs a retirement warning instead of being a
  silent no-op (the deleted rewrite's comment documented it as an operator
  escape hatch).
- Batch producer docstring updated (still described the tick-toggle-only
  paint wait); stub tween observer reshaped to a void fn (observeTweenCall)
  so no arg-rewriting seam survives.

Validation: canary suite 7/7 (58.30/43.13/54.15dB, d95f20b6 clean);
root-fade A/B self-verify 4x inf + whole-video PSNR=inf; engine suite 905
passed (1 pre-existing); tsc/oxlint/oxfmt clean; stub regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: WaterrrForever <miao.yang@heygen.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-07 01:52:26 -07:00
committed by GitHub
co-authored by Claude Fable 5 WaterrrForever
parent 574da2b215
commit 337d0b51bc
4 changed files with 164 additions and 269 deletions
@@ -142,10 +142,11 @@ export async function injectDrawElementCanvas(
({ w, h }: { w: number; h: number }) => { ({ w, h }: { w: number; h: number }) => {
const root = document.querySelector("[data-composition-id]") as HTMLElement | null; const root = document.querySelector("[data-composition-id]") as HTMLElement | null;
if (!root || document.getElementById("__hf_de_canvas")) return; if (!root || document.getElementById("__hf_de_canvas")) return;
// Record the root's base opacity now (timeline at 0, before any entrance/ // Record the root's base opacity (timeline at 0, before any entrance/
// outro tween) so the per-frame capture can correct drawElementImage's stale // outro tween) for the LEGACY root-opacity ratio correction. The
// opacity by the ratio current/base. (Transform is corrected unconditionally // correction only runs on paths whose paint does not bake the root's
// and needs no base; see captureDrawElementFrame.) // current opacity into the snapshot — BeginFrame (sync=false) captures
// and builds without canvas.requestPaint(); see __hfDeInvalidate below.
try { try {
(window as unknown as { __HF_ROOT_BASE_OPACITY__?: number }).__HF_ROOT_BASE_OPACITY__ = (window as unknown as { __HF_ROOT_BASE_OPACITY__?: number }).__HF_ROOT_BASE_OPACITY__ =
parseFloat(getComputedStyle(root).opacity) || 1; parseFloat(getComputedStyle(root).opacity) || 1;
@@ -165,14 +166,46 @@ export async function injectDrawElementCanvas(
parent.insertBefore(canvas, root); parent.insertBefore(canvas, root);
canvas.appendChild(root); canvas.appendChild(root);
// Invalidation sentinel: a canvas child OUTSIDE the captured root. // Invalidation sentinel: a canvas child OUTSIDE the captured root.
// Toggling its `left` each capture dirties the layoutsubtree so a paint // Toggling its background each capture is a PAINT-level dirty
// (and a fresh snapshot) is guaranteed even for static frames — without // (layout/transform toggles do NOT fire the canvas `paint` event), so a
// ever appearing in drawElementImage(root) output. // paint — and a fresh snapshot — is guaranteed even for static frames,
// without the sentinel ever appearing in drawElementImage(root) output.
const tick = document.createElement("div"); const tick = document.createElement("div");
tick.id = "__hf_de_tick"; tick.id = "__hf_de_tick";
tick.style.cssText = tick.style.cssText =
"position:absolute;left:0px;top:0;width:1px;height:1px;background:#000;opacity:0.01;pointer-events:none"; "position:absolute;left:0px;top:0;width:1px;height:1px;background:#000;opacity:0.01;pointer-events:none";
canvas.appendChild(tick); canvas.appendChild(tick);
// Per-frame invalidation helper shared by every paint-wait site (serial
// capture, worker produce, batch produce). Two mechanisms, both applied:
// - the sentinel toggle guarantees a paint even if requestPaint() were
// to elide one on a clean subtree, and remains the sole mechanism on
// builds without requestPaint;
// - canvas.requestPaint() is the html-in-canvas API's intended
// invalidation (crbug 529829538 triage): it refreshes the subtree's
// paint records — compositor-applied props included — where the
// sentinel's dirty alone would not on pre-151 builds. Guarded so a
// throwing implementation degrades to sentinel-only instead of
// rejecting the capture.
// Returns true when requestPaint was called — the snapshot then bakes
// the root's CURRENT compositor-applied opacity (measured on 151/152),
// so callers must skip the legacy root-opacity ratio correction.
interface RequestPaintCanvas extends HTMLCanvasElement {
requestPaint?: () => void;
}
(window as Window & { __hfDeInvalidate?: () => boolean }).__hfDeInvalidate = () => {
tick.style.backgroundColor =
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
const cvp: RequestPaintCanvas = canvas;
if (typeof cvp.requestPaint === "function") {
try {
cvp.requestPaint();
return true;
} catch {
// Feature drift — sentinel dirty above still forces the paint.
}
}
return false;
};
}, },
{ w: width, h: height }, { w: width, h: height },
); );
@@ -245,8 +278,15 @@ export async function captureDrawElementFrame(
__hf_accel_canvases?: HTMLCanvasElement[]; __hf_accel_canvases?: HTMLCanvasElement[];
__hf_canvas_2d?: HTMLCanvasElement[]; __hf_canvas_2d?: HTMLCanvasElement[];
__hf3d?: { update: () => void }; __hf3d?: { update: () => void };
__hfDeInvalidate?: () => boolean;
__HF_ROOT_PROPS__?: boolean;
__HF_ROOT_BASE_OPACITY__?: number;
}; };
const aw = window as AccelWindow; const aw = window as AccelWindow;
// True when this frame's paint was requested via canvas.requestPaint()
// — the snapshot then bakes the root's current compositor-applied
// opacity, so the legacy ratio correction below must be skipped.
let usedRequestPaint = false;
// Re-project CSS 3D contexts for THIS frame (threeDProjection.ts) so // Re-project CSS 3D contexts for THIS frame (threeDProjection.ts) so
// their WebGL canvases are fresh before being drawImage-composited // their WebGL canvases are fresh before being drawImage-composited
// below. Must run before the paint wait for the same reason as the // below. Must run before the paint wait for the same reason as the
@@ -319,38 +359,36 @@ export async function captureDrawElementFrame(
// Zero-sized or not-yet-configured canvas — skip this frame. // Zero-sized or not-yet-configured canvas — skip this frame.
} }
} }
// drawElementImage does not reflect post-paint changes to compositor- // Root compositor-prop corrections. Two distinct behaviours:
// applied properties on the captured element ITSELF — opacity, transform, // • transform — the snapshot NEVER bakes the captured element's
// and filter on the root are applied by its parent at composite time and // own transform (even a static one renders unscaled — the
// never enter the root's content snapshot (baked at the load-time/base // parent applies it at composite time; verified still true
// value). Re-apply them to the 2D context, comparing against the base // under the requestPaint contract, crbug 529829538 triage,
// recorded at injection so a static root is a no-op (no double-apply / no // probed on 151 + 152 canary 2026-07-07). Apply the current
// regression) and an animated root is corrected. // matrix unconditionally about the transform-origin.
// Two distinct behaviours, both measured: // • opacity — DEPENDS on how this frame's paint was produced.
// • opacity — the content snapshot DOES bake the root's load-time // A requestPaint()-driven paint bakes the root's CURRENT
// opacity, so correct by the ratio current/base (static ⇒ ratio 1 ⇒ // opacity into the snapshot (pixel alpha) — the legacy ratio
// no-op; animated ⇒ corrected). // correction DOUBLE-APPLIES there (~0.92 → 0.85 effective,
// • transform — the snapshot NEVER bakes the root's own transform (even // 30.1 dB self-verify failure on a root-fade A/B) and must be
// a static transform renders unscaled), so apply the current matrix // skipped. BeginFrame captures (sync=false) and builds without
// unconditionally about the transform-origin (no transform ⇒ no-op). // requestPaint keep the pre-existing ratio correction: on
// filter is intentionally NOT corrected: the per-frame sentinel repaint // those paths the snapshot holds the load-time opacity.
// already bakes it into the snapshot (correcting it double-applies). // filter is never corrected (paint wait bakes it).
const __rw = window as unknown as {
__HF_ROOT_PROPS__?: boolean;
__HF_ROOT_BASE_OPACITY__?: number;
};
let __appliedAlpha = false;
let __appliedTransform = false; let __appliedTransform = false;
if (__rw.__HF_ROOT_PROPS__) { let __appliedAlpha = false;
if (aw.__HF_ROOT_PROPS__) {
try { try {
const rcs = getComputedStyle(root); const rcs = getComputedStyle(root);
const baseOp = __rw.__HF_ROOT_BASE_OPACITY__ ?? 1; if (!usedRequestPaint) {
const curOp = parseFloat(rcs.opacity); const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
if (baseOp > 0.001 && Number.isFinite(curOp)) { const curOp = parseFloat(rcs.opacity);
const ratio = curOp / baseOp; if (baseOp > 0.001 && Number.isFinite(curOp)) {
if (Math.abs(ratio - 1) > 0.002) { const ratio = curOp / baseOp;
ctx.globalAlpha = Math.max(0, Math.min(1, ratio)); if (Math.abs(ratio - 1) > 0.002) {
__appliedAlpha = true; ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
__appliedAlpha = true;
}
} }
} }
const curTransform = rcs.transform; const curTransform = rcs.transform;
@@ -418,20 +456,14 @@ export async function captureDrawElementFrame(
canvas.addEventListener("paint", onPaint); canvas.addEventListener("paint", onPaint);
// Force an invalidation so a paint is guaranteed even when this frame's // Force an invalidation so a paint is guaranteed even when this frame's
// seek produced no paint-level change (static scene, or transform-only // seek produced no paint-level change (static scene, or transform-only
// GSAP updates that are compositor-side and never repaint). The sentinel // GSAP updates that are compositor-side and never repaint). Sentinel
// is a 1x1 canvas child OUTSIDE the captured root (see // dirty + requestPaint(), installed by injectDrawElementCanvas — see
// injectDrawElementCanvas): toggling its background is a PAINT-level // __hfDeInvalidate there for the full mechanism/rationale.
// change (layout/transform toggles do NOT fire the paint event), so a usedRequestPaint = aw.__hfDeInvalidate?.() === true;
// paint + fresh snapshot follow promptly — without the sentinel ever
// appearing in drawElementImage(root) output.
const tick = document.getElementById("__hf_de_tick");
if (tick) {
tick.style.backgroundColor =
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
}
// Safety net: if the paint event doesn't arrive (feature drift / // Safety net: if the paint event doesn't arrive (feature drift /
// throttled page), fall back to an unsynchronized draw after 250 ms — // throttled page), fall back to an unsynchronized draw after 250 ms —
// worst case one-frame-stale content rather than a hung render. // worst case one-frame-stale content (the root's alpha may lag its
// transform by that frame) rather than a hung render.
setTimeout(() => { setTimeout(() => {
canvas.removeEventListener("paint", onPaint); canvas.removeEventListener("paint", onPaint);
drawAndEncode(); drawAndEncode();
@@ -682,6 +714,9 @@ export async function produceDrawElementFrame(
__hf_accel_canvases?: HTMLCanvasElement[]; __hf_accel_canvases?: HTMLCanvasElement[];
__hf_canvas_2d?: HTMLCanvasElement[]; __hf_canvas_2d?: HTMLCanvasElement[];
__hf3d?: { update: () => void }; __hf3d?: { update: () => void };
__hfDeInvalidate?: () => boolean;
__HF_ROOT_PROPS__?: boolean;
__HF_ROOT_BASE_OPACITY__?: number;
}; };
const aw = window as AccelWindow; const aw = window as AccelWindow;
aw.__hf3d?.update(); aw.__hf3d?.update();
@@ -700,6 +735,7 @@ export async function produceDrawElementFrame(
return new Promise<void>((resolveCapture, rejectCapture) => { return new Promise<void>((resolveCapture, rejectCapture) => {
let settled = false; let settled = false;
let usedRequestPaint = false;
const drawAndKick = () => { const drawAndKick = () => {
if (settled) return; if (settled) return;
settled = true; settled = true;
@@ -729,27 +765,23 @@ export async function produceDrawElementFrame(
// skip // skip
} }
} }
// Re-apply the captured root's compositor-applied opacity/transform — // Root compositor-prop corrections — identical to the serial path
// identical to the serial path (see drawAndEncode). drawElementImage does // (see drawAndEncode's comment): transform always; opacity ratio
// not reflect post-paint changes to these on the root itself; without this // only when this frame's paint was NOT requestPaint-driven.
// the worker path damages comps with an animated root opacity/transform
// (the serial path corrects it, so worker-on diverged from serial DE).
const __rw = window as unknown as {
__HF_ROOT_PROPS__?: boolean;
__HF_ROOT_BASE_OPACITY__?: number;
};
let __appliedAlpha = false;
let __appliedTransform = false; let __appliedTransform = false;
if (__rw.__HF_ROOT_PROPS__) { let __appliedAlpha = false;
if (aw.__HF_ROOT_PROPS__) {
try { try {
const rcs = getComputedStyle(root); const rcs = getComputedStyle(root);
const baseOp = __rw.__HF_ROOT_BASE_OPACITY__ ?? 1; if (!usedRequestPaint) {
const curOp = parseFloat(rcs.opacity); const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
if (baseOp > 0.001 && Number.isFinite(curOp)) { const curOp = parseFloat(rcs.opacity);
const ratio = curOp / baseOp; if (baseOp > 0.001 && Number.isFinite(curOp)) {
if (Math.abs(ratio - 1) > 0.002) { const ratio = curOp / baseOp;
ctx.globalAlpha = Math.max(0, Math.min(1, ratio)); if (Math.abs(ratio - 1) > 0.002) {
__appliedAlpha = true; ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
__appliedAlpha = true;
}
} }
} }
const curTransform = rcs.transform; const curTransform = rcs.transform;
@@ -816,11 +848,9 @@ export async function produceDrawElementFrame(
drawAndKick(); drawAndKick();
}; };
canvas.addEventListener("paint", onPaint); canvas.addEventListener("paint", onPaint);
const tick = document.getElementById("__hf_de_tick"); // Sentinel dirty + requestPaint() — see __hfDeInvalidate in
if (tick) { // injectDrawElementCanvas.
tick.style.backgroundColor = usedRequestPaint = aw.__hfDeInvalidate?.() === true;
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
}
setTimeout(() => { setTimeout(() => {
canvas.removeEventListener("paint", onPaint); canvas.removeEventListener("paint", onPaint);
drawAndKick(); drawAndKick();
@@ -835,8 +865,9 @@ export async function produceDrawElementFrame(
/** /**
* P6 prototype (HF_DE_BATCH): batch-produce N consecutive frames in ONE CDP * P6 prototype (HF_DE_BATCH): batch-produce N consecutive frames in ONE CDP
* round-trip. In-page loop per frame: `__hf.seek(t)` → paint-wait (tick toggle + * round-trip. In-page loop per frame: `__hf.seek(t)` → paint-wait
* canvas `paint` event) → drawElementImage composite → createImageBitmap → * (__hfDeInvalidate: sentinel dirty + requestPaint, then the canvas `paint`
* event) → drawElementImage composite → createImageBitmap →
* postMessage to the encode worker. Bitmaps are posted per-frame (encode starts * postMessage to the encode worker. Bitmaps are posted per-frame (encode starts
* immediately); only the CDP protocol round-trips are amortized N-fold. * immediately); only the CDP protocol round-trips are amortized N-fold.
* Micro-pipeline inside the batch: frame i+1's seek/paint-wait overlaps frame * Micro-pipeline inside the batch: frame i+1's seek/paint-wait overlaps frame
@@ -910,11 +941,13 @@ export async function produceDrawElementFrameBatch(
__hf_accel_canvases?: HTMLCanvasElement[]; __hf_accel_canvases?: HTMLCanvasElement[];
__hf3d?: { update: () => void }; __hf3d?: { update: () => void };
__hf?: { seek?: (t: number) => void }; __hf?: { seek?: (t: number) => void };
__hfDeInvalidate?: () => boolean;
__HF_ROOT_PROPS__?: boolean; __HF_ROOT_PROPS__?: boolean;
__HF_ROOT_BASE_OPACITY__?: number; __HF_ROOT_BASE_OPACITY__?: number;
__hfEncWorker?: Worker; __hfEncWorker?: Worker;
}; };
const aw = window as AccelWindow; const aw = window as AccelWindow;
let usedRequestPaint = false;
const waitPaint = (): Promise<void> => const waitPaint = (): Promise<void> =>
new Promise((res) => { new Promise((res) => {
@@ -926,11 +959,9 @@ export async function produceDrawElementFrameBatch(
res(); res();
}; };
canvas.addEventListener("paint", settle); canvas.addEventListener("paint", settle);
const tick = document.getElementById("__hf_de_tick"); // Sentinel dirty + requestPaint() — see __hfDeInvalidate in
if (tick) { // injectDrawElementCanvas.
tick.style.backgroundColor = usedRequestPaint = aw.__hfDeInvalidate?.() === true;
tick.style.backgroundColor === "rgb(0, 0, 0)" ? "rgb(1, 1, 1)" : "rgb(0, 0, 0)";
}
setTimeout(settle, 250); setTimeout(settle, 250);
}); });
@@ -978,20 +1009,23 @@ export async function produceDrawElementFrameBatch(
// skip // skip
} }
} }
// Root compositor-applied opacity/transform correction — mirrors // Root compositor-prop corrections — mirrors produceDrawElementFrame
// produceDrawElementFrame (see its comment). // (see drawAndEncode's comment): transform always; opacity ratio only
let appliedAlpha = false; // when this frame's paint was NOT requestPaint-driven.
let appliedTransform = false; let appliedTransform = false;
let appliedAlpha = false;
if (aw.__HF_ROOT_PROPS__) { if (aw.__HF_ROOT_PROPS__) {
try { try {
const rcs = getComputedStyle(root); const rcs = getComputedStyle(root);
const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1; if (!usedRequestPaint) {
const curOp = parseFloat(rcs.opacity); const baseOp = aw.__HF_ROOT_BASE_OPACITY__ ?? 1;
if (baseOp > 0.001 && Number.isFinite(curOp)) { const curOp = parseFloat(rcs.opacity);
const ratio = curOp / baseOp; if (baseOp > 0.001 && Number.isFinite(curOp)) {
if (Math.abs(ratio - 1) > 0.002) { const ratio = curOp / baseOp;
ctx.globalAlpha = Math.max(0, Math.min(1, ratio)); if (Math.abs(ratio - 1) > 0.002) {
appliedAlpha = true; ctx.globalAlpha = Math.max(0, Math.min(1, ratio));
appliedAlpha = true;
}
} }
} }
const curTransform = rcs.transform; const curTransform = rcs.transform;
+17 -46
View File
@@ -501,18 +501,6 @@ async function initDrawElementOrTransparentBackground(
"hint forced screenshot capture (e.g. raw requestAnimationFrame composition).", "hint forced screenshot capture (e.g. raw requestAnimationFrame composition).",
); );
} }
// Retract the per-page autoAlpha rewrite flag when a runtime gate routes the
// session to screenshot mode. evaluateOnNewDocument already fired; a follow-up
// evaluate overrides it in the live page context so hideTransparentAutoAlpha-
// Targets does not hide elements on the fallback screenshot render
// (up to 21 dB damage if not retracted, A/B proven 2026-06-12).
async function retractAutoAlphaFlag(): Promise<void> {
await page.evaluate(() => {
(
window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }
).__HF_FAST_CAPTURE_AUTOALPHA__ = false;
});
}
if (useDrawElement) { if (useDrawElement) {
session.isSwiftShader = await detectSwiftShader(page); session.isSwiftShader = await detectSwiftShader(page);
const transparent = session.options.format === "png"; const transparent = session.options.format === "png";
@@ -521,7 +509,6 @@ async function initDrawElementOrTransparentBackground(
if (transparent) { if (transparent) {
await initTransparentBackground(session.page); await initTransparentBackground(session.page);
} }
await retractAutoAlphaFlag();
// Static-frame dedup is capture-mode-independent (the serial path reuses // Static-frame dedup is capture-mode-independent (the serial path reuses
// lastFrameBuffer regardless of how the frame was captured) and lossless // lastFrameBuffer regardless of how the frame was captured) and lossless
// (anchor-verified). A comp only reaches THIS fallback with useDrawElement=true // (anchor-verified). A comp only reaches THIS fallback with useDrawElement=true
@@ -646,14 +633,10 @@ async function initDrawElementOrTransparentBackground(
// screenshots would capture black <video> boxes, and once the canvas is // screenshots would capture black <video> boxes, and once the canvas is
// injected they can never be retaken. The capture stage completes the // injected they can never be retaken. The capture stage completes the
// init after prepareCaptureSessionForReuse attaches the injector. // init after prepareCaptureSessionForReuse attaches the injector.
// Retract the autoAlpha rewrite flag while deferred — if no path ever
// completes the init (e.g. the disk path takes over), the session
// captures via screenshot, where the armed flag causes measured damage.
if (!session.onBeforeCapture && !forceDE) { if (!session.onBeforeCapture && !forceDE) {
const hasVideos = await page.evaluate(() => document.querySelector("video") !== null); const hasVideos = await page.evaluate(() => document.querySelector("video") !== null);
if (hasVideos) { if (hasVideos) {
session.deInitDeferred = true; session.deInitDeferred = true;
await retractAutoAlphaFlag();
logInitPhase("drawElement init deferred: video comp awaiting frame injector"); logInitPhase("drawElement init deferred: video comp awaiting frame injector");
return; return;
} }
@@ -721,15 +704,10 @@ async function finalizeDrawElementInit(
* Complete a deferred drawElement init (see CaptureSession.deInitDeferred). * Complete a deferred drawElement init (see CaptureSession.deInitDeferred).
* Call after prepareCaptureSessionForReuse has attached the video-frame * Call after prepareCaptureSessionForReuse has attached the video-frame
* injector; no-op when the session is not deferred or still has no injector. * injector; no-op when the session is not deferred or still has no injector.
* Re-asserts the autoAlpha rewrite flag retracted at deferral time.
*/ */
export async function completeDeferredDrawElementInit(session: CaptureSession): Promise<void> { export async function completeDeferredDrawElementInit(session: CaptureSession): Promise<void> {
if (!session.deInitDeferred || !session.onBeforeCapture) return; if (!session.deInitDeferred || !session.onBeforeCapture) return;
const page = session.page; const page = session.page;
await page.evaluate(() => {
(window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }).__HF_FAST_CAPTURE_AUTOALPHA__ =
true;
});
const logInitPhase = (phase: string) => const logInitPhase = (phase: string) =>
console.log(`[initSession:${session.captureMode}] ${phase} (deferred drawElement init)`); console.log(`[initSession:${session.captureMode}] ${phase} (deferred drawElement init)`);
await finalizeDrawElementInit(session, page, logInitPhase, { await finalizeDrawElementInit(session, page, logInitPhase, {
@@ -844,31 +822,24 @@ export async function createCaptureSession(
if (useDrawElement) { if (useDrawElement) {
await page.evaluateOnNewDocument(instrumentAcceleratedCanvases); await page.evaluateOnNewDocument(instrumentAcceleratedCanvases);
} }
// Signal the producer's GSAP stub to rewrite `opacity``autoAlpha` in tween // The opacity → autoAlpha tween rewrite was RETIRED with the requestPaint
// vars (stacked opacity-0 caption layers break drawElementImage capture). // contract adoption (crbug 529829538 closed WAI): a requestPaint-driven
// // paint refreshes nested opacity layers natively, and the rewrite itself
// DEFAULT OFF (opt in with HF_FAST_CAPTURE_AUTOALPHA=true). The rewrite is baked // measured ~28 dB of damage on comps whose fades it touched. Warn instead
// at tween-creation (page load) and `retractAutoAlphaFlag` (flag-only) can't // of silently ignoring the old escape hatch.
// un-bake it: GSAP autoAlpha sets visibility:hidden under seek capture and renders if (process.env.HF_FAST_CAPTURE_AUTOALPHA !== undefined) {
// ~28 dB below a clean opacity baseline (corpus eval 2026-06-16, e.g. console.warn(
// 05f22830/06167790: autoAlpha-off = ∞, autoAlpha-on = 28 dB). The rewrite was a "[engine] HF_FAST_CAPTURE_AUTOALPHA is retired and ignored — the requestPaint " +
// workaround for the stacked-fade opacity-layer drop (crbug 521861819), now fixed "paint contract captures animated opacity natively (see drawElementService.ts).",
// in Chrome 151 — so it damages more than it fixes. Re-enable per render only if a );
// drawElement comp shows transparent-layer drop on the pinned 151 floor.
if (useDrawElement && process.env.HF_FAST_CAPTURE_AUTOALPHA === "true") {
await page.evaluateOnNewDocument(() => {
(
window as Window & { __HF_FAST_CAPTURE_AUTOALPHA__?: boolean }
).__HF_FAST_CAPTURE_AUTOALPHA__ = true;
});
} }
// Re-apply the captured root's own computed opacity to the 2D context: // Re-apply the captured root's own compositor-applied props to the 2D
// drawElementImage does not reflect post-paint changes to compositor-applied // context where the snapshot does not carry them (see the correction
// properties on the captured element itself (the root's opacity is applied by // comment in drawElementService.ts drawAndEncode: transform always —
// its parent at composite time, never baked into its content snapshot), so an // never baked, verified under the requestPaint contract 2026-07-07;
// animated root fade renders at full opacity. captureDrawElementFrame corrects // opacity ratio only on non-requestPaint paints, where the snapshot holds
// this by the ratio current/base opacity (no-op for a static root). On by // the load-time value). On by default; disable with
// default; disable with HF_FAST_CAPTURE_ROOT_PROPS=false. // HF_FAST_CAPTURE_ROOT_PROPS=false.
if (useDrawElement && process.env.HF_FAST_CAPTURE_ROOT_PROPS !== "false") { if (useDrawElement && process.env.HF_FAST_CAPTURE_ROOT_PROPS !== "false") {
await page.evaluateOnNewDocument(() => { await page.evaluateOnNewDocument(() => {
(window as unknown as { __HF_ROOT_PROPS__?: boolean }).__HF_ROOT_PROPS__ = true; (window as unknown as { __HF_ROOT_PROPS__?: boolean }).__HF_ROOT_PROPS__ = true;
@@ -1,6 +1,6 @@
// AUTO-GENERATED by scripts/build-hf-early-stub.ts — do not edit // AUTO-GENERATED by scripts/build-hf-early-stub.ts — do not edit
const HF_EARLY_STUB_IIFE: string = const HF_EARLY_STUB_IIFE: string =
'"use strict";(()=>{var b=100,O=[],u=[],w=!1,f=!1,c=new Set,p=new Set;function y(n){let e=window.__HF_VIRTUAL_TIME__?.originalRequestAnimationFrame;return typeof e=="function"?e(n):requestAnimationFrame(n)}function x(n){let e=window.__HF_VIRTUAL_TIME__?.originalSetTimeout;if(typeof e=="function"){e(n,0);return}setTimeout(n,0)}function S(n){return n!==null&&typeof n=="object"&&"__hfIsProxy"in n?n.__hfReal:n}function _(n){if(n===null||typeof n!="object"||Array.isArray(n))return n;let e=n;if(!("opacity"in e)||"autoAlpha"in e||"visibility"in e)return n;let o={};for(let t of Object.keys(e))t==="opacity"?o.autoAlpha=e[t]:o[t]=e[t];return o}function T(n,e){if(n!=="add"&&v(e),window.__HF_FAST_CAPTURE_AUTOALPHA__!==!0||n==="add")return e;let o=e.slice(),t=!1;if(o.length>1){let i=_(o[1]);i!==o[1]&&(t=!0),o[1]=i}if(n==="fromTo"&&o.length>2){let i=_(o[2]);i!==o[2]&&(t=!0),o[2]=i}return t&&o[0]!==null&&o[0]!==void 0&&c.add(o[0]),o}function h(n){if(n===null||typeof n!="object"||Array.isArray(n))return!1;let e=n;return"rotationX"in e||"rotationY"in e||"transformPerspective"in e}var m=new Set;function v(n){let e=n[0];if(e==null)return;let o=window;m.has(e)||(m.add(e),o.__hfAllTweenTargets=Array.from(m)),(h(n[1])||h(n[2]))&&(p.add(e),o.__hf3dTweenTargets=Array.from(p))}function R(n){if(typeof n=="string")try{return Array.from(document.querySelectorAll(n))}catch{return[]}if(n instanceof Element)return[n];if(Array.isArray(n)||typeof NodeList<"u"&&n instanceof NodeList){let e=[];for(let o of n)o instanceof Element&&e.push(o);return e}return[]}function F(){if(window.__HF_FAST_CAPTURE_AUTOALPHA__!==!0||c.size===0)return;let n=Array.from(c);c.clear();for(let e of n)for(let o of R(e)){let t=o;if(!t.style||t.style.visibility!=="")continue;let i="";try{i=getComputedStyle(o).opacity}catch{continue}i==="0"&&(t.style.visibility="hidden")}}function g(n){let e=n.proxy.__hfReal,o=e[n.method];if(typeof o=="function"){let t=n.method==="add"?n.args.map(S):n.args;o.call(e,...t)}}function l(n,e,o){let t={proxy:n,method:e,args:T(e,o)};return n.__hfQueue.push(t),u.push(t),I(),n}function A(n){let e=n.proxy.__hfQueue.indexOf(n);e>=0&&n.proxy.__hfQueue.splice(e,1)}function r(){for(;u.length>0;){let n=u.shift();n&&(A(n),g(n))}E()}function k(){f=!1,F(),window.__hfTimelinesBuilding=!1;try{window.dispatchEvent(new CustomEvent("hf-timelines-built"))}catch{}}function E(){f||(f=!0,x(()=>{u.length===0?k():f=!1}))}function P(){w=!1;let n=u.splice(0,b);for(let e of n)A(e),g(e);u.length>0?(w=!0,y(P)):k()}function I(){w||(w=!0,window.__hfTimelinesBuilding=!0,y(P))}var C=new Set(["to","from","fromTo","set","add"]);function G(n,e){let o=e;for(;o!==null&&o!==Object.prototype;){for(let t of Object.getOwnPropertyNames(o)){if(t==="constructor"||t==="then"||t in n||C.has(t)||t.charAt(0)==="_")continue;let i=Object.getOwnPropertyDescriptor(o,t);if(!i||typeof i.value!="function")continue;let s=i.value;n[t]=function(...d){r();let a=s.call(e,...d);return a===e?n:a}}o=Object.getPrototypeOf(o)}}function H(n){let e={__hfReal:n,__hfQueue:[],__hfIsProxy:!0,to(...o){return l(e,"to",o)},from(...o){return l(e,"from",o)},fromTo(...o){return l(e,"fromTo",o)},set(...o){return l(e,"set",o)},add(...o){return l(e,"add",o)},pause(...o){return r(),n.pause(...o),e},play(...o){return r(),n.play(...o),e},seek(...o){return r(),n.seek(...o),e},totalTime(...o){return r(),o.length>0?(n.totalTime(...o),e):n.totalTime()},time(...o){return r(),o.length>0?(n.time(...o),e):n.time()},duration(...o){return r(),o.length>0?(n.duration(...o),e):n.duration()},getChildren(...o){r();let t=n.getChildren(...o);return Array.isArray(t)?t:[]},paused(...o){return r(),o.length>0?(n.paused(...o),e):n.paused()},timeScale(...o){return r(),o.length>0?(n.timeScale(...o),e):n.timeScale()},kill(){r(),n.kill()}};return G(e,n),O.push(e),e}if(typeof window<"u"){window.__hf||(window.__hf={}),window.__hfTimelinesBuilding=!1,window.__hfFlushSync=()=>{r(),u.length===0&&window.__hfTimelinesBuilding&&k()};let n=null;try{Object.defineProperty(window,"gsap",{configurable:!0,enumerable:!0,get(){return n},set(e){if(n=e,!e||typeof e.timeline!="function")return;let o=e.timeline.bind(e);e.timeline=i=>H(o(i));for(let i of["to","from","set"]){let s=e[i];if(typeof s!="function")continue;let d=s.bind(e);e[i]=(...a)=>d(...T(i,a))}let t=e.fromTo;if(typeof t=="function"){let i=t.bind(e);e.fromTo=(...s)=>i(...T("fromTo",s))}}})}catch{}}})();\n'; '"use strict";(()=>{var P=100,x=[],s=[],w=!1,f=!1,T=new Set;function p(n){let e=window.__HF_VIRTUAL_TIME__?.originalRequestAnimationFrame;return typeof e=="function"?e(n):requestAnimationFrame(n)}function b(n){let e=window.__HF_VIRTUAL_TIME__?.originalSetTimeout;if(typeof e=="function"){e(n,0);return}setTimeout(n,0)}function O(n){return n!==null&&typeof n=="object"&&"__hfIsProxy"in n?n.__hfReal:n}function m(n,e){n!=="add"&&v(e)}function h(n){if(n===null||typeof n!="object"||Array.isArray(n))return!1;let e=n;return"rotationX"in e||"rotationY"in e||"transformPerspective"in e}var d=new Set;function v(n){let e=n[0];if(e==null)return;let o=window;d.has(e)||(d.add(e),o.__hfAllTweenTargets=Array.from(d)),(h(n[1])||h(n[2]))&&(T.add(e),o.__hf3dTweenTargets=Array.from(T))}function _(n){let e=n.proxy.__hfReal,o=e[n.method];if(typeof o=="function"){let i=n.method==="add"?n.args.map(O):n.args;o.call(e,...i)}}function l(n,e,o){m(e,o);let i={proxy:n,method:e,args:o};return n.__hfQueue.push(i),s.push(i),R(),n}function y(n){let e=n.proxy.__hfQueue.indexOf(n);e>=0&&n.proxy.__hfQueue.splice(e,1)}function t(){for(;s.length>0;){let n=s.shift();n&&(y(n),_(n))}A()}function k(){f=!1,window.__hfTimelinesBuilding=!1;try{window.dispatchEvent(new CustomEvent("hf-timelines-built"))}catch{}}function A(){f||(f=!0,b(()=>{s.length===0?k():f=!1}))}function g(){w=!1;let n=s.splice(0,P);for(let e of n)y(e),_(e);s.length>0?(w=!0,p(g)):k()}function R(){w||(w=!0,window.__hfTimelinesBuilding=!0,p(g))}var S=new Set(["to","from","fromTo","set","add"]);function I(n,e){let o=e;for(;o!==null&&o!==Object.prototype;){for(let i of Object.getOwnPropertyNames(o)){if(i==="constructor"||i==="then"||i in n||S.has(i)||i.charAt(0)==="_")continue;let u=Object.getOwnPropertyDescriptor(o,i);if(!u||typeof u.value!="function")continue;let r=u.value;n[i]=function(...c){t();let a=r.call(e,...c);return a===e?n:a}}o=Object.getPrototypeOf(o)}}function G(n){let e={__hfReal:n,__hfQueue:[],__hfIsProxy:!0,to(...o){return l(e,"to",o)},from(...o){return l(e,"from",o)},fromTo(...o){return l(e,"fromTo",o)},set(...o){return l(e,"set",o)},add(...o){return l(e,"add",o)},pause(...o){return t(),n.pause(...o),e},play(...o){return t(),n.play(...o),e},seek(...o){return t(),n.seek(...o),e},totalTime(...o){return t(),o.length>0?(n.totalTime(...o),e):n.totalTime()},time(...o){return t(),o.length>0?(n.time(...o),e):n.time()},duration(...o){return t(),o.length>0?(n.duration(...o),e):n.duration()},getChildren(...o){t();let i=n.getChildren(...o);return Array.isArray(i)?i:[]},paused(...o){return t(),o.length>0?(n.paused(...o),e):n.paused()},timeScale(...o){return t(),o.length>0?(n.timeScale(...o),e):n.timeScale()},kill(){t(),n.kill()}};return I(e,n),x.push(e),e}if(typeof window<"u"){window.__hf||(window.__hf={}),window.__hfTimelinesBuilding=!1,window.__hfFlushSync=()=>{t(),s.length===0&&window.__hfTimelinesBuilding&&k()};let n=null;try{Object.defineProperty(window,"gsap",{configurable:!0,enumerable:!0,get(){return n},set(e){if(n=e,!e||typeof e.timeline!="function")return;let o=e.timeline.bind(e);e.timeline=u=>G(o(u));for(let u of["to","from","set"]){let r=e[u];if(typeof r!="function")continue;let c=r.bind(e);e[u]=(...a)=>(m(u,a),c(...a))}let i=e.fromTo;if(typeof i=="function"){let u=i.bind(e);e.fromTo=(...r)=>(m("fromTo",r),u(...r))}}})}catch{}}})();\n';
/** /**
* Returns the pre-built HyperFrames early stub IIFE as a string constant. * Returns the pre-built HyperFrames early stub IIFE as a string constant.
+24 -134
View File
@@ -62,12 +62,6 @@ declare global {
originalRequestAnimationFrame?: typeof window.requestAnimationFrame; originalRequestAnimationFrame?: typeof window.requestAnimationFrame;
originalSetTimeout?: typeof window.setTimeout; originalSetTimeout?: typeof window.setTimeout;
}; };
/**
* Set by the engine (evaluateOnNewDocument, before this stub) when fast
* capture (drawElementImage) is active. Opt out per render with
* HF_FAST_CAPTURE_AUTOALPHA=false.
*/
__HF_FAST_CAPTURE_AUTOALPHA__?: boolean;
} }
} }
@@ -133,13 +127,6 @@ const activeProxies: TimelineProxy[] = [];
const pendingOperations: TimelineOperation[] = []; const pendingOperations: TimelineOperation[] = [];
let batchScheduled = false; let batchScheduled = false;
let publishCheckScheduled = false; let publishCheckScheduled = false;
/**
* Tween targets whose vars got the opacity autoAlpha rewrite. Resolved and
* visibility-hidden at flush completion when still at computed opacity 0
* see hideTransparentAutoAlphaTargets.
*/
const autoAlphaRewrittenTargets = new Set<unknown>();
/** /**
* Tween targets animated with 3D transform vars (rotationX / rotationY / * Tween targets animated with 3D transform vars (rotationX / rotationY /
* transformPerspective). drawElementImage cannot paint 3D transforms the * transformPerspective). drawElementImage cannot paint 3D transforms the
@@ -182,63 +169,20 @@ function unwrapTimelineArg(arg: unknown): unknown {
} }
/** /**
* Fast-capture autoAlpha rewrite. * Record tween targets (3D + all-targets) from a tween call's args so the
* engine's 3D projection and at-risk scans can see to()-style tweens whose
* computed style is still flat/opaque at t=0. Pure observer tween args are
* NEVER modified on their way to GSAP.
* *
* Elements at `opacity: 0` still paint as transparent promoted compositor * (The former fast-capture opacity autoAlpha rewrite that lived here was
* layers. Stacked opacity-0 containers (the word-by-word caption pattern) * removed: it existed for crbug 521861819 fixed in Chrome 151, the pinned
* break drawElementImage capture (blackout / misplaced paint records) and * floor and the rewrite itself measured ~28 dB of damage on comps whose
* stall SwiftShader's first BeginFrame. GSAP's `autoAlpha` is the same fade * fades it touched. drawElementImage now captures animated opacity correctly
* but additionally sets `visibility: hidden` at 0, removing the element from * when the capture is synchronized via canvas.requestPaint(); see the
* the paint tree measured on the chat CI comp: 29.4 49.6 dB * engine's drawElementService.)
* fast-vs-baseline with zero blackout frames.
*
* When the engine signals fast capture (`__HF_FAST_CAPTURE_AUTOALPHA__`),
* rewrite `opacity` `autoAlpha` in tween vars. Skipped when the author
* already manages `autoAlpha` or `visibility` themselves. Baseline renders
* never see this the flag is only set on fast-capture sessions.
*/ */
function convertVarsOpacityToAutoAlpha(vars: unknown): unknown { function observeTweenCall(method: TimelineOperationMethod, args: unknown[]): void {
if (vars === null || typeof vars !== "object" || Array.isArray(vars)) return vars;
const record = vars as Record<string, unknown>;
if (!("opacity" in record) || "autoAlpha" in record || "visibility" in record) return vars;
const converted: Record<string, unknown> = {};
for (const key of Object.keys(record)) {
if (key === "opacity") converted.autoAlpha = record[key];
else converted[key] = record[key];
}
return converted;
}
/**
* Apply the autoAlpha rewrite to the vars argument(s) of a tween call.
* `to`/`from`/`set` carry vars at index 1; `fromTo` at indexes 1 and 2.
*/
function convertTweenArgs(method: TimelineOperationMethod, args: unknown[]): unknown[] {
// Always record tween targets (3D + all-targets) regardless of the autoAlpha
// rewrite flag, so HF_FAST_CAPTURE_AUTOALPHA=false doesn't blind the 3D
// projection's quad-animation check.
if (method !== "add") recordThreeDTweenTarget(args); if (method !== "add") recordThreeDTweenTarget(args);
if (window.__HF_FAST_CAPTURE_AUTOALPHA__ !== true) return args;
if (method === "add") return args;
const out = args.slice();
let rewritten = false;
if (out.length > 1) {
const converted = convertVarsOpacityToAutoAlpha(out[1]);
if (converted !== out[1]) rewritten = true;
out[1] = converted;
}
if (method === "fromTo" && out.length > 2) {
const converted = convertVarsOpacityToAutoAlpha(out[2]);
if (converted !== out[2]) rewritten = true;
out[2] = converted;
}
// convertVarsOpacityToAutoAlpha returns the input object untouched when no
// rewrite applies, so identity inequality means this target's opacity is
// GSAP-controlled via autoAlpha and is safe to visibility-hide at flush.
if (rewritten && out[0] !== null && out[0] !== undefined) {
autoAlphaRewrittenTargets.add(out[0]);
}
return out;
} }
function varsHasThreeD(vars: unknown): boolean { function varsHasThreeD(vars: unknown): boolean {
@@ -270,66 +214,6 @@ function recordThreeDTweenTarget(args: unknown[]): void {
} }
} }
/** Resolve a GSAP tween target (selector / Element / NodeList / array) to elements. */
function resolveTweenTargets(target: unknown): Element[] {
if (typeof target === "string") {
try {
return Array.from(document.querySelectorAll(target));
} catch {
return [];
}
}
if (target instanceof Element) return [target];
if (Array.isArray(target) || (typeof NodeList !== "undefined" && target instanceof NodeList)) {
const out: Element[] = [];
for (const item of target as Iterable<unknown>) {
if (item instanceof Element) out.push(item);
}
return out;
}
return [];
}
/**
* Flush-time transparent-layer hide.
*
* The autoAlpha rewrite only fixes opacity GSAP touches. Elements created
* with inline `opacity: 0` (the gen_os caption-pill pattern:
* `pill.style.cssText = "... opacity:0"` then `tl.set(pill, {opacity: 1})`
* at each caption start) sit in the paint tree as transparent promoted
* layers from frame 0 until their first autoAlpha event long enough to
* trigger the drawElementImage caption blackout mid-video.
*
* At flush completion (timeline built, still paused, nothing applied yet)
* every recorded rewrite target that is STILL at computed opacity 0 gets
* `visibility: hidden`. Safe by construction: only elements with a queued
* autoAlpha tween are touched, and that tween restores visibility
* (autoAlpha > 0 sets `visibility: inherit`) on its first applied frame.
* Elements faded in by CSS animations, WAAPI, or raw style writes are never
* recorded here and are left alone. Authors who set inline `visibility`
* themselves are also skipped, mirroring convertVarsOpacityToAutoAlpha.
*/
function hideTransparentAutoAlphaTargets(): void {
if (window.__HF_FAST_CAPTURE_AUTOALPHA__ !== true) return;
if (autoAlphaRewrittenTargets.size === 0) return;
const targets = Array.from(autoAlphaRewrittenTargets);
autoAlphaRewrittenTargets.clear();
for (const target of targets) {
for (const el of resolveTweenTargets(target)) {
const styled = el as Element & { style?: CSSStyleDeclaration };
if (!styled.style) continue;
if (styled.style.visibility !== "") continue;
let computedOpacity = "";
try {
computedOpacity = getComputedStyle(el).opacity;
} catch {
continue;
}
if (computedOpacity === "0") styled.style.visibility = "hidden";
}
}
}
function applyTimelineOperation(entry: TimelineOperation): void { function applyTimelineOperation(entry: TimelineOperation): void {
const real = entry.proxy.__hfReal; const real = entry.proxy.__hfReal;
const fn = real[entry.method]; const fn = real[entry.method];
@@ -344,7 +228,8 @@ function enqueueTimelineOperation(
method: TimelineOperationMethod, method: TimelineOperationMethod,
args: unknown[], args: unknown[],
): TimelineProxy { ): TimelineProxy {
const entry = { proxy, method, args: convertTweenArgs(method, args) }; observeTweenCall(method, args);
const entry = { proxy, method, args };
proxy.__hfQueue.push(entry); proxy.__hfQueue.push(entry);
pendingOperations.push(entry); pendingOperations.push(entry);
scheduleBatch(); scheduleBatch();
@@ -368,7 +253,6 @@ function flushPendingOperations(): void {
function publishTimelinesBuilt(): void { function publishTimelinesBuilt(): void {
publishCheckScheduled = false; publishCheckScheduled = false;
hideTransparentAutoAlphaTargets();
window.__hfTimelinesBuilding = false; window.__hfTimelinesBuilding = false;
try { try {
window.dispatchEvent(new CustomEvent("hf-timelines-built")); window.dispatchEvent(new CustomEvent("hf-timelines-built"));
@@ -592,19 +476,25 @@ if (typeof window !== "undefined") {
if (!g || typeof g.timeline !== "function") return; if (!g || typeof g.timeline !== "function") return;
const origTimeline = g.timeline.bind(g) as (params?: unknown) => GsapTimeline; const origTimeline = g.timeline.bind(g) as (params?: unknown) => GsapTimeline;
g.timeline = (params?: unknown): GsapTimeline => wrapTimeline(origTimeline(params)); g.timeline = (params?: unknown): GsapTimeline => wrapTimeline(origTimeline(params));
// Fast-capture autoAlpha rewrite for top-level gsap.to/from/set/fromTo // Tween-target tracking for top-level gsap.to/from/set/fromTo calls
// calls (compositions often use `gsap.set(el, { opacity: 0 })` for // (compositions often use `gsap.set(el, { opacity: 0 })` for initial
// initial state — the timeline proxy never sees those). // state — the timeline proxy never sees those).
for (const method of ["to", "from", "set"] as const) { for (const method of ["to", "from", "set"] as const) {
const orig = g[method]; const orig = g[method];
if (typeof orig !== "function") continue; if (typeof orig !== "function") continue;
const bound = (orig as (...a: unknown[]) => unknown).bind(g); const bound = (orig as (...a: unknown[]) => unknown).bind(g);
g[method] = (...args: unknown[]): unknown => bound(...convertTweenArgs(method, args)); g[method] = (...args: unknown[]): unknown => {
observeTweenCall(method, args);
return bound(...args);
};
} }
const origFromTo = g.fromTo; const origFromTo = g.fromTo;
if (typeof origFromTo === "function") { if (typeof origFromTo === "function") {
const bound = (origFromTo as (...a: unknown[]) => unknown).bind(g); const bound = (origFromTo as (...a: unknown[]) => unknown).bind(g);
g.fromTo = (...args: unknown[]): unknown => bound(...convertTweenArgs("fromTo", args)); g.fromTo = (...args: unknown[]): unknown => {
observeTweenCall("fromTo", args);
return bound(...args);
};
} }
}, },
}); });