JEPA4Japan · tutorials

Chapter 1: Do Not Draw Yet—Canvas Is Not a Product Architecture

2,597 words 12 min read #Canvas#Frontend Engineering#Infinite Canvas#ELI5

Render the same business diagram with DOM, SVG, Canvas, and GPU approaches, then record the first renderer-selection ADR.

Course progress Course outline 18 of 18 lessons available

Part I: Choose the Surface Before You Draw—Product, Pixels, and Coordinates

  1. 01 Chapter 1: Do Not Draw Yet—Canvas Is Not a Product Architecture Current lesson
  2. 02 Chapter 2: A Sheet of Pixels That Forgets available now
  3. 03 Chapter 3: Turn Drawing into a Replayable Recipe available now
  4. 04 Chapter 4: Four Maps and a Camera available now

Part II: Give the Pixel World a Brain—Model, Scheduling, Input, and Tools

  1. 05 Chapter 5: Give the Pixel World a Registry available now
  2. 06 Chapter 6: Redraw Only When the Light Turns On—Render Scheduling and the React Boundary available now
  3. 07 Chapter 7: Mouse, Touch, and Pen Speak One Language available now
  4. 08 Chapter 8: Find the Big Box Before Inspecting the Edge available now
  5. 09 Chapter 9: Tools Are Traffic Lights, Not a Bag of Booleans available now

Part III: From “It Drags” to “It Is Trustworthy”—Interaction, Text, Assets, and Recovery

  1. 10 Chapter 10: Make the Editor Feel Right available now
  2. 11 Chapter 11: Drawn Text Is Not Editable Text available now
  3. 12 Chapter 12: Borrowed Images Cannot Be Packed Without Rules available now
  4. 13 Chapter 13: Time Machines and Old Boxes available now
  5. 14 Chapter 14: Looking Correct Is Not Being Correct available now

Part IV: Master-Level Decisions—Performance, Workers, GPU, SDKs, Collaboration, and AI

  1. 15 Chapter 15: Do Not Search Ten Thousand Children One by One available now
  2. 16 Chapter 16: Keep the Front Desk Out of the Kitchen—Worker and GPU Upgrades available now
  3. 17 Chapter 17: Build the Car or Buy a Proven Chassis? available now
  4. 18 Chapter 18: People and AI Edit the Same Ledger available now

Start with a game a five-year-old can understand

Prepare a sheet of white paper, a transparent plastic sheet, a box of building blocks, and a small erasable chalkboard. Write “Order A” on the paper, draw an arrow on the plastic, arrange the blocks into two squares, and finally color a patch of the chalkboard blue.

Before continuing, predict four things: Which material makes text easiest to edit? Which lets you see text underneath it? Which can you move directly by hand? After erasing it, which makes it hardest to know what used to be there? Actually try it once, then change the A in “Order A” to B. You will discover that “they can all represent the same picture” does not mean “the same operation costs the same on each one.”

  1. Write on paperReadable and selectable
  2. Draw on filmStill sharp when scaled
  3. Arrange blocksEach block has an identity
  4. Color the boardOnly pixels remain
Predict “how will I edit this?” before choosing “how will I draw this?” The material is not the product.

That is the single truth of this chapter: an Infinite Canvas is a product interaction model in which users can pan, zoom, and place objects; it is not a rendering technology. DOM, SVG, Canvas 2D, a GPU, or several of them together can implement it.

Translate the toys into Canvas engineering

Toy materialEngineering equivalentThe question you actually need to ask
Text on white paperDOMMust users type, select, hear, or automatically lay out the content?
A line on transparent filmSVGDo you need vector geometry and addressable nodes?
Color on an erasable boardCanvas 2D BitmapDo you need a large amount of custom 2D pixel drawing?
Many chefs plating at onceWebGL / WebGPUDo you have evidence that batching or parallel computation will remove the bottleneck?
Paper attached to the boardHybrid DOM/CanvasKeep text editing in the DOM and give graphical drawing to Canvas
Paper attached to a GPU displayHybrid DOM/GPUKeep the semantic layer in the DOM and give high-throughput rendering to the GPU
“A desktop that can move forever”Infinite CanvasA product behavior that does not prescribe the underlying material

The analogy breaks down here: a physical material can occupy only one place at a time, while the browser can layer DOM over Canvas. SVG is not literal film either; the browser rasterizes it too. Nor is a GPU a magical chef that is always faster: data uploads, shaders, device loss, and accessibility all impose costs. The analogy helps us ask the right question first; it cannot replace measurement, specifications, and compatibility testing.

Kill the misleading intuitions first

  1. “An infinite canvas should use <canvas>.” Wrong. An infinite canvas promises only camera and object interactions. When a product contains a lot of text, a moderate number of objects, and important semantics, DOM/SVG is often the better fit.
  2. “Many objects always mean Canvas.” Wrong. One hundred thousand total objects with ten visible objects have a different cost from two thousand visible objects containing shadows and images. Measure the visible count, change rate, and pixel area first.
  3. “Three.js is an upgrade from Canvas 2D.” Wrong. Three.js addresses 3D concerns such as Scene, Camera, Mesh, Material, and Depth. If an ordinary 2D whiteboard does not need those things, Three.js introduces a different, more complex set of constraints.
  4. “tldraw draws every Shape with Canvas 2D.” Wrong. The official tldraw Shape contract requires component() to return a React component, while its export path generates SVG. A library name and the look of its product do not prove its Renderer. This describes the primary Shape rendering architecture; it does not deny that an individual component may use <canvas> internally.

Production backpack

Prerequisite contract

This chapter does not require knowledge of matrices or hit testing. You only need to be able to run a browser, TypeScript, and Vitest. First, freeze the product requirements: Canvas Lab is a business-process diagram editor. Cards require native title editing, connectors must remain sharp while zooming, the grid moves frequently, exports must include PNG and structured JSON, and neither keyboard nor screen-reader access may be absent.

Formal knowledge: choose by dimensions, not beliefs

RendererStrengthsMain costsIts place in Canvas Lab
DOMNative text, CSS Layout, semantics, focus, and automated testingMany nodes changing frequently increase style/layout costsToolbar, forms, text editing, Inspector
SVGVectors, addressable geometry nodes, CSS, and sharp exportsMany continuously changing nodes require measurement of DOM costsCandidate for small-scale connectors or exports
Canvas 2DDirect control of 2D drawing, mature APIs, and frequent redrawsNo retained objects, native semantics, or text editingGrid, Shapes, and Overlay in the teaching kernel
WebGLMature GPU pipeline, batching, and ShadersYou must own Buffers, textures, picking, and context lossReplacement candidate after profiling provides evidence
WebGPUMore modern GPU compute/rendering interfaceSupport surface, device/pipeline management, and fallback costsProgressive-enhancement experiment, not the baseline
DOM/CanvasSemantic UI and a high-frequency pixel layer each do their own jobCoordinates, focus, and stacking must stay synchronized across two layersCurrent choice
DOM/GPUNative UI plus high-throughput renderingTwo lifecycles and a higher engineering thresholdWhen scale is high and GPU benefit is proven

A review must record every dimension: native text editing, DOM semantics, Accessibility, visible object count, total object count, changes per second, pixel effects, visual and semantic export requirements, Renderer replacement cost, multiplayer collaboration model, target browsers, and whether the team can maintain geometry/GPU code. Collaboration alone does not determine a Renderer; it synchronizes Document operations, not pixels.

Evidence and compatibility

Browsers define <canvas> as an element that can bind contexts such as 2d, webgl, webgl2, and webgpu. That fact itself demonstrates that the element is not a specific product. Before release, check compatibility tables for target browsers and benchmark real devices; do not treat “Baseline” as a performance guarantee.

Sources above were checked on 2026-08-29. WebGPU availability and capabilities must be detected at runtime; this tutorial does not freeze a browser version number as a permanent fact.

This chapter’s engineering increment

Starting point: an empty page and a single set of business data. Finish line: DOM, SVG, and Canvas 2D implementations display the same “Order → Shipping” business diagram, and the project produces its first ADR. Planned files:

canvas-lab/
  src/lab/ch01/render-three-ways.ts
  src/lab/ch01/index.html
  src/lab/ch01/renderer-choice.ts
  src/lab/ch01/renderer-choice.test.ts
  docs/adr/0001-canvas-2d-teaching-kernel.md

Complete HTML container:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Canvas Lab / Renderer</title>
  </head>
  <body>
    <main>
      <section>
        <h1>DOM</h1>
        <div id="dom-demo"></div>
      </section>
      <section>
        <h1>SVG</h1>
        <svg id="svg-demo" viewBox="0 0 360 140" aria-label="Order process diagram"></svg>
      </section>
      <section>
        <h1>Canvas</h1>
        <canvas id="canvas-demo" width="360" height="140">Order to shipping</canvas>
      </section>
    </main>
    <script type="module" src="./render-three-ways.ts"></script>
  </body>
</html>

Complete TypeScript: all three paths consume the same data, so no business object is locked to a Renderer.

type Card = { id: string; label: string; x: number; y: number };
type Diagram = { cards: readonly Card[]; edge: readonly [string, string] };

export const diagram: Diagram = {
  cards: [
    { id: 'order', label: 'Order', x: 20, y: 40 },
    { id: 'ship', label: 'Shipping', x: 240, y: 40 },
  ],
  edge: ['order', 'ship'],
};

function boxStyle(card: Card): string {
  return `position:absolute;left:${card.x}px;top:${card.y}px;width:100px;height:56px;border:2px solid #2563eb;border-radius:10px;display:grid;place-items:center`;
}

export function renderDOM(root: HTMLElement, model: Diagram): void {
  root.replaceChildren();
  root.style.cssText = 'position:relative;width:360px;height:140px';
  const line = document.createElement('div');
  line.style.cssText =
    'position:absolute;left:120px;top:68px;width:120px;border-top:2px solid #64748b';
  root.append(line);
  for (const card of model.cards) {
    const article = document.createElement('article');
    article.dataset.shapeId = card.id;
    article.style.cssText = boxStyle(card);
    article.textContent = card.label;
    root.append(article);
  }
}

export function renderSVG(root: SVGSVGElement, model: Diagram): void {
  const ns = 'http://www.w3.org/2000/svg';
  root.replaceChildren();
  const line = document.createElementNS(ns, 'line');
  Object.entries({
    x1: '120',
    y1: '68',
    x2: '240',
    y2: '68',
    stroke: '#64748b',
    'stroke-width': '2',
  }).forEach(([key, value]) => line.setAttribute(key, value));
  root.append(line);
  for (const card of model.cards) {
    const group = document.createElementNS(ns, 'g');
    group.dataset.shapeId = card.id;
    const rect = document.createElementNS(ns, 'rect');
    Object.entries({
      x: String(card.x),
      y: String(card.y),
      width: '100',
      height: '56',
      rx: '10',
      fill: 'white',
      stroke: '#2563eb',
      'stroke-width': '2',
    }).forEach(([key, value]) => rect.setAttribute(key, value));
    const text = document.createElementNS(ns, 'text');
    text.setAttribute('x', String(card.x + 50));
    text.setAttribute('y', String(card.y + 34));
    text.setAttribute('text-anchor', 'middle');
    text.textContent = card.label;
    group.append(rect, text);
    root.append(group);
  }
}

export function renderCanvas(canvas: HTMLCanvasElement, model: Diagram): void {
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Canvas 2D unavailable');
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = '#64748b';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(120, 68);
  ctx.lineTo(240, 68);
  ctx.stroke();
  for (const card of model.cards) {
    ctx.fillStyle = 'white';
    ctx.strokeStyle = '#2563eb';
    ctx.beginPath();
    ctx.roundRect(card.x, card.y, 100, 56, 10);
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = '#0f172a';
    ctx.textAlign = 'center';
    ctx.font = '16px system-ui';
    ctx.fillText(card.label, card.x + 50, card.y + 34);
  }
}

renderDOM(document.querySelector('#dom-demo')!, diagram);
renderSVG(document.querySelector('#svg-demo')!, diagram);
renderCanvas(document.querySelector('#canvas-demo')!, diagram);

The decision interface and tests give the ADR verifiable inputs:

export type ProductNeeds = Readonly<{
  nativeSceneText: boolean;
  semanticScene: boolean;
  visibleDynamicShapes: number;
  changesPerSecond: number;
  benchmark?: Readonly<{ domP95Ms: number; canvasP95Ms: number; frameBudgetMs: number }>;
}>;
type SceneChoice = 'dom' | 'canvas2d' | 'prototype-required';

export function chooseLayers(n: ProductNeeds): { ui: 'dom'; scene: SceneChoice } {
  if (n.nativeSceneText || n.semanticScene) return { ui: 'dom', scene: 'dom' };
  if (!n.benchmark) return { ui: 'dom', scene: 'prototype-required' };
  const canvasWins =
    n.benchmark.canvasP95Ms <= n.benchmark.frameBudgetMs &&
    n.benchmark.canvasP95Ms < n.benchmark.domP95Ms;
  return { ui: 'dom', scene: canvasWins ? 'canvas2d' : 'dom' };
}
import { describe, expect, it } from 'vitest';
import { chooseLayers } from './renderer-choice';
describe('renderer ADR rule', () => {
  it('never moves semantic product UI into the bitmap', () => {
    expect(
      chooseLayers({
        nativeSceneText: true,
        semanticScene: true,
        visibleDynamicShapes: 2000,
        changesPerSecond: 120,
      }),
    ).toEqual({ ui: 'dom', scene: 'dom' });
  });
  it('refuses to turn an object count into an unmeasured renderer verdict', () => {
    expect(
      chooseLayers({
        nativeSceneText: false,
        semanticScene: false,
        visibleDynamicShapes: 2000,
        changesPerSecond: 120,
      }).scene,
    ).toBe('prototype-required');
  });
  it('permits Canvas only when the measured scene meets its frame budget', () => {
    expect(
      chooseLayers({
        nativeSceneText: false,
        semanticScene: false,
        visibleDynamicShapes: 2000,
        changesPerSecond: 120,
        benchmark: { domP95Ms: 24, canvasP95Ms: 8, frameBudgetMs: 12 },
      }).scene,
    ).toBe('canvas2d');
  });
});

The ADR must state that Canvas 2D was chosen for the teaching kernel so you can implement the Bitmap lifecycle, Renderer, geometry, and scheduling yourself. The Toolbar, forms, and text editing remain in the DOM. Replacement triggers include “return to DOM/SVG when native text and semantics dominate” and “move to a GPU when benchmarks prove Canvas 2D misses the p95 frame budget and a GPU prototype improves it.” Record compatibility, team capability, exports, collaboration, and exit costs as well.

visibleDynamicShapes and changesPerSecond enter the experimental scenario and ADR, but they are not universal thresholds across products. Returning “prototype required” without a Benchmark prevents sample numbers from masquerading as laws. Run npm run dev; expect the browser to show three diagrams with identical content, with selectable DOM text. Run npx vitest run src/lab/ch01/renderer-choice.test.ts; expect 3 passed.

Return to the game: we did not argue whether paper or chalkboards are “more advanced.” We put editable text on paper and placed the scene that must be redrawn repeatedly on the board.

Break it on purpose

Draw the heading and an ordinary form inside Canvas too, then remove the real <h1>, <label>, and <input> elements from the page.

InjectionSymptomEvidenceFixRegression testRecovery
Pixel headingThe heading cannot be selected and is absent from the document outlineNo heading in the DevTools Accessibility TreeUse a real <h1>; Canvas draws only the scenegetByRole('heading') succeedsRestore the DOM heading
Pixel inputTab cannot enter it; a screen reader does not know its label/valueKeyboard recording and Accessibility TreeUse a <label><input> DOM OverlayEnter and submit with keyboard onlyDelete the fake input
Canvas-laid-out formCSS Grid, wrapping, and validation all require hand-written replacementsCompare implementation size and browser-test countKeep product UI in the DOMNo overlap at 200% ZoomRestore the original layout
Coordinate-assertion testsA font or DPR change makes them brittleTest fails on another operating systemQuery DOM roles; test the Canvas model and screenshotsNode unit tests plus browser visual testsRemove hard-coded coordinates

Screen Reader, Keyboard, Text Selection, CSS, Layout, and Testing are not decorations to add later; they are Renderer-selection costs. After completing the failure experiment, restore the real DOM controls and rerun keyboard and screen-reader checks.

Pass with evidence

Claim to proveAcceptable evidenceUnacceptable answer
Which layer needs CanvasDynamic visible-object and frame Traces, an ADR, and a prototype“Canvas is faster”
Which layer remains DOMKeyboard, semantic-tree, and text-editing requirements“Everyone does it this way”
Renderer is replaceableAll three implementations consume the same Diagram typeStoring business objects as DOM nodes
Compatibility is controlledTarget-browser testing and feature detectionLooking only at a global support table
  • I can explain the difference between an Infinite Canvas and a <canvas> element.
  • I compared DOM, SVG, Canvas 2D, WebGL, WebGPU, and both Hybrid options.
  • I filled in all twelve selection dimensions instead of only the object count.
  • The three implementations show the same business diagram and consume the same data.
  • The fake-form Canvas failure has recordings, semantic-tree evidence, and tests, and has been recovered.
  • The ADR documents the current choice, replacement conditions, and collaboration/export/licensing/team costs.

Explain it to a five-year-old

Without using the words “Renderer,” “DOM,” or “GPU,” answer this: Why should a storybook that can read itself aloud not be drawn entirely as one photograph? Then answer this: Why might a fast-moving, colorful background not be made from ten thousand editable paper notes?

Expand a good jargon-free answer

A photograph is very good at showing you many colors at once, but it does not know which part is a title or where someone can type. A machine that reads books does not know what to read first either. That is why words and buttons should remain things that can be found, selected, and operated. In the other direction, if a large patch of constantly moving color is divided into too many little paper notes, arranging every note whenever it moves becomes a lot of work. The best approach is to preserve identities for the things people must understand and operate, and give the parts that must be redrawn quickly to a drawing board—just like attaching the paper to the chalkboard in our game.