Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/core/src/studio-api/routes/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
quality?: string;
format?: string;
resolution?: string;
composition?: string;
};
const VALID_FORMATS = new Set(["mp4", "webm", "mov"]);
const FORMAT_EXT: Record<string, string> = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
Expand All @@ -76,6 +77,10 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
const outputResolution = VALID_RESOLUTIONS.has(body.resolution ?? "")
? (body.resolution as CanvasResolution)
: undefined;
const composition =
typeof body.composition === "string" && body.composition.length > 0
? body.composition
: undefined;

const now = new Date();
const datePart = now.toISOString().slice(0, 10);
Expand All @@ -94,6 +99,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
quality,
jobId,
outputResolution,
composition,
});
(jobState as RenderJobState & { createdAt: number }).createdAt = Date.now();
renderJobs.set(jobId, jobState as RenderJobState & { createdAt: number });
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/studio-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface StudioApiAdapter {
* the producer for the integer-scale + aspect + HDR constraints.
*/
outputResolution?: CanvasResolution;
/** Entry file relative to projectDir (e.g. "compositions/intro.html"). Defaults to index.html. */
composition?: string;
}): RenderJobState;

/** Optional: generate a JPEG thumbnail via Puppeteer or similar. */
Expand Down
14 changes: 12 additions & 2 deletions packages/studio/src/components/StudioLeftSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RefObject } from "react";
import { useCallback, type RefObject } from "react";
import { SourceEditor } from "./editor/SourceEditor";
import { LeftSidebar, type LeftSidebarHandle } from "./sidebar/LeftSidebar";
import { MediaPreview } from "./MediaPreview";
Expand Down Expand Up @@ -28,7 +28,7 @@ export function StudioLeftSidebar({
handlePanelResizeMove,
handlePanelResizeEnd,
} = usePanelLayoutContext();
const { projectId } = useStudioContext();
const { projectId, renderQueue, waitForPendingDomEditSaves } = useStudioContext();
const {
compositions,
assets,
Expand All @@ -45,6 +45,14 @@ export function StudioLeftSidebar({
handleContentChange,
} = useFileManagerContext();

const handleRenderComposition = useCallback(
async (comp: string) => {
await waitForPendingDomEditSaves();
await renderQueue.startRender({ composition: comp });
},
[renderQueue, waitForPendingDomEditSaves],
);

if (leftCollapsed) {
return (
<div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
Expand Down Expand Up @@ -107,6 +115,8 @@ export function StudioLeftSidebar({
)
) : undefined
}
onRenderComposition={handleRenderComposition}
isRendering={renderQueue.isRendering}
onLint={onLint}
linting={linting}
onToggleCollapse={toggleLeftSidebar}
Expand Down
12 changes: 11 additions & 1 deletion packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,17 @@ export function StudioRightPanel({
onClearCompleted={renderQueue.clearCompleted}
onStartRender={async (format, quality, resolution, fps) => {
await waitForPendingDomEditSaves();
await renderQueue.startRender({ fps, quality, format, resolution });
const composition =
activeCompPath && activeCompPath !== "index.html"
? activeCompPath
: undefined;
await renderQueue.startRender({
fps,
quality,
format,
resolution,
composition,
});
}}
compositionDimensions={compositionDimensions}
isRendering={renderQueue.isRendering}
Expand Down
12 changes: 11 additions & 1 deletion packages/studio/src/components/renders/useRenderQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface StartRenderOptions {
format?: "mp4" | "webm" | "mov";
/** `"auto"` (default) renders at the composition's authored dimensions. */
resolution?: ResolutionPreset | "auto";
/** Render a specific composition file instead of index.html. */
composition?: string;
}

export function useRenderQueue(projectId: string | null) {
Expand Down Expand Up @@ -86,17 +88,25 @@ export function useRenderQueue(projectId: string | null) {
const quality = opts.quality ?? "standard";
const format = opts.format ?? "mp4";
const resolution = opts.resolution;
const composition = opts.composition;

const startTime = Date.now();
// "auto" / undefined means "render at the composition's authored size".
// Omit the field entirely — sending "auto" would trip the route's
// enum validation set.
const body: { fps: number; quality: string; format: string; resolution?: string } = {
const body: {
fps: number;
quality: string;
format: string;
resolution?: string;
composition?: string;
} = {
fps,
quality,
format,
};
if (resolution && resolution !== "auto") body.resolution = resolution;
if (composition) body.composition = composition;
let res: Response;
try {
res = await fetch(`/api/projects/${projectId}/render`, {
Expand Down
43 changes: 42 additions & 1 deletion packages/studio/src/components/sidebar/CompositionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ interface CompositionsTabProps {
compositions: string[];
activeComposition: string | null;
onSelect: (comp: string) => void;
onRenderComposition?: (comp: string) => void;
isRendering?: boolean;
}

const DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
Expand Down Expand Up @@ -94,11 +96,15 @@ function CompCard({
comp,
isActive,
onSelect,
onRender,
isRendering,
}: {
projectId: string;
comp: string;
isActive: boolean;
onSelect: () => void;
onRender?: () => void;
isRendering?: boolean;
}) {
const [hovered, setHovered] = useState(false);
const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
Expand Down Expand Up @@ -158,7 +164,7 @@ function CompCard({
onClick={onSelect}
onPointerEnter={handleEnter}
onPointerLeave={handleLeave}
className={`w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${
className={`group/card w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${
isActive
? "bg-studio-accent/10 border-l-2 border-studio-accent"
: "border-l-2 border-transparent hover:bg-neutral-800/50"
Expand Down Expand Up @@ -200,6 +206,37 @@ function CompCard({
<span className="text-[11px] font-medium text-neutral-300 truncate block">{name}</span>
<span className="text-[9px] text-neutral-600 truncate block">{comp}</span>
</div>
{onRender && (
<button
type="button"
title={isRendering ? "Rendering..." : `Render ${name}`}
disabled={isRendering}
onClick={(e) => {
e.stopPropagation();
onRender();
}}
className={`flex-shrink-0 p-1 rounded transition-colors ${
isRendering
? "text-neutral-600 cursor-not-allowed"
: "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
}`}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</button>
)}
</div>
);
}
Expand All @@ -209,6 +246,8 @@ export const CompositionsTab = memo(function CompositionsTab({
compositions,
activeComposition,
onSelect,
onRenderComposition,
isRendering,
}: CompositionsTabProps) {
if (compositions.length === 0) {
return (
Expand All @@ -227,6 +266,8 @@ export const CompositionsTab = memo(function CompositionsTab({
comp={comp}
isActive={activeComposition === comp}
onSelect={() => onSelect(comp)}
onRender={onRenderComposition ? () => onRenderComposition(comp) : undefined}
isRendering={isRendering}
/>
))}
</div>
Expand Down
6 changes: 6 additions & 0 deletions packages/studio/src/components/sidebar/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ interface LeftSidebarProps {
onDuplicateFile?: (path: string) => void;
onMoveFile?: (oldPath: string, newPath: string) => void;
codeChildren?: ReactNode;
onRenderComposition?: (comp: string) => void;
isRendering?: boolean;
onLint?: () => void;
linting?: boolean;
onToggleCollapse?: () => void;
Expand All @@ -69,6 +71,8 @@ export const LeftSidebar = memo(
onDuplicateFile,
onMoveFile,
codeChildren,
onRenderComposition,
isRendering,
onLint,
linting,
onToggleCollapse,
Expand Down Expand Up @@ -169,6 +173,8 @@ export const LeftSidebar = memo(
compositions={compositions}
activeComposition={activeComposition}
onSelect={onSelectComposition}
onRenderComposition={onRenderComposition}
isRendering={isRendering}
/>
)}
{tab === "assets" && (
Expand Down
1 change: 1 addition & 0 deletions packages/studio/vite.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export function createViteAdapter(dataDir: string, server: ViteDevServer): Studi
format: opts.format,
...(renderBodyScripts.length > 0 ? { renderBodyScripts } : {}),
outputResolution: opts.outputResolution,
...(opts.composition ? { entryFile: opts.composition } : {}),
});
const onProgress = (j: { progress: number; currentStage?: string }) => {
state.progress = j.progress;
Expand Down
Loading