wall
A seamless-looping media wall — columns of your image/video/screenshot assets, each scrolling on its own.
wall is a local generator — it takes no url. Instead it composites your assets into a marquee
of tiles: columns that pan horizontally while each column scrolls vertically on its own, looping
seamlessly. Each column lists the tiles it stacks — other assets by name (the wall derives its
dependencies from them, so those producers run first) and/or local files directly via { src }
(photos and clips straight from disk, no producer asset needed).
{
name: "lookbook-wall",
generator: "wall",
options: {
columns: [
{
tiles: [{ src: "img/coat.jpg" }, "ui-home"],
direction: "down",
},
{
tiles: [{ src: "img/crew.jpg" }, "ui-shop"],
direction: "up",
stagger: 0.4,
},
{
tiles: [{ src: "img/editorial.jpg" }, "ui-about"],
stagger: 0.15,
},
],
},
}Every tile fills its column's width and takes its own height from its media's aspect ratio — a 16:9 asset is short, a 9:16 asset is tall, all the same width — so the columns read as a natural masonry rather than a rigid grid. You don't set a tile size; the content decides it.
Motion is one uniform pulse model. A track's travel =
loopscontinuous whole-clip periods plus the sum of itspulses(each an eased move ofdistanceperiods, starting atat, lastingspan). The total is rounded up to a whole number of periods — the remainder folds into the continuous scroll — so every track lands back on its start at the clip's end: the wall always loops seamlessly, for anydurationMs.loopsdefaults to0, so a column is static unless it has a pulse or an explicitloops; adding a single pulse makes the column travel exactly one loop.
Config options
Only columns is required. Reference is the interactive view; TypeScript is the same shape
in code, every option at its default.
columnsColumn[]required
The columns (≥3) — its length is the column count (fewer columns = larger tiles). Each entry owns both its content (tiles) and its own motion (direction / loops / pulses); omitted motion fields inherit the wall-level defaults.
tiles(string | { src: string })[]requiredTiles stacked in this column top→bottom, cycled to fill (and scroll through) the height (≥1): other assets by name and/or local files via { src: "path" }. Each keeps the column width and takes its own height from its media.
direction'up' | 'down'default 'down'Vertical scroll direction. Set "up" per column where you want it.
loopsnumberContinuous whole-clip scroll periods for this column. Omit to inherit motion.loops.
pulsesPulse[]This column's pulses (see the Pulse shape below). Omit to inherit motion.pulses.
staggernumber (0–1)default 0Constant start-position shift, as a fraction of one tile-set — de-aligns columns with similar content (e.g. an all-image top row). A fixed phase offset, so it preserves the loop.
output{ width, height, deviceScaleFactor, fps, crf, fileName }default {}
Frame size + encoding.
widthnumberdefault 1920Output frame width in CSS px.
heightnumberdefault 1080Output frame height in CSS px.
deviceScaleFactornumberdefault 2Render scale, max 4 — higher captures crisper, then downscales into the video. 2 = retina-crisp.
fpsnumberdefault 30Output frames per second (max 120).
crfnumberdefault 18x264 quality, 0–51 (lower = better quality / larger file).
fileNamestringOutput filename. Defaults to "<slug(asset name)>.mp4".
render{ capture, workers, frameFormat }default {}
Capture strategy + frame format.
capture'frames' | 'realtime'default 'frames'"frames" steps a virtual clock per frame — deterministic, crisp, parallelisable; "realtime" records the live scene once (faster — handy while iterating).
workersnumberParallel frame-render workers (frames only), each its own browser context. Each warms every tile video's decoder before capture, so video-heavy walls parallelise safely. Omit to auto-pick from cores + free memory.
frameFormat'jpeg' | 'png'default 'jpeg'Intermediate frame format (frames only). "jpeg" is faster; "png" is lossless.
layout{ background, gap, tileAspect, cornerRadius }default {}
Backdrop, gaps, tile aspect + corners.
backgroundstringdefault '#0b0b0f'Backdrop shown in the gutters and behind tiles.
gapnumberdefault 8Gap between columns and between stacked tiles (px).
tileAspectnumberdefault 0.75Fallback aspect (w/h) only — real tiles use their media's own aspect. Used for faux (preview) tiles that don't set their own aspect. 0.75 = 3:4 portrait.
cornerRadiusnumberdefault 6Tile corner radius (px).
motion{ durationMs, pan, loops, pulses }default {}
Clip length + the uniform pulse model.
durationMsnumberdefault 16000Clip length (ms) — the whole loop. Tile videos should loop within a length that divides this.
pan{ direction, loops, pulses }default no pan
System 1 — the whole wall pans on X (columns are System 2, each scrolling on Y). Omit for no pan.
direction'left' | 'right'default 'left'Pan direction.
loopsnumberdefault 0Continuous whole-clip horizontal loops (0 = no pan unless pulses move it).
pulsesPulse[]default []Pulses added on top of the base loops (same Pulse shape as below).
loopsnumberdefault 0Default continuous whole-clip loops for columns that omit their own. 0 = static unless a pulse moves it.
pulsesPulse[]default []
Default pulses for columns that omit their own — the uniform wall-level motion. Each entry is a Pulse:
atnumber (0–1)requiredWhen the pulse starts, as a fraction of the clip.
spannumber (0–1)requiredHow long the move takes, as a fraction of the clip. If at + span > 1, the start shifts back so it ends exactly at the loop point (a 0.2 pulse at 0.9 starts at 0.8) — a pulse can never overrun the clip.
distancenumberrequiredHow far it travels, in periods (1 = one full tile-set / wrap). Usually 0–1.
easing'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'ease-out-strong' | 'ease-in-out-strong'default 'ease-in-out'Easing of the move's ramp.
preview{ enabled, tiles }default {}
Fast faux-tile preview mode.
enabledbooleandefault falsePreview mode: render every tile as a flat labeled colour box (see preview.tiles) instead of the real assets. No producers run, so the wall renders in seconds — dial in layout + motion, then turn it off for the real render.
tiles{ [name]: { color?, caption?, aspect? } }default {}
Per-tile faux appearance for preview mode, keyed by tile name. Unlisted tiles auto-colour from their name and show that name.
colorstringBox fill (any CSS colour). Omit to auto-derive a distinct colour from the tile name.
captionstringCaption shown under the name (e.g. "16:9") — purely cosmetic.
aspectnumberFaux tile aspect (w/h): 1.78 = 16:9 (short), 0.56 = 9:16 (tall), 1 = square. Omit to use layout.tileAspect — set it to mirror the final masonry.
{
name: "lookbook-wall",
generator: "wall", // local — no url; deps derived from column tiles
options: {
// --- columns (≥3): each its own tiles + motion ---
columns: [
{
tiles: [{ src: "img/coat.jpg" }, "ui-home"],
direction: "down",
},
{
tiles: [{ src: "img/crew.jpg" }, "ui-shop"],
direction: "up",
stagger: 0.4,
},
{
tiles: [{ src: "img/editorial.jpg" }, "ui-about"],
stagger: 0.15,
},
// per-column overrides: loops, pulses: [{ at, span, distance, easing }]
],
// --- output: sizing & encoding ---
output: {
width: 1920,
height: 1080,
deviceScaleFactor: 2,
fps: 30,
crf: 18,
// fileName: "lookbook-wall.mp4", // optional; defaults to <slug(name)>.mp4
},
// --- render: capture strategy & frame format ---
render: {
capture: "frames",
// workers: 4, // optional; auto-picks from cores + free memory
frameFormat: "jpeg",
},
// --- layout ---
layout: {
background: "#0b0b0f",
gap: 8,
tileAspect: 0.75,
cornerRadius: 6,
},
// --- motion (uniform pulse model) ---
motion: {
durationMs: 16000,
// pan: { direction: "left", loops: 1, pulses: [] }, // optional; whole-wall X pan
loops: 0, // default column loops (0 = static unless a pulse moves it)
pulses: [], // default column pulses: [{ at, span, distance, easing }]
},
// --- preview (fast faux-tile mode) ---
preview: {
enabled: false,
tiles: {}, // e.g. { "ui-home": { color: "#7a5234", caption: "16:9", aspect: 1.78 } }
},
},
}The schema is strict — an unknown key (a typo, or a stale option name) is rejected with an error rather than silently ignored. Every option also has hover docs in
pro-visu.config.ts.
Test mode (fast preview)
Dialing in columns, motion, and stagger against real assets is slow — every tile has to be generated
first. Flip preview: { enabled: true } (and capture: "realtime" to record the scene once) and
the wall renders every tile as a flat labelled colour box, no producers, in seconds. Dial in the
layout and motion here, then drop preview/realtime for the real frame-stepped render.
{name: "lookbook-wall",generator: "wall",options: { // faux labelled boxes, no producers preview: { enabled: true }, // record the scene once render: { capture: "realtime" }, output: { width: 960, height: 540 }, // 9:16 tiles on a dark ground, small gaps + rounded corners layout: { background: "#1a1714", gap: 4, cornerRadius: 4, tileAspect: 0.5625 }, motion: { durationMs: 8000, loops: 1 }, columns: [ { tiles: ["home", "pricing"], direction: "down", }, { tiles: ["product", "lookbook"], direction: "up", stagger: 0.4, }, { tiles: ["about", "contact"], direction: "down", stagger: 0.2, }, ],},}Examples
A simple wall
Three columns of photos as direct { src } tiles — no producer assets at all — each with one gentle
drift, looping seamlessly.
{name: "lookbook-wall",generator: "wall",options: { output: { width: 960, height: 540 }, layout: { background: "#1a1714", gap: 4, cornerRadius: 4 }, // one gentle drift over the clip motion: { durationMs: 8000, loops: 1 }, columns: [ { tiles: [{ src: "img/coat.jpg" }, { src: "img/editorial.jpg" }], direction: "down", }, { tiles: [{ src: "img/hero.jpg" }, { src: "img/tote.jpg" }], direction: "up", stagger: 0.4, }, { tiles: [{ src: "img/slip.jpg" }, { src: "img/atelier.jpg" }], direction: "down", stagger: 0.2, }, ],},}Screens + stills (producer tiles)
The full pattern: phone-sized interaction clips referenced by name (the wall runs those
producers first) mixed with full-res photos via { src }. Clip lengths (8s / 12s) divide the 24s
wall, so every tile loops cleanly inside the seamless loop.
{name: "lookbook-wall",generator: "wall",options: { layout: { background: "#1a1714", gap: 2, cornerRadius: 0 }, // one drift per column; 24s divides the 8s/12s tile clips motion: { durationMs: 24000, loops: 1 }, columns: [ { tiles: ["clip-cart", { src: "img/hero.jpg" }], direction: "down", }, { tiles: ["clip-menu", { src: "img/editorial.jpg" }], direction: "up", stagger: 0.42, }, { tiles: ["clip-wishlist", { src: "img/atelier.jpg" }], direction: "down", stagger: 0.68, }, { tiles: ["clip-size", { src: "img/tote.jpg" }], direction: "up", stagger: 0.18, }, { tiles: ["clip-cart-trouser", "clip-wishlist-slip"], direction: "down", stagger: 0.54, }, ],},}

