# 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: 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: ```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 ``` ## 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. SpacetimeDB-integrasjon Canvas-primitivet selv har ingen SpacetimeDB-kobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til SpacetimeDB: ```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 å 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 ✅ (implementert) - `` 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 - `` 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