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:
@@ -630,7 +630,7 @@ export default defineCommand({
|
|||||||
resolution: {
|
resolution: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description:
|
description:
|
||||||
"Canvas resolution preset: landscape (1920x1080), portrait (1080x1920), landscape-4k (3840x2160), portrait-4k (2160x3840). Aliases: 1080p, 4k, uhd. Default: keep template dimensions (typically 1920x1080).",
|
"Canvas resolution preset: landscape (1920x1080), portrait (1080x1920), landscape-4k (3840x2160), portrait-4k (2160x3840), square (1080x1080), square-4k (2160x2160). Aliases: 1080p, 4k, uhd. Default: keep template dimensions (typically 1920x1080).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async run({ args }) {
|
async run({ args }) {
|
||||||
@@ -670,7 +670,7 @@ export default defineCommand({
|
|||||||
console.error(
|
console.error(
|
||||||
c.error(
|
c.error(
|
||||||
`Invalid --resolution: "${args.resolution}". ` +
|
`Invalid --resolution: "${args.resolution}". ` +
|
||||||
`Use one of: landscape, portrait, landscape-4k, portrait-4k (or aliases 1080p, 4k, uhd).`,
|
`Use one of: landscape, portrait, landscape-4k, portrait-4k, square, square-4k (or aliases 1080p, 4k, uhd).`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export default defineCommand({
|
|||||||
resolution: {
|
resolution: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description:
|
description:
|
||||||
"Output resolution preset: landscape (1920x1080), portrait (1080x1920), landscape-4k (3840x2160), portrait-4k (2160x3840). Aliases: 1080p, 4k, uhd. The composition is unchanged — Chrome renders at higher DPR (deviceScaleFactor) so the captured screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not yet supported with --hdr.",
|
"Output resolution preset: landscape (1920x1080), portrait (1080x1920), landscape-4k (3840x2160), portrait-4k (2160x3840), square (1080x1080), square-4k (2160x2160). Aliases: 1080p, 4k, uhd. The composition is unchanged — Chrome renders at higher DPR (deviceScaleFactor) so the captured screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not yet supported with --hdr.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async run({ args }) {
|
async run({ args }) {
|
||||||
@@ -263,7 +263,7 @@ export default defineCommand({
|
|||||||
if (!outputResolution) {
|
if (!outputResolution) {
|
||||||
errorBox(
|
errorBox(
|
||||||
"Invalid resolution",
|
"Invalid resolution",
|
||||||
`Got "${args.resolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k (or aliases 1080p, 4k, uhd).`,
|
`Got "${args.resolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k, square, square-4k (or aliases 1080p, 4k, uhd).`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ export const CANVAS_DIMENSIONS = {
|
|||||||
portrait: { width: 1080, height: 1920 },
|
portrait: { width: 1080, height: 1920 },
|
||||||
"landscape-4k": { width: 3840, height: 2160 },
|
"landscape-4k": { width: 3840, height: 2160 },
|
||||||
"portrait-4k": { width: 2160, height: 3840 },
|
"portrait-4k": { width: 2160, height: 3840 },
|
||||||
|
square: { width: 1080, height: 1080 },
|
||||||
|
"square-4k": { width: 2160, height: 2160 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Single source of truth: derive the type from the table so adding a preset
|
// 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",
|
"4k": "landscape-4k",
|
||||||
uhd: "landscape-4k",
|
uhd: "landscape-4k",
|
||||||
"4k-portrait": "portrait-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.portrait).toEqual({ width: 1080, height: 1920 });
|
||||||
expect(core.CANVAS_DIMENSIONS["landscape-4k"]).toEqual({ width: 3840, height: 2160 });
|
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["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", () => {
|
it("exports VALID_CANVAS_RESOLUTIONS derived from CANVAS_DIMENSIONS", () => {
|
||||||
@@ -18,6 +20,8 @@ describe("@hyperframes/core public API exports", () => {
|
|||||||
"portrait",
|
"portrait",
|
||||||
"landscape-4k",
|
"landscape-4k",
|
||||||
"portrait-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("1080p")).toBe("landscape");
|
||||||
expect(core.normalizeResolutionFlag("landscape-4k")).toBe("landscape-4k");
|
expect(core.normalizeResolutionFlag("landscape-4k")).toBe("landscape-4k");
|
||||||
expect(core.normalizeResolutionFlag("UHD")).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("8k")).toBeUndefined();
|
||||||
expect(core.normalizeResolutionFlag(undefined)).toBeUndefined();
|
expect(core.normalizeResolutionFlag(undefined)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -275,10 +275,7 @@ describe("parseHtml", () => {
|
|||||||
expect(result.resolution).toBe("landscape");
|
expect(result.resolution).toBe("landscape");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies square compositions as portrait by convention", () => {
|
it("infers square resolution from equal width/height", () => {
|
||||||
// 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.
|
|
||||||
const html = `
|
const html = `
|
||||||
<html data-composition-width="1080" data-composition-height="1080">
|
<html data-composition-width="1080" data-composition-height="1080">
|
||||||
<body>
|
<body>
|
||||||
@@ -290,7 +287,22 @@ describe("parseHtml", () => {
|
|||||||
`;
|
`;
|
||||||
const result = parseHtml(html);
|
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", () => {
|
it("extracts x, y, scale, opacity from data attributes", () => {
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ function parseResolutionFromHtml(doc: Document): CanvasResolution | null {
|
|||||||
resolutionAttr === "landscape" ||
|
resolutionAttr === "landscape" ||
|
||||||
resolutionAttr === "portrait" ||
|
resolutionAttr === "portrait" ||
|
||||||
resolutionAttr === "landscape-4k" ||
|
resolutionAttr === "landscape-4k" ||
|
||||||
resolutionAttr === "portrait-4k"
|
resolutionAttr === "portrait-4k" ||
|
||||||
|
resolutionAttr === "square" ||
|
||||||
|
resolutionAttr === "square-4k"
|
||||||
) {
|
) {
|
||||||
return resolutionAttr;
|
return resolutionAttr;
|
||||||
}
|
}
|
||||||
@@ -143,17 +145,17 @@ function parseResolutionFromHtml(doc: Document): CanvasResolution | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveResolutionFromDimensions(width: number, height: number): CanvasResolution {
|
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);
|
const longSide = Math.max(width, height);
|
||||||
// UHD cutoff is the long side of `landscape-4k` / `portrait-4k` (3840). A
|
// UHD cutoff is the long side of the 4K presets (3840 for `landscape-4k` /
|
||||||
// looser threshold (e.g. ≥ 2560) would silently misclassify QHD/1440p
|
// `portrait-4k`, 2160 for `square-4k`). A looser threshold (e.g. ≥ 2560)
|
||||||
// (2560×1440) as 4K, which is the wrong default for a common authoring
|
// would silently misclassify QHD/1440p (2560×1440) as 4K, which is the
|
||||||
// resolution closer to 1080p than to UHD. Authors who genuinely want the
|
// wrong default for a common authoring resolution closer to 1080p than to
|
||||||
// 4K preset can still set `data-resolution="landscape-4k"` explicitly.
|
// 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;
|
const isUhd = longSide >= 3840;
|
||||||
if (isLandscape) return isUhd ? "landscape-4k" : "landscape";
|
if (isLandscape) return isUhd ? "landscape-4k" : "landscape";
|
||||||
return isUhd ? "portrait-4k" : "portrait";
|
return isUhd ? "portrait-4k" : "portrait";
|
||||||
|
|||||||
@@ -839,4 +839,37 @@ describe("resolveDeviceScaleFactor", () => {
|
|||||||
}),
|
}),
|
||||||
).toThrow(/aspect ratio|non-integer/);
|
).toThrow(/aspect ratio|non-integer/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns 1 for a square comp matching the square preset", () => {
|
||||||
|
expect(
|
||||||
|
resolveDeviceScaleFactor({
|
||||||
|
...defaults,
|
||||||
|
compositionWidth: 1080,
|
||||||
|
compositionHeight: 1080,
|
||||||
|
outputResolution: "square",
|
||||||
|
}),
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 2 for square 1080 → square-4k", () => {
|
||||||
|
expect(
|
||||||
|
resolveDeviceScaleFactor({
|
||||||
|
...defaults,
|
||||||
|
compositionWidth: 1080,
|
||||||
|
compositionHeight: 1080,
|
||||||
|
outputResolution: "square-4k",
|
||||||
|
}),
|
||||||
|
).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects landscape preset on a square composition", () => {
|
||||||
|
expect(() =>
|
||||||
|
resolveDeviceScaleFactor({
|
||||||
|
...defaults,
|
||||||
|
compositionWidth: 1080,
|
||||||
|
compositionHeight: 1080,
|
||||||
|
outputResolution: "landscape",
|
||||||
|
}),
|
||||||
|
).toThrow(/aspect ratio/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ interface RenderQueueProps {
|
|||||||
) => void;
|
) => void;
|
||||||
isRendering: boolean;
|
isRendering: boolean;
|
||||||
/**
|
/**
|
||||||
* Authored dimensions of the active composition, used to derive
|
* Authored dimensions of the active composition. Used to pick the
|
||||||
* landscape vs portrait when the user picks a 1080p or 4K scale.
|
* matching preset (landscape / portrait / square) when the user selects
|
||||||
* `null` falls back to landscape (legacy default).
|
* a 1080p or 4K scale. `null` falls back to landscape (legacy default).
|
||||||
*/
|
*/
|
||||||
compositionDimensions?: CompositionDimensions | null;
|
compositionDimensions?: CompositionDimensions | null;
|
||||||
}
|
}
|
||||||
@@ -39,11 +39,26 @@ const SCALE_LABEL: Record<RenderScale, string> = {
|
|||||||
"4k": "4K",
|
"4k": "4K",
|
||||||
};
|
};
|
||||||
|
|
||||||
function isPortraitComp(dims: CompositionDimensions | null | undefined): boolean {
|
// Mirrors `CANVAS_DIMENSIONS` in @hyperframes/core. Studio can't import from
|
||||||
// Squares and missing dims fall through to landscape — matches the legacy
|
// the core barrel (it transitively pulls in node:fs) and the values are stable.
|
||||||
// default ("landscape" was the first preset). The auto option exists for
|
const CANVAS_DIMENSIONS: Record<ResolutionPreset, CompositionDimensions> = {
|
||||||
// users who want exact authored dimensions.
|
landscape: { width: 1920, height: 1080 },
|
||||||
return dims != null && dims.height > dims.width;
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
type CompAspect = "landscape" | "portrait" | "square";
|
||||||
|
|
||||||
|
function compAspect(dims: CompositionDimensions | null | undefined): CompAspect {
|
||||||
|
// Missing dims fall through to landscape (legacy default — "landscape" was
|
||||||
|
// the first preset). Studio shows resolved dims inline, so the user can see
|
||||||
|
// when this fallback is in effect.
|
||||||
|
if (dims == null) return "landscape";
|
||||||
|
if (dims.width === dims.height) return "square";
|
||||||
|
return dims.height > dims.width ? "portrait" : "landscape";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveResolution(
|
function resolveResolution(
|
||||||
@@ -51,9 +66,13 @@ function resolveResolution(
|
|||||||
dims: CompositionDimensions | null | undefined,
|
dims: CompositionDimensions | null | undefined,
|
||||||
): ResolutionPreset | "auto" {
|
): ResolutionPreset | "auto" {
|
||||||
if (scale === "auto") return "auto";
|
if (scale === "auto") return "auto";
|
||||||
const portrait = isPortraitComp(dims);
|
const aspect = compAspect(dims);
|
||||||
if (scale === "1080p") return portrait ? "portrait" : "landscape";
|
if (scale === "1080p") return aspect;
|
||||||
return portrait ? "portrait-4k" : "landscape-4k";
|
return aspect === "landscape"
|
||||||
|
? "landscape-4k"
|
||||||
|
: aspect === "portrait"
|
||||||
|
? "portrait-4k"
|
||||||
|
: "square-4k";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvedDimensions(
|
function resolvedDimensions(
|
||||||
@@ -61,11 +80,24 @@ function resolvedDimensions(
|
|||||||
dims: CompositionDimensions | null | undefined,
|
dims: CompositionDimensions | null | undefined,
|
||||||
): CompositionDimensions | null {
|
): CompositionDimensions | null {
|
||||||
if (scale === "auto") return dims ?? null;
|
if (scale === "auto") return dims ?? null;
|
||||||
const portrait = isPortraitComp(dims);
|
const preset = resolveResolution(scale, dims);
|
||||||
if (scale === "1080p") {
|
return preset === "auto" ? null : CANVAS_DIMENSIONS[preset];
|
||||||
return portrait ? { width: 1080, height: 1920 } : { width: 1920, height: 1080 };
|
}
|
||||||
}
|
|
||||||
return portrait ? { width: 2160, height: 3840 } : { width: 3840, height: 2160 };
|
// Mirrors the producer's resolveDeviceScaleFactor validation
|
||||||
|
// (renderOrchestrator.ts:608): the chosen preset must match the comp's aspect
|
||||||
|
// ratio exactly (cross-multiplied), can't downsample, and must be an integer
|
||||||
|
// scale factor. Without this guard the user can pick a preset that throws at
|
||||||
|
// render time — e.g. 1080p on a 1080×1080 square or 1080p on a 1280×720 comp
|
||||||
|
// (1.5× isn't integer).
|
||||||
|
function scaleApplies(scale: RenderScale, dims: CompositionDimensions | null | undefined): boolean {
|
||||||
|
if (scale === "auto" || dims == null) return true;
|
||||||
|
const preset = resolveResolution(scale, dims);
|
||||||
|
if (preset === "auto") return true;
|
||||||
|
const target = CANVAS_DIMENSIONS[preset];
|
||||||
|
if (target.width * dims.height !== target.height * dims.width) return false;
|
||||||
|
if (target.width < dims.width) return false;
|
||||||
|
return Number.isInteger(target.width / dims.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scaleOptionLabel(
|
function scaleOptionLabel(
|
||||||
@@ -171,6 +203,11 @@ function FormatExportButton({
|
|||||||
const [quality, setQuality] = useState<"draft" | "standard" | "high">("standard");
|
const [quality, setQuality] = useState<"draft" | "standard" | "high">("standard");
|
||||||
const [scale, setScale] = useState<RenderScale>("auto");
|
const [scale, setScale] = useState<RenderScale>("auto");
|
||||||
|
|
||||||
|
// If the user previously picked 1080p / 4K for a 16:9 comp and then switches
|
||||||
|
// to a square (or any non-matching) comp, fall back to "auto" without
|
||||||
|
// discarding their preference — switching back to 16:9 re-applies it.
|
||||||
|
const effectiveScale: RenderScale = scaleApplies(scale, compositionDimensions) ? scale : "auto";
|
||||||
|
|
||||||
// MOV (ProRes) is a fixed-quality codec — quality selector has no effect.
|
// MOV (ProRes) is a fixed-quality codec — quality selector has no effect.
|
||||||
const showQuality = format !== "mov";
|
const showQuality = format !== "mov";
|
||||||
|
|
||||||
@@ -182,13 +219,13 @@ function FormatExportButton({
|
|||||||
(feature-flag, etc.), move `rounded-l` to whichever element ends up
|
(feature-flag, etc.), move `rounded-l` to whichever element ends up
|
||||||
leftmost. */}
|
leftmost. */}
|
||||||
<select
|
<select
|
||||||
value={scale}
|
value={effectiveScale}
|
||||||
onChange={(e) => setScale(e.target.value as RenderScale)}
|
onChange={(e) => setScale(e.target.value as RenderScale)}
|
||||||
disabled={isRendering}
|
disabled={isRendering}
|
||||||
className="h-5 px-1 text-[10px] rounded-l bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
className="h-5 px-1 text-[10px] rounded-l bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{SCALE_OPTION_ORDER.map((value) => (
|
{SCALE_OPTION_ORDER.map((value) => (
|
||||||
<option key={value} value={value}>
|
<option key={value} value={value} disabled={!scaleApplies(value, compositionDimensions)}>
|
||||||
{scaleOptionLabel(value, compositionDimensions)}
|
{scaleOptionLabel(value, compositionDimensions)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
@@ -220,7 +257,7 @@ function FormatExportButton({
|
|||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onStartRender(format, quality, resolveResolution(scale, compositionDimensions))
|
onStartRender(format, quality, resolveResolution(effectiveScale, compositionDimensions))
|
||||||
}
|
}
|
||||||
disabled={isRendering}
|
disabled={isRendering}
|
||||||
className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-semibold rounded-r bg-studio-accent text-[#09090B] hover:brightness-110 transition-colors disabled:opacity-50"
|
className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-semibold rounded-r bg-studio-accent text-[#09090B] hover:brightness-110 transition-colors disabled:opacity-50"
|
||||||
|
|||||||
@@ -14,8 +14,14 @@ export interface RenderJob {
|
|||||||
// Mirrors `CanvasResolution` from @hyperframes/core. Kept local because
|
// Mirrors `CanvasResolution` from @hyperframes/core. Kept local because
|
||||||
// studio's tsconfig doesn't include node types, and the core barrel
|
// studio's tsconfig doesn't include node types, and the core barrel
|
||||||
// transitively pulls in modules with `node:fs` imports. Drift risk is
|
// transitively pulls in modules with `node:fs` imports. Drift risk is
|
||||||
// low (4 string literals tied to a stable enum).
|
// low (string literals tied to a stable enum).
|
||||||
export type ResolutionPreset = "landscape" | "portrait" | "landscape-4k" | "portrait-4k";
|
export type ResolutionPreset =
|
||||||
|
| "landscape"
|
||||||
|
| "portrait"
|
||||||
|
| "landscape-4k"
|
||||||
|
| "portrait-4k"
|
||||||
|
| "square"
|
||||||
|
| "square-4k";
|
||||||
|
|
||||||
export interface StartRenderOptions {
|
export interface StartRenderOptions {
|
||||||
fps?: number;
|
fps?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user