You're about to copy-paste the pixel loop 3 more times. The senior dev stops you:
"If you find a bug in the loop, you'll fix it in one place and forget the other three."
function applyFilter(ctx, width, height, filterFn) {
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const [newR, newG, newB] = filterFn(
pixels[i], pixels[i+1], pixels[i+2]
);
pixels[i] = clamp(newR);
pixels[i + 1] = clamp(newG);
pixels[i + 2] = clamp(newB);
}
ctx.putImageData(imageData, 0, 0);
}
function clamp(value) {
return Math.min(255, Math.max(0, Math.round(value)));
}
function grayscale(r, g, b) {
const avg = (r + g + b) / 3;
return [avg, avg, avg];
}
function brightness(r, g, b, amount) {
return [r + amount, g + amount, b + amount];
}
function contrast(r, g, b, factor) {
return [
((r / 255 - 0.5) * factor + 0.5) * 255,
((g / 255 - 0.5) * factor + 0.5) * 255,
((b / 255 - 0.5) * factor + 0.5) * 255,
];
}
function invert(r, g, b) {
return [255 - r, 255 - g, 255 - b];
}
grayscaleBtn.addEventListener('click', () => {
applyFilter(ctx, canvas.width, canvas.height, grayscale);
});
brightnessSlider.addEventListener('input', (e) => {
const amount = parseInt(e.target.value);
applyFilter(ctx, canvas.width, canvas.height,
(r, g, b) => brightness(r, g, b, amount)
);
});
Test all 4 filters on the same image. Verify each produces the expected result.
git switch -c feature/PIXELCRAFT-010-more-filters
git add src/scripts/
git commit -m "Implement brightness, contrast, invert filters with reusable applyFilter (PIXELCRAFT-010)"
git push origin feature/PIXELCRAFT-010-more-filters
# PR → Review → Merge → Close ticket ✅
Functions are the fundamental unit of abstraction.
applyFilter(data, fn) doesn't know which filter it's applying — it just loops over pixels and calls whatever function you give it. This separation of mechanism (the loop) from policy (the math) is the foundation of reusable software.