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.
let originalImageData = null;
// On upload:
function onImageLoaded() {
originalImageData = ctx.getImageData(
0, 0, canvas.width, canvas.height
);
}
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.
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);
}
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.
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 ✅
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.