Offline-first, local-first collaborative SVG canvas and tabletop tool.
Legend: [x] done · [~] partial / placeholder · [ ] not started
- Schema v4 —
Y.XmlFragmentofY.XmlElement(CRDT tree = SVG tree) - Shape-type registry (
SHAPE_TYPES) — rect & circle - Split render pipelines — doc-driven vs presence-driven
- Generic SVG mirror (
yXmlToDom) — recursive, namespace-aware, replaces per-type renderers - Modal UI — Draw mode vs Toys mode, layer pointer-event gating
- Selection model carries
{kind, id}— shape or toy, broadcast via awareness
- Background layer — static hex-tile image (
bg_slatehex.png, 1384×998) - Drawing layer — rects & circles, full CRDT, selection, deletion
- [~] Toys layer — placed, colorized, draggable, selectable, synced
- SVG full-embed model (CRDT tree = toy sub-document)
-
feColorMatrixrecolorization with playerc1 - Drag-to-move — local DOM preview, single CRDT commit on drop
- Player Marker toy
- [~] D6 toy — placed & colorized; roll behavior not yet wired
- Toy event system (
onLift,onPlace,onTrigger,dblclick→ roll) - Right-click context menu for toy events
- Toy sandbox — user-authored JS, isolated execution
- Boundaries & positions layer — snap zones, area rules, boundary backgrounds
- Effects layer — animations, glows, particles (ephemeral, rAF-driven, not CRDT-backed)
- Per-layer rendering latency / priority differentiation
-
TOY_TYPESregistry — seed of the toy library - SVG importer — id namespacing, reference rewriting, script & foreign-namespace stripping
- [~] Default toys
- Player Marker — placed & colorized
- [~] D6 — placed & colorized; roll not yet wired
- Other dice — d4, d8, d10, d12, d20, d00
- Trays — containment model, event distribution to children
- Token (solid color)
- Custom toy creation — user-defined SVG + event handlers
- Toy library UI — import from file, browse available toys
- Awareness — cursor, selection, player identity broadcast
- Selection overlay — gradient stroke per peer, name label, drag-tracking
- Peer count display
- Live cursor rendering per peer
- Soft-locking — claim exclusive edit rights on an object
- Lock claim via awareness (peers see lock indicator)
- Locked objects visually distinct (hatching, icon, owner label)
- UI to request unlock from the lock-holding peer
- Lock holder can grant or deny the unlock request
- Stale lock expiry on peer disconnect or idle timeout
- Cross-table presence — lobby / friend list
- Durable player identity — persistent
myIdacross sessions (localStorage) - Display name — set once, shown in overlays and toy-list ownership
- Manual colour / gradient override — choose hue or pick from palette instead of
entityGradientdefault - Profile persisted locally and broadcast via awareness on join
- Profile UI — accessible from topbar, editable in-session
- IndexedDB persistence — per-room, loads before WebRTC sync
- WebRTC P2P sync via
y-webrtc - Connection status indicator
- [-] Save / load
- Export table as SVG — flat rendering of all layers, for sharing / printing
- Export table as Yjs binary snapshot (
.crdt) — full fidelity, re-importable - Import / restore from
.crdtsnapshot
- Table selection landing page — open tables, friend presence, create new
- [~] Per-table tool configuration — tools added/removed per table
- Registry exists (
SHAPE_TYPES,TOY_TYPES) - UI toggle for enabling/disabling tools per table
- Registry exists (
- Side documents — HTML attachments per table
- Move / drag drawing shapes (parallels toy drag)
- Resize handles for selected shapes
- Bring to front / send to back
- More shape types — line, polygon, text
-
shapes.test.js— unit tests: CRDT ops, convergence,selectionGeometry, z-order -
toys.test.js— unit tests: importer, colour matrix, placement, movement, convergence - Playwright e2e — smoke tests, cross-tab sync
- Offline-first, local-first. IndexedDB is the source of truth on load; WebRTC sync is additive.
- Minimise dependencies. No framework, no build step, no surveillance-capitalism services.
- Open source philosophy. SVG, HTML, hyperlinks, public domain. No corporate control.
- CRDT tree = SVG tree. The Yjs
Y.XmlFragmentIS the SVG document — attributes are SVG-native, tags are SVG tags. Rendering is a mirror, not a translation. - Ephemeral vs document state. Anything per-frame (effects, cursors, particles) lives in awareness or local state, never in the CRDT doc.
- Registries over branching. New shape types and toy types are one registry entry each — no scattered
if/else.