diff options
| author | cashmere <cashmere@autistici.org> | 2026-08-02 19:59:32 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-08-08 10:40:12 +0200 |
| commit | 237248321a1a40b9b98e7e0a6df156d0aa247ef0 (patch) | |
| tree | 62f2f46474b08c3b7723e78a2f4dd9cf58d64494 /apps/web/src/effects/definitions | |
| parent | 465071d8232e74b1690280a38a86d435269fc35d (diff) | |
| download | opencut-237248321a1a40b9b98e7e0a6df156d0aa247ef0.tar.gz | |
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
Diffstat (limited to 'apps/web/src/effects/definitions')
| -rw-r--r-- | apps/web/src/effects/definitions/index.ts | 3 | ||||
| -rw-r--r-- | apps/web/src/effects/definitions/motion.ts | 132 |
2 files changed, 134 insertions, 1 deletions
diff --git a/apps/web/src/effects/definitions/index.ts b/apps/web/src/effects/definitions/index.ts index 511df042..e38e5710 100644 --- a/apps/web/src/effects/definitions/index.ts +++ b/apps/web/src/effects/definitions/index.ts @@ -1,7 +1,8 @@ import { effectsRegistry } from "../registry"; import { blurEffectDefinition } from "./blur"; +import { motionEffectDefinitions } from "./motion"; -const defaultEffects = [blurEffectDefinition]; +const defaultEffects = [blurEffectDefinition, ...motionEffectDefinitions]; export function registerDefaultEffects(): void { for (const definition of defaultEffects) { 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<EffectDefinition, "type" | "name" | "keywords"> & { + evaluate: NonNullable<EffectDefinition["motion"]>["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 }), + }), +]; |
