server/docs/features/canvas_primitiv.md
vegard 7babafc65f Storyboard-spec, canvas-primitiv og universell overføring
Tre nye/omskrevne dokumenter som definerer fritt-canvas arkitekturen:
- Canvas-primitiv: felles underlag for whiteboard og storyboard (pan, zoom, drag, viewport culling)
- Universell overføring: message_placements-tabell og blokk-til-blokk drag-and-drop
- Storyboard: full spec med episode-sekvens, LiveKit-kobling, inter-board overføring

Inkluderer også storyboard-relaterte mini-proposals (ghost cards, pinboard mode,
flow meter, emotion tags, card chaining, collaborative cursors, card heat map).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:10:31 +01:00

7.6 KiB
Raw Blame History

Feature: Canvas-primitiv — felles fritt-canvas underlag

Filsti: docs/features/canvas_primitiv.md

1. Konsept

Canvas-primitivet er den felles underliggende komponenten for alle friform-views i Sidelinja: whiteboard (tegning), storyboard (kort-canvas), og fremtidige canvas-baserte visninger. Det håndterer kamera (pan, zoom), viewport-styring, objekt-plassering og interaksjon — men vet ingenting om hva som rendres.

1.1 Hvorfor et felles primitiv?

Whiteboard og storyboard har identisk infrastruktur-behov:

  • Uendelig canvas med pan og zoom
  • Objekter med (x, y)-posisjon
  • Drag-and-drop av objekter
  • Viewport culling (ikke render det som er utenfor synsfeltet)
  • Touch-støtte (pinch-zoom, to-finger-pan)
  • Responsivt design (fungerer på mobil, tablet, desktop)

Forskjellen er innholdet: whiteboard rendrer streker/figurer, storyboard rendrer meldingsboks-kort. Primitivet abstraherer det felles, slik at begge views gjenbruker 100 % av canvas-logikken.

1.2 Arkitekturprinsipp

Canvas-primitiv (felles)
├── Kamera: pan, zoom, transform matrix
├── Viewport: culling, synlige objekter
├── Interaksjon: pointer events, touch, drag
├── Grid: valgfri snap, hjelpelinje
└── Render-delegering: slot/callback for innhold

Whiteboard (consumer)
├── Tegneverktøy: penn, linje, rektangel, tekst
├── Strøk-modell: SVG paths / canvas paths
└── SpacetimeDB: strøk-synkronisering

Storyboard (consumer)
├── Kort-rendering: <MessageBox> i kompakt modus
├── Status-modell: Klar / Tatt opp / Droppet / Arkivert
├── Portal-soner: overføringsmekanikk til andre blokker
└── SpacetimeDB: kort-posisjon + status-synkronisering

2. Kamera-modell

2.1 Transform

Kameraet representeres som en 2D affin transformasjon:

interface Camera {
    x: number;      // pan offset X (world coords)
    y: number;      // pan offset Y (world coords)
    zoom: number;   // scale factor (1.0 = 100%)
}

Rendring via CSS transform på en wrapper-div:

.canvas-world {
    transform: translate(calc(var(--cam-x) * 1px), calc(var(--cam-y) * 1px))
               scale(var(--cam-zoom));
    transform-origin: 0 0;
}

2.2 Zoom-begrensning

  • Min zoom: 0.1 (10 % — fugleperspektiv, brukes av Pinboard Mode)
  • Max zoom: 3.0 (300 % — detalj)
  • Default: 1.0
  • Zoom pivoterer rundt musepeker/finger-midtpunkt

2.3 Pan

  • Desktop: Hold mellomknapp eller mellomrom + dra. Alternativt: to-finger-drag på trackpad.
  • Touch: To-finger-pan (én finger = dra objekter, to fingre = pan).
  • Edge-pan: Når man drar et objekt nær kanten av viewport, scroller canvaset automatisk i den retningen.

3. Viewport Culling

Bare objekter som overlapper med det synlige viewport-rektangelet rendres i DOM. For storyboard med 50200 kort er dette en optimalisering som holder DOM-et lett.

function visibleObjects(objects: CanvasObject[], camera: Camera, viewportSize: { w: number, h: number }): CanvasObject[] {
    const worldRect = screenToWorld(camera, viewportSize);
    return objects.filter(obj => intersects(obj.bounds, worldRect));
}

En margin (f.eks. 200px i world-space) legges til for å unngå pop-in ved pan.

4. Objektmodell

Canvas-primitivet opererer på generiske objekter:

interface CanvasObject {
    id: string;
    x: number;
    y: number;
    width: number;
    height: number;
    // Consumer-spesifikk data håndteres via generics/props
}

Consumer (whiteboard, storyboard) bestemmer hva som rendres for hvert objekt via en render-callback eller Svelte snippet:

<Canvas objects={cards} let:object>
    <!-- Consumer bestemmer innholdet -->
    <StoryboardCard card={object} />
</Canvas>

5. Interaksjon

5.1 Pointer events

All interaksjon håndteres via pointer events (unified mouse + touch):

Gest Desktop Touch Handling
Pan Mellomknapp-drag / Space+drag To-finger-drag Flytt kamera
Zoom Scroll wheel Pinch Zoom inn/ut
Velg Klikk Tap Velg objekt
Flytt Venstreklikk-drag på objekt Én-finger-drag på objekt Flytt objekt
Multi-select Shift+klikk / lasso Lang-trykk + drag Velg flere

5.2 Snap-to-grid (valgfri)

Når aktivert, snapper objekter til et rutenett ved drag-slipp:

function snap(value: number, gridSize: number): number {
    return Math.round(value / gridSize) * gridSize;
}

Default: av. Kan toggles via hurtigtast eller toolbar.

5.3 Seleksjon

  • Klikk på tom flate: deselect alle
  • Klikk på objekt: velg det (deselect andre)
  • Shift+klikk: toggle seleksjon
  • Lasso: dra på tom flate uten Space = tegn seleksjonsboks

6. Responsivt design

Canvas-primitivet skal fungere på alle skjermstørrelser:

Skjerm Tilpasning
Desktop (>1024px) Full interaksjon, alle hurtigtaster
Tablet (7681024px) Touch-gester, toolbar i bunn
Mobil (<768px) Forenklet toolbar, større treffområder for objekter, ingen lasso

Touch-treffområder skal være minimum 44x44px (WCAG 2.5.5).

7. Fullskjerm-modus (BlockShell-feature)

Enhver blokk i BlockShell kan gå i fullskjerm. Dette er en generell feature, ikke spesifikk for canvas:

  • Toggle: Dobbeltklikk på blokk-headeren, eller knapp i header
  • Implementering: Blokken settes til position: fixed; inset: 0; z-index: 50
  • Escape: Trykk Esc eller klikk "minimer"-knapp for å gå tilbake
  • URL-state: Fullskjerm-tilstand lagres ikke i URL — det er en visuell modus, ikke en side

8. SpacetimeDB-integrasjon

Canvas-primitivet selv har ingen SpacetimeDB-kobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til SpacetimeDB:

interface CanvasEvents {
    onObjectMove: (id: string, x: number, y: number) => void;
    onObjectResize: (id: string, w: number, h: number) => void;
    onCameraChange: (camera: Camera) => void;
    onSelectionChange: (ids: string[]) => void;
}

Storyboard-consumeren bruker onObjectMove til å kalle en SpacetimeDB-reducer for å synkronisere posisjon til andre klienter.

9. Bygger på

  • SvelteKit: Svelte 5 $state/$derived for reaktiv kamera- og objekt-state
  • CSS transforms: Ingen Canvas2D eller WebGL — DOM-basert rendering for å beholde Svelte-komponent-rendering inne i objektene
  • Pointer Events API: Unified input for mus og touch

10. Implementeringsstrategi

Fase 1: Kjerne-primitiv

  • <Canvas> Svelte-komponent med kamera (pan/zoom), viewport culling, og objekt-drag
  • Touch-støtte (pinch-zoom, to-finger-pan)
  • BlockShell fullskjerm-toggle

Fase 2: Storyboard som første consumer

  • <StoryboardCard> rendrer meldingsboks-kort på canvaset
  • SpacetimeDB-synk for posisjon og status
  • Portal-soner for overføring

Fase 3: Whiteboard-migrering

  • Migrere eksisterende whiteboard-spec til å bruke canvas-primitivet
  • Tegneverktøy som overlay oppå primitivet

11. Instruks for Claude Code

  • Canvas-primitivet er en ren Svelte-komponent uten backend-avhengigheter
  • Bruk CSS transforms, ikke Canvas2D — innholdet inne i objekter er vanlige Svelte-komponenter
  • All state styres via Svelte 5 $state og $derived — ingen external state management
  • Pointer events, ikke mouse events — unified input
  • Test med touch-emulering i DevTools for responsivitet
  • Viewport culling er påkrevd fra dag 1 — ikke optimaliser bort