Nystart basert på arkitektonisk innsikt fra Sidelinja v1. Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid. Inneholder: - Komplett arkitekturdokumentasjon (docs/arkitektur.md) - 6 vedtatte retninger (docs/retninger/) - Alle concepts, features, proposals og erfaringer fra v1 - Server-oppsett og drift (docs/setup/) - LiteLLM-konfigurasjon (API-nøkler via env) - Editor.svelte referanse fra v1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.6 KiB
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 50–200 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 (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:
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/$derivedfor 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
$stateog$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