From 237248321a1a40b9b98e7e0a6df156d0aa247ef0 Mon Sep 17 00:00:00 2001 From: cashmere Date: Sun, 2 Aug 2026 19:59:32 +0200 Subject: feat: pan & zoom motion presets Ken Burns-style motion effects (zoom in/out, pan left/right/up/down) that drag onto clips like regular effects. Instead of shader passes they evaluate a per-frame transform offset in resolveVisualState, so they stack with keyframed transforms and work in preview and export. - intensity param (0-100%, keyframable) scales the whole effect - easing select maps progress through the graph editor's bezier presets - supportsStandalone: false hides them from effect-track creation - empty shader pass groups are filtered out so pass-less effects and zero-intensity blur no longer crash the compositor --- apps/web/src/effects/definitions/motion.ts | 132 +++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 apps/web/src/effects/definitions/motion.ts (limited to 'apps/web/src/effects/definitions/motion.ts') diff --git a/apps/web/src/effects/definitions/motion.ts b/apps/web/src/effects/definitions/motion.ts new file mode 100644 index 00000000..8c81d801 --- /dev/null +++ b/apps/web/src/effects/definitions/motion.ts @@ -0,0 +1,132 @@ +import type { ParamDefinition, ParamValues } from "@/params"; +import type { EffectDefinition, MotionOffset } from "@/effects/types"; +import { MOTION_EASING_OPTIONS, getMotionIntensity } from "@/effects/motion"; + +const ZOOM_RANGE = 0.35; +const PAN_ZOOM_HEADROOM = 0.15; +const PAN_TRAVEL = 0.06; + +function motionParams(): ParamDefinition[] { + return [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + keyframable: true, + }, + { + key: "easing", + label: "Easing", + type: "select", + default: "smooth", + options: MOTION_EASING_OPTIONS, + }, + ]; +} + +function zoomOffset({ + effectParams, + progress, + direction, +}: { + effectParams: ParamValues; + progress: number; + direction: 1 | -1; +}): MotionOffset { + const intensity = getMotionIntensity({ effectParams }); + const amount = + direction === 1 ? progress * intensity : (1 - progress) * intensity; + return { + scale: 1 + ZOOM_RANGE * amount, + translateX: 0, + translateY: 0, + }; +} + +function panOffset({ + effectParams, + progress, + axis, + direction, +}: { + effectParams: ParamValues; + progress: number; + axis: "x" | "y"; + direction: 1 | -1; +}): MotionOffset { + const intensity = getMotionIntensity({ effectParams }); + const travel = (progress - 0.5) * 2 * PAN_TRAVEL * intensity * direction; + return { + scale: 1 + PAN_ZOOM_HEADROOM * intensity, + translateX: axis === "x" ? travel : 0, + translateY: axis === "y" ? travel : 0, + }; +} + +function buildMotionDefinition({ + type, + name, + keywords, + evaluate, +}: Pick & { + evaluate: NonNullable["evaluate"]; +}): EffectDefinition { + return { + type, + name, + keywords, + params: motionParams(), + renderer: { passes: [] }, + motion: { evaluate }, + supportsStandalone: false, + }; +} + +export const motionEffectDefinitions: EffectDefinition[] = [ + buildMotionDefinition({ + type: "zoom-in", + name: "Zoom In", + keywords: ["zoom", "ken burns", "scale", "push in"], + evaluate: ({ effectParams, progress }) => + zoomOffset({ effectParams, progress, direction: 1 }), + }), + buildMotionDefinition({ + type: "zoom-out", + name: "Zoom Out", + keywords: ["zoom", "ken burns", "scale", "pull out"], + evaluate: ({ effectParams, progress }) => + zoomOffset({ effectParams, progress, direction: -1 }), + }), + buildMotionDefinition({ + type: "pan-left", + name: "Pan Left", + keywords: ["pan", "ken burns", "slide", "left"], + evaluate: ({ effectParams, progress }) => + panOffset({ effectParams, progress, axis: "x", direction: -1 }), + }), + buildMotionDefinition({ + type: "pan-right", + name: "Pan Right", + keywords: ["pan", "ken burns", "slide", "right"], + evaluate: ({ effectParams, progress }) => + panOffset({ effectParams, progress, axis: "x", direction: 1 }), + }), + buildMotionDefinition({ + type: "pan-up", + name: "Pan Up", + keywords: ["pan", "ken burns", "slide", "up", "tilt"], + evaluate: ({ effectParams, progress }) => + panOffset({ effectParams, progress, axis: "y", direction: -1 }), + }), + buildMotionDefinition({ + type: "pan-down", + name: "Pan Down", + keywords: ["pan", "ken burns", "slide", "down", "tilt"], + evaluate: ({ effectParams, progress }) => + panOffset({ effectParams, progress, axis: "y", direction: 1 }), + }), +]; -- cgit v1.3.1