@hyperframes/core 0.8.205 → 0.8.207
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/compiler/compositionScoping.d.ts +2 -0
- package/dist/compiler/compositionScoping.js +11 -1
- package/dist/compiler/htmlBundler.js +3 -1
- package/dist/compiler/inlineSubCompositions.js +2 -2
- package/dist/compiler/pngColourTags.d.ts +2 -0
- package/dist/compiler/pngColourTags.js +27 -0
- package/dist/compiler/selectorIdTokens.d.ts +1 -1
- package/dist/core.types.d.ts +2 -66
- package/dist/core.types.js +1 -102
- package/dist/fps.d.ts +17 -0
- package/dist/fps.js +58 -0
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/generators/hyperframes.js +4 -1
- package/dist/hyperframe.runtime.iife.js +75 -68
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/removeTree.d.ts +24 -0
- package/dist/removeTree.js +35 -0
- package/dist/runtime/media.d.ts +6 -3
- package/dist/runtime/media.js +54 -52
- package/dist/runtime/mediaClock.d.ts +2 -0
- package/dist/runtime/mediaClock.js +4 -0
- package/dist/runtime/mediaSteer.d.ts +10 -0
- package/dist/runtime/mediaSteer.js +40 -0
- package/dist/runtime/preloadMedia.js +2 -1
- package/dist/runtime/protocol.js +2 -1
- package/dist/runtime/startResolver.js +6 -8
- package/dist/studioPreviewMark.d.ts +2 -0
- package/dist/studioPreviewMark.js +5 -0
- package/package.json +20 -4
|
@@ -13,6 +13,8 @@ export declare function hoistFontFaceRules(styleTexts: string[]): {
|
|
|
13
13
|
styles: string[];
|
|
14
14
|
faces: string[];
|
|
15
15
|
};
|
|
16
|
+
export declare const AUTHORED_SOURCE_BEGIN = "/*hf-authored-begin*/";
|
|
17
|
+
export declare const AUTHORED_SOURCE_END = "/*hf-authored-end*/";
|
|
16
18
|
export declare function scopedModulePrelude(timelineCompositionId: string, compositionSrc?: string | null): string;
|
|
17
19
|
export declare function wrapScopedCompositionScript(source: string, compositionId: string, errorLabel?: string, scopeSelectorOverride?: string, timelineCompositionId?: string, authoredRootId?: string | null, compositionSrc?: string | null): string;
|
|
18
20
|
export declare function wrapInlineScriptWithErrorBoundary(source: string, errorLabel: string): string;
|
|
@@ -295,6 +295,8 @@ const SCOPED_HYPERFRAMES_EXPRESSION = `!__hfBaseHyperframes
|
|
|
295
295
|
return scoped ? Object.assign({}, scoped) : {};
|
|
296
296
|
},
|
|
297
297
|
})`;
|
|
298
|
+
export const AUTHORED_SOURCE_BEGIN = "/*hf-authored-begin*/";
|
|
299
|
+
export const AUTHORED_SOURCE_END = "/*hf-authored-end*/";
|
|
298
300
|
export function scopedModulePrelude(timelineCompositionId, compositionSrc) {
|
|
299
301
|
return `const __hyperframes = (function(__hfBaseHyperframes, __hfTimelineCompId, __hfCompositionSrc) {
|
|
300
302
|
return ${SCOPED_HYPERFRAMES_EXPRESSION};
|
|
@@ -310,6 +312,7 @@ export function wrapScopedCompositionScript(source, compositionId, errorLabel =
|
|
|
310
312
|
const authoredRootIdLiteral = jsonScriptLiteral(authoredRootId?.trim() || null);
|
|
311
313
|
const scopeSelectorLiteral = jsonScriptLiteral(scopeSelectorOverride ?? null);
|
|
312
314
|
const rootSelectorPatternLiteral = jsonScriptLiteral(String.raw `\[\s*data-composition-id\s*=\s*(?:"${escapedCompositionId}"|'${escapedCompositionId}')\s*\]`);
|
|
315
|
+
const timelineRootSelectorPatternLiteral = jsonScriptLiteral(String.raw `\[\s*data-composition-id\s*=\s*(?:"${escapeRegExp(timelineCompositionId)}"|'${escapeRegExp(timelineCompositionId)}')\s*\]`);
|
|
313
316
|
const timingSelectorPatternLiteral = jsonScriptLiteral(String.raw `\s*\[\s*data-(?:start|duration)\s*=\s*(?:"[^"]*"|'[^']*')\s*\]`);
|
|
314
317
|
const aliasInstallerOutsideAuthoredStrictness = `(function(){ ${getSvgSelectorAliasesScript()} })();`;
|
|
315
318
|
return `(function(){
|
|
@@ -328,6 +331,7 @@ export function wrapScopedCompositionScript(source, compositionId, errorLabel =
|
|
|
328
331
|
: "");
|
|
329
332
|
var __hfRoot = null;
|
|
330
333
|
var __hfRootSelectorPattern = ${rootSelectorPatternLiteral};
|
|
334
|
+
var __hfTimelineRootSelectorPattern = ${timelineRootSelectorPatternLiteral};
|
|
331
335
|
var __hfTimingSelectorPattern = ${timingSelectorPatternLiteral};
|
|
332
336
|
var __hfAuthoredRootSelector = __hfAuthoredRootId
|
|
333
337
|
? "[" + __hfAuthoredRootAttr + '="' + __hfEscapeAttr(__hfAuthoredRootId) + '"]'
|
|
@@ -339,7 +343,11 @@ export function wrapScopedCompositionScript(source, compositionId, errorLabel =
|
|
|
339
343
|
if (!__hfCompId || typeof selector !== "string") return selector;
|
|
340
344
|
var normalized = selector
|
|
341
345
|
.replace(new RegExp(__hfRootSelectorPattern + '(?:' + __hfTimingSelectorPattern + ')+', 'g'), __hfRootSelector)
|
|
342
|
-
.replace(new RegExp('(?:' + __hfTimingSelectorPattern + ')+' + __hfRootSelectorPattern, 'g'), __hfRootSelector)
|
|
346
|
+
.replace(new RegExp('(?:' + __hfTimingSelectorPattern + ')+' + __hfRootSelectorPattern, 'g'), __hfRootSelector)
|
|
347
|
+
.replace(new RegExp(__hfRootSelectorPattern, 'g'), __hfRootSelector);
|
|
348
|
+
if (__hfTimelineCompId !== __hfCompId) {
|
|
349
|
+
normalized = normalized.replace(new RegExp(__hfTimelineRootSelectorPattern, 'g'), __hfRootSelector);
|
|
350
|
+
}
|
|
343
351
|
return window.__hfSvgSelectorAliases.rewrite(normalized, __hfAuthoredRootEntries);
|
|
344
352
|
};
|
|
345
353
|
var __hfFindRoot = function() {
|
|
@@ -581,7 +589,9 @@ export function wrapScopedCompositionScript(source, compositionId, errorLabel =
|
|
|
581
589
|
if (typeof report === "function") report("start", __hfCompositionSrc || "index.html");
|
|
582
590
|
try {
|
|
583
591
|
(function(document, gsap, window, __hyperframes) {
|
|
592
|
+
${AUTHORED_SOURCE_BEGIN}
|
|
584
593
|
${source.replace(/<\/(script)/gi, "<\\/$1")}
|
|
594
|
+
${AUTHORED_SOURCE_END}
|
|
585
595
|
}).call(window, __hfScopedDocument, __hfScopedGsap, __hfScopedWindow, __hfScopedHyperframes);
|
|
586
596
|
} catch (_err) {
|
|
587
597
|
console.error(__hfErrorLabel, __hfCompId, _err);
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { compositionStyle, cssStyleMergeKey, deferScriptsUntilFonts, isAlwaysAppliedStyle, headStyleRuns, INLINED_FILE_ATTR, inlineScriptRuns, styleElementsFor, } from "./scriptRuns.js";
|
|
3
3
|
import { countPageRoot, executableScripts, isJavaScriptType, planCompositionAssembly, } from "./compositionAssembly.js";
|
|
4
4
|
import { SCENE_FONTS_ATTR, SCENE_PART_ATTR } from "../sceneParts.js";
|
|
5
|
+
import { stripSdrPngColourTags } from "./pngColourTags.js";
|
|
5
6
|
import { ensureExternalScriptTag, readExternalScriptAttributes, } from "./externalScripts.js";
|
|
6
7
|
import { emitMountedModuleScripts, parseImportMap } from "./importMaps.js";
|
|
7
8
|
import { markFlattenedInnerRoot } from "../runtime/flattenedRoot.js";
|
|
@@ -313,7 +314,8 @@ function createAssetInliner(resolveProjectPath, inlineAssets) {
|
|
|
313
314
|
const byteLength = safeStatSize(filePath);
|
|
314
315
|
if (byteLength !== null && byteLength > MAX_INLINE_ASSET_BYTES)
|
|
315
316
|
return { tooLarge: byteLength };
|
|
316
|
-
const
|
|
317
|
+
const read = safeReadFileBuffer(filePath);
|
|
318
|
+
const content = read && mimeType === "image/png" ? (stripSdrPngColourTags(read) ?? read) : read;
|
|
317
319
|
return {
|
|
318
320
|
dataUrl: content === null ? null : `data:${mimeType};base64,${content.toString("base64")}`,
|
|
319
321
|
};
|
|
@@ -13,7 +13,7 @@ import { compositionStyle, cssStyleMergeKey, hasSameLink, } from "./scriptRuns.j
|
|
|
13
13
|
*/
|
|
14
14
|
import { rewriteAssetPath, rewriteAssetPaths, rewriteCssAssetUrls, rewriteInlineStyleAssetUrls, } from "./rewriteSubCompPaths.js";
|
|
15
15
|
import { warnUnknownEnumValues } from "../runtime/getVariables.js";
|
|
16
|
-
import { createCompositionCssScoper, wrapInlineScriptWithErrorBoundary, scopedModulePrelude, wrapScopedCompositionScript, } from "./compositionScoping.js";
|
|
16
|
+
import { createCompositionCssScoper, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END, wrapInlineScriptWithErrorBoundary, scopedModulePrelude, wrapScopedCompositionScript, } from "./compositionScoping.js";
|
|
17
17
|
import { namespaceCollidingSvgIds, rewriteSvgIdReferencesInCss, } from "./svgIdNamespacing.js";
|
|
18
18
|
import { checkSubCompositionUsability, resolveSubCompositionContent, } from "@hyperframes/parsers/sub-composition-validity";
|
|
19
19
|
import { enumerateNestedCompositionHosts, planCompositionAssembly, extractedCompositionAssets, } from "./compositionAssembly.js";
|
|
@@ -419,7 +419,7 @@ export function inlineSubCompositions(document, hosts, options) {
|
|
|
419
419
|
const prelude = scriptCompositionId
|
|
420
420
|
? scopedModulePrelude(runtimeCompId || scopeCompId || scriptCompositionId, src)
|
|
421
421
|
: "";
|
|
422
|
-
moduleScripts.push(prelude
|
|
422
|
+
moduleScripts.push(`${prelude}${AUTHORED_SOURCE_BEGIN}\n${scriptEl.textContent || ""}\n${AUTHORED_SOURCE_END}`);
|
|
423
423
|
}
|
|
424
424
|
else if (externalSrc) {
|
|
425
425
|
if (!externalScriptSrcs.includes(externalSrc)) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
2
|
+
const PNG_SIGNATURE_BYTES = PNG_SIGNATURE.length;
|
|
3
|
+
const CHUNK_OVERHEAD_BYTES = 12;
|
|
4
|
+
const REMOVED_CHUNKS = new Set(["cICP", "gAMA", "cHRM"]);
|
|
5
|
+
const HDR_CICP_TRANSFERS = new Set([16, 18]);
|
|
6
|
+
/** An SDR PNG's copy without the tags that make Chrome re-map its pixels; null if none apply or it is HDR. */
|
|
7
|
+
export function stripSdrPngColourTags(png) {
|
|
8
|
+
const bytes = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
|
|
9
|
+
if (!bytes.subarray(0, PNG_SIGNATURE_BYTES).equals(PNG_SIGNATURE))
|
|
10
|
+
return null;
|
|
11
|
+
const kept = [bytes.subarray(0, PNG_SIGNATURE_BYTES)];
|
|
12
|
+
let removed = false;
|
|
13
|
+
for (let at = PNG_SIGNATURE_BYTES; at + CHUNK_OVERHEAD_BYTES <= bytes.length;) {
|
|
14
|
+
const end = at + CHUNK_OVERHEAD_BYTES + bytes.readUInt32BE(at);
|
|
15
|
+
if (end > bytes.length)
|
|
16
|
+
return null;
|
|
17
|
+
const type = bytes.toString("latin1", at + 4, at + 8);
|
|
18
|
+
if (type === "cICP" && HDR_CICP_TRANSFERS.has(bytes[at + 9] ?? 0))
|
|
19
|
+
return null;
|
|
20
|
+
if (REMOVED_CHUNKS.has(type))
|
|
21
|
+
removed = true;
|
|
22
|
+
else
|
|
23
|
+
kept.push(bytes.subarray(at, end));
|
|
24
|
+
at = end;
|
|
25
|
+
}
|
|
26
|
+
return removed ? Buffer.concat(kept) : null;
|
|
27
|
+
}
|
|
@@ -62,7 +62,7 @@ export declare function parseAttributeSelector(predicate: string): {
|
|
|
62
62
|
name: string;
|
|
63
63
|
rawName: string;
|
|
64
64
|
namespace: AttributeNamespace;
|
|
65
|
-
operator: "=" | "
|
|
65
|
+
operator: "=" | "*=" | "|=" | "^=" | "~=" | "$=";
|
|
66
66
|
value: string;
|
|
67
67
|
flag: string | undefined;
|
|
68
68
|
} | null;
|
package/dist/core.types.d.ts
CHANGED
|
@@ -1,70 +1,6 @@
|
|
|
1
1
|
export type ExecutionMode = "planning" | "design" | "execution" | null;
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* than collapsing to `29.97`) lets us pass NTSC / drop-frame rates straight
|
|
5
|
-
* through to FFmpeg via `-r 30000/1001` without any decimal round-trip.
|
|
6
|
-
*
|
|
7
|
-
* Integer fps is represented with `den: 1` (e.g. `{ num: 30, den: 1 }`).
|
|
8
|
-
*
|
|
9
|
-
* Use {@link fpsToNumber} when arithmetic forces a decimal (e.g. `setTimeout`
|
|
10
|
-
* intervals) and {@link fpsToFfmpegArg} when emitting FFmpeg `-r` /
|
|
11
|
-
* `-framerate` strings.
|
|
12
|
-
*/
|
|
13
|
-
export interface Fps {
|
|
14
|
-
num: number;
|
|
15
|
-
den: number;
|
|
16
|
-
}
|
|
17
|
-
export type FpsInput = number | Fps;
|
|
18
|
-
export declare function toFps(input: FpsInput): Fps;
|
|
19
|
-
/**
|
|
20
|
-
* Decimal value of an {@link Fps} rational. Used at sites that need a
|
|
21
|
-
* `number` for arithmetic (frame-index → time, frame intervals, telemetry
|
|
22
|
-
* payloads) where the small precision loss of the decimal is acceptable.
|
|
23
|
-
*/
|
|
24
|
-
export declare function fpsToNumber(fps: Fps): number;
|
|
25
|
-
/**
|
|
26
|
-
* FFmpeg-style fps argument. Returns `"30"` for integer fps and `"30000/1001"`
|
|
27
|
-
* for rationals — both forms are accepted verbatim by FFmpeg's `-r` and
|
|
28
|
-
* `-framerate` flags. We keep integer fps as a bare integer so existing
|
|
29
|
-
* snapshot tests / log output don't churn for the common case.
|
|
30
|
-
*/
|
|
31
|
-
export declare function fpsToFfmpegArg(fps: Fps): string;
|
|
32
|
-
/**
|
|
33
|
-
* Discriminated parse result for {@link parseFps}. Lets the CLI / route
|
|
34
|
-
* validation own its own error UX without losing the structured failure
|
|
35
|
-
* reason.
|
|
36
|
-
*/
|
|
37
|
-
export type FpsParseResult = {
|
|
38
|
-
ok: true;
|
|
39
|
-
value: Fps;
|
|
40
|
-
} | {
|
|
41
|
-
ok: false;
|
|
42
|
-
reason: "empty" | "not-a-number" | "non-positive" | "out-of-range" | "invalid-fraction" | "ambiguous-decimal";
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Parse a user-supplied fps spec into an {@link Fps} rational.
|
|
46
|
-
*
|
|
47
|
-
* Accepted forms:
|
|
48
|
-
* - integer string `"30"` → `{ num: 30, den: 1 }`
|
|
49
|
-
* - integer number `30` → `{ num: 30, den: 1 }`
|
|
50
|
-
* - rational string `"30000/1001"` → `{ num: 30000, den: 1001 }` (exact NTSC)
|
|
51
|
-
*
|
|
52
|
-
* Rejected:
|
|
53
|
-
* - empty / non-numeric input
|
|
54
|
-
* - decimals like `"29.97"` — callers must spell rationals with `/` so the
|
|
55
|
-
* exact denominator is unambiguous (FFmpeg treats `29.97` as a slightly
|
|
56
|
-
* different framerate than `30000/1001`).
|
|
57
|
-
* - division by zero, negative or zero numerator
|
|
58
|
-
* - decimal value outside `[1, 240]` — defensive bounds for "human" fps
|
|
59
|
-
* ranges (24, 25, 30, 50, 60, 120, 240, plus the NTSC trio).
|
|
60
|
-
*/
|
|
61
|
-
export declare function parseFps(input: string | number): FpsParseResult;
|
|
62
|
-
/**
|
|
63
|
-
* Convenience wrapper around {@link parseFps} for callsites that want the
|
|
64
|
-
* default-30-fps fallback when input is `undefined`. Does NOT swallow parse
|
|
65
|
-
* errors — those still surface via the discriminated result.
|
|
66
|
-
*/
|
|
67
|
-
export declare function parseFpsWithDefault(input: string | number | undefined): FpsParseResult;
|
|
2
|
+
export type { Fps, FpsInput, FpsParseResult } from "./fps.js";
|
|
3
|
+
export { toFps, fpsToNumber, fpsToFfmpegArg, parseFps, parseFpsWithDefault } from "./fps.js";
|
|
68
4
|
/** Video orientation / aspect ratio. */
|
|
69
5
|
export type Orientation = "16:9" | "9:16";
|
|
70
6
|
export type { Asset, TimelineElementType, MediaElementType, TimelineElementBase, TimelineMediaElement, WaveformData, TimelineTextElement, TimelineCompositionElement, CompositionVariableType, CompositionVariableBase, StringVariable, NumberVariable, ColorVariable, BooleanVariable, EnumVariable, CompositionVariable, CompositionSpec, TimelineElement, MediaFile, CanvasResolution, CompositionAPI, PlayerAPI, AddElementData, ValidationResult, CompositionAsset, Keyframe, KeyframeProperties, ElementKeyframes, StageZoom, StageZoomKeyframe, } from "@hyperframes/parsers";
|
package/dist/core.types.js
CHANGED
|
@@ -1,104 +1,3 @@
|
|
|
1
1
|
// ── Shared cross-package types ──────────────────────────────────────────────
|
|
2
|
-
export
|
|
3
|
-
if (typeof input === "number") {
|
|
4
|
-
return { num: input, den: 1 };
|
|
5
|
-
}
|
|
6
|
-
return input;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Decimal value of an {@link Fps} rational. Used at sites that need a
|
|
10
|
-
* `number` for arithmetic (frame-index → time, frame intervals, telemetry
|
|
11
|
-
* payloads) where the small precision loss of the decimal is acceptable.
|
|
12
|
-
*/
|
|
13
|
-
export function fpsToNumber(fps) {
|
|
14
|
-
return fps.num / fps.den;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* FFmpeg-style fps argument. Returns `"30"` for integer fps and `"30000/1001"`
|
|
18
|
-
* for rationals — both forms are accepted verbatim by FFmpeg's `-r` and
|
|
19
|
-
* `-framerate` flags. We keep integer fps as a bare integer so existing
|
|
20
|
-
* snapshot tests / log output don't churn for the common case.
|
|
21
|
-
*/
|
|
22
|
-
export function fpsToFfmpegArg(fps) {
|
|
23
|
-
return fps.den === 1 ? String(fps.num) : `${fps.num}/${fps.den}`;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Parse a user-supplied fps spec into an {@link Fps} rational.
|
|
27
|
-
*
|
|
28
|
-
* Accepted forms:
|
|
29
|
-
* - integer string `"30"` → `{ num: 30, den: 1 }`
|
|
30
|
-
* - integer number `30` → `{ num: 30, den: 1 }`
|
|
31
|
-
* - rational string `"30000/1001"` → `{ num: 30000, den: 1001 }` (exact NTSC)
|
|
32
|
-
*
|
|
33
|
-
* Rejected:
|
|
34
|
-
* - empty / non-numeric input
|
|
35
|
-
* - decimals like `"29.97"` — callers must spell rationals with `/` so the
|
|
36
|
-
* exact denominator is unambiguous (FFmpeg treats `29.97` as a slightly
|
|
37
|
-
* different framerate than `30000/1001`).
|
|
38
|
-
* - division by zero, negative or zero numerator
|
|
39
|
-
* - decimal value outside `[1, 240]` — defensive bounds for "human" fps
|
|
40
|
-
* ranges (24, 25, 30, 50, 60, 120, 240, plus the NTSC trio).
|
|
41
|
-
*/
|
|
42
|
-
export function parseFps(input) {
|
|
43
|
-
if (typeof input === "number") {
|
|
44
|
-
if (!Number.isFinite(input))
|
|
45
|
-
return { ok: false, reason: "not-a-number" };
|
|
46
|
-
if (!Number.isInteger(input))
|
|
47
|
-
return { ok: false, reason: "ambiguous-decimal" };
|
|
48
|
-
if (input <= 0)
|
|
49
|
-
return { ok: false, reason: "non-positive" };
|
|
50
|
-
if (input > 240)
|
|
51
|
-
return { ok: false, reason: "out-of-range" };
|
|
52
|
-
return { ok: true, value: { num: input, den: 1 } };
|
|
53
|
-
}
|
|
54
|
-
const raw = input.trim();
|
|
55
|
-
if (raw === "")
|
|
56
|
-
return { ok: false, reason: "empty" };
|
|
57
|
-
if (raw.includes("/")) {
|
|
58
|
-
const parts = raw.split("/");
|
|
59
|
-
if (parts.length !== 2)
|
|
60
|
-
return { ok: false, reason: "invalid-fraction" };
|
|
61
|
-
const num = Number(parts[0]);
|
|
62
|
-
const den = Number(parts[1]);
|
|
63
|
-
if (!Number.isFinite(num) || !Number.isFinite(den)) {
|
|
64
|
-
return { ok: false, reason: "not-a-number" };
|
|
65
|
-
}
|
|
66
|
-
if (!Number.isInteger(num) || !Number.isInteger(den)) {
|
|
67
|
-
return { ok: false, reason: "invalid-fraction" };
|
|
68
|
-
}
|
|
69
|
-
if (den <= 0)
|
|
70
|
-
return { ok: false, reason: "invalid-fraction" };
|
|
71
|
-
if (num <= 0)
|
|
72
|
-
return { ok: false, reason: "non-positive" };
|
|
73
|
-
const decimal = num / den;
|
|
74
|
-
if (decimal < 1 || decimal > 240)
|
|
75
|
-
return { ok: false, reason: "out-of-range" };
|
|
76
|
-
return { ok: true, value: { num, den } };
|
|
77
|
-
}
|
|
78
|
-
// Integer-only path — reject `"29.97"` so users are explicit about the
|
|
79
|
-
// exact rational they want.
|
|
80
|
-
if (!/^-?\d+$/.test(raw)) {
|
|
81
|
-
// Allow caller to differentiate "29.97" from "abc" if they want; both
|
|
82
|
-
// are user errors but the message can be friendlier for decimals.
|
|
83
|
-
if (/^-?\d*\.\d+$/.test(raw))
|
|
84
|
-
return { ok: false, reason: "ambiguous-decimal" };
|
|
85
|
-
return { ok: false, reason: "not-a-number" };
|
|
86
|
-
}
|
|
87
|
-
const n = Number(raw);
|
|
88
|
-
if (n <= 0)
|
|
89
|
-
return { ok: false, reason: "non-positive" };
|
|
90
|
-
if (n > 240)
|
|
91
|
-
return { ok: false, reason: "out-of-range" };
|
|
92
|
-
return { ok: true, value: { num: n, den: 1 } };
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Convenience wrapper around {@link parseFps} for callsites that want the
|
|
96
|
-
* default-30-fps fallback when input is `undefined`. Does NOT swallow parse
|
|
97
|
-
* errors — those still surface via the discriminated result.
|
|
98
|
-
*/
|
|
99
|
-
export function parseFpsWithDefault(input) {
|
|
100
|
-
if (input === undefined || input === "")
|
|
101
|
-
return { ok: true, value: { num: 30, den: 1 } };
|
|
102
|
-
return parseFps(input);
|
|
103
|
-
}
|
|
2
|
+
export { toFps, fpsToNumber, fpsToFfmpegArg, parseFps, parseFpsWithDefault } from "./fps.js";
|
|
104
3
|
export { CANVAS_DIMENSIONS, VALID_CANVAS_RESOLUTIONS, normalizeResolutionFlag, isAspectAgnosticResolutionAlias, resolveResolutionFlagPair, checkOutputResolutionCompatibility, outputResolutionForSize, suggestMatchingPreset, COMPOSITION_VARIABLE_TYPES, TIMELINE_COLORS, DEFAULT_DURATIONS, getDefaultStageZoom, isTextElement, isMediaElement, isCompositionElement, } from "@hyperframes/parsers";
|
package/dist/fps.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Fps {
|
|
2
|
+
num: number;
|
|
3
|
+
den: number;
|
|
4
|
+
}
|
|
5
|
+
export type FpsInput = number | Fps;
|
|
6
|
+
export declare function toFps(input: FpsInput): Fps;
|
|
7
|
+
export declare function fpsToNumber(fps: Fps): number;
|
|
8
|
+
export declare function fpsToFfmpegArg(fps: Fps): string;
|
|
9
|
+
export type FpsParseResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
value: Fps;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: "empty" | "not-a-number" | "non-positive" | "out-of-range" | "invalid-fraction" | "ambiguous-decimal";
|
|
15
|
+
};
|
|
16
|
+
export declare function parseFps(input: string | number): FpsParseResult;
|
|
17
|
+
export declare function parseFpsWithDefault(input: string | number | undefined): FpsParseResult;
|
package/dist/fps.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function toFps(input) {
|
|
2
|
+
if (typeof input === "number") {
|
|
3
|
+
return { num: input, den: 1 };
|
|
4
|
+
}
|
|
5
|
+
return input;
|
|
6
|
+
}
|
|
7
|
+
export function fpsToNumber(fps) {
|
|
8
|
+
return fps.num / fps.den;
|
|
9
|
+
}
|
|
10
|
+
export function fpsToFfmpegArg(fps) {
|
|
11
|
+
return fps.den === 1 ? String(fps.num) : `${fps.num}/${fps.den}`;
|
|
12
|
+
}
|
|
13
|
+
const fail = (reason) => ({ ok: false, reason });
|
|
14
|
+
function rangedFps(num, den) {
|
|
15
|
+
if (num <= 0)
|
|
16
|
+
return fail("non-positive");
|
|
17
|
+
const decimal = num / den;
|
|
18
|
+
if (decimal < 1 || decimal > 240)
|
|
19
|
+
return fail("out-of-range");
|
|
20
|
+
return { ok: true, value: { num, den } };
|
|
21
|
+
}
|
|
22
|
+
function numericFps(input) {
|
|
23
|
+
if (!Number.isFinite(input))
|
|
24
|
+
return fail("not-a-number");
|
|
25
|
+
if (!Number.isInteger(input))
|
|
26
|
+
return fail("ambiguous-decimal");
|
|
27
|
+
return rangedFps(input, 1);
|
|
28
|
+
}
|
|
29
|
+
function fractionFps(raw) {
|
|
30
|
+
const parts = raw.split("/");
|
|
31
|
+
if (parts.length !== 2)
|
|
32
|
+
return fail("invalid-fraction");
|
|
33
|
+
const num = Number(parts[0]);
|
|
34
|
+
const den = Number(parts[1]);
|
|
35
|
+
if (!Number.isFinite(num) || !Number.isFinite(den))
|
|
36
|
+
return fail("not-a-number");
|
|
37
|
+
if (!Number.isInteger(num) || !Number.isInteger(den) || den <= 0)
|
|
38
|
+
return fail("invalid-fraction");
|
|
39
|
+
return rangedFps(num, den);
|
|
40
|
+
}
|
|
41
|
+
// Decimals like "29.97" are rejected: FFmpeg reads them as a different rate than 30000/1001.
|
|
42
|
+
export function parseFps(input) {
|
|
43
|
+
if (typeof input === "number")
|
|
44
|
+
return numericFps(input);
|
|
45
|
+
const raw = input.trim();
|
|
46
|
+
if (raw === "")
|
|
47
|
+
return fail("empty");
|
|
48
|
+
if (raw.includes("/"))
|
|
49
|
+
return fractionFps(raw);
|
|
50
|
+
if (/^-?\d+$/.test(raw))
|
|
51
|
+
return rangedFps(Number(raw), 1);
|
|
52
|
+
return fail(/^-?\d*\.\d+$/.test(raw) ? "ambiguous-decimal" : "not-a-number");
|
|
53
|
+
}
|
|
54
|
+
export function parseFpsWithDefault(input) {
|
|
55
|
+
if (input === undefined || input === "")
|
|
56
|
+
return { ok: true, value: { num: 30, den: 1 } };
|
|
57
|
+
return parseFps(input);
|
|
58
|
+
}
|