mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
refactor(core): unify composition contract (#2157)
* refactor(core): unify composition contract * fix(parsers): parse start expressions linearly
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from "@hyperframes/parsers/composition-contract";
|
||||
@@ -54,8 +54,10 @@ describe("generateHyperframesHtml", () => {
|
||||
|
||||
expect(html).toContain('id="my-text"');
|
||||
expect(html).toContain('data-start="2"');
|
||||
expect(html).toContain('data-end="5"');
|
||||
expect(html).toContain('data-layer="1"');
|
||||
expect(html).toContain('data-duration="3"');
|
||||
expect(html).toContain('data-track-index="1"');
|
||||
expect(html).not.toContain('data-end="');
|
||||
expect(html).not.toContain('data-layer="');
|
||||
});
|
||||
|
||||
it("includes GSAP CDN script tag when includeScripts is true", () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { GsapAnimation } from "@hyperframes/parsers";
|
||||
import { serializeGsapAnimations, keyframesToGsapAnimations } from "@hyperframes/parsers";
|
||||
import { GSAP_CDN, BASE_STYLES, ZOOM_CONTAINER_STYLES } from "../templates/constants";
|
||||
import { COMPOSITION_ATTRIBUTES } from "../compositionContract.js";
|
||||
|
||||
const GOOGLE_FONTS_BASE = "https://fonts.googleapis.com/css2";
|
||||
const FONT_WEIGHTS: Record<string, string> = {
|
||||
@@ -446,9 +447,9 @@ function generateElementHtml(element: TimelineElement, keyframes?: Keyframe[]):
|
||||
const baseAttrs = [
|
||||
`id="${element.id}"`,
|
||||
`data-hf-id="${element.id}"`,
|
||||
`data-start="${element.startTime}"`,
|
||||
`data-end="${element.startTime + element.duration}"`,
|
||||
`data-layer="${element.zIndex}"`,
|
||||
`${COMPOSITION_ATTRIBUTES.start}="${element.startTime}"`,
|
||||
`${COMPOSITION_ATTRIBUTES.duration}="${element.duration}"`,
|
||||
`${COMPOSITION_ATTRIBUTES.trackIndex}="${element.zIndex}"`,
|
||||
`data-name="${element.name}"`,
|
||||
];
|
||||
|
||||
|
||||
@@ -241,6 +241,20 @@ export {
|
||||
parseNumeric,
|
||||
type ReferenceExpression,
|
||||
} from "./runtime/startExpression.js";
|
||||
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";
|
||||
|
||||
// Variable validation (CLI / tooling-side)
|
||||
export {
|
||||
|
||||
@@ -4,6 +4,7 @@ export const RUNTIME_PROTOCOL_CAPABILITIES = [
|
||||
"seconds-time",
|
||||
"rational-fps",
|
||||
"seek-keep-playing",
|
||||
"composition-manifest-v1",
|
||||
] as const;
|
||||
|
||||
export type RuntimeProtocolFps = {
|
||||
|
||||
@@ -12,36 +12,8 @@
|
||||
* - `"intro - 0.5"` -> 0.5s before `intro` ends (overlap)
|
||||
*/
|
||||
|
||||
export type ReferenceExpression =
|
||||
| { kind: "absolute"; value: number }
|
||||
| { kind: "reference"; refId: string; offset: number };
|
||||
|
||||
/** Parse a value to a finite number, or `null` if it isn't one. */
|
||||
export function parseNumeric(value: string | null | undefined): number | null {
|
||||
if (value == null || value === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a raw `data-start` value into an absolute time or a clip reference.
|
||||
* Returns `null` when the value is empty or not a recognized expression.
|
||||
*/
|
||||
export function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {
|
||||
const normalized = (raw ?? "").trim();
|
||||
if (!normalized) return null;
|
||||
const absolute = parseNumeric(normalized);
|
||||
if (absolute != null) {
|
||||
return { kind: "absolute", value: absolute };
|
||||
}
|
||||
const referenceMatch = normalized.match(/^([A-Za-z0-9_.:-]+)(?:\s*([+-])\s*([0-9]*\.?[0-9]+))?$/);
|
||||
if (!referenceMatch) return null;
|
||||
const refId = (referenceMatch[1] ?? "").trim();
|
||||
if (!refId) return null;
|
||||
const sign = referenceMatch[2] ?? "+";
|
||||
const offsetRaw = referenceMatch[3] ?? "0";
|
||||
const parsedOffset = Number.parseFloat(offsetRaw);
|
||||
const offsetMagnitude = Number.isFinite(parsedOffset) ? Math.max(0, parsedOffset) : 0;
|
||||
const offset = sign === "-" ? -offsetMagnitude : offsetMagnitude;
|
||||
return { kind: "reference", refId, offset };
|
||||
}
|
||||
export {
|
||||
parseNumeric,
|
||||
parseStartExpression,
|
||||
type ReferenceExpression,
|
||||
} from "@hyperframes/parsers/composition-contract";
|
||||
|
||||
@@ -18,6 +18,12 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
expect(result.durationInFrames).toBeGreaterThanOrEqual(1);
|
||||
expect(result.compositionWidth).toBe(1920);
|
||||
expect(result.compositionHeight).toBe(1080);
|
||||
expect(result).toMatchObject({
|
||||
protocolVersion: 1,
|
||||
compositionContractVersion: 1,
|
||||
durationSeconds: 1,
|
||||
fps: { numerator: 30, denominator: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
// Regression: id-less timed elements (root index.html children carry
|
||||
|
||||
@@ -10,6 +10,8 @@ import { readElementPlaybackRate } from "./media";
|
||||
import { resolveCssStackingContextId } from "./stackingContext";
|
||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||
import { isSceneLikeCompositionId } from "../slideshow/index.js";
|
||||
import { COMPOSITION_CONTRACT_VERSION } from "../compositionContract.js";
|
||||
import { runtimeProtocolMetadata } from "./protocol.js";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
@@ -630,8 +632,11 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
? Number.POSITIVE_INFINITY
|
||||
: Math.max(1, Math.ceil(safeDuration * Math.max(1, params.canonicalFps)));
|
||||
return {
|
||||
...runtimeProtocolMetadata(params.canonicalFps),
|
||||
source: "hf-preview",
|
||||
type: "timeline",
|
||||
compositionContractVersion: COMPOSITION_CONTRACT_VERSION,
|
||||
durationSeconds: shouldEmitNonDeterministicInf ? Number.POSITIVE_INFINITY : safeDuration,
|
||||
durationInFrames,
|
||||
clips,
|
||||
scenes,
|
||||
|
||||
@@ -10,6 +10,7 @@ export type RuntimeJson =
|
||||
|
||||
import type { HyperframeControlAction } from "../inline-scripts/runtimeContract.js";
|
||||
import type { HyperframePickerElementInfo } from "../inline-scripts/pickerApi.js";
|
||||
import type { RuntimeProtocolV1 } from "./protocol.js";
|
||||
|
||||
export type RuntimeBridgeControlAction =
|
||||
| HyperframeControlAction
|
||||
@@ -79,9 +80,11 @@ export type RuntimeTimelineScene = {
|
||||
avatarName: string | null;
|
||||
};
|
||||
|
||||
export type RuntimeTimelineMessage = {
|
||||
export type RuntimeTimelineMessage = RuntimeProtocolV1 & {
|
||||
source: "hf-preview";
|
||||
type: "timeline";
|
||||
compositionContractVersion: 1;
|
||||
durationSeconds: number;
|
||||
durationInFrames: number;
|
||||
clips: RuntimeTimelineClip[];
|
||||
scenes: RuntimeTimelineScene[];
|
||||
|
||||
Reference in New Issue
Block a user