mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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:
co-authored by
Claude Fable 5
WaterrrForever
parent
574da2b215
commit
337d0b51bc
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user