summaryrefslogtreecommitdiff
path: root/apps/web/src/effects/definitions/motion.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-08-09 02:37:37 +0200
committercashmere <cashmere@autistici.org>2026-08-09 02:37:37 +0200
commitc555f1eaebff90749bdc543592cfa80a060b29e2 (patch)
treee6a04adb212871063653b66c17eb1b8c17c7d22f /apps/web/src/effects/definitions/motion.ts
parentdf82306a86d14bca0149e6d756ba82b75659af43 (diff)
downloadopencut-c555f1eaebff90749bdc543592cfa80a060b29e2.tar.gz
feat: lens effects tab, standalone motion effects, clip/asset rename, preview popout
- New Lens category in the assets panel: Blur, Motion Blur, Zoom In/Out, Pan x4, Shake, Pulse, Spin, Custom Zoom. EffectDefinition gains a category field; tabs with an empty category are hidden, and the properties-panel 'Open effects' button opens the first non-empty one. - Motion effects work standalone: dropping them on the timeline creates an effect-track element that animates (scale/translate/rotate) every track below it, computed purely in the TS frame descriptor - no wasm changes, identical in preview and export. supportsStandalone removed. - Motion Blur: directional gaussian smear (amount + angle) reusing the existing WGSL shader with arbitrary u_direction. - Custom Zoom: start/end scale params (keyframable clip-level). - MotionOffset gains optional rotate, accumulated clip-level and applied to standalone layer quads via rotationDegrees. - Rename clips (timeline context menu) and media assets (media panel context menu) via a shared RenameDialog; asset rename persists through storageService with rollback on error. - Preview pops out into a Document Picture-in-Picture window and docks back; button hidden where the API is unsupported. - Export keeps running when the export popover is closed; only the explicit Cancel button aborts. - Captions and Adjustment tabs removed from the assets panel.
Diffstat (limited to 'apps/web/src/effects/definitions/motion.ts')
-rw-r--r--apps/web/src/effects/definitions/motion.ts115
1 files changed, 114 insertions, 1 deletions
diff --git a/apps/web/src/effects/definitions/motion.ts b/apps/web/src/effects/definitions/motion.ts
index 8c81d801..2e3292a2 100644
--- a/apps/web/src/effects/definitions/motion.ts
+++ b/apps/web/src/effects/definitions/motion.ts
@@ -5,6 +5,76 @@ import { MOTION_EASING_OPTIONS, getMotionIntensity } from "@/effects/motion";
const ZOOM_RANGE = 0.35;
const PAN_ZOOM_HEADROOM = 0.15;
const PAN_TRAVEL = 0.06;
+const SHAKE_SCALE_HEADROOM = 0.08;
+const SHAKE_TRAVEL_X = 0.025;
+const SHAKE_TRAVEL_Y = 0.02;
+const SHAKE_FREQUENCY = 6;
+const SHAKE_Y_DETUNE = 1.31;
+const PULSE_RANGE = 0.12;
+const PULSE_FREQUENCY = 3;
+const SPIN_FULL_TURN_DEGREES = 360;
+const CUSTOM_ZOOM_MIN_SCALE = 25;
+const CUSTOM_ZOOM_MAX_SCALE = 400;
+
+function readScaleParam({
+ effectParams,
+ key,
+}: {
+ effectParams: ParamValues;
+ key: string;
+}): number {
+ const raw = effectParams[key];
+ const value = typeof raw === "number" ? raw : Number.parseFloat(String(raw));
+ return Number.isFinite(value) ? value : 100;
+}
+
+export const customZoomEffectDefinition: EffectDefinition = {
+ type: "custom-zoom",
+ name: "Custom Zoom",
+ keywords: ["zoom", "ken burns", "custom", "scale", "start", "end"],
+ category: "lens",
+ params: [
+ {
+ key: "startScale",
+ label: "Start scale",
+ type: "number",
+ default: 100,
+ min: CUSTOM_ZOOM_MIN_SCALE,
+ max: CUSTOM_ZOOM_MAX_SCALE,
+ step: 1,
+ keyframable: true,
+ },
+ {
+ key: "endScale",
+ label: "End scale",
+ type: "number",
+ default: 135,
+ min: CUSTOM_ZOOM_MIN_SCALE,
+ max: CUSTOM_ZOOM_MAX_SCALE,
+ step: 1,
+ keyframable: true,
+ },
+ {
+ key: "easing",
+ label: "Easing",
+ type: "select",
+ default: "smooth",
+ options: MOTION_EASING_OPTIONS,
+ },
+ ],
+ renderer: { passes: [] },
+ motion: {
+ evaluate: ({ effectParams, progress }) => {
+ const startScale = readScaleParam({ effectParams, key: "startScale" });
+ const endScale = readScaleParam({ effectParams, key: "endScale" });
+ return {
+ scale: (startScale + (endScale - startScale) * progress) / 100,
+ translateX: 0,
+ translateY: 0,
+ };
+ },
+ },
+};
function motionParams(): ParamDefinition[] {
return [
@@ -79,10 +149,10 @@ function buildMotionDefinition({
type,
name,
keywords,
+ category: "lens",
params: motionParams(),
renderer: { passes: [] },
motion: { evaluate },
- supportsStandalone: false,
};
}
@@ -129,4 +199,47 @@ export const motionEffectDefinitions: EffectDefinition[] = [
evaluate: ({ effectParams, progress }) =>
panOffset({ effectParams, progress, axis: "y", direction: 1 }),
}),
+ buildMotionDefinition({
+ type: "shake",
+ name: "Shake",
+ keywords: ["shake", "camera", "handheld", "wobble", "jitter"],
+ evaluate: ({ effectParams, progress }) => {
+ const intensity = getMotionIntensity({ effectParams });
+ const angle = progress * Math.PI * 2 * SHAKE_FREQUENCY;
+ return {
+ scale: 1 + SHAKE_SCALE_HEADROOM * intensity,
+ translateX: Math.sin(angle) * SHAKE_TRAVEL_X * intensity,
+ translateY:
+ Math.cos(angle * SHAKE_Y_DETUNE) * SHAKE_TRAVEL_Y * intensity,
+ };
+ },
+ }),
+ buildMotionDefinition({
+ type: "pulse",
+ name: "Pulse",
+ keywords: ["pulse", "beat", "pump", "scale", "bounce"],
+ evaluate: ({ effectParams, progress }) => ({
+ scale:
+ 1 +
+ Math.sin(progress * Math.PI * 2 * PULSE_FREQUENCY) *
+ PULSE_RANGE *
+ getMotionIntensity({ effectParams }),
+ translateX: 0,
+ translateY: 0,
+ }),
+ }),
+ buildMotionDefinition({
+ type: "spin",
+ name: "Spin",
+ keywords: ["spin", "rotate", "turn", "twirl"],
+ evaluate: ({ effectParams, progress }) => ({
+ scale: 1,
+ translateX: 0,
+ translateY: 0,
+ rotate:
+ progress *
+ SPIN_FULL_TURN_DEGREES *
+ getMotionIntensity({ effectParams }),
+ }),
+ }),
];