-
Notifications
You must be signed in to change notification settings - Fork 0
Adding a Filter
This guide walks through every file that needs to be touched to add a brand-new filter effect end-to-end.
| 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 |
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
}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.
-
Uint8ClampedArrayclamps values to[0, 255]on assignment, so you don't need manual clamping. - Keep the function pure — no side effects, no async.
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
}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.
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.
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).
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>npm run dev- Open the Adjustments tab and verify your new section appears.
- Drag sliders and confirm the canvas updates in real time.
- Apply a preset — confirm it doesn't break (your new params should have the defaults you set).
- Export an image — the Web Worker path must also work since
filter.worker.tsimportspipeline.tswhich now calls your effect. - Run
npm run buildand resolve any TypeScript errors before submitting a PR.
Using GrainLab
Developer Docs