Montage grid of effect variants

Generates a 3x3 contact sheet from a single input image by applying various effects (grayscale, sepia, invert, etc.) and stitching them into a grid with customizable padding and background color.

INPUT
INPUT — Montage grid of effect variants
OUTPUT
OUTPUT — Montage grid of effect variants
JavaScript
// Montage grid of effect variants
// demo_contact_sheet.js
//!OUTPUT: OUTPUT
//!PARAM: COLS:integer=3,min=2,max=100, PADDING:integer=8,min=0,max=200, BG_COLOR:string=#1a1a2e

// demo_contact_sheet.js — montage grid of effect variants
//
// Takes one input image, applies a set of looks, then stitches them into
// a grid using montageH (row) + montageV (combine rows).
// Padding between cells is added with padLeft / padTop on each cell.
// The background color comes from BG_COLOR (CSS hex).

const src = Engine.loadImage(INPUT);

const COLS_N  = Math.max(1, Math.round(COLS));
const PAD     = Math.max(0, Math.round(PADDING));
const BG      = BG_COLOR || '#1a1a2e';

// --- produce the effect variants ---
const cells = [
  src.clone(),                                        // 0  original
  src.clone().grayscale(),                            // 1  B&W
  src.clone().sepia(),                                // 2  sepia
  src.clone().brightness(1.25).saturation(1.5),       // 3  vivid
  src.clone().invert(),                               // 4  negative
  src.clone().hue(180).saturation(1.3),               // 5  complementary hue
  src.clone().posterize(5),                           // 6  poster
  src.clone().threshold(0.5).grayscale(),             // 7  binary
  src.clone().pixelate(8),                            // 8  mosaic
];

// add pad between cells (left + top) so there's gutter space
const padded = cells.map(img =>
  img.padLeft(PAD, BG).padTop(PAD, BG)
);

// split into rows of COLS_N, then combine rows vertically
const rows = [];
for (let i = 0; i < padded.length; i += COLS_N) {
  const row = padded[i].montageH(...padded.slice(i + 1, i + COLS_N));
  rows.push(row);
}
const sheet = rows[0].montageV(...rows.slice(1));

// add trailing right + bottom border to close the grid
sheet.padRight(PAD, BG).padBottom(PAD, BG);

sheet.save(OUTPUT);

// Free everything explicitly so the per-iteration RSS in batch /
// live mode stays flat. The FinalizationRegistry would catch these
// on the next GC sweep, but explicit .free() is the convention in
// our reference samples — readers learn the cleanup pattern.
//
// padLeft / padTop / padRight / padBottom are in-place (return
// `this`), so `padded[i]` and `cells[i]` are the same handle — free
// once. montageH / montageV allocate a fresh handle each.
src.free();
for (const c of cells) c.free();
for (const r of rows) r.free();
sheet.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0