# render/ — shared frontend surfaces

Two pages that every endpoint can show, kept here rather than inside one component
because they belong to no single service: the now-playing screen reads Home Assistant,
the floorplan reads `identity`, and both are shown on thin clients, touch panels, the
kitchen and door panels alike.

Same rules as `digest-canvas-sdk` and `canvas-sdk`: **vendored, dependency-free, no
build step**, config from `?query=params` so nothing here holds a secret. Copy the
directory into a host's image; do not serve it from one place, or a kiosk that cannot
reach the container host shows nothing.

| | What | Reads |
|---|---|---|
| `media-visualiser/` | now-playing: circular spectrum, album-art colours, lyrics | HA `media_player` state |
| `floorplan-3d/` | who is in which room, extruded | `identity` `GET /floorplan/presence` |

## media-visualiser

```
nowplaying.html?ha=http://ha:8123&token=<HA token>&entity=media_player.living_room
```

**Two tiers, and the distinction is load-bearing.** CAVA reads an audio stream; most
endpoints do not have one — a kitchen panel showing what the *living room* is playing
has no audio to analyse and never will. So:

- **reactive** — audio is local. Real FFT via WebAudio (`attachAudio(el)`), or levels
  pushed from `cava -r` by the host's agent (`pushLevels([...])`).
- **synthetic** — everywhere else. The ring breathes from track position and tempo.

The synthetic tier **says so on screen** ("visual rhythm — not an audio analysis") and
sets `data-tier` on the canvas. That matters: the moment somebody believes it is a
spectrum, every bass drop it misses becomes a bug report.

Palette extraction rejects near-greys and near-blacks *before* ranking (album art is
full of both, and a naive palette off a dark cover is four indistinguishable greys),
then lifts each colour until it clears the background — the step that gets skipped, and
why so many art-coloured visualisers are invisible on dark covers.

Lyrics: synced LRC highlights and scrolls; **plain text is shown but never
auto-scrolled at a guessed rate**, which is wrong within ten seconds and stays wrong.
Most tracks have none, so the layout treats absent as the normal case.

## floorplan-3d

```
floorplan.html?api=http://identity:8097&token=<identity token>[&level=<id>]
```

**Canvas 2D, not three.js** — a change from what `docs/endpoint-surfaces.md` planned,
made while building it. The scene is prisms on a plane: no lighting model, no textures,
no model import. An isometric projection with painter's-algorithm sorting draws exactly
that in ~200 lines, runs predictably on the small panels (where a WebGL context is much
less predictable), and keeps the frontend at zero dependencies. If this ever needs real
lighting or an imported model, three.js becomes right and this becomes the fallback.

Inherited from the Pebble watchapp deliberately, because they render the same payload
and divergence would mean one of them is lying:

- Occupied rooms **lit**, empty rooms **dark** — carried in lightness, not hue, because
  colour already means *who*.
- A **third state**, dashed, for rooms HA never reports on. Drawing "no data" as
  "empty" is a quiet lie.
- Occupants as **colour + initial**, upgraded to their photo where there is room, with
  the colour ring kept either way — it is what ties this marker to the same person on
  the watch and in the admin panel.
- `unplaced` people get a **visible shelf**. They are who you are most often looking for.

**Exact positions**, when the level has a metre scale and mmWave sensors placed on it:
a person fused to a single radar target is drawn *at* that coordinate with a footprint
dot on the floor (without it, a marker floated above the floor reads as further back in
the room). An unattributed target is a **hollow dashed puck with a question mark** — no
colour, no initial, because every visual language here for a *person* is reserved for
people the system can name. A radar target lights the room even with nobody named:
somebody is in there, and that the house can't say who is a fact about the house.

Drag to orbit; pitch is clamped and there is no free-fly, because an unconstrained
camera on a wall panel is something people knock askew and cannot get back.

**The caveat no rendering fixes:** this is only as right as room-level presence, which
has never been measured in this house. A beautiful 3D house with everybody on the
unplaced shelf is a beautiful picture of nothing — test that first with the admin
panel's floorplan tab, **Live** ticked.
