# ATProto PDX stream scenes

One procedural background for the whole meetup stream: a Truchet tile board of winding
paths that quietly rearranges itself, one snappy rose tile-flip at a time. A single
self-contained page ([index.html](index.html), fonts embedded, no network needed) watches
OBS state through the injected `window.obsstudio` API and recasts itself per scene:

- **Starting Soon** — copy block with date/time, lively cadence (a flip every second or so)
- **Thank You** — copy block with a sign-off note, the board winding down
- **Background** — no text, full-frame board at a middle cadence, for layering slides
  or screen shares on top

When the stream goes live (`obsStreamingStarted`), the board plays a wavefront of flips
radiating from center and the text replays its entrance. The text entrance also plays on
scene changes — but *not* on page load, so the animation is saved for moments viewers
actually see, and the source can stay loaded all stream without cold canvas starts.

## OBS setup

Add **one** Browser Source and reuse it in every scene that wants the board:

1. Leave **Local file** unchecked and put the file path in **URL** so you can pass
   parameters, e.g. `file:///home/graham/atproto-pdx-scenes/index.html?when=Thursday, July 16 · 6:30 PM`
   (`file:///C:/Users/graha/...` on Windows)
2. **Width** 1920, **Height** 1080
3. **FPS** 30 (check "Use custom frame rate") — the page also caps itself at 30
4. Leave **Shutdown source when not visible** and **Refresh browser when scene becomes
   active** *unchecked* — the page is designed to stay resident and react to events
5. Set **Page permissions** to at least "Read access to OBS status information" so the
   page receives scene and streaming events

### Scene name mapping

On every `obsSceneChanged`, the page matches the new scene's name (case-insensitive
substring) to pick its mode:

| Mode | Default keywords | Override param |
|---|---|---|
| Starting Soon | `soon`, `intro`, `preshow`, `pre-show` | `?soonMatch=a,b,c` |
| Thank You | `thank`, `outro`, `end`, `bye` | `?thanksMatch=a,b,c` |
| Background | anything else | — |

So scenes named "Starting Soon", "Intro", "Thanks + Outro" just work; if your scene
names don't fit, pass your own keyword lists.

## Text parameters

All text is set via URL query parameters; no code edits on event night.
Pass an empty value (e.g. `&handle=`) to hide that line.

| Param | Default | Used in |
|---|---|---|
| `title` | `ATProto PDX` | both text modes |
| `handle` | `@pdx.atproto.camp` | both text modes |
| `status` | `Starting soon` | Starting Soon |
| `when` | *(hidden)* | Starting Soon — pass explicitly, e.g. `?when=Thursday, July 16 · 6:30 PM` |
| `thanksStatus` | `Thanks for watching` | Thank You |
| `note` | `See you at the next one.` | Thank You |
| `mode` | `soon` | initial mode before any OBS event, and the mode used outside OBS (`soon`/`thanks`/`bg`) |

## Live chat

In the Starting Soon and Thank You modes, the page shows the stream.place chat for the
channel in a column at bottom-right. It subscribes to a
[Jetstream](https://github.com/bluesky-social/jetstream) firehose for
`place.stream.chat.message` records and keeps the ones whose `streamer` DID matches the
channel. Author DIDs resolve through their DID document (PLC directory or did:web) to
get the handle plus PDS, then the chatter's `place.stream.chat.profile` is fetched for
their chosen name color (dark colors are lifted to a legibility floor for the slate
background; chatters without one get the house rose). Everything is cached per DID.
Record deletions on the firehose remove the message from the overlay, so moderation
actions are honored. The last 8 messages show, oldest fading out through a mask.
Live-only: messages sent before the page loaded don't appear.

Chat requires network access from the browser source; if offline, it reconnects with
backoff and the rest of the scene is unaffected.

| Param | Default | |
|---|---|---|
| `channel` | `pdx.atproto.camp` | the streamer whose chat to show (handle or DID) |
| `jetstream` | `wss://jetstream2.us-east.bsky.network/subscribe` | alternate Jetstream instance |
| `chat` | `on` | pass `chat=off` to disable entirely |

## Previewing outside OBS

Open the page in any browser (`python -m http.server 8143`, then
`http://localhost:8143/`). Without `window.obsstudio` the entrance plays on
load, and keyboard controls stand in for OBS events:

| Key | Action |
|---|---|
| `1` / `2` / `0` | switch to Starting Soon / Thank You / Background |
| `r` | small ripple |
| `l` | simulate going live: board-wide ripple + text entrance |
| `c` | inject a demo chat message |

`window.__spChat` in the console shows chat connection state (connected, resolved
channel DID, messages shown/dropped) for debugging.

## If the host machine struggles

The scene is light (144 arc tiles redrawn at a capped 30fps, most frames identical), but
if rehearsal shows the encoder fighting for headroom, screen-record 60–90 seconds and
swap the Browser Source for a looping Media Source — same look, decode-only cost (you
lose the OBS-event reactivity).

## Design notes

Palette, typography, and motion rules live in [DESIGN.md](DESIGN.md); strategic context
in [PRODUCT.md](PRODUCT.md).
