Documentation is work in progress and not completeEditor
FlashCC/The canvas/Section 5 of 29

The canvas

Four nested elements, artboard-space hit testing, cursor-anchored zoom, the tool and drag modes, snapping, keyboard and snapshot history.

Canvas.tsx owns zoom, pan, tools, drag, resize, marquee, drawing, snapping, keyboard and media drops. useStudio.ts owns all state and history.

Structure

Four nested elements, and the split between them is load-bearing:

  1. 1.host — overflow-hidden, owns the wheel and pointer listeners
  2. 2.centring wrapper — translate(-50%,-50%) translate(panX, panY)
  3. 3.board — sized width*zoom × height*zoom, painted by slidePaint. Selection chrome, handles and the marquee are children here, in screen space
  4. 4.inner — sized width × height with transform: scale(zoom). LayerViews, safe zones and snap guides live here, in artboard space

toBoard(clientX, clientY) = (client - boardRect.left) / zoom. Everything past that point is artboard pixels.

Every layer is pointerEvents: "none". All hit testing is done in artboard coordinates by Canvas against geometry.ts, never by DOM events. Only a contentEditable being edited re-enables pointer events.

Zoom and pan

ControlBehaviour
Fitmin((hostW - 120)/w, (hostH - 120)/h), clamped [0.05, 2]
Wheelzooms — inverted from the browser default. ctrl/meta/shift pans. Step ×1.12, clamp [0.05, 4]
KeyboardCmd+= / - / 0, step ×1.2

Zoom is cursor-anchored: pan is recomputed so the artboard point under the pointer stays put. The wheel handler is registered once with { passive: false } and reads zoom through a ref, because the handler from whichever render installed it would otherwise hold a stale value.

A ResizeObserver on the host re-runs fit, which discards the user’s pan and zoom on any host resize.

Tools and drag

Tool = select | text | rect | ellipse | triangle | line | icon.

Pointer-down with a draw tool creates a 1×1 layer and enters draw mode. On release, anything still ≤4px in either axis was a click rather than a drag and gets a default size: text 640×120, line 400×8, everything else 240×240. Text then enters editing. The tool resets to select — draw tools are single-shot.

Hit testing runs topmost first and requires visible && !locked. Shift toggles membership. Clicking an already-selected layer preserves the whole selection, so multi-drag works.

ModeBehaviour
moveSnaps unless Alt is held; positions rounded
resizeShift constrains aspect. No snapping. Every selected layer re-derives from its captured origin rect
marqueeSelects everything intersecting — including locked layers, unlike click
drawNormalised, clamped to ≥4
panSpace held, or middle mouse button

Snapping

SNAP_PX = 6 screen pixels, divided by zoom before reaching geometry.snap.

Targets per axis: artboard 0, midpoint and full extent, plus every other rect’s start, mid and end. Moving edges are the moving box’s start, mid and end. Tolerance is strict <, so exactly-at-tolerance does not snap.

Guides only appear when a correction is actually applied. A box already perfectly aligned produces dx = 0 and therefore no visible guide.

Keyboard

Window-level, skipped while typing (INPUT, TEXTAREA, contentEditable).

KeyAction
SpacePan (checked before the typing guard)
Cmd+Z / Shift+Cmd+ZUndo / redo
Cmd+D / Cmd+ADuplicate / select all on slide
Cmd+] / Cmd+[Forward / backward
Delete / BackspaceRemove selection
EscapeClear editing, clear selection, tool → select
EnterEdit text (exactly one text layer selected)
v t r o l iselect, text, rect, ellipse, line, icon
ArrowsNudge 1px, 10px with Shift

There is no shortcut for the triangle tool.

History

Snapshot-based: commit(doc, coalesceTag?) pushes the previous Doc onto past. LIMIT = 120 (the array peaks at 121). Undo covers everything — layer edits, reorders, slide operations — with no command class per action.

Coalescing suppresses a push when the tag matches the previous one, so a drag is one undo step rather than sixty. It has no time or gesture boundary — see the defect list.

commit is also where auto-naming happens: a document whose name is one the app supplied takes its name from the first text layer on slide 1. A name you chose is never overwritten.