Canvas
more-shadcn-noair/canvasFreeComponent
An infinite pannable, zoomable field with draggable nodes and connectors.
Live preview
live · rendered by the registry
Rendered by more-shadcn-noair on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://more-shadcn.noair.fun/r/canvas.jsonSource
1<script lang="ts">2 import { cn } from '$UTILS$';3 import { untrack, type Snippet } from 'svelte';4 import {5 clamp,6 nodeBounds,7 setCanvasContext,8 type CanvasPoint,9 type CanvasRect10 } from './ctx.svelte';1112 let {13 x = $bindable(0),14 y = $bindable(0),15 zoom = $bindable(1),16 minZoom = 0.2,17 maxZoom = 3,18 zoomSpeed = 0.0015,19 grid = 'dots',20 gridSize = 24,21 snap = 0,22 pannable = true,23 zoomable = true,24 panOnMiddleClick = true,25 class: className,26 onpan,27 onzoom,28 children,29 ...rest30 }: {31 /** Pan offset in screen pixels. */32 x?: number;33 y?: number;34 zoom?: number;35 minZoom?: number;36 maxZoom?: number;37 zoomSpeed?: number;38 grid?: 'dots' | 'lines' | 'none';39 gridSize?: number;40 /** Grid step nodes snap to while dragging. 0 disables snapping. */41 snap?: number;42 pannable?: boolean;43 zoomable?: boolean;44 panOnMiddleClick?: boolean;45 class?: string;46 onpan?: (offset: CanvasPoint) => void;47 onzoom?: (zoom: number) => void;48 children: Snippet;49 [key: string]: unknown;50 } = $props();5152 let viewport = $state<HTMLDivElement | null>(null);53 let overlay = $state<HTMLDivElement | null>(null);54 let panning = $state(false);55 let spaceHeld = $state(false);56 let nodes = $state<Record<string, CanvasRect>>({});5758 let panStart: { x: number; y: number; originX: number; originY: number } | null = null;5960 function toCanvas(clientX: number, clientY: number): CanvasPoint {61 const rect = viewport?.getBoundingClientRect();62 if (!rect) return { x: 0, y: 0 };63 return { x: (clientX - rect.left - x) / zoom, y: (clientY - rect.top - y) / zoom };64 }6566 function toScreen(point: CanvasPoint): CanvasPoint {67 return { x: point.x * zoom + x, y: point.y * zoom + y };68 }6970 function panBy(dx: number, dy: number) {71 x += dx;72 y += dy;73 onpan?.({ x, y });74 }7576 function panTo(nextX: number, nextY: number) {77 x = nextX;78 y = nextY;79 onpan?.({ x, y });80 }8182 /**83 * Scales around a viewport-relative origin so the point under the cursor84 * stays put. Defaults to the centre when no origin is given.85 */86 function zoomTo(next: number, origin?: CanvasPoint) {87 const rect = viewport?.getBoundingClientRect();88 const target = clamp(next, minZoom, maxZoom);89 if (target === zoom) return;9091 const pivot = origin ?? { x: (rect?.width ?? 0) / 2, y: (rect?.height ?? 0) / 2 };92 const ratio = target / zoom;9394 x = pivot.x - (pivot.x - x) * ratio;95// … truncated
More from more-shadcn-noair
All 43 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Audioaudio | more-shadcn-noair | Components | Free | no deps | |
| Audio Waveaudio-wave | more-shadcn-noair | Components | Free | no deps | |
| Autocompleteautocomplete | more-shadcn-noair | Components | Free | no deps | |
| Bannerbanner | more-shadcn-noair | Components | Free | no deps | |
| Big Calendarbig-calendar | more-shadcn-noair | Components | Free | no deps | |
| Bottom Navigationbottom-nav | more-shadcn-noair | Components | Free | no deps | |
| Chipchip | more-shadcn-noair | Components | Free | no deps | |
| Choiceboxchoicebox | more-shadcn-noair | Components | Free | no deps |
