summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/assets/views
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-28 18:41:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-28 18:41:22 +0100
commit216e3e0c397be3719a94e1a22bc1c2ce2de3856c (patch)
tree883bdda1539f11ccf5d6fe50a994e4e6dbee0d75 /apps/web/src/components/editor/panels/assets/views
parenta9e93471a74a3d1978278f09f35632d4f6588a76 (diff)
downloadopencut-216e3e0c397be3719a94e1a22bc1c2ce2de3856c.tar.gz
feat: introduce WebGL effects system and Blur effect
This implements the foundational architecture for video effects, starting with a multi-pass WebGL rendering pipeline and a customizable Gaussian Blur effect. Key changes: - WebGL Engine: Added `raw-loader` for `.glsl` shaders, multi-pass framebuffer rendering, and live offscreen canvas previews. - Node Architecture: Replaced hardcoded background blur with `CompositeEffectNode` and added `EffectLayerNode` to apply effects to specific visual elements. - Timeline & DND: Added a new `effect` track type. Upgraded drag-and-drop to support dropping effects directly onto the timeline. Consolidated track constants into a cleaner `TRACK_CONFIG`. - UI/UX: Added an Effects tab in the assets panel with live previews. Added an Effect Properties panel with sliders and inputs for fine-tuning parameters. - Data Model: Added `sourceDuration` to video and audio elements, and wrote a v8 storage migration to update existing projects to the new schema. - Docs: Added `CHANGELOG.md` tracking v0.1.0 and v0.2.0, plus `docs/effects-renderer.md` to document the new WebGL pipeline.
Diffstat (limited to 'apps/web/src/components/editor/panels/assets/views')
-rw-r--r--apps/web/src/components/editor/panels/assets/views/assets.tsx6
-rw-r--r--apps/web/src/components/editor/panels/assets/views/effects.tsx95
2 files changed, 101 insertions, 0 deletions
diff --git a/apps/web/src/components/editor/panels/assets/views/assets.tsx b/apps/web/src/components/editor/panels/assets/views/assets.tsx
index 808961f1..4c6922ff 100644
--- a/apps/web/src/components/editor/panels/assets/views/assets.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/assets.tsx
@@ -426,6 +426,9 @@ function GridView({
type: "media",
mediaType: item.type,
name: item.name,
+ ...(item.type !== "audio" && {
+ targetElementTypes: ["video", "image"] as const,
+ }),
}}
shouldShowPlusOnDrag={false}
onAddToTimeline={({ currentTime }) =>
@@ -476,6 +479,9 @@ function ListView({
type: "media",
mediaType: item.type,
name: item.name,
+ ...(item.type !== "audio" && {
+ targetElementTypes: ["video", "image"] as const,
+ }),
}}
shouldShowPlusOnDrag={false}
onAddToTimeline={({ currentTime }) =>
diff --git a/apps/web/src/components/editor/panels/assets/views/effects.tsx b/apps/web/src/components/editor/panels/assets/views/effects.tsx
new file mode 100644
index 00000000..c9b8b2dd
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/effects.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { useEffect, useRef, useCallback } from "react";
+import { PanelView } from "@/components/editor/panels/assets/views/base-view";
+import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
+import { getAllEffects, EFFECT_TARGET_ELEMENT_TYPES } from "@/lib/effects";
+import {
+ effectPreviewService,
+ onPreviewImageReady,
+} from "@/services/renderer/effect-preview";
+import { useEditor } from "@/hooks/use-editor";
+import { buildEffectElement } from "@/lib/timeline/element-utils";
+import type { EffectDefinition } from "@/types/effects";
+
+export function EffectsView() {
+ const effects = getAllEffects();
+
+ return (
+ <PanelView title="Effects">
+ <EffectsGrid effects={effects} />
+ </PanelView>
+ );
+}
+
+function EffectsGrid({ effects }: { effects: EffectDefinition[] }) {
+ return (
+ <div
+ className="grid gap-2"
+ style={{ gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))" }}
+ >
+ {effects.map((effect) => (
+ <EffectItem key={effect.type} effect={effect} />
+ ))}
+ </div>
+ );
+}
+
+function EffectPreviewCanvas({ effectType }: { effectType: string }) {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+
+ useEffect(() => {
+ const render = () => {
+ if (canvasRef.current) {
+ effectPreviewService.renderPreview({
+ effectType,
+ params: {},
+ targetCanvas: canvasRef.current,
+ });
+ }
+ };
+
+ render();
+ return onPreviewImageReady({ callback: render });
+ }, [effectType]);
+
+ return <canvas ref={canvasRef} className="size-full" />;
+}
+
+function EffectItem({ effect }: { effect: EffectDefinition }) {
+ const editor = useEditor();
+
+ const handleAddToTimeline = useCallback(() => {
+ const currentTime = editor.playback.getCurrentTime();
+ const element = buildEffectElement({
+ effectType: effect.type,
+ startTime: currentTime,
+ });
+
+ editor.timeline.insertElement({
+ placement: { mode: "auto", trackType: "effect" },
+ element,
+ });
+ }, [editor, effect.type]);
+
+ const preview = <EffectPreviewCanvas effectType={effect.type} />;
+
+ return (
+ <DraggableItem
+ name={effect.name}
+ preview={preview}
+ dragData={{
+ id: effect.type,
+ name: effect.name,
+ type: "effect",
+ effectType: effect.type,
+ targetElementTypes: EFFECT_TARGET_ELEMENT_TYPES,
+ }}
+ onAddToTimeline={handleAddToTimeline}
+ aspectRatio={1}
+ isRounded
+ variant="card"
+ containerClassName="w-full"
+ />
+ );
+}