027
LVL 01 — WAVE DEFENDER SESSION 027 DAY 27

FILTER STACKING

🎫 PIXELCRAFT-015
🐛 Bug | 🟡 Medium | Priority: 🔴 Critical

Applying brightness +20, then contrast +10 resets brightness to 0. Filters overwrite each other. Users want to stack multiple adjustments.
CONCEPTS.UNLOCKED
🔒
Non-Destructive Editing
Keep the original image pristine. Reapply ALL filters from scratch on every change. Never mutate the source — always derive the output.
📊
State Management
Track all active filters and their values in a single state object. Every UI change updates the state, and the state drives the rendering. Single source of truth.
📋
Array Methods
.forEach() (iterate), .map() (transform each), .filter() (keep matching), .find() (first match), .some() (any match?), .every() (all match?).
...
The Spread Operator
[...array] creates a copy of an array. { ...object } creates a copy of an object. Essential for non-destructive data manipulation — copy first, then modify the copy.
🔗
Thinking in Pipelines
original → filter1 → filter2 → filter3 → display. Each step transforms data and passes it to the next. The output of one filter is the input to the next.
📸
ImageData Cloning
new ImageData(new Uint8ClampedArray(original.data), w, h) creates a deep copy of pixel data. This is how you start from a fresh copy every render.
HANDS-ON.TASKS
01
Understand the Problem

Each filter modifies the canvas directly, destroying the previous filter's result. Brightness changes the pixels, then contrast reads those changed pixels — not the original.

Solution: keep a pristine copy of the original image data.

02
Store the Original Image
let originalImageData = null; // On upload: function onImageLoaded() { originalImageData = ctx.getImageData( 0, 0, canvas.width, canvas.height ); }
This snapshot is captured once when the image loads. It's never modified. Every filter operation starts from this pristine copy.
03
Track Filter State
const filterState = { brightness: 0, contrast: 1, saturation: 0, grayscale: false, invert: false, };

One object holds the truth about all active filters. Every UI control reads from and writes to this state.

04
Reapply ALL Filters from Original
function applyAllFilters() { if (!originalImageData) return; // Start from a fresh copy of the original const imageData = new ImageData( new Uint8ClampedArray(originalImageData.data), originalImageData.width, originalImageData.height ); const pixels = imageData.data; for (let i = 0; i < pixels.length; i += 4) { let [r, g, b] = [pixels[i], pixels[i+1], pixels[i+2]]; // Apply each active filter in sequence if (filterState.brightness !== 0) { [r, g, b] = brightness(r, g, b, filterState.brightness); } if (filterState.contrast !== 1) { [r, g, b] = contrast(r, g, b, filterState.contrast); } if (filterState.grayscale) { [r, g, b] = grayscale(r, g, b); } if (filterState.invert) { [r, g, b] = invert(r, g, b); } pixels[i] = clamp(r); pixels[i + 1] = clamp(g); pixels[i + 2] = clamp(b); } ctx.putImageData(imageData, 0, 0); }
05
Connect Sliders & Test

Every slider change updates filterState then calls applyAllFilters().

Test: brightness +50, then contrast +1.5, then grayscale. All should compose correctly — brightness doesn't reset when you adjust contrast.

06
Close the Ticket
git switch -c bugfix/PIXELCRAFT-015-filter-stacking git add src/scripts/ git commit -m "Implement non-destructive filter pipeline (PIXELCRAFT-015)" git push origin bugfix/PIXELCRAFT-015-filter-stacking # PR → Review → Merge → Close ticket ✅
CS.DEEP-DIVE

Immutability and non-destructive operations.

Keeping the original unchanged and always recomputing sounds wasteful, but it's enormously powerful: undo is trivial, any combination can be previewed, and data is never corrupted.

// This principle is the foundation of:

Functional      → pure functions never
  programming     mutate input data
Git             → every commit is a snapshot
                  originals are never modified
Database logs   → append changes,
                  never overwrite
Event sourcing  → store events, derive state

// Original data is sacred.
// Everything else is derived.
"Filter Lab"
[A] Add a "Reset All" button that sets every filterState value back to its default and calls applyAllFilters() — the image returns to its original state instantly.
[B] Add a "Before/After" toggle: hold a button to show the original image, release to show the filtered version. Instant comparison.
[C] Use .map() to create an array of active filter names from filterState. Display them as tags below the canvas: "Active: Brightness +50, Contrast ×1.5".
REF.MATERIAL
ARTICLE
Mozilla Developer Network
Complete reference for every array method: forEach, map, filter, reduce, find, some, every, flat, and more.
ARRAYSMDNREFERENCE
VIDEO
Web Dev Simplified
Practical walkthrough of forEach, map, filter, find, some, every, reduce, and includes with real examples.
ARRAYSESSENTIAL
ARTICLE
javascript.info
Interactive deep dive into array methods with runnable examples. Covers the "functional trio": map, filter, reduce.
ARRAYSINTERACTIVE
VIDEO
Fun Fun Function
Why immutability matters, how to avoid mutations, and the spread operator. Connects to functional programming principles.
IMMUTABILITYFUNCTIONAL
ARTICLE
Mozilla Developer Network
The ImageData API: creating, cloning, and manipulating pixel arrays. Essential for the non-destructive editing pipeline.
CANVASIMAGEDATA
// LEAVE EXCITED BECAUSE
Filters compose! Brightness + contrast + saturation all stack beautifully. The architecture is clean: original image is sacred, everything else is derived. This is how Photoshop works.