From fc0a801e8829b623d62137a3f7cbdec38c6d555a Mon Sep 17 00:00:00 2001 From: cashmere Date: Sun, 26 Jul 2026 21:06:33 +0200 Subject: feat: freeze frame at playhead Enable the timeline toolbar freeze-frame button: capture the composited canvas as PNG, add it as an image media asset, split the video element at the playhead, shift the right part by the freeze duration, and insert a 3-second image element in the gap. Adds RendererManager.captureFrame(), reused by createSnapshot(). Also adds the Nix devshell (flake.nix/flake.lock: bun + nodejs) and AGENTS.md repository guidelines. --- apps/web/src/core/managers/renderer-manager.ts | 35 +++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) (limited to 'apps/web/src/core/managers') diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts index 7984655c..38d1e57f 100644 --- a/apps/web/src/core/managers/renderer-manager.ts +++ b/apps/web/src/core/managers/renderer-manager.ts @@ -78,18 +78,18 @@ export class RendererManager { } } - private async createSnapshot(): Promise { + async captureFrame(): Promise { try { const renderTree = this.getRenderTree(); const activeProject = this.editor.project.getActive(); if (!renderTree || !activeProject) { - return { success: false, error: "No project or scene to capture" }; + return null; } const duration = this.editor.timeline.getTotalDuration(); if (duration === 0) { - return { success: false, error: "Project is empty" }; + return null; } const { canvasSize, fps } = activeProject.settings; @@ -114,9 +114,36 @@ export class RendererManager { targetCanvas: tempCanvas, }); - const blob = await new Promise((resolve) => { + return await new Promise((resolve) => { tempCanvas.toBlob((result) => resolve(result), "image/png"); }); + } catch (error) { + console.error("Frame capture failed:", error); + return null; + } + } + + private async createSnapshot(): Promise { + try { + const renderTree = this.getRenderTree(); + const activeProject = this.editor.project.getActive(); + + if (!renderTree || !activeProject) { + return { success: false, error: "No project or scene to capture" }; + } + + const duration = this.editor.timeline.getTotalDuration(); + if (duration === 0) { + return { success: false, error: "Project is empty" }; + } + + const { fps } = activeProject.settings; + const renderTime = Math.min( + this.editor.playback.getCurrentTime(), + this.editor.timeline.getLastFrameTime(), + ); + + const blob = await this.captureFrame(); if (!blob) { return { success: false, error: "Failed to create image" }; -- cgit v1.3.1