mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(core,studio,cli): add square + square-4k canvas resolutions
The four existing presets only cover 16:9 (landscape) and 9:16 (portrait) aspect ratios. A 1080×1080 square comp had nowhere to land at any scale: "Auto" rendered at the comp's authored 1080×1080, and picking 1080p or 4K mapped to a landscape/portrait preset whose aspect ratio mismatched, which the producer's resolveDeviceScaleFactor validator rejects with "does not match the aspect ratio of the composition". Add `square` (1080×1080) and `square-4k` (2160×2160) to CANVAS_DIMENSIONS in core. The existing `keyof typeof CANVAS_DIMENSIONS` derivation extends the `CanvasResolution` union and `VALID_CANVAS_RESOLUTIONS` array automatically, so the producer's validator, the render API route, and the CLI `--resolution` flag pick the new presets up without further changes. - core: extend CANVAS_DIMENSIONS, RESOLUTION_ALIASES, and the htmlParser to recognize `data-resolution="square|square-4k"` and to infer square from equal width/height (vs. the prior "square defaults to portrait" tie-breaker). - studio: extend the local ResolutionPreset / CANVAS_DIMENSIONS mirrors; collapse isPortraitComp into a 3-way `compAspect` helper so resolveResolution returns the square preset for square comps. - cli: update --resolution help text on `init` and `render` to mention the new presets. - tests: add square cases to renderOrchestrator's resolveDeviceScaleFactor suite (returns 1 for square→square, 2 for square→square-4k, rejects landscape preset on square comp), update the htmlParser test that previously pinned the "square→portrait" tiebreaker.
This commit is contained in:
@@ -149,6 +149,8 @@ export const CANVAS_DIMENSIONS = {
|
||||
portrait: { width: 1080, height: 1920 },
|
||||
"landscape-4k": { width: 3840, height: 2160 },
|
||||
"portrait-4k": { width: 2160, height: 3840 },
|
||||
square: { width: 1080, height: 1080 },
|
||||
"square-4k": { width: 2160, height: 2160 },
|
||||
} as const;
|
||||
|
||||
// Single source of truth: derive the type from the table so adding a preset
|
||||
@@ -172,6 +174,9 @@ const RESOLUTION_ALIASES: Record<string, CanvasResolution> = {
|
||||
"4k": "landscape-4k",
|
||||
uhd: "landscape-4k",
|
||||
"4k-portrait": "portrait-4k",
|
||||
"1080p-square": "square",
|
||||
"square-1080p": "square",
|
||||
"4k-square": "square-4k",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,8 @@ describe("@hyperframes/core public API exports", () => {
|
||||
expect(core.CANVAS_DIMENSIONS.portrait).toEqual({ width: 1080, height: 1920 });
|
||||
expect(core.CANVAS_DIMENSIONS["landscape-4k"]).toEqual({ width: 3840, height: 2160 });
|
||||
expect(core.CANVAS_DIMENSIONS["portrait-4k"]).toEqual({ width: 2160, height: 3840 });
|
||||
expect(core.CANVAS_DIMENSIONS.square).toEqual({ width: 1080, height: 1080 });
|
||||
expect(core.CANVAS_DIMENSIONS["square-4k"]).toEqual({ width: 2160, height: 2160 });
|
||||
});
|
||||
|
||||
it("exports VALID_CANVAS_RESOLUTIONS derived from CANVAS_DIMENSIONS", () => {
|
||||
@@ -18,6 +20,8 @@ describe("@hyperframes/core public API exports", () => {
|
||||
"portrait",
|
||||
"landscape-4k",
|
||||
"portrait-4k",
|
||||
"square",
|
||||
"square-4k",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -27,6 +31,10 @@ describe("@hyperframes/core public API exports", () => {
|
||||
expect(core.normalizeResolutionFlag("1080p")).toBe("landscape");
|
||||
expect(core.normalizeResolutionFlag("landscape-4k")).toBe("landscape-4k");
|
||||
expect(core.normalizeResolutionFlag("UHD")).toBe("landscape-4k");
|
||||
expect(core.normalizeResolutionFlag("square")).toBe("square");
|
||||
expect(core.normalizeResolutionFlag("square-4k")).toBe("square-4k");
|
||||
expect(core.normalizeResolutionFlag("1080p-square")).toBe("square");
|
||||
expect(core.normalizeResolutionFlag("4k-square")).toBe("square-4k");
|
||||
expect(core.normalizeResolutionFlag("8k")).toBeUndefined();
|
||||
expect(core.normalizeResolutionFlag(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -275,10 +275,7 @@ describe("parseHtml", () => {
|
||||
expect(result.resolution).toBe("landscape");
|
||||
});
|
||||
|
||||
it("classifies square compositions as portrait by convention", () => {
|
||||
// 1080×1080 has no obvious orientation. The parser collapses the tie to
|
||||
// portrait — same bias the prior `w > h ? landscape : portrait` ternary
|
||||
// had. Pinning so a future refactor doesn't silently flip it.
|
||||
it("infers square resolution from equal width/height", () => {
|
||||
const html = `
|
||||
<html data-composition-width="1080" data-composition-height="1080">
|
||||
<body>
|
||||
@@ -290,7 +287,22 @@ describe("parseHtml", () => {
|
||||
`;
|
||||
const result = parseHtml(html);
|
||||
|
||||
expect(result.resolution).toBe("portrait");
|
||||
expect(result.resolution).toBe("square");
|
||||
});
|
||||
|
||||
it("infers square-4k from equal width/height ≥ 2160", () => {
|
||||
const html = `
|
||||
<html data-composition-width="2160" data-composition-height="2160">
|
||||
<body>
|
||||
<div id="stage">
|
||||
<div id="text1" data-start="0" data-end="5"><div>Hello</div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const result = parseHtml(html);
|
||||
|
||||
expect(result.resolution).toBe("square-4k");
|
||||
});
|
||||
|
||||
it("extracts x, y, scale, opacity from data attributes", () => {
|
||||
|
||||
@@ -124,7 +124,9 @@ function parseResolutionFromHtml(doc: Document): CanvasResolution | null {
|
||||
resolutionAttr === "landscape" ||
|
||||
resolutionAttr === "portrait" ||
|
||||
resolutionAttr === "landscape-4k" ||
|
||||
resolutionAttr === "portrait-4k"
|
||||
resolutionAttr === "portrait-4k" ||
|
||||
resolutionAttr === "square" ||
|
||||
resolutionAttr === "square-4k"
|
||||
) {
|
||||
return resolutionAttr;
|
||||
}
|
||||
@@ -143,17 +145,17 @@ function parseResolutionFromHtml(doc: Document): CanvasResolution | null {
|
||||
}
|
||||
|
||||
function resolveResolutionFromDimensions(width: number, height: number): CanvasResolution {
|
||||
// `width === height` (square) falls into the portrait branch by convention —
|
||||
// the same bias the previous `w > h ? landscape : portrait` ternary used.
|
||||
// Square compositions are rare; pick portrait-as-default so we don't surprise
|
||||
// the existing call sites that depend on this behavior.
|
||||
const isLandscape = width > height;
|
||||
const longSide = Math.max(width, height);
|
||||
// UHD cutoff is the long side of `landscape-4k` / `portrait-4k` (3840). A
|
||||
// looser threshold (e.g. ≥ 2560) would silently misclassify QHD/1440p
|
||||
// (2560×1440) as 4K, which is the wrong default for a common authoring
|
||||
// resolution closer to 1080p than to UHD. Authors who genuinely want the
|
||||
// 4K preset can still set `data-resolution="landscape-4k"` explicitly.
|
||||
// UHD cutoff is the long side of the 4K presets (3840 for `landscape-4k` /
|
||||
// `portrait-4k`, 2160 for `square-4k`). A looser threshold (e.g. ≥ 2560)
|
||||
// would silently misclassify QHD/1440p (2560×1440) as 4K, which is the
|
||||
// wrong default for a common authoring resolution closer to 1080p than to
|
||||
// UHD. Authors who genuinely want the 4K preset can still set
|
||||
// `data-resolution="..."` explicitly.
|
||||
if (width === height) {
|
||||
return longSide >= 2160 ? "square-4k" : "square";
|
||||
}
|
||||
const isLandscape = width > height;
|
||||
const isUhd = longSide >= 3840;
|
||||
if (isLandscape) return isUhd ? "landscape-4k" : "landscape";
|
||||
return isUhd ? "portrait-4k" : "portrait";
|
||||
|
||||
Reference in New Issue
Block a user