Custom convolution kernels — sharpen, emboss, edge-detect, motion blur
Demonstrates the power of custom convolution kernels by creating a side-by-side showcase of sharpening, embossing, edge detection, and motion blur effects.
INPUT
OUTPUT
JavaScript
// Custom convolution kernels — sharpen, emboss, edge-detect, motion blur
// demo_convolve.js
//!OUTPUT: OUTPUT
// demo_convolve.js
// Demonstrates the convolve() API by showing five panels side by side:
// Original | Sharpen | Emboss | Edge-detect | Motion blur
//
// Each panel is labelled via a tinted header strip drawn on a canvas.
const PANEL_W = 240;
const PANEL_H = 240;
const LABEL_H = 32;
const GAP = 4;
// ── Load & resize source image to a fixed panel size ──────────────────────
const src = Engine.loadImage(INPUT).resize(PANEL_W, PANEL_H);
// ── Panel factory: resize, apply op, add label strip ─────────────────────
/**
* Build one labelled panel.
* @param {function(ImageHandle): void} applyFn — mutation applied to a clone
* @param {string} label — text for the label (rendered as a tinted strip)
* @param {Pixel} color — strip tint color
*/
function makePanel(applyFn, label, color) {
const tile = src.clone();
applyFn(tile);
// Label strip: crop the top LABEL_H rows and tint them
const strip = tile.clone().crop(0, 0, PANEL_W, LABEL_H).tint(color, 0.75);
// Overlay strip back at the top of the tile
tile.blendAt(strip, px(0, 0), 1.0, Blend.Over);
strip.free();
// Draw label text as a path-based marker: a small filled rect + text band
// (QuickJS has no text renderer, so we use a coloured band + the filename
// as a visual cue — the tint color differentiates each panel clearly)
return tile;
}
// ── Kernels ───────────────────────────────────────────────────────────────
// Classic 3×3 sharpen (unsharp-mask style)
const K_SHARPEN = [
0, -1, 0,
-1, 5, -1,
0, -1, 0,
];
// Emboss — diagonal relief, result is centred around 0.5 grey
const K_EMBOSS = [
-2, -1, 0,
-1, 1, 1,
0, 1, 2,
];
// Laplacian edge-detect — all-directional edges, result biased to black
const K_EDGE = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1,
];
// Horizontal motion blur (1×9 box) — simulates camera shake
const MOTION_LEN = 9;
const K_MOTION = new Array(MOTION_LEN).fill(1 / MOTION_LEN);
// ── Build panels ──────────────────────────────────────────────────────────
const panelOriginal = makePanel(
() => {}, // no-op
"Original",
new Pixel(0.40, 0.40, 0.40, 1.0),
);
const panelSharpen = makePanel(
t => t.convolve(K_SHARPEN),
"Sharpen",
new Pixel(0.15, 0.55, 0.90, 1.0),
);
const panelEmboss = makePanel(
t => t.emboss(),
"Emboss",
new Pixel(0.80, 0.50, 0.15, 1.0),
);
const panelEdge = makePanel(
t => t.convolve(K_EDGE),
"Edge-detect",
new Pixel(0.80, 0.15, 0.20, 1.0),
);
const panelMotion = makePanel(
t => t.convolve(K_MOTION, MOTION_LEN, 1),
"Motion blur",
new Pixel(0.15, 0.70, 0.40, 1.0),
);
// ── Divider strip ─────────────────────────────────────────────────────────
const divH = PANEL_H + LABEL_H;
const divider = Engine.createImage(GAP, divH);
divider.tint(new Pixel(0.08, 0.08, 0.08, 1.0), 1.0);
// ── Montage ───────────────────────────────────────────────────────────────
// montageH joins panels left-to-right
panelOriginal
.montageH(divider, panelSharpen)
.montageH(divider, panelEmboss)
.montageH(divider, panelEdge)
.montageH(divider, panelMotion)
.save(OUTPUT);
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0