@hyperframes/core 0.8.185 → 0.8.188

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -61,4 +61,4 @@ export { ITEM_TYPES, FILE_TYPES, ITEM_TYPE_DIRS, isExampleItem, isBlockItem, isC
61
61
  export { canaryBucket, evaluateCanary, parseCanaryOverride, type CanaryDecision, type CanaryInput, type CanaryReason, } from "./canary.js";
62
62
  export { CANARIES, canaryEnvVar, findCanary, overdueCanaries, type CanaryDefinition, } from "./canaryRegistry.js";
63
63
  export { HF_VFX_ATTR, HF_VFX_CHAIN_VERSION, HF_VFX, chainCapture, getVfxDef, parseVfxChain, serializeVfxChain, normalizeVfxParams, type HfVfxCapture, type HfVfxChain, type HfVfxDef, type HfVfxNode, type HfVfxParam, type HfVfxParamValues, } from "./vfx.js";
64
- export { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL } from "./pageMotionBlur.js";
64
+ export { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL, acceleratedCanvasTrackerSource, } from "./pageMotionBlur.js";
package/dist/index.js CHANGED
@@ -64,4 +64,4 @@ export { canaryBucket, evaluateCanary, parseCanaryOverride, } from "./canary.js"
64
64
  export { CANARIES, canaryEnvVar, findCanary, overdueCanaries, } from "./canaryRegistry.js";
65
65
  // VFX chain (data-vfx-chain) — defs, chain parse/serialize/normalize
66
66
  export { HF_VFX_ATTR, HF_VFX_CHAIN_VERSION, HF_VFX, chainCapture, getVfxDef, parseVfxChain, serializeVfxChain, normalizeVfxParams, } from "./vfx.js";
67
- export { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL } from "./pageMotionBlur.js";
67
+ export { PAGE_DEVICE_SCALE_GLOBAL, PAGE_MOTION_BLUR_OFF_GLOBAL, acceleratedCanvasTrackerSource, } from "./pageMotionBlur.js";
@@ -3,6 +3,9 @@ export type HyperframesRuntimeBuildOptions = {
3
3
  defaultParityMode?: boolean;
4
4
  minify?: boolean;
5
5
  };
6
+ export declare const RUNTIME_ESBUILD_DEFINE: {
7
+ console: string;
8
+ };
6
9
  /**
7
10
  * Build the runtime IIFE from source via esbuild.
8
11
  *
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ export const RUNTIME_ESBUILD_DEFINE = { console: "globalThis.console" };
5
6
  function applyDefaultParityMode(script, enabled) {
6
7
  const parityFlagPattern = /var\s+_parityModeEnabled\s*=\s*(?:true|false)\s*;/;
7
8
  if (!parityFlagPattern.test(script))
@@ -29,6 +30,7 @@ export function buildHyperframesRuntimeScript(options = {}) {
29
30
  target: ["es2020"],
30
31
  minify: options.minify ?? true,
31
32
  legalComments: "none",
33
+ define: RUNTIME_ESBUILD_DEFINE,
32
34
  });
33
35
  let script = result.outputFiles[0]?.text ?? "";
34
36
  if (typeof options.defaultParityMode === "boolean") {
@@ -1,3 +1,11 @@
1
1
  export declare const PAGE_MOTION_BLUR_OFF_GLOBAL = "__HF_PAGE_MOTION_BLUR_OFF__";
2
2
  export declare const PAGE_DEVICE_SCALE_GLOBAL = "__HF_DEVICE_SCALE_FACTOR__";
3
3
  export declare function supportsCaptureMotionBlur(win?: Window): boolean;
4
+ /**
5
+ * Remember canvases html-in-canvas cannot capture: WebGL, WebGPU, or handed to an OffscreenCanvas.
6
+ * Self-contained: the engine and the preview run its source ahead of every author script.
7
+ */
8
+ export declare function installAcceleratedCanvasTracker(): void;
9
+ export declare function isAcceleratedCanvas(canvas: HTMLCanvasElement): boolean;
10
+ /** The tracker as a script body, for `evaluateOnNewDocument` and inline `<script>` injection. */
11
+ export declare function acceleratedCanvasTrackerSource(): string;
@@ -1,9 +1,48 @@
1
- // Set by the engine: its own sub-frame blur runs, or the Studio preview could not blur.
1
+ import { htmlInCanvasForm } from "./htmlInCanvas.js";
2
+ // Engine-set: the off switch (its sub-frame blur runs, or the preview could not blur); the render scale (dpr falls back to 1).
2
3
  export const PAGE_MOTION_BLUR_OFF_GLOBAL = "__HF_PAGE_MOTION_BLUR_OFF__";
3
4
  export const PAGE_DEVICE_SCALE_GLOBAL = "__HF_DEVICE_SCALE_FACTOR__";
4
5
  export function supportsCaptureMotionBlur(win = window) {
5
- const w = win;
6
- const ctx = w.CanvasRenderingContext2D?.prototype;
7
- const canvas = w.HTMLCanvasElement?.prototype;
8
- return !!ctx && !!canvas && "drawElementImage" in ctx && "requestPaint" in canvas;
6
+ return htmlInCanvasForm(win) !== null;
7
+ }
8
+ const ACCELERATED_CANVASES_GLOBAL = "__hfAcceleratedCanvases";
9
+ /**
10
+ * Remember canvases html-in-canvas cannot capture: WebGL, WebGPU, or handed to an OffscreenCanvas.
11
+ * Self-contained: the engine and the preview run its source ahead of every author script.
12
+ */
13
+ export function installAcceleratedCanvasTracker() {
14
+ const key = "__hfAcceleratedCanvases";
15
+ const known = Reflect.get(window, key);
16
+ const seen = known instanceof WeakSet ? known : new WeakSet();
17
+ if (seen !== known)
18
+ Object.defineProperty(window, key, { value: seen });
19
+ const accelerated = ["webgl", "webgl2", "experimental-webgl", "webgpu"];
20
+ const wrap = (name, onCall) => {
21
+ const proto = HTMLCanvasElement.prototype;
22
+ const original = Reflect.get(proto, name);
23
+ if (typeof original !== "function" || "__hfAccelTracked" in original)
24
+ return;
25
+ const tracked = function (...args) {
26
+ const result = Reflect.apply(original, this, args);
27
+ onCall(this, args, result);
28
+ return result;
29
+ };
30
+ Object.defineProperty(tracked, "__hfAccelTracked", { value: true });
31
+ Object.defineProperty(proto, name, { value: tracked, writable: true, configurable: true });
32
+ };
33
+ wrap("getContext", (canvas, args, ctx) => {
34
+ if (ctx && accelerated.includes(String(args[0])))
35
+ seen.add(canvas);
36
+ });
37
+ wrap("transferControlToOffscreen", (canvas) => {
38
+ seen.add(canvas);
39
+ });
40
+ }
41
+ export function isAcceleratedCanvas(canvas) {
42
+ const seen = Reflect.get(window, ACCELERATED_CANVASES_GLOBAL);
43
+ return seen instanceof WeakSet && seen.has(canvas);
44
+ }
45
+ /** The tracker as a script body, for `evaluateOnNewDocument` and inline `<script>` injection. */
46
+ export function acceleratedCanvasTrackerSource() {
47
+ return `(${installAcceleratedCanvasTracker.toString()})();`;
9
48
  }
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * The volume lane's gain for an element at a moment in its clip.
3
3
  *
4
- * Volume is not scheduled onto the gain AudioParam the way FX parameters are.
5
- * The runtime rewrites that gain every tick from `data-volume` and the GSAP
6
- * seeked value, which would erase a scheduled envelope within a frame — so the
7
- * lane feeds the same per-tick path the probed `volumeKeyframes` already use,
8
- * and the render still bakes it into the samples.
4
+ * Read per tick for a clip that sounds through `el.volume`. A clip the Web Audio
5
+ * transport routes plays its lane scheduled on a gain of its own
6
+ * (`attachClipStages`), out of reach of the per-tick gain write.
9
7
  */
10
8
  import { type HfAutomationLane } from "../audioAutomation.js";
11
9
  /** The lane for `target` inside a `data-automation` value, or null when absent or unreadable. */
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * The volume lane's gain for an element at a moment in its clip.
3
3
  *
4
- * Volume is not scheduled onto the gain AudioParam the way FX parameters are.
5
- * The runtime rewrites that gain every tick from `data-volume` and the GSAP
6
- * seeked value, which would erase a scheduled envelope within a frame — so the
7
- * lane feeds the same per-tick path the probed `volumeKeyframes` already use,
8
- * and the render still bakes it into the samples.
4
+ * Read per tick for a clip that sounds through `el.volume`. A clip the Web Audio
5
+ * transport routes plays its lane scheduled on a gain of its own
6
+ * (`attachClipStages`), out of reach of the per-tick gain write.
9
7
  */
10
8
  import { HF_AUDIO_AUTOMATION_ATTR, parseAutomation, sampleAutomationLane, VOLUME_TARGET, } from "../audioAutomation.js";
11
9
  /**
@@ -109,7 +109,8 @@ export declare function syncRuntimeMedia(params: {
109
109
  * outbound message; further invocations are suppressed by the caller.
110
110
  */
111
111
  onAutoplayBlocked?: () => void;
112
- onElementVolume?: (el: HTMLMediaElement, effectiveVolume: number, authorVolume: number) => void;
112
+ /** `clipGain` is `data-volume` for a routed clip, played ahead of its FX; 1 otherwise. */
113
+ onElementVolume?: (el: HTMLMediaElement, effectiveVolume: number, authorVolume: number, clipGain: number) => void;
113
114
  /** Is THIS element owned by the Web Audio transport? Owned → mute it (transport
114
115
  * plays it); not owned → leave audible (HTMLMedia fallback). Per-element, not a
115
116
  * global flag, so a not-yet-claimed track isn't muted by other tracks. */
@@ -6,7 +6,7 @@ import { elementVolumeLaneGain } from "./audioAutomationVolume.js";
6
6
  import { fadeGain, NO_FADES, readElementFades } from "../audioFade.js";
7
7
  import { readElementPlaybackRate, readElementRateSpec, readMediaStart } from "./playbackRate.js";
8
8
  import { rateAt, sourceTimeAt, timeAtSourceTime } from "../speedRamp.js";
9
- import { clampAudioGain } from "../audioGain.js";
9
+ import { clampAudioGain, clipGain, gainTimesLane } from "../audioGain.js";
10
10
  import { isMemberGroupHidden } from "../audioGroups.js";
11
11
  import { findInjectedRenderFrame } from "./renderFrameSibling.js";
12
12
  import { registerSeekCompletion } from "./adapters/seek-dispatch.js";
@@ -312,7 +312,7 @@ export function syncRuntimeMedia(params) {
312
312
  const previousRuntimeVolume = lastRuntimeAppliedVolume.get(el);
313
313
  const currentElementVolume = clampVolume(el.volume);
314
314
  let authorVolume;
315
- // An explicit volume lane owns the fader. It is checked before the probed
315
+ // An explicit volume lane, times `data-volume`, sets the level. Checked before the probed
316
316
  // keyframes because the two would otherwise fight, and it is the one the
317
317
  // author drew — `lint` warns when a track carries both.
318
318
  // Clip-local, NOT `relTime`. A lane's `t` is "seconds from the start of
@@ -324,8 +324,11 @@ export function syncRuntimeMedia(params) {
324
324
  // trimmed clip. The FX lanes on this same feature use clip-local elapsed;
325
325
  // there is one time base, and this is it.
326
326
  const laneGain = elementVolumeLaneGain(el, params.timeSeconds - origin);
327
+ const routed = params.isWebAudioRouted?.(el) === true;
328
+ const tweened = laneGain === null && (clip.volumeKeyframes?.length ?? 0) > 0;
329
+ const preFxGain = routed ? clipGain(fallbackAuthorVolume, tweened) : 1;
327
330
  if (laneGain !== null) {
328
- authorVolume = clampAudioGain(laneGain);
331
+ authorVolume = routed ? 1 : gainTimesLane(fallbackAuthorVolume, laneGain);
329
332
  }
330
333
  else if (clip.volumeKeyframes && clip.volumeKeyframes.length > 0) {
331
334
  // Keyframes probed from the GSAP timeline — same source as the renderer.
@@ -338,8 +341,8 @@ export function syncRuntimeMedia(params) {
338
341
  const elapsedInClip = params.timeSeconds - origin;
339
342
  authorVolume = clampAudioGain(interpolateVolumeGain(clip.volumeKeyframes, elapsedInClip));
340
343
  }
341
- else if (params.isWebAudioRouted?.(el)) {
342
- authorVolume = fallbackAuthorVolume;
344
+ else if (routed) {
345
+ authorVolume = 1;
343
346
  }
344
347
  else if (previousRuntimeVolume === undefined) {
345
348
  // First tick this clip is active. The transport has already seeked GSAP
@@ -387,7 +390,7 @@ export function syncRuntimeMedia(params) {
387
390
  if (Math.abs(el.volume - effectiveVolume) > 1e-6)
388
391
  el.volume = effectiveVolume;
389
392
  lastRuntimeAppliedVolume.set(el, effectiveVolume);
390
- params.onElementVolume?.(el, effectiveVolume, authorVolume);
393
+ params.onElementVolume?.(el, effectiveVolume, authorVolume, preFxGain);
391
394
  // Mute only when force-muted or the transport owns this element; an unclaimed
392
395
  // track stays audible via the HTMLMedia fallback.
393
396
  if ((forceMuteAll || params.isWebAudioOwned?.(el)) && !el.muted)
@@ -17,7 +17,7 @@
17
17
  */
18
18
  /**
19
19
  * The render id an element is addressed by: its stamped, document-unique id
20
- * when the producer compiled this document, else its author id. The two are
20
+ * when the compiler stamped this document, else its author id. The two are
21
21
  * the same string whenever no collision forced a suffix.
22
22
  */
23
23
  export declare function readMediaRenderId(media: Element): string | null;
@@ -19,7 +19,7 @@ import { MEDIA_RENDER_ID_ATTR } from "../compiler/mediaRenderIds.js";
19
19
  import { isImageElement } from "./domRealm.js";
20
20
  /**
21
21
  * The render id an element is addressed by: its stamped, document-unique id
22
- * when the producer compiled this document, else its author id. The two are
22
+ * when the compiler stamped this document, else its author id. The two are
23
23
  * the same string whenever no collision forced a suffix.
24
24
  */
25
25
  export function readMediaRenderId(media) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/core",
3
- "version": "0.8.185",
3
+ "version": "0.8.188",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -84,6 +84,10 @@
84
84
  "import": "./dist/keyframeDefaults.js",
85
85
  "types": "./dist/keyframeDefaults.d.ts"
86
86
  },
87
+ "./html-in-canvas": {
88
+ "import": "./dist/htmlInCanvas.js",
89
+ "types": "./dist/htmlInCanvas.d.ts"
90
+ },
87
91
  "./gsap-cdn": {
88
92
  "import": "./dist/gsapCdn.js",
89
93
  "types": "./dist/gsapCdn.d.ts"
@@ -381,9 +385,9 @@
381
385
  "linkedom": "^0.18.12",
382
386
  "postcss": "^8.5.8",
383
387
  "wawoff2": "^2.0.1",
384
- "@hyperframes/lint": "0.8.185",
385
- "@hyperframes/studio-server": "0.8.185",
386
- "@hyperframes/parsers": "0.8.185"
388
+ "@hyperframes/parsers": "0.8.188",
389
+ "@hyperframes/studio-server": "0.8.188",
390
+ "@hyperframes/lint": "0.8.188"
387
391
  },
388
392
  "devDependencies": {
389
393
  "@fontsource/archivo-black": "^5.2.8",