diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/lib/effects | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/lib/effects')
| -rw-r--r-- | apps/web/src/lib/effects/definitions/blur.frag.glsl | 10 | ||||
| -rw-r--r-- | apps/web/src/lib/effects/definitions/blur.ts | 109 | ||||
| -rw-r--r-- | apps/web/src/lib/effects/definitions/index.ts | 6 | ||||
| -rw-r--r-- | apps/web/src/lib/effects/index.ts | 45 | ||||
| -rw-r--r-- | apps/web/src/lib/effects/registry.ts | 33 | ||||
| -rw-r--r-- | apps/web/src/lib/effects/types.ts | 42 |
6 files changed, 171 insertions, 74 deletions
diff --git a/apps/web/src/lib/effects/definitions/blur.frag.glsl b/apps/web/src/lib/effects/definitions/blur.frag.glsl index 2c015725..c9ad2eae 100644 --- a/apps/web/src/lib/effects/definitions/blur.frag.glsl +++ b/apps/web/src/lib/effects/definitions/blur.frag.glsl @@ -3,6 +3,7 @@ precision mediump float; uniform sampler2D u_texture; uniform vec2 u_resolution; uniform float u_sigma; +uniform float u_step; uniform vec2 u_direction; varying vec2 v_texCoord; @@ -13,13 +14,12 @@ void main() { vec4 color = vec4(0.0); float totalWeight = 0.0; - // step=1 texel — scaling step size instead causes discrete ghosting artifacts for (int i = -30; i <= 30; i++) { - float fi = float(i); - float weight = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma)); - color += texture2D(u_texture, v_texCoord + texelSize * u_direction * fi) * weight; + float pos = float(i) * u_step; + float weight = exp(-(pos * pos) / (2.0 * u_sigma * u_sigma)); + color += texture2D(u_texture, v_texCoord + texelSize * u_direction * pos) * weight; totalWeight += weight; } gl_FragColor = color / totalWeight; -} +}
\ No newline at end of file diff --git a/apps/web/src/lib/effects/definitions/blur.ts b/apps/web/src/lib/effects/definitions/blur.ts index 3819f82c..54934402 100644 --- a/apps/web/src/lib/effects/definitions/blur.ts +++ b/apps/web/src/lib/effects/definitions/blur.ts @@ -1,6 +1,72 @@ -import type { EffectDefinition } from "@/types/effects"; +import type { EffectDefinition, ResolvedEffectPass } from "@/lib/effects/types"; import blurFragmentShader from "./blur.frag.glsl"; +const MAX_SINGLE_PASS_SIGMA = 10; +const MAX_STEP = 4; +const MAX_EFFECTIVE_SIGMA = MAX_SINGLE_PASS_SIGMA * MAX_STEP; +const MAX_ITERATIONS = 8; + +/** + * Builds multi-pass gaussian blur passes for a given sigma. + * Shared by the blur effect and background blur — they each + * compute their own sigma from their own intensity scale. + */ +export function buildGaussianBlurPasses({ + sigmaX, + sigmaY, +}: { + sigmaX: number; + sigmaY: number; +}): ResolvedEffectPass[] { + const maxSigma = Math.max(sigmaX, sigmaY); + if (maxSigma < 0.001) return []; + + const iterations = Math.min( + MAX_ITERATIONS, + Math.max( + 1, + Math.ceil( + (maxSigma * maxSigma) / + (MAX_EFFECTIVE_SIGMA * MAX_EFFECTIVE_SIGMA), + ), + ), + ); + const perPassSigmaX = sigmaX / Math.sqrt(iterations); + const perPassSigmaY = sigmaY / Math.sqrt(iterations); + const stepX = Math.max(1, perPassSigmaX / MAX_SINGLE_PASS_SIGMA); + const stepY = Math.max(1, perPassSigmaY / MAX_SINGLE_PASS_SIGMA); + + const passes: ResolvedEffectPass[] = []; + for (let i = 0; i < iterations; i++) { + passes.push({ + fragmentShader: blurFragmentShader, + uniforms: { + u_sigma: perPassSigmaX, + u_step: stepX, + u_direction: [1, 0], + }, + }); + passes.push({ + fragmentShader: blurFragmentShader, + uniforms: { + u_sigma: perPassSigmaY, + u_step: stepY, + u_direction: [0, 1], + }, + }); + } + return passes; +} + +function intensityToSigma(intensity: number, resolution: number, reference: number): number { + return (intensity / 5) * (resolution / reference); +} + +function parseIntensity(effectParams: Record<string, unknown>): number { + const raw = effectParams.intensity; + return typeof raw === "number" ? raw : Number.parseFloat(String(raw)); +} + export const blurEffectDefinition: EffectDefinition = { type: "blur", name: "Blur", @@ -19,32 +85,29 @@ export const blurEffectDefinition: EffectDefinition = { renderer: { type: "webgl", passes: [ - { - fragmentShader: blurFragmentShader, - uniforms: ({ effectParams, width }) => { - const intensity = - typeof effectParams.intensity === "number" - ? effectParams.intensity - : Number.parseFloat(String(effectParams.intensity)); - return { - u_sigma: Math.max((intensity / 5) * (width / 1920), 0.001), + { + fragmentShader: blurFragmentShader, + uniforms: ({ effectParams, width }) => ({ + u_sigma: Math.max(intensityToSigma(parseIntensity(effectParams), width, 1920), 0.001), + u_step: 1, u_direction: [1, 0], - }; + }), }, - }, - { - fragmentShader: blurFragmentShader, - uniforms: ({ effectParams, height }) => { - const intensity = - typeof effectParams.intensity === "number" - ? effectParams.intensity - : Number.parseFloat(String(effectParams.intensity)); - return { - u_sigma: Math.max((intensity / 5) * (height / 1080), 0.001), + { + fragmentShader: blurFragmentShader, + uniforms: ({ effectParams, height }) => ({ + u_sigma: Math.max(intensityToSigma(parseIntensity(effectParams), height, 1080), 0.001), + u_step: 1, u_direction: [0, 1], - }; + }), }, - }, ], + buildPasses: ({ effectParams, width, height }) => { + const intensity = parseIntensity(effectParams); + return buildGaussianBlurPasses({ + sigmaX: intensityToSigma(intensity, width, 1920), + sigmaY: intensityToSigma(intensity, height, 1080), + }); + }, }, }; diff --git a/apps/web/src/lib/effects/definitions/index.ts b/apps/web/src/lib/effects/definitions/index.ts index 1742b1d7..edc8c5e2 100644 --- a/apps/web/src/lib/effects/definitions/index.ts +++ b/apps/web/src/lib/effects/definitions/index.ts @@ -1,13 +1,13 @@ -import { hasEffect, registerEffect } from "../registry"; +import { effectsRegistry } from "../registry"; import { blurEffectDefinition } from "./blur"; const defaultEffects = [blurEffectDefinition]; export function registerDefaultEffects(): void { for (const definition of defaultEffects) { - if (hasEffect({ effectType: definition.type })) { + if (effectsRegistry.has(definition.type)) { continue; } - registerEffect({ definition }); + effectsRegistry.register(definition.type, definition); } } diff --git a/apps/web/src/lib/effects/index.ts b/apps/web/src/lib/effects/index.ts index 14d06d84..549811f5 100644 --- a/apps/web/src/lib/effects/index.ts +++ b/apps/web/src/lib/effects/index.ts @@ -1,29 +1,42 @@ import { generateUUID } from "@/utils/id"; -import { getEffect } from "./registry"; -import type { Effect, EffectParamValues } from "@/types/effects"; -import type { VisualElement } from "@/types/timeline"; +import { buildDefaultParamValues } from "@/lib/registry"; +import { effectsRegistry } from "./registry"; +import type { ParamValues } from "@/lib/params"; +import type { Effect, EffectDefinition, ResolvedEffectPass } from "@/lib/effects/types"; +import { VISUAL_ELEMENT_TYPES } from "@/lib/timeline"; -export { getEffect, getAllEffects, hasEffect, registerEffect } from "./registry"; +export { effectsRegistry } from "./registry"; export { registerDefaultEffects } from "./definitions"; -export const EFFECT_TARGET_ELEMENT_TYPES: VisualElement["type"][] = [ - "video", - "image", - "text", - "sticker", -]; +export function resolveEffectPasses({ + definition, + effectParams, + width, + height, +}: { + definition: EffectDefinition; + effectParams: ParamValues; + width: number; + height: number; +}): ResolvedEffectPass[] { + if (definition.renderer.buildPasses) { + return definition.renderer.buildPasses({ effectParams, width, height }); + } + return definition.renderer.passes.map((pass) => ({ + fragmentShader: pass.fragmentShader, + uniforms: pass.uniforms({ effectParams, width, height }), + })); +} + +export const EFFECT_TARGET_ELEMENT_TYPES = VISUAL_ELEMENT_TYPES; export function buildDefaultEffectInstance({ effectType, }: { effectType: string; }): Effect { - const definition = getEffect({ effectType }); - - const params: EffectParamValues = {}; - for (const paramDef of definition.params) { - params[paramDef.key] = paramDef.default; - } + const definition = effectsRegistry.get(effectType); + const params: ParamValues = buildDefaultParamValues(definition.params); return { id: generateUUID(), diff --git a/apps/web/src/lib/effects/registry.ts b/apps/web/src/lib/effects/registry.ts index 568b8924..2b85007a 100644 --- a/apps/web/src/lib/effects/registry.ts +++ b/apps/web/src/lib/effects/registry.ts @@ -1,31 +1,10 @@ -import type { EffectDefinition } from "@/types/effects"; +import { DefinitionRegistry } from "@/lib/registry"; +import type { EffectDefinition } from "@/lib/effects/types"; -const effectDefinitions = new Map<string, EffectDefinition>(); - -export function registerEffect({ - definition, -}: { - definition: EffectDefinition; -}): void { - effectDefinitions.set(definition.type, definition); -} - -export function hasEffect({ effectType }: { effectType: string }): boolean { - return effectDefinitions.has(effectType); -} - -export function getEffect({ - effectType, -}: { - effectType: string; -}): EffectDefinition { - const definition = effectDefinitions.get(effectType); - if (!definition) { - throw new Error(`Unknown effect type: ${effectType}`); +export class EffectsRegistry extends DefinitionRegistry<string, EffectDefinition> { + constructor() { + super("effect"); } - return definition; } -export function getAllEffects(): EffectDefinition[] { - return Array.from(effectDefinitions.values()); -} +export const effectsRegistry = new EffectsRegistry(); diff --git a/apps/web/src/lib/effects/types.ts b/apps/web/src/lib/effects/types.ts new file mode 100644 index 00000000..7f85389f --- /dev/null +++ b/apps/web/src/lib/effects/types.ts @@ -0,0 +1,42 @@ +import type { ParamDefinition, ParamValues } from "@/lib/params"; + +export interface Effect { + id: string; + type: string; + params: ParamValues; + enabled: boolean; +} + +export interface ResolvedEffectPass { + fragmentShader: string; + uniforms: Record<string, number | number[]>; +} + +export interface WebGLEffectPass { + fragmentShader: string; + uniforms(params: { + effectParams: ParamValues; + width: number; + height: number; + }): Record<string, number | number[]>; +} + +export interface WebGLEffectRenderer { + type: "webgl"; + passes: WebGLEffectPass[]; + buildPasses?: (params: { + effectParams: ParamValues; + width: number; + height: number; + }) => ResolvedEffectPass[]; +} + +export type EffectRenderer = WebGLEffectRenderer; + +export interface EffectDefinition { + type: string; + name: string; + keywords: string[]; + params: ParamDefinition[]; + renderer: EffectRenderer; +} |
