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
INPUT — Custom convolution kernels — sharpen, emboss, edge-detect, motion blur
OUTPUT
OUTPUT — Custom convolution kernels — sharpen, emboss, edge-detect, motion blur
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