# wall

URL: /docs/generators/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).

```ts
{
  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.

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` | `Column[]` |  | 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. _(required)_ |
| `columns.tiles` | `(string \| { src: string })[]` |  | 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. _(required)_ |
| `columns.direction` | `'up' \| 'down'` | `'down'` | Vertical scroll direction. Set "up" per column where you want it. |
| `columns.loops` | `number` |  | Continuous whole-clip scroll periods for this column. Omit to inherit motion.loops. |
| `columns.pulses` | `Pulse[]` |  | This column's pulses (see the Pulse shape below). Omit to inherit motion.pulses. |
| `columns.stagger` | `number (0–1)` | `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 }` | `{}` | Frame size + encoding. |
| `output.width` | `number` | `1920` | Output frame width in CSS px. |
| `output.height` | `number` | `1080` | Output frame height in CSS px. |
| `output.deviceScaleFactor` | `number` | `2` | Render scale, max 4 — higher captures crisper, then downscales into the video. 2 = retina-crisp. |
| `output.fps` | `number` | `30` | Output frames per second (max 120). |
| `output.crf` | `number` | `18` | x264 quality, 0–51 (lower = better quality / larger file). |
| `output.fileName` | `string` |  | Output filename. Defaults to "<slug(asset name)>.mp4". |
| `render` | `{ capture, workers, frameFormat }` | `{}` | Capture strategy + frame format. |
| `render.capture` | `'frames' \| 'realtime'` | `'frames'` | "frames" steps a virtual clock per frame — deterministic, crisp, parallelisable; "realtime" records the live scene once (faster — handy while iterating). |
| `render.workers` | `number` |  | 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. |
| `render.frameFormat` | `'jpeg' \| 'png'` | `'jpeg'` | Intermediate frame format (frames only). "jpeg" is faster; "png" is lossless. |
| `layout` | `{ background, gap, tileAspect, cornerRadius }` | `{}` | Backdrop, gaps, tile aspect + corners. |
| `layout.background` | `string` | `'#0b0b0f'` | Backdrop shown in the gutters and behind tiles. |
| `layout.gap` | `number` | `8` | Gap between columns and between stacked tiles (px). |
| `layout.tileAspect` | `number` | `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. |
| `layout.cornerRadius` | `number` | `6` | Tile corner radius (px). |
| `motion` | `{ durationMs, pan, loops, pulses }` | `{}` | Clip length + the uniform pulse model. |
| `motion.durationMs` | `number` | `16000` | Clip length (ms) — the whole loop. Tile videos should loop within a length that divides this. |
| `motion.pan` | `{ direction, loops, pulses }` | `no pan` | System 1 — the whole wall pans on X (columns are System 2, each scrolling on Y). Omit for no pan. |
| `motion.pan.direction` | `'left' \| 'right'` | `'left'` | Pan direction. |
| `motion.pan.loops` | `number` | `0` | Continuous whole-clip horizontal loops (0 = no pan unless pulses move it). |
| `motion.pan.pulses` | `Pulse[]` | `[]` | Pulses added on top of the base loops (same Pulse shape as below). |
| `motion.loops` | `number` | `0` | Default continuous whole-clip loops for columns that omit their own. 0 = static unless a pulse moves it. |
| `motion.pulses` | `Pulse[]` | `[]` | Default pulses for columns that omit their own — the uniform wall-level motion. Each entry is a Pulse: |
| `motion.pulses.at` | `number (0–1)` |  | When the pulse starts, as a fraction of the clip. _(required)_ |
| `motion.pulses.span` | `number (0–1)` |  | 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. _(required)_ |
| `motion.pulses.distance` | `number` |  | How far it travels, in periods (1 = one full tile-set / wrap). Usually 0–1. _(required)_ |
| `motion.pulses.easing` | `'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'ease-out-strong' \| 'ease-in-out-strong'` | `'ease-in-out'` | Easing of the move's ramp. |
| `preview` | `{ enabled, tiles }` | `{}` | Fast faux-tile preview mode. |
| `preview.enabled` | `boolean` | `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. |
| `preview.tiles` | `{ [name]: { color?, caption?, aspect? } }` | `{}` | Per-tile faux appearance for preview mode, keyed by tile name. Unlisted tiles auto-colour from their name and show that name. |
| `preview.tiles.color` | `string` |  | Box fill (any CSS colour). Omit to auto-derive a distinct colour from the tile name. |
| `preview.tiles.caption` | `string` |  | Caption shown under the name (e.g. "16:9") — purely cosmetic. |
| `preview.tiles.aspect` | `number` |  | 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. |

**TypeScript**

```ts
{
  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.

```ts
{
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,
    },
  ],
},
}
```

*Output: 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.

```ts
{
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,
    },
  ],
},
}
```

*Output: 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.

```ts
{
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,
    },
  ],
},
}
```

*Output: Five columns of phone clips (producer assets, by name) and campaign stills (\{ src }) — one gentle drift each, looping seamlessly.*
