Concepts
Architecture
How the browser and TouchDesigner divide the work, and why that matters for what you can and can't do.
Moonshine is two halves. Understanding which half does what makes the rest of the docs easier — and explains why some things are instant and others travel over the network.
The split
Browser (operator) LAN TouchDesigner (host)
───────────────────── ───── ───────────────────────
UI components WS ◀──┐ State authority (slices)
3D viewport + vertex math │ │ Render engine
Mask painting (canvas) ▼ ▲ Asset thumbnailing
Local state mirror control Mask compositing
Projector outputs
RTC ◀──────── Live composite preview
Control plane flows over WebSocket. Every state change — vertex drag, mask stroke commit, playlist reorder, projector parameter change — is sent as a structured message, applied authoritatively in TouchDesigner, and broadcast back to all connected operators.
Preview plane flows over WebRTC. TouchDesigner encodes the live composite and streams it to each browser. This is what you see in the viewport when you’re looking at “what’s actually being projected.” Latency is sub-100ms peer-to-peer.
Asset bytes flow over HTTP (the same listener as WebSocket). Large file uploads are chunked so they don’t block the control channel.
Why this split
The architecture rule is: TouchDesigner is the source of truth. The browser is a fast cache + UI surface.
When you drag a vertex, you’re not directly editing the projector output. You’re editing your local optimistic copy of the warp, sending a message to TouchDesigner that says “vertex N moved to (x, y, z),” and TD applies the change to its authoritative state and broadcasts to other operators. Your browser sees the broadcast, reconciles, and you see the result through the WebRTC preview.
This is why:
- The same show looks identical to every operator — they’re all seeing TD’s broadcast, not their own optimistic state
- Crashing your browser doesn’t lose the show — state is in TD
- Lag on your laptop doesn’t lag the projector output — projector output is rendered by TD regardless of what any browser is doing
- Two operators editing two different things at once doesn’t conflict — they’re modifying different parts of TD’s state authoritatively
What lives in the browser
- All UI rendering (React)
- Local state mirror (Zustand stores)
- 3D viewport (Three.js) — vertex selection, drag math, soft-selection falloff calculation
- Mask painting canvas — bitmap brush strokes, vector Bezier handles
- Live cursor tracking and presence
- Undo stack (frontend-local, backed by snapshots)
What lives in TouchDesigner
- Authoritative state for every entity (projectors, scenes, masks, playlists, etc.)
- Asset storage and chunked upload
- Mask compositing into final raster textures
- The render engine — warping, masking, blending, output to projectors
- Projector monitoring (PJLink, telemetry)
- Persistence (project files)
Connection model
Operators connect to TouchDesigner over WebSocket on port 9980. On connect, the client receives a sync.full_state message — a snapshot of everything. After that, all changes are deltas.
If the connection drops, the client tries to reconnect with exponential backoff. On reconnect it receives another sync.full_state and the local state is fully replaced — no merge, no conflict resolution required because TD has been authoritative the whole time.
This is also why refresh works without losing anything: a refresh is just a fresh sync.full_state from TD.
Related
- Projectors — the projector entity model
- Collaboration — how multi-user state works under this architecture