# Artwork controls

Declare your artwork's interactions in the posted script. The viewer creates touch buttons and native text fields, while your artwork remains responsible for its own keyboard behavior.

## A declaration

```javascript
let motion = true;
function toggleMotion() {
  motion = !motion;
  motion ? p.loop() : p.noLoop();
}
p.keyTyped = () => { if (p.key === 'A') toggleMotion(); };
p.immutablesControls = {
  version: 1,
  actions: [
    { id: 'motion', label: 'Animate', type: 'toggle', key: 'A' },
    { id: 'png', label: 'Save PNG', type: 'export' }
  ],
  getState: () => ({ values: { motion }, busy: false }),
  onAction: id => { if (id === 'png') p.saveCanvas('my-edition', 'png'); }
};
```

For other renderers use `t.immutablesControls`, `v.immutablesControls` or `g.immutablesControls`. Use callbacks without a `key` for their buttons and toggles; implement physical keyboard input in your own script.

## Actions and state

Declare at most 12 actions. Each needs a unique lowercase ID beginning with a letter and containing only letters, digits or hyphens, up to 40 characters. Labels are plain text, up to 48 characters.

- `button`: one action. With a key, p5's keyboard handler runs; without one, `onAction(id)` runs.
- `toggle`: also needs `getState().values[id]` as a Boolean. Without a key, `onAction(id, proposedBoolean)` receives the proposed value; update your own state.
- `text`: a native field and Send button. `maxLength` is optional, 1–500, default 120. Always provide `onAction`.
- `navigation`: Up, Down, Open and Back buttons. Always provide `onAction`.
- `export`: appears under Save. It runs only after the visitor chooses it; use it for explicit captures rather than automatic downloads.

Keys preserve case. Supported keys are single letters/digits, a space, arrows, Enter and Escape. A text/navigation `key` is a shortcut label; those controls always use callbacks, so implement the corresponding physical shortcut yourself.

## Text and navigation

```javascript
p.immutablesControls = {
  version: 1,
  actions: [{ id: 'message', label: 'Message', type: 'text', maxLength: 60 }],
  getState: () => ({ busy: thinking, activeControl: mode }),
  onAction: (id, event) => {
    if (event.action === 'open') mode = id;
    if (event.action === 'change') draft = event.text;
    if (event.action === 'submit') respond(event.text);
    if (event.action === 'close') mode = '';
  }
};
```

`thinking`, `mode`, `draft` and `respond` above stand for your own state and functions. Text events are `open`, `change`, `submit`, `close`; change and submit include `text`. Navigation events are `open`, `up`, `down`, `back`, `close`.

Keep `getState()` cheap and free of side effects. Return `busy: true` during work. Optional `activeControl` follows modes opened by the artwork's own keys or pointer handlers; return an action ID while open and an empty string when closed. If omitted, the viewer manages its own panel selection. Navigation Back stays available during busy work.

## Visibility and styling

On mobile the panel opens automatically. On desktop it starts collapsed, with an icon and the bottom-bar **Artwork controls** entry. After four idle seconds it fades to a faint lower-right icon. Typing, navigation, an open Save menu, busy work and keyboard focus keep it open. It does not resize the canvas.

The controls live inside `#root` and use existing `.btn`, `.btn-secondary`, `.btn-outline-secondary`, `.form-control`, `.modal-content` and `.font-display` classes. Your [project CSS](/docs/project-style) can style them. There is no shadow DOM.

Stable selectors include `.artwork-controls`, `.artwork-controls-panel`, `.artwork-control`, `.artwork-controls-handle`, `.artwork-controls-form`, `.artwork-controls-navigation`, `.artwork-controls-exports` and `.artwork-controls-status`. Individual actions have `data-action`; the outer element has `data-project-id`, `.is-touch` or `.is-desktop`, and `.is-open` or `.is-idle`.

## Existing projects and automatic discovery

Reviewed profiles match the exact script SHA-256 and project ID. They provide known controls for Syncret, Firmament and PILLARS; those editions open live even when their grids use cached images. Reviewed profiles take priority over declarations.

Otherwise an explicit declaration takes priority. With no declaration, the viewer conservatively detects literal comparisons inside `p.keyTyped` / `p.keyPressed` and offers simple Key buttons. It does not infer semantic labels or toggle state. Unannotated S/P exports and contextual Enter/Escape are omitted. Unsupported syntax may yield no automatic controls.

An invalid declaration produces no panel. `{ version: 1, actions: [] }` opts out for an unreviewed script. Grids and still-image views do not execute interactive controls. Test your own declaration in **Preview in UI**, not only the wizard's isolated canvas.

Source basis: `artworkControlConventions`, `artworkControlProfile`, `artworkInput`, `ArtworkControls` and `config/artwork-controls.json` in `clients/art/src/`.
