pro-visu docs
Generators

wall

A seamless-looping media wall — columns of your image/video/screenshot assets, each scrolling on its own.

For AI / LLMs: View Markdown

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 = loops continuous whole-clip periods plus the sum of its pulses (each an eased move of distance periods, starting at at, lasting span). 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 any durationMs. loops defaults to 0, so a column is static unless it has a pulse or an explicit loops; 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 })[]required

Tiles 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.

loopsnumber

Continuous 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 0

Constant 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 1920

Output frame width in CSS px.

heightnumberdefault 1080

Output frame height in CSS px.

deviceScaleFactornumberdefault 2

Render scale, max 4 — higher captures crisper, then downscales into the video. 2 = retina-crisp.

fpsnumberdefault 30

Output frames per second (max 120).

crfnumberdefault 18

x264 quality, 0–51 (lower = better quality / larger file).

fileNamestring

Output 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).

workersnumber

Parallel 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 8

Gap between columns and between stacked tiles (px).

tileAspectnumberdefault 0.75

Fallback 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 6

Tile corner radius (px).

motion{ durationMs, pan, loops, pulses }default {}

Clip length + the uniform pulse model.

durationMsnumberdefault 16000

Clip 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 0

Continuous 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 0

Default 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)required

When the pulse starts, as a fraction of the clip.

spannumber (0–1)required

How 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.

distancenumberrequired

How 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 false

Preview 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.

colorstring

Box fill (any CSS colour). Omit to auto-derive a distinct colour from the tile name.

captionstring

Caption shown under the name (e.g. "16:9") — purely cosmetic.

aspectnumber

Faux 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,    },  ],},}
preview + capture: realtime — faux boxes labelled with their tile names, no producers, rendered in seconds.

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,    },  ],},}
Three columns of photos as direct { src } tiles — no producer assets at all.

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,    },  ],},}
Five columns of phone clips (producer assets, by name) and campaign stills ({ src }) — one gentle drift each, looping seamlessly.

On this page