summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers/renderer-manager.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-26 21:06:33 +0200
committercashmere <cashmere@autistici.org>2026-07-26 21:06:33 +0200
commitfc0a801e8829b623d62137a3f7cbdec38c6d555a (patch)
treeb4c9a0458e92eaf44d3331ce3b0ea3ff906a9d15 /apps/web/src/core/managers/renderer-manager.ts
parentcf5e79e919144200294fb9fed22a222592a0aeea (diff)
downloadopencut-fc0a801e8829b623d62137a3f7cbdec38c6d555a.tar.gz
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.
Diffstat (limited to 'apps/web/src/core/managers/renderer-manager.ts')
-rw-r--r--apps/web/src/core/managers/renderer-manager.ts35
1 files changed, 31 insertions, 4 deletions
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<SnapshotResult> {
+ async captureFrame(): Promise<Blob | null> {
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<Blob | null>((resolve) => {
+ return await new Promise<Blob | null>((resolve) => {
tempCanvas.toBlob((result) => resolve(result), "image/png");
});
+ } catch (error) {
+ console.error("Frame capture failed:", error);
+ return null;
+ }
+ }
+
+ private async createSnapshot(): Promise<SnapshotResult> {
+ 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" };