@hyperframes/core 0.8.199 → 0.8.201

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.
Files changed (42) hide show
  1. package/dist/audioAnalysis.d.ts +2 -0
  2. package/dist/audioAnalysis.js +14 -5
  3. package/dist/audioGroups.d.ts +3 -7
  4. package/dist/audioGroups.js +4 -8
  5. package/dist/audioResample.d.ts +2 -0
  6. package/dist/audioResample.js +7 -0
  7. package/dist/audioRhythm.d.ts +3 -0
  8. package/dist/audioRhythm.js +37 -20
  9. package/dist/audioSteps.d.ts +3 -0
  10. package/dist/audioSteps.js +7 -0
  11. package/dist/beats/beatDetection.js +22 -7
  12. package/dist/compiler/htmlBundler.js +15 -40
  13. package/dist/compiler/inlineSubCompositions.d.ts +2 -0
  14. package/dist/compiler/inlineSubCompositions.js +64 -5
  15. package/dist/compiler/srcset.d.ts +1 -0
  16. package/dist/compiler/srcset.js +46 -0
  17. package/dist/generated/position-edits-render-inline.js +1 -1
  18. package/dist/generated/runtime-inline.js +1 -1
  19. package/dist/hyperframe.runtime.iife.js +66 -62
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +3 -1
  22. package/dist/mediaGradeAnalyzer.d.ts +1 -0
  23. package/dist/mediaGradeAnalyzer.js +32 -20
  24. package/dist/previewProxyBox.d.ts +14 -0
  25. package/dist/previewProxyBox.js +15 -0
  26. package/dist/runtime/bridge.d.ts +1 -0
  27. package/dist/runtime/bridge.js +1 -0
  28. package/dist/runtime/clipReveal.d.ts +1 -1
  29. package/dist/runtime/diagnostics.d.ts +2 -0
  30. package/dist/runtime/diagnostics.js +40 -1
  31. package/dist/runtime/media.d.ts +1 -0
  32. package/dist/runtime/media.js +15 -21
  33. package/dist/runtime/timedClipHide.d.ts +2 -1
  34. package/dist/runtime/timedClipHide.js +8 -4
  35. package/dist/runtime/timeline.d.ts +8 -1
  36. package/dist/runtime/timeline.js +32 -13
  37. package/dist/runtime/types.d.ts +2 -1
  38. package/dist/runtime/validateVariables.js +6 -7
  39. package/dist/studioPreviewMark.d.ts +3 -1
  40. package/dist/studioPreviewMark.js +4 -0
  41. package/dist/variables.d.ts +1 -1
  42. package/package.json +4 -4
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export { AUDIO_GROUP_RENDER_ID_ATTR, MEDIA_RENDER_ID_ATTR, assignMediaRenderIds,
22
22
  export { RENDER_FRAME_ID_PREFIX, RENDER_FRAME_ID_SUFFIX, renderFrameIdForRenderId, } from "./runtime/renderFrameSibling.js";
23
23
  export { rewriteAssetPaths, rewriteAssetPath, rewriteCssAssetUrls, rewriteInlineStyleAssetUrls, } from "./compiler/rewriteSubCompPaths.js";
24
24
  export { CSS_URL_RE, isNonRelativeUrl, isPathInside } from "./compiler/assetPaths.js";
25
+ export { rewriteSrcsetUrls } from "./compiler/srcset.js";
25
26
  export { checkSubCompositionUsability, type ParsableDocumentLike, type SubCompositionValidity, type SubCompositionValidityReason, } from "./compiler/subCompositionValidity.js";
26
27
  export { RUNTIME_BOOTSTRAP_ATTR, stripEmbeddedRuntimeScripts } from "./compiler/htmlDocument.js";
27
28
  export { COMPOSITION_SOURCE_URL } from "./compiler/scriptRuns.js";
@@ -32,7 +33,7 @@ export { clearGifFramesBeforeNext, gifClearsAfterLeavingFrameInPlace, parseAnima
32
33
  export { HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_ADJUST_KEYS, HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS, HF_COLOR_GRADING_ANIMATABLE_PROPERTIES, HF_COLOR_GRADING_CANVAS_ID_PREFIX, HF_COLOR_GRADING_COLOR_SPACE, HF_COLOR_GRADING_CURVE_KEYS, HF_COLOR_GRADING_DETAIL_KEYS, HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS, HF_COLOR_GRADING_EFFECT_KEYS, HF_COLOR_GRADING_EFFECT_PRESETS, HF_COLOR_GRADING_GRADE_PRESETS, HF_COLOR_GRADING_HUE_CURVE_KEYS, HF_COLOR_GRADING_LUT_KEYS, HF_COLOR_GRADING_PALETTES, HF_COLOR_GRADING_PRESETS, HF_COLOR_GRADING_TOP_LEVEL_KEYS, HF_COLOR_GRADING_WHEEL_KEYS, getHfColorGradingCapabilities, hasHfColorGradingAuthoredValues, isHfColorGradingActive, normalizeHfColorGrading, normalizeHfColorGradingWithVariables, resolveHfColorGradingVariables, serializeHfColorGrading, type HfColorGrading, type HfColorGradingAdjust, type HfColorGradingAdjustKey, type HfColorGradingAnimatablePath, type HfColorGradingActiveEffectKey, type HfColorGradingDetailKey, type HfColorGradingDetails, type HfColorGradingEffectKey, type HfColorGradingEffects, type HfColorGradingLutRef, type HfColorGradingPreset, type HfColorGradingPresetId, type HfColorGradingTarget, type HfColorGradingVariableMap, type HfColorGradingCapabilities, type HfColorGradingCurveKey, type HfColorGradingCurves, type HfColorGradingHueCurveKey, type HfColorGradingHueCurves, type HfColorGradingHueRange, type HfColorGradingSecondary, type HfColorGradingSecondaryCorrection, type HfColorGradingSoftRange, type HfColorGradingTonalWheel, type HfColorGradingWheelKey, type HfColorGradingWheels, type HfColorCurvePoint, type HfHueCurvePoint, type NormalizedHfColorGrading, type NormalizedHfColorGradingCurves, type NormalizedHfColorGradingHueCurves, type NormalizedHfColorGradingSecondary, type NormalizedHfColorGradingWheels, type ResolvedHfColorGrading, } from "./colorGrading.js";
33
34
  export { parseCubeLut, CubeLutParseError, type ParseCubeLutOptions } from "./colorLuts.js";
34
35
  export { firstFrameColourArgs, hdrToSdrToneMapFilter, parseFirstFrameColour, type ToneMapSourceColour, } from "./hdrToneMap.js";
35
- export { PREVIEW_PROXY_BOX_PARAM, formatPreviewProxyBox, parsePreviewProxyBox, quantizePreviewProxyBox, type PreviewProxyBox, } from "./previewProxyBox.js";
36
+ export { HEAVY_BITRATE, HEAVY_PIXEL_SHARE, PREVIEW_PROXY_BOX_PARAM, PREVIEW_PROXY_LIGHT_PARAM, PREVIEW_QUALITY_LEVELS, formatPreviewProxyBox, isHeavyForPreview, isPreviewQuality, parsePreviewProxyBox, quantizePreviewProxyBox, scalePreviewProxyBox, type PreviewProxyBox, type PreviewQuality, type PreviewSourceFacts, } from "./previewProxyBox.js";
36
37
  export { HYPERFRAME_RUNTIME_ARTIFACTS, HYPERFRAME_RUNTIME_CONTRACT, loadHyperframeRuntimeSource, type HyperframeRuntimeContract, } from "./inline-scripts/hyperframe.js";
37
38
  export { HYPERFRAME_RUNTIME_GLOBALS, HYPERFRAME_BRIDGE_SOURCES, HYPERFRAME_CONTROL_ACTIONS, type HyperframeControlAction, } from "./inline-scripts/runtimeContract.js";
38
39
  export { getHyperframeRuntimeScript } from "./generated/runtime-inline.js";
@@ -49,6 +50,7 @@ export { fitTextFontSize } from "./text/index.js";
49
50
  export type { FitTextOptions, FitTextResult } from "./text/index.js";
50
51
  export { formatRenderOutputTimestamp } from "./utils/renderOutputTimestamp.js";
51
52
  export { getVariables } from "./runtime/getVariables.js";
53
+ export { SCENE_CALLBACK_ERROR_PREFIX } from "./runtime/diagnostics.js";
52
54
  export { parseStartExpression, parseNumeric, type ReferenceExpression, } from "./runtime/startExpression.js";
53
55
  export { COMPOSITION_CONTRACT_VERSION, COMPOSITION_ATTRIBUTES, CANONICAL_AUTHORED_TIMING_ATTRIBUTES, DERIVED_TIMING_ATTRIBUTES, LEGACY_TIMING_ATTRIBUTES, readClipTiming, writeClipTiming, ClipTimingWriteError, type ClipTiming, type ClipTimingDiagnostic, type ClipTimingDiagnosticCode, type ClipTimingUpdate, } from "./compositionContract.js";
54
56
  export { createRuntimeStartTimeResolver } from "./runtime/startResolver.js";
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export { RENDER_FRAME_ID_PREFIX, RENDER_FRAME_ID_SUFFIX, renderFrameIdForRenderI
18
18
  // package (which imports core utilities back).
19
19
  export { rewriteAssetPaths, rewriteAssetPath, rewriteCssAssetUrls, rewriteInlineStyleAssetUrls, } from "./compiler/rewriteSubCompPaths.js";
20
20
  export { CSS_URL_RE, isNonRelativeUrl, isPathInside } from "./compiler/assetPaths.js";
21
+ export { rewriteSrcsetUrls } from "./compiler/srcset.js";
21
22
  export { checkSubCompositionUsability, } from "./compiler/subCompositionValidity.js";
22
23
  export { RUNTIME_BOOTSTRAP_ATTR, stripEmbeddedRuntimeScripts } from "./compiler/htmlDocument.js";
23
24
  export { COMPOSITION_SOURCE_URL } from "./compiler/scriptRuns.js";
@@ -28,7 +29,7 @@ export { clearGifFramesBeforeNext, gifClearsAfterLeavingFrameInPlace, parseAnima
28
29
  export { HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_ADJUST_KEYS, HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS, HF_COLOR_GRADING_ANIMATABLE_PROPERTIES, HF_COLOR_GRADING_CANVAS_ID_PREFIX, HF_COLOR_GRADING_COLOR_SPACE, HF_COLOR_GRADING_CURVE_KEYS, HF_COLOR_GRADING_DETAIL_KEYS, HF_COLOR_GRADING_EFFECT_APPLY_DEFAULTS, HF_COLOR_GRADING_EFFECT_KEYS, HF_COLOR_GRADING_EFFECT_PRESETS, HF_COLOR_GRADING_GRADE_PRESETS, HF_COLOR_GRADING_HUE_CURVE_KEYS, HF_COLOR_GRADING_LUT_KEYS, HF_COLOR_GRADING_PALETTES, HF_COLOR_GRADING_PRESETS, HF_COLOR_GRADING_TOP_LEVEL_KEYS, HF_COLOR_GRADING_WHEEL_KEYS, getHfColorGradingCapabilities, hasHfColorGradingAuthoredValues, isHfColorGradingActive, normalizeHfColorGrading, normalizeHfColorGradingWithVariables, resolveHfColorGradingVariables, serializeHfColorGrading, } from "./colorGrading.js";
29
30
  export { parseCubeLut, CubeLutParseError } from "./colorLuts.js";
30
31
  export { firstFrameColourArgs, hdrToSdrToneMapFilter, parseFirstFrameColour, } from "./hdrToneMap.js";
31
- export { PREVIEW_PROXY_BOX_PARAM, formatPreviewProxyBox, parsePreviewProxyBox, quantizePreviewProxyBox, } from "./previewProxyBox.js";
32
+ export { HEAVY_BITRATE, HEAVY_PIXEL_SHARE, PREVIEW_PROXY_BOX_PARAM, PREVIEW_PROXY_LIGHT_PARAM, PREVIEW_QUALITY_LEVELS, formatPreviewProxyBox, isHeavyForPreview, isPreviewQuality, parsePreviewProxyBox, quantizePreviewProxyBox, scalePreviewProxyBox, } from "./previewProxyBox.js";
32
33
  // Inline scripts
33
34
  export { HYPERFRAME_RUNTIME_ARTIFACTS, HYPERFRAME_RUNTIME_CONTRACT, loadHyperframeRuntimeSource, } from "./inline-scripts/hyperframe.js";
34
35
  export { HYPERFRAME_RUNTIME_GLOBALS, HYPERFRAME_BRIDGE_SOURCES, HYPERFRAME_CONTROL_ACTIONS, } from "./inline-scripts/runtimeContract.js";
@@ -44,6 +45,7 @@ export { fitTextFontSize } from "./text/index.js";
44
45
  export { formatRenderOutputTimestamp } from "./utils/renderOutputTimestamp.js";
45
46
  // Runtime helpers (composition-side)
46
47
  export { getVariables } from "./runtime/getVariables.js";
48
+ export { SCENE_CALLBACK_ERROR_PREFIX } from "./runtime/diagnostics.js";
47
49
  export { parseStartExpression, parseNumeric, } from "./runtime/startExpression.js";
48
50
  export { COMPOSITION_CONTRACT_VERSION, COMPOSITION_ATTRIBUTES, CANONICAL_AUTHORED_TIMING_ATTRIBUTES, DERIVED_TIMING_ATTRIBUTES, LEGACY_TIMING_ATTRIBUTES, readClipTiming, writeClipTiming, ClipTimingWriteError, } from "./compositionContract.js";
49
51
  // Also exposed via the ./runtime/start-resolver subpath. This root re-export
@@ -56,6 +56,7 @@ type NumericStats = Readonly<Record<string, number | undefined>>;
56
56
  export declare function parseMediaTreatmentSignalStats(raw: string): GradeSignalFrame[];
57
57
  export declare function statsToAdjust(stats: NumericStats): Pick<MediaTreatmentAnalysis, "adjust" | "measured">;
58
58
  export declare function summarizeMediaTreatmentAnalysis(probe: GradeMediaProbe, frames: readonly GradeSignalFrame[]): MediaTreatmentAnalysis;
59
+ export declare function gradeSampleTimes(duration: number): number[];
59
60
  export declare function analyzeMediaGrade(mediaPath: string, options?: {
60
61
  ffmpegPath?: string;
61
62
  ffprobePath?: string;
@@ -212,43 +212,55 @@ export function summarizeMediaTreatmentAnalysis(probe, frames) {
212
212
  warnings,
213
213
  };
214
214
  }
215
- export function analyzeMediaGrade(mediaPath, options = {}) {
216
- const ffmpegPath = options.ffmpegPath ?? "ffmpeg";
217
- const ffprobePath = options.ffprobePath ?? "ffprobe";
215
+ export function gradeSampleTimes(duration) {
216
+ return Array.from({ length: SAMPLE_FRAMES }, (_, i) => (duration * (i + 0.5)) / SAMPLE_FRAMES);
217
+ }
218
+ function readSignalStats(ffmpegPath, mediaPath, seekSeconds, frames) {
219
+ const timeoutMs = Number(process.env.HYPERFRAMES_ANALYZE_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
220
+ const keyframeSeek = seekSeconds === null ? [] : ["-noaccurate_seek", "-ss", seekSeconds.toFixed(3)];
218
221
  try {
219
- const probe = probeMedia(mediaPath, ffprobePath);
220
- const isImage = IMAGE_EXTENSIONS.has(extname(mediaPath).toLowerCase());
221
- const fps = !isImage && probe.duration
222
- ? Math.max(0.1, Math.min(2, SAMPLE_FRAMES / probe.duration))
223
- : null;
224
- const filters = [
225
- fps ? `fps=${fps.toFixed(4)}` : null,
226
- "format=yuv444p",
227
- "signalstats",
228
- "metadata=print:file=-",
229
- ]
230
- .filter(Boolean)
231
- .join(",");
232
- const raw = execFileSync(ffmpegPath, [
222
+ return execFileSync(ffmpegPath, [
233
223
  "-hide_banner",
234
224
  "-nostdin",
235
225
  "-v",
236
226
  "error",
227
+ ...keyframeSeek,
237
228
  "-i",
238
229
  mediaPath,
239
230
  "-vf",
240
- filters,
231
+ `trim=end_frame=${frames},format=yuv444p,signalstats,metadata=print:file=-`,
241
232
  "-frames:v",
242
- String(SAMPLE_FRAMES),
233
+ String(frames),
243
234
  "-f",
244
235
  "null",
245
236
  "-",
246
237
  ], {
247
238
  encoding: "utf8",
248
- timeout: Number(process.env.HYPERFRAMES_ANALYZE_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS,
239
+ timeout: timeoutMs,
249
240
  stdio: ["ignore", "pipe", "pipe"],
250
241
  windowsHide: true,
251
242
  });
243
+ }
244
+ catch (error) {
245
+ if (error.code !== "ETIMEDOUT")
246
+ throw error;
247
+ const target = seekSeconds === null
248
+ ? `the first ${frames} frames`
249
+ : `the frame at ${seekSeconds.toFixed(1)}s`;
250
+ throw new Error(`FFmpeg took longer than ${timeoutMs / 1000}s to measure ${target}; set HYPERFRAMES_ANALYZE_TIMEOUT_MS to allow more`);
251
+ }
252
+ }
253
+ export function analyzeMediaGrade(mediaPath, options = {}) {
254
+ const ffmpegPath = options.ffmpegPath ?? "ffmpeg";
255
+ const ffprobePath = options.ffprobePath ?? "ffprobe";
256
+ try {
257
+ const probe = probeMedia(mediaPath, ffprobePath);
258
+ const isImage = IMAGE_EXTENSIONS.has(extname(mediaPath).toLowerCase());
259
+ const raw = !isImage && probe.duration
260
+ ? gradeSampleTimes(probe.duration)
261
+ .map((time) => readSignalStats(ffmpegPath, mediaPath, time, 1))
262
+ .join("\n")
263
+ : readSignalStats(ffmpegPath, mediaPath, null, SAMPLE_FRAMES);
252
264
  return summarizeMediaTreatmentAnalysis(probe, parseMediaTreatmentSignalStats(raw));
253
265
  }
254
266
  catch (error) {
@@ -1,5 +1,6 @@
1
1
  /** Query parameter carrying the largest box, in device pixels, a preview shows a proxied video at. */
2
2
  export declare const PREVIEW_PROXY_BOX_PARAM = "hf-proxy-box";
3
+ export declare const PREVIEW_PROXY_LIGHT_PARAM = "hf-proxy-light";
3
4
  export type PreviewProxyBox = {
4
5
  width: number;
5
6
  height: number;
@@ -9,3 +10,16 @@ export declare function quantizePreviewProxyBox(width: number, height: number):
9
10
  export declare function formatPreviewProxyBox(box: PreviewProxyBox): string;
10
11
  /** Accepts only ladder rungs, so a client cannot make one cached copy per pixel. */
11
12
  export declare function parsePreviewProxyBox(raw: string): PreviewProxyBox | null;
13
+ export declare const PREVIEW_QUALITY_LEVELS: readonly [1, 0.5, 0.25, 0.125];
14
+ export type PreviewQuality = (typeof PREVIEW_QUALITY_LEVELS)[number];
15
+ export declare function isPreviewQuality(value: unknown): value is PreviewQuality;
16
+ export type PreviewSourceFacts = {
17
+ width: number;
18
+ height: number;
19
+ bitDepth: number;
20
+ bitrate: number;
21
+ };
22
+ export declare const HEAVY_PIXEL_SHARE = 0.5;
23
+ export declare const HEAVY_BITRATE = 40000000;
24
+ export declare function scalePreviewProxyBox(box: PreviewProxyBox, level: PreviewQuality): PreviewProxyBox;
25
+ export declare function isHeavyForPreview(facts: PreviewSourceFacts, copyBox: PreviewProxyBox): boolean;
@@ -1,5 +1,6 @@
1
1
  /** Query parameter carrying the largest box, in device pixels, a preview shows a proxied video at. */
2
2
  export const PREVIEW_PROXY_BOX_PARAM = "hf-proxy-box";
3
+ export const PREVIEW_PROXY_LIGHT_PARAM = "hf-proxy-light";
3
4
  // 256 * 2^(k/4), even: nearby boxes share one cached copy, and a rung overshoots by at most ~19%.
4
5
  const RUNGS = [
5
6
  256, 304, 362, 430, 512, 608, 724, 862, 1024, 1218, 1448, 1722, 2048, 2436, 2896, 3444, 4096,
@@ -28,3 +29,17 @@ export function parsePreviewProxyBox(raw) {
28
29
  const height = Number(match[2]);
29
30
  return RUNGS.includes(width) && RUNGS.includes(height) ? { width, height } : null;
30
31
  }
32
+ export const PREVIEW_QUALITY_LEVELS = [1, 0.5, 0.25, 0.125];
33
+ export function isPreviewQuality(value) {
34
+ return PREVIEW_QUALITY_LEVELS.some((level) => level === value);
35
+ }
36
+ export const HEAVY_PIXEL_SHARE = 0.5;
37
+ export const HEAVY_BITRATE = 40_000_000;
38
+ export function scalePreviewProxyBox(box, level) {
39
+ return quantizePreviewProxyBox(box.width * level, box.height * level) ?? box;
40
+ }
41
+ export function isHeavyForPreview(facts, copyBox) {
42
+ return (copyBox.width * copyBox.height <= HEAVY_PIXEL_SHARE * facts.width * facts.height ||
43
+ facts.bitDepth > 8 ||
44
+ facts.bitrate >= HEAVY_BITRATE);
45
+ }
@@ -22,6 +22,7 @@ type BridgeDeps = {
22
22
  onSetRuntimeData?: (channel: string, payload: unknown, requestId?: number) => void;
23
23
  onClearRuntimeData?: (channel: string, requestId?: number) => void;
24
24
  onSetDisplayScale?: (scale: number) => void;
25
+ onSetPreviewQuality?: (level: number) => void;
25
26
  getCanonicalFps: () => number;
26
27
  };
27
28
  export declare function setRuntimeProtocolFps(fps: number): void;
@@ -30,6 +30,7 @@ const CONTROL_HANDLERS = new Map(Object.entries({
30
30
  "set-playback-rate": (data, deps) => deps.onSetPlaybackRate(Number(data.playbackRate ?? 1)),
31
31
  "set-idle-heartbeat": (data, deps) => deps.onSetIdleHeartbeat(Boolean(data.slow)),
32
32
  "set-display-scale": (data, deps) => deps.onSetDisplayScale?.(Number(data.scale)),
33
+ "set-preview-quality": (data, deps) => deps.onSetPreviewQuality?.(Number(data.level)),
33
34
  "set-root-duration": (data, deps) => deps.onSetRootDuration(Number(data.durationSeconds ?? 0)),
34
35
  "set-play-range": (data, deps) => deps.onSetPlayRange(Number(data.startSeconds ?? 0), data.endSeconds == null ? null : Number(data.endSeconds)),
35
36
  "set-color-grading": (data, deps) => deps.onSetColorGrading(data.target ?? null, data.grading ?? null),
@@ -1,7 +1,7 @@
1
1
  /** The visibility pass's mark on a timed clip it hides; the runtime's rules hide the clip's contents by it. */
2
2
  export declare const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
3
3
  /** Shows a clip the visibility pass hid, contents included; returns a function that puts the mark back. */
4
- export declare function revealClip(el: HTMLElement, priority?: string): () => void;
4
+ export declare function revealClip(el: Element & ElementCSSInlineStyle, priority?: string): () => void;
5
5
  /** Called by shader-transitions builds on load: their revealers go through revealClip. */
6
6
  export declare function declareClipRevealAware(): void;
7
7
  /** A shader-transitions build is on the page whose revealers predate revealClip. */
@@ -1,3 +1,5 @@
1
1
  /** The named error readiness waits fail with once start-up or the render-ready publish throws; it stays set. */
2
2
  export declare function recordStartupError(error: unknown): void;
3
+ /** Starts the warning for a scene callback that threw; the render log shows it once per render. */
4
+ export declare const SCENE_CALLBACK_ERROR_PREFIX = "[hyperframes] scene timeline callback threw:";
3
5
  export declare function swallow(label: string, error?: unknown): void;
@@ -32,15 +32,24 @@ import { getDebugSurface } from "./globals.js";
32
32
  export function recordStartupError(error) {
33
33
  let detail = "unknown error";
34
34
  try {
35
- detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
35
+ if (error instanceof Error) {
36
+ detail = `${error.name}: ${error.message}`;
37
+ detail += startupErrorStack(error, detail);
38
+ }
39
+ else {
40
+ detail = String(error);
41
+ }
36
42
  }
37
43
  catch (err) {
38
44
  swallow("runtime.startupError", err);
39
45
  }
46
+ detail = detail.replace(/\s+/g, " ").trim();
40
47
  window.__hfStartupError =
41
48
  `HyperFrames runtime failed: ${detail}. Check the composition's scripts and window.__timelines ` +
42
49
  `entries; if they look right, report this as a HyperFrames bug.`;
43
50
  }
51
+ /** Starts the warning for a scene callback that threw; the render log shows it once per render. */
52
+ export const SCENE_CALLBACK_ERROR_PREFIX = "[hyperframes] scene timeline callback threw:";
44
53
  export function swallow(label, error) {
45
54
  if (typeof window === "undefined")
46
55
  return;
@@ -63,3 +72,33 @@ export function swallow(label, error) {
63
72
  console.debug(`[hyperframes] ${label} swallowed:`, error);
64
73
  }
65
74
  }
75
+ function isCompositionFrame(frame) {
76
+ const location = /(?:\(|@|\bat )(\S+?):\d+(?::\d+)?\)?$/.exec(frame)?.[1];
77
+ if (!location || location.startsWith("hyperframes://"))
78
+ return false;
79
+ return (location.startsWith("hyperframes-composition://") ||
80
+ /\.html?(?:[?#]|$)/i.test(location) ||
81
+ (typeof document !== "undefined" && location === document.URL));
82
+ }
83
+ function startupErrorStack(error, header) {
84
+ try {
85
+ const stack = error.stack;
86
+ if (typeof stack !== "string")
87
+ return "";
88
+ const hasHeader = stack === header || stack.startsWith(`${header}\n`) || stack.startsWith(`${header}\r`);
89
+ const body = hasHeader ? stack.slice(header.length) : stack;
90
+ const frames = body
91
+ .split(/[\r\n]+/)
92
+ .map((line) => line.trim().replace(/\s+/g, " "))
93
+ .filter((line) => /^at\s+\S/.test(line) || /^[^@]*@.+:\d+(?::\d+)?$/.test(line));
94
+ const selected = frames.slice(0, 3);
95
+ const composition = frames.find(isCompositionFrame);
96
+ if (composition && !selected.includes(composition))
97
+ selected.push(composition);
98
+ return selected.length ? ` [thrown at: ${selected.join(" <- ")}]` : "";
99
+ }
100
+ catch (err) {
101
+ swallow("runtime.startupError.stack", err);
102
+ return "";
103
+ }
104
+ }
@@ -81,6 +81,7 @@ export declare function evictMediaSyncState(el: HTMLMediaElement): void;
81
81
  export declare function hasMediaSyncStateForTest(el: HTMLMediaElement): boolean;
82
82
  export declare const MEDIA_HARD_SYNC_SECONDS = 0.5;
83
83
  export declare const VIDEO_PRESEEK_SECONDS = 1.5;
84
+ export declare function mediaSourceTimeAt(clip: RuntimeMediaClip, timeSeconds: number): number;
84
85
  /** Drift a playing audio element may carry before sync pulls it back onto the playhead. */
85
86
  export declare const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04;
86
87
  export declare function syncRuntimeMedia(params: {
@@ -193,6 +193,19 @@ export function hasMediaSyncStateForTest(el) {
193
193
  }
194
194
  export const MEDIA_HARD_SYNC_SECONDS = 0.5;
195
195
  export const VIDEO_PRESEEK_SECONDS = 1.5;
196
+ export function mediaSourceTimeAt(clip, timeSeconds) {
197
+ const elapsed = Math.max(0, Math.min(timeSeconds, clip.end) - (clip.origin ?? clip.start));
198
+ const target = clip.mediaStart + sourceTimeAt(clip.rate ?? clip.playbackRate, elapsed);
199
+ if (clip.sourceDuration == null)
200
+ return target;
201
+ const loopLength = clip.sourceDuration - clip.mediaStart;
202
+ if (clip.loop && loopLength > 0 && target >= clip.sourceDuration)
203
+ return clip.mediaStart + ((target - clip.mediaStart) % loopLength);
204
+ // A non-loop video past its source holds the last frame.
205
+ if (!clip.loop && clip.el.tagName === "VIDEO")
206
+ return Math.min(target, clip.sourceDuration);
207
+ return target;
208
+ }
196
209
  function prepareUpcomingVideo(clip, time, playing, rate) {
197
210
  const { el } = clip;
198
211
  const dueIn = clip.start - time;
@@ -206,15 +219,7 @@ function prepareUpcomingVideo(clip, time, playing, rate) {
206
219
  findInjectedRenderFrame(el) ||
207
220
  ("__HF_EXPORT_RENDER_SEEK_CONFIG" in window && window.__HF_EXPORT_RENDER_SEEK_CONFIG))
208
221
  return;
209
- let target = clip.mediaStart +
210
- sourceTimeAt(clip.rate ?? clip.playbackRate, clip.start - (clip.origin ?? clip.start));
211
- if (clip.sourceDuration != null) {
212
- const loopLength = clip.sourceDuration - clip.mediaStart;
213
- if (clip.loop && loopLength > 0 && target >= clip.sourceDuration)
214
- target = clip.mediaStart + ((target - clip.mediaStart) % loopLength);
215
- else if (!clip.loop)
216
- target = Math.min(target, clip.sourceDuration);
217
- }
222
+ const target = mediaSourceTimeAt(clip, clip.start);
218
223
  if (!Number.isFinite(target) || target < 0 || Math.abs(el.currentTime - target) <= 0.001)
219
224
  return;
220
225
  try {
@@ -282,13 +287,9 @@ export function syncRuntimeMedia(params) {
282
287
  !inWindow &&
283
288
  (params.timeSeconds >= clip.end || sameInstant(params.timeSeconds, clip.end)) &&
284
289
  isClipVisibleAt(params.timeSeconds, clip.start, clip.end, params.getCompositionDuration());
285
- let relTime = sourceTimeAt(clipRate, Math.max(0, Math.min(params.timeSeconds, clip.end) - origin)) +
286
- clip.mediaStart;
290
+ const relTime = mediaSourceTimeAt(clip, params.timeSeconds);
287
291
  const isHeldVideoTail = isTerminalVideo ||
288
292
  (isNonLoopVideo && clip.sourceDuration != null && relTime >= clip.sourceDuration && inWindow);
289
- if (isHeldVideoTail && clip.sourceDuration != null) {
290
- relTime = Math.min(relTime, clip.sourceDuration);
291
- }
292
293
  const previousRelativeTime = lastRelativeTime.get(el);
293
294
  const audioReenteredAfterBackwardSeek = el.tagName === "AUDIO" &&
294
295
  (previousRelativeTime === undefined || relTime < previousRelativeTime - 0.04);
@@ -308,13 +309,6 @@ export function syncRuntimeMedia(params) {
308
309
  (!el.ended || clip.loop || isHeldVideoTail || canSeekEndedMediaBackward);
309
310
  if (isActive) {
310
311
  lastRelativeTime.set(el, relTime);
311
- // Loop wrapping: when media reaches end, restart from mediaStart
312
- if (clip.loop && clip.sourceDuration != null && clip.sourceDuration > 0) {
313
- const loopLength = clip.sourceDuration - clip.mediaStart;
314
- if (loopLength > 0 && relTime >= clip.sourceDuration) {
315
- relTime = clip.mediaStart + ((relTime - clip.mediaStart) % loopLength);
316
- }
317
- }
318
312
  const userVol = clampVolume(params.userVolume ?? 1);
319
313
  const fallbackAuthorVolume = clampAudioGain(clip.volume ?? 1);
320
314
  const previousRuntimeVolume = lastRuntimeAppliedVolume.get(el);
@@ -1,7 +1,8 @@
1
1
  import { CLIP_HIDDEN_ATTR } from "./clipReveal.js";
2
2
  export { CLIP_HIDDEN_ATTR };
3
3
  export declare const SKIPPED_CLIP = "[data-start]:not(video, audio, img, [data-hf-upcoming], [data-hf-autostamped])[data-hf-clip-hidden]";
4
- export declare function showHiddenClips(): () => void;
4
+ export declare function showHiddenClips(only?: (el: HTMLElement) => boolean): () => void;
5
+ export declare function showClips(hidden: Array<Element & ElementCSSInlineStyle>): () => void;
5
6
  export declare function defersMedia(): boolean;
6
7
  /** True in Studio's preview, where hidden clips' images are neither fetched nor decoded until due. */
7
8
  export declare function skipsHiddenImages(): boolean;
@@ -21,12 +21,16 @@ const hideHiddenClipContents = (clip) => {
21
21
  .join(", ");
22
22
  return `${clip} * { visibility: hidden !important; } ${filterOutputLayers} { filter: none !important; }`;
23
23
  };
24
- export function showHiddenClips() {
25
- const hidden = Array.from(document.querySelectorAll(`[${CLIP_HIDDEN_ATTR}]`));
24
+ export function showHiddenClips(only = () => true) {
25
+ return showClips(Array.from(document.querySelectorAll(`[${CLIP_HIDDEN_ATTR}]`)).filter(only));
26
+ }
27
+ export function showClips(hidden) {
26
28
  const putMarksBack = hidden.map((el) => revealClip(el));
27
29
  return () => {
28
- for (const el of hidden)
29
- el.style.visibility = "hidden";
30
+ for (const el of hidden) {
31
+ if (el.style.getPropertyValue("visibility") !== "hidden")
32
+ el.style.setProperty("visibility", "hidden", "important");
33
+ }
30
34
  for (const putMarkBack of putMarksBack)
31
35
  putMarkBack();
32
36
  };
@@ -1,4 +1,4 @@
1
- import type { RuntimeTimelineMessage, RuntimeTimelineLike } from "./types.js";
1
+ import type { RuntimeTimelineClip, RuntimeTimelineMessage, RuntimeTimelineLike } from "./types.js";
2
2
  import { createRuntimeStartTimeResolver } from "./startResolver.js";
3
3
  export declare function runtimeElementWindow(rawNode: Element, options: {
4
4
  timelineRegistry: Record<string, RuntimeTimelineLike | undefined>;
@@ -15,12 +15,19 @@ export declare function isRuntimeElementVisibleAt(rawNode: Element, options: {
15
15
  timelineRegistry: Record<string, RuntimeTimelineLike | undefined>;
16
16
  resolver: ReturnType<typeof createRuntimeStartTimeResolver>;
17
17
  }): boolean;
18
+ export declare function parseElementDurationAttr(element: Element): number | null;
18
19
  /**
19
20
  * Parse an authored track attribute, honoring 0 (a valid top-lane index).
20
21
  * `parseInt(...) || fallback` silently replaced authored track 0 with the
21
22
  * synthetic fallback, so track-0 clips drifted to the bottom of the timeline.
22
23
  */
23
24
  export declare function parseAuthoredTrack(el: Element, fallback: number): number;
25
+ export type ParkedTimelineScene = {
26
+ name: string;
27
+ host: () => Element;
28
+ };
24
29
  export declare function collectRuntimeTimelinePayload(params: {
25
30
  canonicalFps: number;
31
+ parked?: ParkedTimelineScene[];
32
+ parkedClips?: Map<string, RuntimeTimelineClip[]>;
26
33
  }): RuntimeTimelineMessage;
@@ -40,9 +40,10 @@ export function runtimeElementWindow(rawNode, options) {
40
40
  duration = liveDuration;
41
41
  }
42
42
  }
43
+ const isRoot = compId !== null && rawNode === findRootCompositionElement(rawNode.ownerDocument);
43
44
  return {
44
45
  start,
45
- end: duration != null && duration > 0 ? start + duration : Number.POSITIVE_INFINITY,
46
+ end: !isRoot && duration != null && duration > 0 ? start + duration : Number.POSITIVE_INFINITY,
46
47
  };
47
48
  }
48
49
  export function isRuntimeElementVisibleAt(rawNode, options) {
@@ -58,7 +59,7 @@ export function isRuntimeElementVisibleAt(rawNode, options) {
58
59
  function parseNum(value) {
59
60
  return parseStrictFiniteTimingNumber(value);
60
61
  }
61
- function parseElementDurationAttr(element) {
62
+ export function parseElementDurationAttr(element) {
62
63
  const publicDuration = element.getAttribute("data-duration");
63
64
  const authoredDuration = element.getAttribute(AUTHORED_DURATION_ATTR);
64
65
  const resolved = resolveAuthoredTimingWindow({
@@ -353,13 +354,14 @@ export function collectRuntimeTimelinePayload(params) {
353
354
  // as clips — they have no declared timing so the timeline can't show their
354
355
  // actual visibility window. They can still appear as scenes via the separate
355
356
  // scene collection below.
356
- const nodes = Array.from(document.querySelectorAll("[data-start], [data-track-index], [data-composition-id], video, audio, img"));
357
+ const CLIP_NODES = "[data-start], [data-track-index], [data-composition-id], video, audio, img";
358
+ const nodes = Array.from(document.querySelectorAll(CLIP_NODES));
357
359
  let maxEnd = 0;
358
- for (const [i, node] of nodes.entries()) {
360
+ const clipOf = (node, i) => {
359
361
  if (node === root)
360
- continue;
362
+ return null;
361
363
  if (["SCRIPT", "STYLE", "LINK", "META", "TEMPLATE", "NOSCRIPT"].includes(node.tagName))
362
- continue;
364
+ return null;
363
365
  const compositionContext = resolveNearestCompositionContext(node, root);
364
366
  const tag = node.tagName.toLowerCase();
365
367
  let start = tag === "video" || tag === "audio"
@@ -387,21 +389,19 @@ export function collectRuntimeTimelinePayload(params) {
387
389
  }
388
390
  }
389
391
  if (duration == null || duration <= 0)
390
- continue;
392
+ return null;
391
393
  duration = clampDurationToRootWindow(start, duration);
392
394
  if (duration <= 0)
393
- continue;
395
+ return null;
394
396
  const slots = startResolver.resolveHostSlotsForElement(node);
395
397
  if (slots.length > 0) {
396
398
  const played = cutToHostSlots({ start, end: start + duration }, slots);
397
399
  const keepZeroLengthSoStudioNestsChildren = isNestedHost;
398
400
  if (played.end <= played.start && !keepZeroLengthSoStudioNestsChildren)
399
- continue;
401
+ return null;
400
402
  start = played.start;
401
403
  duration = played.end - played.start;
402
404
  }
403
- const end = start + duration;
404
- maxEnd = Math.max(maxEnd, end);
405
405
  const kind = nodeCompositionId && nodeCompositionId !== rootCompositionId
406
406
  ? "composition"
407
407
  : tag === "video"
@@ -411,7 +411,7 @@ export function collectRuntimeTimelinePayload(params) {
411
411
  : tag === "img"
412
412
  ? "image"
413
413
  : "element";
414
- clips.push({
414
+ return {
415
415
  id: stableClipId(node) ?? nodeCompositionId ?? null,
416
416
  label: buildTimelineClipLabel(node, kind, clips.length),
417
417
  start,
@@ -433,7 +433,26 @@ export function collectRuntimeTimelinePayload(params) {
433
433
  timelineLabel: node.getAttribute("data-timeline-label"),
434
434
  timelineGroup: node.getAttribute("data-timeline-group"),
435
435
  timelinePriority: parseNum(node.getAttribute("data-timeline-priority")),
436
- });
436
+ };
437
+ };
438
+ const addClip = (clip) => {
439
+ clips.push(clip);
440
+ maxEnd = Math.max(maxEnd, clip.start + clip.duration);
441
+ };
442
+ for (const [i, node] of nodes.entries()) {
443
+ const clip = clipOf(node, i);
444
+ if (clip)
445
+ addClip(clip);
446
+ }
447
+ for (const scene of params.parked ?? []) {
448
+ let parkedClips = params.parkedClips?.get(scene.name);
449
+ if (!parkedClips) {
450
+ const offset = nodes.length;
451
+ parkedClips = Array.from(scene.host().querySelectorAll(CLIP_NODES)).flatMap((node, j) => clipOf(node, offset + j) ?? []);
452
+ params.parkedClips?.set(scene.name, parkedClips);
453
+ }
454
+ for (const clip of parkedClips)
455
+ addClip(clip);
437
456
  }
438
457
  // ── GSAP introspection ──────────────────────────────────────────────────
439
458
  // Discover elements animated by GSAP that weren't picked up by the DOM query
@@ -6,7 +6,7 @@ export type RuntimeJson = string | number | boolean | null | RuntimeJson[] | {
6
6
  import type { HyperframeControlAction } from "../inline-scripts/runtimeContract.js";
7
7
  import type { HyperframePickerElementInfo } from "../inline-scripts/pickerApi.js";
8
8
  import type { RuntimeProtocolV1 } from "./protocol.js";
9
- type RuntimeBridgeControlActionBase = HyperframeControlAction | "tick" | "set-volume" | "set-media-output-muted" | "set-native-media-sync-disabled" | "set-web-audio-media-disabled" | "set-idle-heartbeat" | "set-display-scale" | "set-root-duration" | "set-play-range" | "stop-media" | "flash-elements";
9
+ type RuntimeBridgeControlActionBase = HyperframeControlAction | "tick" | "set-volume" | "set-media-output-muted" | "set-native-media-sync-disabled" | "set-web-audio-media-disabled" | "set-idle-heartbeat" | "set-display-scale" | "set-preview-quality" | "set-root-duration" | "set-play-range" | "stop-media" | "flash-elements";
10
10
  type RuntimeBridgeControlMessageBase = {
11
11
  source: "hf-parent";
12
12
  type: "control";
@@ -21,6 +21,7 @@ type RuntimeBridgeControlMessageBase = {
21
21
  disabled?: boolean;
22
22
  slow?: boolean;
23
23
  scale?: number;
24
+ level?: number;
24
25
  playbackRate?: number;
25
26
  target?: HfColorGradingTarget | string | null;
26
27
  grading?: RuntimeJson;
@@ -1,3 +1,4 @@
1
+ import { isFontValue, isImageValue } from "@hyperframes/parsers/composition";
1
2
  /**
2
3
  * Compare a flat values map (from `--variables` / `data-variable-values`) to
3
4
  * the declared schema (`data-composition-variables`). Returns issues for keys
@@ -86,10 +87,7 @@ function checkType(value, decl) {
86
87
  actual: jsTypeOf(value),
87
88
  };
88
89
  }
89
- // Object form: require the discriminant fields so a malformed brand-kit
90
- // value ({name: 42} / {}) is caught here rather than surfacing as a bogus
91
- // font-family at render time.
92
- if (typeof value.name !== "string" || typeof value.source !== "string") {
90
+ if (!isFontValue(value)) {
93
91
  return {
94
92
  kind: "type-mismatch",
95
93
  variableId: decl.id,
@@ -111,13 +109,14 @@ function checkType(value, decl) {
111
109
  actual: jsTypeOf(value),
112
110
  };
113
111
  }
114
- // Object form: require the discriminant field.
115
- if (typeof value.url !== "string") {
112
+ if (!isImageValue(value)) {
116
113
  return {
117
114
  kind: "type-mismatch",
118
115
  variableId: decl.id,
119
116
  expected: "image object {url: string}",
120
- actual: "object missing string url",
117
+ actual: typeof value.url !== "string"
118
+ ? "object missing string url"
119
+ : "object with invalid alt/fit",
121
120
  };
122
121
  }
123
122
  return null;
@@ -5,5 +5,7 @@ export declare const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
5
5
  export declare const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
6
6
  export declare const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
7
7
  export declare const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
8
- export declare const PREVIEW_ONLY_ATTRS: readonly ["data-hf-upcoming", "data-hf-preview-lazy", "data-hf-preview-raster", "data-hf-autostamped"];
8
+ export declare const STUDIO_PREVIEW_FAR_SCENE_ATTR = "data-hf-far-scene";
9
+ export declare const STUDIO_PREVIEW_PARKED_SCENE_ATTR = "data-hf-parked-scene";
10
+ export declare const PREVIEW_ONLY_ATTRS: readonly ["data-hf-upcoming", "data-hf-far-scene", "data-hf-parked-scene", "data-hf-preview-lazy", "data-hf-preview-raster", "data-hf-autostamped"];
9
11
  export declare function applyAuthoredTiming(el: Element, attrs: ReadonlyArray<readonly [name: string, value: string]>): void;
@@ -5,8 +5,12 @@ export const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
5
5
  export const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
6
6
  export const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
7
7
  export const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
8
+ export const STUDIO_PREVIEW_FAR_SCENE_ATTR = "data-hf-far-scene";
9
+ export const STUDIO_PREVIEW_PARKED_SCENE_ATTR = "data-hf-parked-scene";
8
10
  export const PREVIEW_ONLY_ATTRS = [
9
11
  STUDIO_PREVIEW_UPCOMING_ATTR,
12
+ STUDIO_PREVIEW_FAR_SCENE_ATTR,
13
+ STUDIO_PREVIEW_PARKED_SCENE_ATTR,
10
14
  STUDIO_PREVIEW_LAZY_ATTR,
11
15
  PREVIEW_RASTER_ATTR,
12
16
  RUNTIME_STAMPED_ATTR,
@@ -5,7 +5,7 @@
5
5
  * the Node-only modules reachable from the core/parsers root entries, so
6
6
  * browser consumers (SDK, Studio, embedders) can bundle it.
7
7
  */
8
- export type { CompositionVariable, CompositionVariableType, CompositionVariableBase, StringVariable, NumberVariable, ColorVariable, BooleanVariable, EnumVariable, FontVariable, ImageVariable, } from "@hyperframes/parsers/composition";
8
+ export type { CompositionVariable, CompositionVariableType, CompositionVariableBase, StringVariable, NumberVariable, ColorVariable, BooleanVariable, EnumVariable, FontVariable, FontValue, ImageVariable, ImageValue, } from "@hyperframes/parsers/composition";
9
9
  export { COMPOSITION_VARIABLE_TYPES, parseCompositionVariables, isCompositionVariable, isScalarVariableValue, scanVariableUsage, } from "@hyperframes/parsers/composition";
10
10
  export type { VariableUsageScan } from "@hyperframes/parsers/composition";
11
11
  export { getVariables, readDeclaredDefaults } from "./runtime/getVariables.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/core",
3
- "version": "0.8.199",
3
+ "version": "0.8.201",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -397,9 +397,9 @@
397
397
  "linkedom": "^0.18.12",
398
398
  "postcss": "^8.5.8",
399
399
  "wawoff2": "^2.0.1",
400
- "@hyperframes/lint": "0.8.199",
401
- "@hyperframes/parsers": "0.8.199",
402
- "@hyperframes/studio-server": "0.8.199"
400
+ "@hyperframes/lint": "0.8.201",
401
+ "@hyperframes/studio-server": "0.8.201",
402
+ "@hyperframes/parsers": "0.8.201"
403
403
  },
404
404
  "devDependencies": {
405
405
  "@fontsource/archivo-black": "^5.2.8",