# 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
└── Sanntid: strøk-synkronisering via WebSocket

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

## 2. Kamera-modell

### 2.1 Transform

Kameraet representeres som en 2D affin transformasjon:

```typescript
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:

```css
.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 50–200 kort er dette en optimalisering som holder DOM-et lett.

```typescript
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:

```typescript
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:

```svelte
<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:

```typescript
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 (768–1024px) | 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. Sanntidsintegrasjon

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

```typescript
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 å oppdatere PG via maskinrommet, som propagerer endringen til andre klienter via WebSocket.

## 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 ✅ (implementert)
- `<Canvas>` Svelte-komponent med kamera (pan/zoom), viewport culling, og objekt-drag
- Touch-støtte (pinch-zoom, to-finger-pan)
- Fullskjerm-toggle (i canvas-toolbar, `position: fixed`)
- **Filer:** `src/lib/components/canvas/Canvas.svelte`, `types.ts`, `index.ts`
- **Bruk:** `import { Canvas } from '$lib/components/canvas'`
- **API:** `renderObject` snippet for consumer-rendering, events via props (`onObjectMove`, `onCameraChange`, `onSelectionChange`)
- **Eksport:** `getCamera()`, `setCamera()`, `zoomToFit()` metoder

### Fase 2: Storyboard som første consumer
- `<StoryboardCard>` rendrer meldingsboks-kort på canvaset
- Sanntidssynk for posisjon og status via WebSocket
- 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
