fix(studio): clean next alpha inspector artifacts
@@ -62,7 +62,8 @@ tmp/
|
||||
packages/core/src/generated/
|
||||
packages/producer/src/services/fontData.generated.ts
|
||||
|
||||
# Test artifacts
|
||||
# Local proof / test artifacts
|
||||
qa-artifacts/
|
||||
my-video/
|
||||
examples/
|
||||
packages/studio/data/
|
||||
|
||||
@@ -162,7 +162,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--example, -e` | Example to scaffold (required in default mode, interactive in `--human-friendly`) |
|
||||
| `--resolution` | Canvas preset: `landscape` (1920×1080), `portrait` (1080×1920), `landscape-4k` (3840×2160), `portrait-4k` (2160×3840). Aliases: `1080p`, `4k`, `uhd`. Default: keep template dimensions. |
|
||||
| `--resolution` | Canvas preset: `landscape` (1920×1080), `portrait` (1080×1920), `landscape-4k` (3840×2160), `portrait-4k` (2160×3840), `square` (1080×1080), `square-4k` (2160×2160). Aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`. Default: keep template dimensions. |
|
||||
| `--video, -V` | Path to a video file (MP4, WebM, MOV) |
|
||||
| `--audio, -a` | Path to an audio file (MP3, WAV, M4A) |
|
||||
| `--tailwind` | Add Tailwind CSS browser-runtime support to scaffolded HTML |
|
||||
@@ -610,7 +610,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
|
||||
| `--quality` | draft, standard, high | standard | Encoding quality preset (drives CRF/bitrate) |
|
||||
| `--crf` | 0-51 | — | Override encoder CRF (lower = higher quality). Mutually exclusive with `--video-bitrate` |
|
||||
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target video bitrate. Mutually exclusive with `--crf` |
|
||||
| `--resolution` | landscape, portrait, landscape-4k, portrait-4k (aliases: `1080p`, `4k`, `uhd`) | — | Output resolution preset. Supersamples a smaller composition via Chrome `deviceScaleFactor` so the screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not supported with `--hdr`. See [4K Rendering](/guides/4k-rendering) |
|
||||
| `--resolution` | landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`) | — | Output resolution preset. Supersamples a smaller composition via Chrome `deviceScaleFactor` so the screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not supported with `--hdr`. See [4K Rendering](/guides/4k-rendering) |
|
||||
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
|
||||
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
|
||||
| `--workers` | 1-8 | 4 | Parallel render workers |
|
||||
|
||||
@@ -51,7 +51,7 @@ import type {
|
||||
TimelineElementType, // "video" | "image" | "text" | "audio" | "composition"
|
||||
CompositionSpec,
|
||||
CompositionVariable,
|
||||
CanvasResolution, // "landscape" | "portrait"
|
||||
CanvasResolution, // "landscape" | "portrait" | "landscape-4k" | "portrait-4k" | "square" | "square-4k"
|
||||
Orientation, // "16:9" | "9:16"
|
||||
FrameAdapter,
|
||||
FrameAdapterContext,
|
||||
|
||||
@@ -207,6 +207,38 @@ describe("applyResolutionPreset", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("swaps to square dimensions for square", () => {
|
||||
withFixture((dir) => {
|
||||
const file = join(dir, "index.html");
|
||||
writeFileSync(file, sampleHtml, "utf-8");
|
||||
|
||||
applyResolutionPreset(dir, "square");
|
||||
const out = readFileSync(file, "utf-8");
|
||||
|
||||
expect(out).toContain('data-width="1080"');
|
||||
expect(out).toContain('data-height="1080"');
|
||||
expect(out).toContain('data-resolution="square"');
|
||||
expect(out).toContain("width: 1080px");
|
||||
expect(out).toContain("height: 1080px");
|
||||
});
|
||||
});
|
||||
|
||||
it("swaps to square-4k dimensions", () => {
|
||||
withFixture((dir) => {
|
||||
const file = join(dir, "index.html");
|
||||
writeFileSync(file, sampleHtml, "utf-8");
|
||||
|
||||
applyResolutionPreset(dir, "square-4k");
|
||||
const out = readFileSync(file, "utf-8");
|
||||
|
||||
expect(out).toContain('data-width="2160"');
|
||||
expect(out).toContain('data-height="2160"');
|
||||
expect(out).toContain('data-resolution="square-4k"');
|
||||
expect(out).toContain("width: 2160px");
|
||||
expect(out).toContain("height: 2160px");
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds a 4k project end-to-end via --resolution 4k", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-init-test-"));
|
||||
const target = join(dir, "proj");
|
||||
|
||||
@@ -626,7 +626,7 @@ export default defineCommand({
|
||||
resolution: {
|
||||
type: "string",
|
||||
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, 1080p-square, square-1080p, 4k-square. Default: keep template dimensions (typically 1920x1080).",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
@@ -658,7 +658,8 @@ export default defineCommand({
|
||||
console.error(
|
||||
c.error(
|
||||
`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, 1080p-square, square-1080p, 4k-square).`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
|
||||
@@ -186,7 +186,7 @@ export default defineCommand({
|
||||
resolution: {
|
||||
type: "string",
|
||||
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, 1080p-square, square-1080p, 4k-square. 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 }) {
|
||||
@@ -224,7 +224,8 @@ export default defineCommand({
|
||||
if (!outputResolution) {
|
||||
errorBox(
|
||||
"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, 1080p-square, square-1080p, 4k-square).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,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
|
||||
@@ -47,6 +49,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();
|
||||
});
|
||||
|
||||
@@ -197,6 +197,21 @@ describe("parseHtml", () => {
|
||||
expect(result.resolution).toBe("portrait");
|
||||
});
|
||||
|
||||
it("keeps explicit portrait resolution even when dimensions are square", () => {
|
||||
const html = `
|
||||
<html data-resolution="portrait" data-composition-width="1080" data-composition-height="1080">
|
||||
<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("portrait");
|
||||
});
|
||||
|
||||
it("defaults to portrait when no resolution info is available", () => {
|
||||
const html = `
|
||||
<html>
|
||||
@@ -275,10 +290,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 +302,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 (2560x1440) 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";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Hono } from "hono";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { VALID_CANVAS_RESOLUTIONS } from "../../core.types";
|
||||
import { registerRenderRoutes } from "./render";
|
||||
import type { StudioApiAdapter } from "../types";
|
||||
|
||||
@@ -98,8 +99,8 @@ describe("POST /projects/:id/render — outputResolution forwarding", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts each of the four canonical preset values", async () => {
|
||||
for (const preset of ["landscape", "portrait", "landscape-4k", "portrait-4k"] as const) {
|
||||
it("accepts each canonical preset value", async () => {
|
||||
for (const preset of VALID_CANVAS_RESOLUTIONS) {
|
||||
const spy = vi.fn();
|
||||
const { app, cleanup } = buildApp(spy);
|
||||
try {
|
||||
|
||||
@@ -778,6 +778,17 @@ describe("resolveDeviceScaleFactor", () => {
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("returns 2 for square 1080p → square-4k", () => {
|
||||
expect(
|
||||
resolveDeviceScaleFactor({
|
||||
...defaults,
|
||||
compositionWidth: 1080,
|
||||
compositionHeight: 1080,
|
||||
outputResolution: "square-4k",
|
||||
}),
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("returns 1 when the composition already matches the requested resolution", () => {
|
||||
expect(
|
||||
resolveDeviceScaleFactor({
|
||||
@@ -815,6 +826,25 @@ describe("resolveDeviceScaleFactor", () => {
|
||||
).toThrow(/aspect ratio/);
|
||||
});
|
||||
|
||||
it("rejects square comp → non-square 4K presets", () => {
|
||||
for (const outputResolution of ["landscape-4k", "portrait-4k"] as const) {
|
||||
expect(() =>
|
||||
resolveDeviceScaleFactor({
|
||||
...defaults,
|
||||
compositionWidth: 1080,
|
||||
compositionHeight: 1080,
|
||||
outputResolution,
|
||||
}),
|
||||
).toThrow(/aspect ratio/);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects landscape comp → square-4k", () => {
|
||||
expect(() => resolveDeviceScaleFactor({ ...defaults, outputResolution: "square-4k" })).toThrow(
|
||||
/aspect ratio/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects downsampling (4K composition → 1080p output)", () => {
|
||||
expect(() =>
|
||||
resolveDeviceScaleFactor({
|
||||
|
||||
@@ -157,6 +157,12 @@ function getTimelineElementLabel(element: TimelineElement): string {
|
||||
return element.label || element.id || element.tag;
|
||||
}
|
||||
|
||||
function confirmElementDelete(label: string, kind: "timeline clip" | "element"): boolean {
|
||||
return window.confirm(
|
||||
`Delete ${kind} "${label}"?\n\nThis removes it from the project source. You can use Undo to restore it.`,
|
||||
);
|
||||
}
|
||||
|
||||
type RightPanelTab = "design" | "motion" | "renders";
|
||||
|
||||
const GENERIC_FONT_FAMILIES = new Set([
|
||||
@@ -1780,6 +1786,8 @@ export function StudioApp() {
|
||||
async (element: TimelineElement) => {
|
||||
const pid = projectIdRef.current;
|
||||
if (!pid) throw new Error("No active project");
|
||||
const label = getTimelineElementLabel(element);
|
||||
if (!confirmElementDelete(label, "timeline clip")) return;
|
||||
|
||||
const targetPath = element.sourceFile || activeCompPath || "index.html";
|
||||
try {
|
||||
@@ -1877,6 +1885,7 @@ export function StudioApp() {
|
||||
);
|
||||
usePlayerStore.getState().setSelectedElementId(null);
|
||||
setRefreshKey((k) => k + 1);
|
||||
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
|
||||
showToast(message);
|
||||
@@ -1889,6 +1898,8 @@ export function StudioApp() {
|
||||
async (selection: DomEditSelection) => {
|
||||
const pid = projectIdRef.current;
|
||||
if (!pid) return;
|
||||
const label = selection.label || selection.id || selection.selector || selection.tagName;
|
||||
if (!confirmElementDelete(label, "element")) return;
|
||||
|
||||
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
||||
try {
|
||||
@@ -1946,6 +1957,7 @@ export function StudioApp() {
|
||||
setDomEditGroupSelections([]);
|
||||
usePlayerStore.getState().setSelectedElementId(null);
|
||||
setRefreshKey((k) => k + 1);
|
||||
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to delete element";
|
||||
showToast(message);
|
||||
|
||||
@@ -4,10 +4,8 @@ import {
|
||||
buildStrokeWidthStyleUpdates,
|
||||
getClipPathInsetPx,
|
||||
getCssFilterFunctionPx,
|
||||
getPropertyPanelVisibleSections,
|
||||
inferBoxShadowPreset,
|
||||
inferClipPathPreset,
|
||||
isPropertyPanelMediaLikeSelection,
|
||||
normalizePanelPxValue,
|
||||
setCssFilterFunctionPx,
|
||||
} from "./PropertyPanel";
|
||||
@@ -66,51 +64,4 @@ describe("PropertyPanel style helpers", () => {
|
||||
expect(buildStrokeStyleUpdates("none", "4px")).toEqual([["border-style", "none"]]);
|
||||
expect(buildStrokeStyleUpdates("solid", "4px")).toEqual([["border-style", "solid"]]);
|
||||
});
|
||||
|
||||
it("orders the simplified default inspector sections around high-confidence edits", () => {
|
||||
expect(
|
||||
getPropertyPanelVisibleSections({
|
||||
hasSelection: true,
|
||||
canEditStyles: true,
|
||||
hasTextControls: true,
|
||||
hasColorControls: true,
|
||||
}),
|
||||
).toEqual(["Text", "Layout", "Colors", "Radius", "Shadow"]);
|
||||
|
||||
expect(
|
||||
getPropertyPanelVisibleSections({
|
||||
hasSelection: true,
|
||||
canEditStyles: true,
|
||||
hasTextControls: false,
|
||||
hasColorControls: false,
|
||||
}),
|
||||
).toEqual(["Layout", "Radius", "Shadow"]);
|
||||
});
|
||||
|
||||
it("treats media tags and background-image layers as image-like controls", () => {
|
||||
expect(
|
||||
isPropertyPanelMediaLikeSelection({
|
||||
tagName: "img",
|
||||
styles: {},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPropertyPanelMediaLikeSelection({
|
||||
tagName: "div",
|
||||
styles: {
|
||||
"background-image": "url(/assets/studio.png)",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isPropertyPanelMediaLikeSelection({
|
||||
tagName: "div",
|
||||
styles: {
|
||||
"background-image": "none",
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,14 @@ export interface RenderJob {
|
||||
// Mirrors `CanvasResolution` from @hyperframes/core. Kept local because
|
||||
// studio's tsconfig doesn't include node types, and the core barrel
|
||||
// transitively pulls in modules with `node:fs` imports. Drift risk is
|
||||
// low (4 string literals tied to a stable enum).
|
||||
export type ResolutionPreset = "landscape" | "portrait" | "landscape-4k" | "portrait-4k";
|
||||
// low (6 string literals kept in sync manually with CANVAS_DIMENSIONS).
|
||||
export type ResolutionPreset =
|
||||
| "landscape"
|
||||
| "portrait"
|
||||
| "landscape-4k"
|
||||
| "portrait-4k"
|
||||
| "square"
|
||||
| "square-4k";
|
||||
|
||||
export interface StartRenderOptions {
|
||||
fps?: number;
|
||||
|
||||
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 503 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 127 KiB |