Skip to content

Adding a Filter

Flying Pizza edited this page Apr 19, 2026 · 2 revisions

This guide walks through every file that needs to be touched to add a brand-new filter effect end-to-end.


Overview of changes

File What to do
src/filters/types.ts Add a params interface and add it to FilterParams
src/filters/yourEffect.ts Implement the effect function
src/filters/pipeline.ts Call the effect in the correct pipeline position
src/presets/index.ts Add default values in every preset
src/stores/editor.ts Add default values for the new params
src/i18n/en.ts (+ all locales) Add UI label strings
src/components/ControlPanel.vue Add sliders / controls to the UI

Step 1 — Define the params interface (src/filters/types.ts)

Add a new interface for your effect's parameters:

export interface MyEffectParams {
  intensity: number // 0 to 100
  // add more fields as needed
}

Then add it to the FilterParams interface:

export interface FilterParams {
  colorGrade: ColorGradeParams
  grain: GrainParams
  // ... existing fields ...
  myEffect: MyEffectParams   // ← add this
}

Step 2 — Implement the effect (src/filters/myEffect.ts)

Create a new file. The function must accept an ImageData and your params, mutate the pixels in-place, and return nothing.

import type { MyEffectParams } from './types'

export function applyMyEffect(imageData: ImageData, params: MyEffectParams): void {
  if (params.intensity === 0) return  // fast-exit when disabled

  const { data, width, height } = imageData
  const factor = params.intensity / 100

  for (let i = 0; i < data.length; i += 4) {
    const r = data[i]
    const g = data[i + 1]
    const b = data[i + 2]
    // ... your pixel math here ...
    data[i]     = r  // write back (Uint8ClampedArray auto-clamps to 0–255)
    data[i + 1] = g
    data[i + 2] = b
    // data[i + 3] is alpha — leave unchanged unless intentional
  }
}

Guidelines:

  • Always add a fast-exit when the effect is disabled (intensity === 0, etc.) to keep preview rendering snappy.
  • Uint8ClampedArray clamps values to [0, 255] on assignment, so you don't need manual clamping.
  • Keep the function pure — no side effects, no async.

Step 3 — Insert into the pipeline (src/filters/pipeline.ts)

Import your function and add a call in the appropriate position. See Filter Pipeline for guidance on ordering.

import { applyMyEffect } from './myEffect'   // ← add import

export function applyFilters(source: ImageData, params: FilterParams): ImageData {
  const result = new ImageData(
    new Uint8ClampedArray(source.data),
    source.width,
    source.height
  )

  applyToneCurve(result, params.toneCurve)
  applyColorGrade(result, params.colorGrade)
  applyFade(result, params.fade)
  applyMyEffect(result, params.myEffect)     // ← insert at the correct stage
  applyHalation(result, params.halation)
  applyBloom(result, params.bloom)
  applyGrain(result, params.grain)
  applyVignette(result, params.vignette)
  applyLightLeak(result, params.lightLeak)

  return result
}

Step 4 — Add defaults to every preset (src/presets/index.ts)

Every FilmPreset in the presets array must include your new field:

// Add to each preset's params object:
myEffect: { intensity: 0 },

Also update the defaultParams object (if one exists) and the cloneParams helper if it explicitly lists fields.


Step 5 — Add default values in the editor store (src/stores/editor.ts)

The mergeParams function deep-merges stored params with defaults. Make sure defaults are present so the app works correctly after upgrading from an older version that didn't have your effect.

Look for where defaultParams (or the first preset's params) is used as the defaults object and confirm your new field is included.


Step 6 — Add UI labels (src/i18n/en.ts + all locale files)

Add label strings for your new sliders to every locale file under the appropriate key. Follow the existing pattern:

// src/i18n/en.ts
effects: {
  // ... existing effects ...
  myEffect: {
    label: 'My Effect',
    intensity: 'Intensity',
    // add a key per parameter
  },
},

Repeat for all 9 locale files (zh-CN, zh-TW, ja, ko, fr, de, es, pt).


Step 7 — Add controls to the UI (src/components/ControlPanel.vue)

Find the section in ControlPanel.vue where existing effects are rendered and add your controls following the same pattern as adjacent effects (typically a toggle switch + one or more <input type="range"> sliders):

<!-- My Effect -->
<div class="effect-section">
  <div class="effect-header">
    <label>{{ t('effects.myEffect.label') }}</label>
    <ToggleSwitch v-model="myEffectEnabled" />
  </div>
  <div v-if="myEffectEnabled" class="effect-controls">
    <div class="slider-row">
      <span>{{ t('effects.myEffect.intensity') }}</span>
      <input type="range" min="0" max="100"
             v-model.number="params.myEffect.intensity" />
      <span class="value">{{ params.myEffect.intensity }}</span>
    </div>
  </div>
</div>

Step 8 — Test

npm run dev
  1. Open the Adjustments tab and verify your new section appears.
  2. Drag sliders and confirm the canvas updates in real time.
  3. Apply a preset — confirm it doesn't break (your new params should have the defaults you set).
  4. Export an image — the Web Worker path must also work since filter.worker.ts imports pipeline.ts which now calls your effect.
  5. Run npm run build and resolve any TypeScript errors before submitting a PR.