# Cardboard API

For agents: start with [llm.txt](https://cardboard-api.pages.dev/llm.txt) for the short contract, readable request example, and documentation links. The same guide is available at `/llms.txt`.

A stateless renderer with a static HTML playground. An external application supplies a scene request; the browser lays it out, loads card artwork, and can export a PNG or SVG. Game state, identity, permissions, and rules belong to the caller. No runtime dependencies, application server, database, or account system are required. Optional Scryfall cards load directly from Scryfall with a three-day browser cache.

Open **index.html** directly, or run **`npm start`** / **`node serve.mjs`** and visit **http://127.0.0.1:8765**. An optional port can be supplied with `node serve.mjs 9000`. Serving locally is useful for browser integrations, but isn't required for the workbench. Use a current Chrome, Firefox, or Safari.

## Static URL API

**`render.html`** is the production render surface. It reads its request from the URL fragment and never loads a saved playground scene. Without a request it shows the default 512 × 512 example. Each request fully determines the arrangement. The same scene schema used by the existing JavaScript API works here, including dimensions, themes, Scryfall IDs, zone focus, and player framing.

Create a URL without loading any of our JavaScript:

```js
const request = {
  stage: { width: 512, height: 512, theme: 'evergreen' },
  players: 2,
  view: { mode: 'player', viewerId: 'p1', playerScope: 'table' }
};
const url = new URL('https://your-site.example/render.html');
url.hash = new URLSearchParams({ config: JSON.stringify(request) }).toString();
// Navigate to url.href, or use it as an iframe src.
```

Or use our helper for a shorter, compressed link:

```js
const url = await Tabletop.createRenderURL(request);
// Default baseURL is render.html alongside the loaded browser scripts.

const embedded = await Tabletop.createRenderURL(request, {
  baseURL: 'https://your-site.example/render.html',
  embed: true // ?embed=1 hides all controls and fits the scene to the viewport
});

// Select a player and either existing framing:
const playerURL = await Tabletop.createRenderURL(
  Tabletop.forPlayer(request, 'p2', { scope: 'self' })
);
```

Node scripts can import `createRenderURL` from `tabletop.mjs`; provide an absolute `baseURL` there. Creating links performs no artwork fetches. Relative asset paths resolve correctly when the site is hosted in a subdirectory or opened through `file://`.

| Surface | Purpose |
| --- | --- |
| `render.html#config=<URL-encoded JSON>` | Simple request URL from any application or language |
| `render.html#scene=v1.gz.<base64url>` | Compressed request generated by `createRenderURL` |
| `render.html#scene=v1.json.<base64url>` | Uncompressed UTF-8 request token; generated with `{compress: false}` |
| `render.html?embed=1#scene=…` | Scene only, suitable for an iframe |
| `index.html#page=stage&scene=…` | Editable playground request, updated as controls change |

Tokens use gzip and base64url with no padding. The browser's native compression APIs provide gzip; encoding falls back to uncompressed tokens if compression is unavailable. `encodeRequest(config)` and `decodeRequest(token)` expose the codec; `readRenderURL(url)` accepts either URL form and returns a normalized request or `null` when none is supplied. Unknown versions, invalid configurations, damaged links, and failed assets produce explicit errors.

The URL renderer supplies **HTML rendered in a browser**, not an HTTP response with `image/png` content. Its Download PNG and SVG buttons generate files locally. Automation can await `Tabletop.viewer.ready`, which resolves to dimensions/metrics after the current scene and artwork finish loading, or rejects on failure. It is replaced when the fragment changes. `document.body.dataset.renderStatus` is `loading`, `ready`, or `error`; a `tabletop:ready` event also fires. `Tabletop.viewer.state` is a copy of the request, suitable for `Tabletop.toPNG()` or `Tabletop.toSVG()`.

Links contain the supplied scene data; compression is not encryption or redaction. The caller must omit information the recipient may not access. `forPlayer()` changes what is drawn, and **does not remove other hands or deck identities from the request**. The render page does not authenticate users or maintain game state. A link is a reproducible request, not a live game session.

Requests are capped at 1,000,000 decoded JSON bytes and complete URLs at 64,000 characters. These are application limits, not guarantees that every browser or messaging app accepts a link that long. Prefer Scryfall IDs or HTTP(S) artwork URLs. Uploaded image bytes and very large scenes are better kept as JSON files for the playground or passed directly to the JavaScript rendering API. The encoder never drops data to shorten a link. If a playground edit exceeds the URL limit, its stale request is removed from the address bar and the scene remains available for JSON export.

## Static hosting

Run **`npm run build`** to create **`dist/`** with only browser assets, examples, and this documentation. On Cloudflare Pages, use `npm run build` as the build command and `dist` as the output directory; the same directory can be served by Workers Static Assets or any ordinary static host. No Worker function or Durable Object is involved. Node is only a local build/preview convenience. Manual deployment can copy the same files without a build tool.

### Connect this repository to Cloudflare Pages

In Cloudflare, create a **Pages** application and connect the private GitHub repository **`kodareef5/cardboard-api`**. Grant the Cloudflare GitHub integration access to this repository, then use:

| Setting | Value |
| --- | --- |
| Project name | `cardboard-api` |
| Production branch | `main` |
| Framework preset | None |
| Root directory | Repository root (leave blank) |
| Build command | `npm run build` |
| Build output directory | `dist` |

No environment variables or application secrets are required. Pages builds the browser assets from the repository on each connected branch's push. The playground is at `/`, and the standalone URL renderer is at `/render.html`. See [Cloudflare's Git integration guide](https://developers.cloudflare.com/pages/get-started/git-integration/) for connecting a private repository.

Both `index.html` and `render.html` are real files, so fragment URLs require no special route rewrites. Scryfall and custom remote art still require a network connection unless cached. The existing browser cache remains per browser/origin. Remote images may display while disallowing PNG export through CORS; Scryfall export is supported and custom hosts must allow fetching artwork.

## The workbench

- **Stage:** dimensions from 64–4096 px per side; square/SMS, portrait/social, and presentation presets; four themes; texture; density; stats; zone labels; and a preview-only safe-area guide. Scene zoom changes the rendered composition. Preview fitting only changes the size it is displayed on your screen.
- **Players:** 1–8 players, names, colors, life, score, and a draw action.
- **Player images:** choose **Player view**, select the recipient, and choose **Everyone’s board + own hand** or **Own board + hand**. The recipient's board and hand get the prominent panel. Under **Players → Player images**, export the selected player, all players in the current version, or both versions for everyone. Batches download as one PNG ZIP with numbered filenames and a manifest.
- **Zones:** per-player counts from 0–1,000,000, custom areas (up to 8 per player), stacks, fans, grids, face visibility, and card transfers. Click a zone in the preview to focus it. Overview shows all players.
- **Cards:** a standard 52-card deck and an original illustrated demo; add, edit, delete, search, and import cards. Large catalogs are paginated. Import accepts a deck object, an array of card objects, or an array of names. Missing card IDs are assigned on file import.
- **API:** editable live JSON configuration, working JavaScript example, and recent state transitions.
- **Render link:** create a standalone or embedded URL for the current request. The playground address bar also tracks changes. URL requests take precedence over locally saved scenes when reopening the playground.
- **Export:** exact-size PNG, portable SVG, or reusable scene JSON. Changes persist in browser storage when available. Save scene is the portable backup; browser storage is specific to the file path/origin and can be cleared by the browser. Undo/redo retain the last 40 commands for the current session.

Standard, Poker, and Commander are **visual setup presets**, all using the same scene. They don't implement game rules, shared poker community-card logic, legal deck sizes, or turn order. Every player's table is an independent fixture. Player-image visibility is enforced while rendering; the studio itself is an operator tool with the complete game state.

## Browser API

The workbench exposes `window.Tabletop`. Its own live engine is available as `Tabletop.studio`; use `createEngine` for an independent engine.

```js
const engine = Tabletop.createEngine({
  stage: { width: 512, height: 512, theme: 'evergreen' },
  players: 2,
  seed: 42
});

engine.dispatch({
  type: 'view.update',
  payload: { mode: 'focus', playerId: 'p1', zoneId: 'hand' }
});

const svg = Tabletop.render(engine.state); // Synchronous SVG string
const portable = await Tabletop.toSVG(engine.state); // Embeds artwork
const png = await Tabletop.toPNG(engine.state); // PNG Blob
Tabletop.download(png, 'hand.png');

const unsubscribe = engine.subscribe((snapshot, event) => {
  console.log(event.type, event.revision, snapshot);
});
engine.undo();
engine.redo();
unsubscribe();
```

You can also call `Tabletop.render(config)` or `Tabletop.toPNG(config)` without creating an engine. `renderDetailed(config)` returns `{svg, metrics}` with total card counts and the number actually drawn. All engine snapshots are copies. Commands validate their complete candidate state before committing, emitting an event, or entering history. Invalid commands throw and leave state unchanged. Importing a valid configuration is an undoable `state.replace` command.

To embed this in another static page, load `js/engine.js`, `js/perspective.js`, `js/layout.js`, `js/scryfall.js`, and `js/renderer.js` in that order. Add `js/archive.js` and `js/export.js` for browser image and ZIP export, and `js/url.js` for URL helpers. The playground in `js/app.js` and standalone viewer in `js/viewer.js` are optional interfaces over the same renderer.

## Images for each player

Both versions use the same saved game state and visibility rules:

- `scope: 'table'`: all player boards, with a larger panel for the recipient's own board and hand. Concealed opponent hands/libraries appear as counts; their card identities aren't rendered. Cards in public zones remain visible.
- `scope: 'self'`: only the recipient's zones, including board, hand, library, graveyard, and custom areas. Their board and hand use the extra space.
- `scope: 'both'`: available for batch operations; produces both of the above for every player.

```js
// One recipient, whole table + their own hand
const view = Tabletop.forPlayer(engine.state, 'p2', { scope: 'table' });
const png = await Tabletop.toPNG(view);
Tabletop.download(png, 'player-2-table.png');

// Just that player's board + hand (synchronous; also works in Node)
const svg = Tabletop.renderPlayer(engine.state, 'p2', { scope: 'self' });

// A PNG Blob per player, or two per player with scope: 'both'
const images = await Tabletop.exportPlayers(engine.state, {
  format: 'png', // or 'svg' for embedded, portable SVGs
  scope: 'both',
  onProgress(done, total, image) { console.log(done, total, image.playerName); }
});
// Each entry: {playerId, playerName, scope, filename, blob}

// Download one ZIP instead of triggering multiple browser downloads
const zip = await Tabletop.toPlayerZIP(engine.state, { scope: 'both' });
Tabletop.download(zip, 'players.zip');

// Native Node: SVG strings with metrics, filenames, and recipient metadata
const rendered = Tabletop.renderPlayers(engine.state, { scope: 'both' });
```

The helpers take a snapshot and do not change the engine, active player, or preview. Batch requests are fixed before the first image is exported. A shared artwork cache avoids fetching the same visible art repeatedly. ZIPs include a `manifest.json` mapping files to players and scope; they do not include the scene JSON or card catalog. Filenames have a player-number prefix so duplicate names cannot overwrite each other.

Set **Zones → Card faces in player images** for each zone:

| `zone.visibility` | Who sees its card faces? |
| --- | --- |
| `public` | Every recipient; the default for boards, graveyards, and special areas |
| `owner` | Only the player who owns this zone; the default for hands |
| `hidden` | No recipient; the default for libraries |

```js
engine.dispatch({
  type: 'zone.update',
  payload: { playerId: 'p2', zoneId: 'battlefield', visibility: 'owner' }
});
```

Zone counts and names remain visible. Concealed cards contribute no card names, IDs, or artwork URLs to the SVG and their artwork is not fetched during export. `view.viewerId` is independent of the selected `view.playerId`: focusing an opponent's hand preserves the recipient and conceals those faces. Returning to **Overview** clears the audience and restores the operator view, which uses the older `faceDown` setting. **Face down in operator view** is separate from audience policy; a player's own hand is revealed in their image even if it was hidden in the operator's overview.

Existing saved scenes acquire these defaults on load: hands are owner-only, other face-down zones are hidden, and other zones are public. Explicit visibility settings are preserved. Use the individual images for players; the studio, `forPlayer()` configuration, and scene JSON contain the full operator state. A batch ZIP contains every recipient's image. This is rendering policy, not an authenticated multiplayer server. Additional registered renderers must honor `Tabletop.canSeeFaces(state, player, zone)` to provide the same visibility behavior.

## Node API and image script

Node 20+ can produce SVG without a browser or npm install:

```sh
node render.mjs --output scene.svg
node render.mjs --config examples/focused-hand.json --output hand.svg
node render.mjs --player p2 --scope self --output player-2.svg
node render.mjs --all-players --scope both --output player-images
npm test
```

```js
import Tabletop from './tabletop.mjs';
import { writeFile } from 'node:fs/promises';

await writeFile('scene.svg', Tabletop.render({
  setup: 'commander',
  stage: { width: 1920, height: 1080, theme: 'midnight' },
  view: { density: 'dense' }
}));
```

The CLI writes SVG; PNG uses the browser's native canvas API. Raw SVG output preserves external artwork URLs. Use the browser's `toSVG()` for a portable file with artwork embedded. `serve.mjs` is only a local static file server. The deployed interface is `render.html` plus its URL request; image bytes are produced in the browser.

## Configuration

Minimal configurations are expanded by `Tabletop.normalize(config)`. Use `Tabletop.toConfig(engine.state)` for a portable configuration; unchanged built-in decks are omitted and restored by ID. Modified built-in decks and imported decks are included. `version: 1` identifies the serialized schema.

| Field | Values |
| --- | --- |
| `scene` | `"tabletop"`, or a registered scene ID |
| `setup` | `"standard"`, `"poker"`, `"commander"` |
| `stage` | `width`, `height`, `theme`, `texture` |
| `stage.theme` | `"evergreen"`, `"midnight"`, `"parchment"`, `"plum"` |
| `players` | Integer 1–8 for generated players, or full player objects |
| `view` | `mode`, `playerId`, `zoneId`, `viewerId`, `playerScope`, `zoom`, `density`, `stats`, `labels` |
| `view.mode` | `"overview"`, `"focus"`, or `"player"` |
| `view.viewerId` | Recipient player ID, or `null` for an operator view |
| `view.playerScope` | `"table"` or `"self"`; controls player-image framing |
| `view.zoom` | Number 0.6–1.8; larger values intentionally crop the scene |
| `view.density` | `"calm"`, `"balanced"`, `"dense"` |
| `view.stats` | `"hidden"`, `"compact"`, `"full"` |
| `seed` | Integer 0–2147483646 |
| `activeDeckId` | The ID of a deck in `decks`, or `"standard"` / `"arcana"` |
| `decks` | Deck objects to add or override, up to 30 including built-ins |

A full player object has `id`, `name`, six-digit hex `color`, numeric `stats`, and `zones`. Stats default to life/score. Compact shows the first stat; full shows two labeled stats in overview and up to six when focused. Zone IDs are local to a player. The same zone IDs can be reused across players.

```json
{
  "id": "p1",
  "name": "Alice",
  "color": "#e2b878",
  "stats": { "life": 40, "score": 3 },
  "zones": [{
    "id": "hand", "name": "Hand", "count": 7,
    "layout": "fan", "faceDown": false, "visibility": "owner",
    "cardIds": ["A-spades", "K-hearts"], "offset": 0
  }]
}
```

`count` is the total, including explicitly assigned `cardIds`. Remaining positions resolve through a deterministic shuffled deck cycle keyed by seed, player, and zone. This permits very large visual fixtures with constant-size state rather than millions of card instances. Different zones can contain the same card; this is a layout sandbox, not deck-conservation enforcement. Explicit card references must exist in the active deck. Changing active decks or shuffling the sample clears explicit references.

`zone.transfer` moves from the front of one zone to the front of another, preserving card identities and remaining order, even between players. Counts are conserved. It moves up to 1,000 cards per command and zones hold at most 10,000 explicit references. Larger aggregate counts remain implicit. Deck editing can change how unassigned sample positions resolve; freeze positions with `cardIds` when exact identities matter.

## Cards and artwork

Import `examples/custom-deck.json`, or use this shape:

```json
{
  "id": "my-deck",
  "name": "My illustrated deck",
  "cards": [
    { "id": "sun", "name": "The Sun", "artUrl": "https://example.com/sun.jpg" },
    { "id": "ace", "name": "Ace of Spades", "rank": "A", "suit": "spades" }
  ]
}
```

Cards need a unique `id` and `name`. Optional fields are `rank`, `suit` (`spades`, `hearts`, `clubs`, `diamonds`), and `artUrl`. The `art` integer (0–3) selects one of the original vector demo illustrations. Artwork takes precedence over playing-card styling. A deck may contain 1–5,000 definitions. Custom card names and URLs are escaped before inclusion in SVG.

Artwork can use HTTP(S) URLs or base64 PNG/JPEG/WebP/GIF/AVIF data URLs. Uploaded images are stored as data URLs in the deck (5 MB per upload). A remote image may display in the preview but still disallow export through CORS. Portable SVG/PNG export fetches and embeds visible art, verifies each image can be decoded, omits credentials, and limits fetch concurrency to four. If a host blocks embedding or an image is unreadable, the export reports an error; upload a valid image or use a CORS-enabled host. Broken preview art reveals the demo placeholder underneath. Artwork that is currently face down isn't fetched for export. Avoid large uploaded decks if you rely on browser local storage; JSON backup is available even when storage is full.

## Magic: The Gathering via Scryfall

In **Cards → Add a card**, choose **Magic: The Gathering · Scryfall**, paste a printing's Scryfall ID, and select **Load card**. Select **Back** for the reverse face of a double-faced card. Import `examples/scryfall-deck.json` through **Import deck**, or load `examples/scryfall-scene.json` in the API editor to try a complete scene. The example repeats one printing as a layout fixture.

Only `scryfallId` is required per card. `id` defaults to that UUID (with `-back` for a back-face definition); `name` is optional and fetched for display. Give repeated definitions distinct local `id` values, or reference the same card ID more than once in a zone's `cardIds`. A list of UUID strings also works in a deck's `cards` array. Use a **Scryfall printing ID**, not an Oracle ID or a deck ID.

```js
const scene = {
  players: 2,
  activeDeckId: 'mtg',
  decks: [{
    id: 'mtg', name: 'Magic: The Gathering',
    cards: [{ scryfallId: '683a5707-cddb-494d-9b41-51b4584ded69' }]
  }]
};

// Browser: existing exports automatically resolve and embed Scryfall artwork.
const view = Tabletop.forPlayer(scene, 'p1', { scope: 'table' });
const png = await Tabletop.toPNG(view);
const zip = await Tabletop.toPlayerZIP(scene, { scope: 'both' });

// Browser or Node: await this when the scene contains Scryfall IDs.
const svg = await Tabletop.renderAsync(view);
// Or {svg, metrics, assets}:
const result = await Tabletop.renderDetailedAsync(view);
```

`Tabletop.render()` remains synchronous: it uses already loaded Scryfall images, or displays a loading placeholder on a cache miss. `renderAsync()`, browser exports, the workbench, and the CLI resolve artwork before producing final output. Node exports the same async functions from `tabletop.mjs`:

```sh
node render.mjs --config examples/scryfall-scene.json --output mtg.svg
node render.mjs --config examples/scryfall-scene.json --all-players --scope both --output player-images
```

Card metadata and image bytes each have a **72-hour TTL from their successful fetch**. Cache hits don't extend that deadline; expired entries refresh on next use. Browser entries persist in IndexedDB per origin, independently of scene JSON. Node uses `.cache/scryfall/` next to the project; set `TABLETOP_SCRYFALL_CACHE` to change the directory. These are separate local caches. When persistent storage is unavailable, the browser falls back to memory for the current page. Use **Cards → Clear card cache** or `await Tabletop.scryfall.clear()` to invalidate that client's cache. Browser storage may also be evicted by the browser.

Exports fetch only distinct card faces actually drawn in the requested perspective, respecting public/owner-only/concealed zones and bounded layout samples. Batches reuse assets across recipients. Requests for the same data are coalesced, API requests are spaced at least 120 ms apart, and a 429 response stops queued lookups for at least the server's `Retry-After` period. Failed requests aren't cached; failed or expired artwork causes an explicit export error rather than a successful placeholder export. Scene snapshots retain lightweight IDs; assets aren't written into saved game state.

Scryfall faces render as full, proportionally scaled card images, including their artist/copyright lines. Custom art remains a separate card source. Image variants and double-faced imagery follow [Scryfall's image documentation](https://scryfall.com/docs/api/images); requests follow its [API guidelines](https://scryfall.com/docs/api). Magic: The Gathering card imagery is copyright Wizards of the Coast; Scryfall and Wizards do not endorse this workbench.

## Services and next scenes

| File | Responsibility |
| --- | --- |
| `js/engine.js` | State schema, validation, deck definitions, seeded card resolution, commands, snapshots, history |
| `js/perspective.js` | Audience rules, player-image configurations, batch requests, and unique filenames |
| `js/layout.js` | Stage geometry, player/zone panels, card placement and overflow bounds |
| `js/scryfall.js` | ID resolution, throttled requests, three-day metadata/art cache, IndexedDB adapter |
| `scryfall-cache.mjs` | Persistent Node disk cache, shared by the CLI and Node API |
| `js/renderer.js` | Scene registry, tabletop scene, original card artwork, deterministic SVG |
| `js/export.js` | Browser asset embedding, PNG rasterization, downloads |
| `js/archive.js` | Dependency-free ZIP packaging for player-image batches |
| `js/url.js` | Versioned request codec, bounded compression/decompression, URL generation and parsing |
| `js/viewer.js` | Independent render page, readiness signal, browser image downloads |
| `js/app.js` | Control panel, card manager, API editor, local persistence |
| `tabletop.mjs` | Native Node entry point for the same services |
| `scripts/build-static.mjs` | Packages only browser assets into `dist/` |

These are in-process services with explicit inputs/outputs. Rendering doesn't mutate state. The UI uses the public commands rather than changing the model directly. The renderer draws bounded samples: up to 24 cards per overview zone or 60 in focus, depending on density; stack illustrations show at most five layers. Labels retain total counts and other layouts show overflow badges. Output geometry adapts to the aspect ratio; very small outputs or very large player counts naturally need zone focus for readable card details.

Add a scene without changing the engine:

```js
Tabletop.registerScene('my-scene', state => ({
  svg: '<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>',
  metrics: { renderedCards: 0, totalCards: 0,
    width: state.stage.width, height: state.stage.height }
}));
const svg = Tabletop.render({ scene: 'my-scene' });
```

Scene renderer functions are trusted application code. The engine accepts scene IDs independently; a renderer must be registered before rendering that ID. The workbench checks imported configurations are renderable before applying them. A new scene can reuse the existing layout service or provide its own. Next additions can be a shared poker-table layout, a detailed battlefield view, richer stats, or card-level transformations, all driven by caller-supplied requests.

## Command reference

Call `engine.dispatch({ type, payload })`. All commands return a new snapshot and emit `{type, revision}`.

| Command | Payload |
| --- | --- |
| `stage.update` | Any stage fields |
| `view.update` | Any view fields |
| `player.count` | `{ count }` |
| `player.update` | `{ playerId, name?, color?, stats? }` |
| `zone.update` | `{ playerId, zoneId, name?, count?, layout?, faceDown?, visibility?, cardIds? }` |
| `zone.add` | `{ playerId, id, name, count?, layout? }` |
| `zone.remove` | `{ playerId, zoneId }` |
| `zone.transfer` | `{ from: {playerId, zoneId}, to: {playerId, zoneId}, count?: 1 }` |
| `deck.select` | `{ id }` |
| `deck.import` | `{ deck }` (selects it; an existing ID is replaced) |
| `card.upsert` | `{ deckId?, card }` |
| `card.remove` | `{ deckId?, id }` |
| `setup.load` | `{ setup }` (resets players/zones and active deck, preserves stage/deck catalog) |
| `scene.load` | `{ id }` |
| `state.replace` | `{ state }` (minimal configuration or full snapshot) |
| `session.shuffle` | No payload; reseeds all fixture zones |

## Verification

`npm test` uses Node's built-in test runner. It checks atomic rejection, immutable snapshots, history, identity-preserving transfers, focus repair, custom decks, deterministic output, escaping, the scene registry, and bounded rendering across 1–8 players, extreme dimensions, every density, and counts up to a million. Audience tests verify each recipient's faces, hidden artwork exclusion, both framings, private focus, scene migration, and stable batch output. Scryfall tests cover exact TTL boundaries, persistent reuse, deduplication, rate limiting, failures, double-faced cards, and fetching only visible faces. URL tests verify compressed and raw round trips, Unicode, fixed snapshots, subdirectory hosting, malformed requests, compatibility fallbacks, and encoded/expanded size limits. Browser export uses native SVG/image/canvas support and should be checked in the target browser when new artwork sources or new scene features are introduced.
