advanced not realistically solo design
Build your own vector canvas editor (a personal Figma)
You'll build a single-player vector canvas editor that runs entirely in the browser: rectangles, ellipses, text and frames you can select, move, restyle, and export as SVG or PNG, with documents saved locally. A canvas is the browser's drawing surface, and vector means shapes are stored as coordinates and paths rather than pixels, so they stay sharp at any zoom. This is a weekend-scale sketch of Figma's core idea, and it stops being a toy exactly where Figma's business begins: many people editing the same file at once. Teams pay Figma because the product is coordination inside one live document, not because drawing rectangles is hard.
What you'll learn
- Rendering a scene graph, a plain list of shape objects, onto an HTML canvas every frame
- Hit testing and handle math: turning pointer coordinates into selections, moves, and resizes
- Designing a small JSON document model with versioned import and export
- Persisting structured data offline with IndexedDB
- Judging which parts of a product are features you can build versus infrastructure you cannot
Before you start
- Node.js 20 or newer installed (check with: node --version)
- A code editor such as VS Code
- A current version of Chrome, Edge, or Firefox, plus basic DevTools familiarity
- Git installed for checkpoint commits as you go
- No accounts needed: the build is local-first and everything stays in your browser
The build
Create a Svelte and Vite project, then define the heart of the app: a document object holding a plain list of shapes with ids, types, and geometry. Write a render function that clears the canvas and draws each shape in order, and confirm you can see seeded shapes before touching interaction code. Keeping rendering separate from input now pays off later, when selection and export both reuse it.
step prompt
Build a single-page vector canvas editor scaffold. Requirements:
- Svelte 5 with Vite: npm create vite@latest canvas-editor -- --template svelte, dev server on localhost:5173
- src/lib/document.js exports createDocument() returning { id, name, schemaVersion: 1, shapes: [] }, where a shape is { id, type: 'rect' | 'ellipse' | 'text', x, y, width, height, fill, stroke, strokeWidth, cornerRadius }
- src/lib/render.js exports render(ctx, doc) that clears the canvas and draws shapes in array order
- Seed the app with 3 rects, including one frame-sized backdrop, and 1 text node, so the first paint shows 4 shapes
- Full-window canvas element with devicePixelRatio scaling so edges stay crisp
- No backend, no router, no state library; module-level state is fine
- No secrets are needed today; if any key ever appears it goes in .env and stays uncommitted
- Out of scope: pan, zoom, undo, and any networking; do not add a sync layer
Pain warning: canvas text metrics vary by platform, so fix text nodes at fontSize 16 with the default font for now.
Hand over a precisely specified interaction chunk: click to select, drag to move, drag eight handles to resize. Geometry like this is deterministic, self-contained, and easy to verify visually, which makes it ideal to delegate whole. Your job is writing the spec tightly, then poking the edges: tiny shapes, negative drags, shapes stacked on top of each other.
step prompt
Add selection, move and resize to the canvas editor. Requirements: - Reuse src/lib/document.js shapes and src/lib/render.js; track the selection in a new src/lib/editorState.js - Click selects the topmost shape whose bounds contain the pointer; clicking empty canvas deselects; Escape deselects - Dragging the body moves the selected shape; 8 square handles (corner handles 8px, edge handles 6px) resize it with a minimum size of 8x8 - Holding Shift while dragging a corner handle preserves aspect ratio - Draw outlines and handles in a new src/lib/overlay.js so render.js stays a pure function - Cursor feedback: move cursor over the body, directional resize cursors over each handle - Out of scope: multi-select, grouping, snapping, and rotation; do not add them Verify against a rect, an ellipse and a text node; if resize misbehaves for one type, report it rather than silently special-casing.
Connect the selection to a right-hand properties panel so clicking a shape exposes editable fill, stroke, stroke width, and corner radius. Do this one yourself: wiring form inputs to your own document model tests every assumption from step one, and the debugging is where the learning lives. Watch the full chain flow: input event, model mutation, re-render, pixels.
step prompt
Add a properties panel bound to the selected shape. Requirements: - A right-side panel 240px wide with fill (color input), stroke (color input), strokeWidth (number input, min 0) and cornerRadius (number input, min 0) - Edits mutate the shape immediately and trigger render; no Apply button - With nothing selected, show the document name and shape count instead of controls - Text nodes get a text-content textarea alongside their fill control - Show cornerRadius for rects only; hide the field for ellipses and text - Store all colors as hex strings in the document so future JSON export round-trips cleanly - Out of scope: gradients, opacity, typography settings, saved style libraries Known friction: number inputs fire events faster than renders want; batch updates with requestAnimationFrame if typing feels laggy.
A layers list mirrors the shapes array as rows and lets dragging reorder them, which changes paint order on the canvas. It consumes state that already exists, touches no geometry, and has obvious done criteria: list order equals array order. That isolation is exactly what makes it safe to hand away.
step prompt
Add a layers panel with drag-reorder. Requirements: - New LayersPanel.svelte docked left, 200px wide, one row per shape showing the type initial and a shortened shape.id - Rows render in the same order as document.shapes; the top row paints last, matching canvas stacking - Reorder with HTML5 drag-and-drop or pointer events; on drop, splice document.shapes to match the list and call render - Clicking a row selects that shape and syncs the properties panel from the previous step - Double-clicking a row renames it via a new name property on the shape model - Out of scope: nested groups, visibility toggles, locks, thumbnails Acceptance check: drag a text row above a rect row and confirm the canvas repaint order matches immediately, no reload.
Make the editor survive a refresh by saving the document to IndexedDB, the browser's built-in database for structured data, shortly after each change. Then add explicit actions that download the document as .json and load one back, turning your work into portable files you own. Autosave protects the session; export creates ownership.
step prompt
Add persistence and JSON import/export to the canvas editor. Requirements: - src/lib/storage.js opens an IndexedDB database named canvas-editor with an object store called documents keyed by id - Save 500ms after the last mutation using a debounce helper; restore the newest saved document on startup - Export JSON downloads <docname>.json via Blob and URL.createObjectURL; Import JSON reads with FileReader, checks that shapes is an Array, then loads it - Reject files whose schemaVersion is missing or not 1, with a visible inline message - Use IndexedDB only; leave localStorage untouched - Out of scope: cloud sync, encryption, multi-document tabs, and conflict resolution between autosave versions Test honestly: export, make a bad edit, refresh, import, and confirm ids, names, positions and hex colors come back unchanged in the JSON.
Finish by exporting the current selection: serialize chosen shapes to SVG markup, the vector format browsers render natively, and rasterize the same markup to PNG through an offscreen canvas. Then write a README that states plainly what this is, a single-player sketch, and points readers who need real-time collaboration to Penpot or Excalidraw. Naming the boundary is part of the craft.
step prompt
Add SVG and PNG export for the current selection. Requirements: - src/lib/exportSvg.js maps each selected shape to SVG elements: rect with rx for cornerRadius, ellipse, text as a text element; wrap them in an svg element with xmlns and a viewBox sized to the selection bounding box plus 16px padding - src/lib/exportPng.js turns that SVG string into a blob URL, draws it onto an offscreen canvas at 2x scale, and downloads a PNG via toBlob - Two toolbar buttons, active only while a selection exists - fill, stroke, strokeWidth, cornerRadius and text content must all appear in the SVG output - Write README.md covering: what it is (a single-player canvas sketch), how to run it (npm install, npm run dev), the schemaVersion note, and pointers to Penpot and Excalidraw for anyone needing real-time collaboration - Out of scope: PDF export, SVG import, clipboard copy, print stylesheets Check by exporting one shape of each type and opening the resulting SVG in a fresh browser tab.
What you won't get
- Editing stays single-player: two people opening the same file will not see each other's changes
- Files stay on your machine unless you export them, so there are no share links, comments, or team workspaces
- There is no plugin system, so the tool does exactly what you build into it and nothing more
- Large files with hundreds of shapes may stutter, since this build skips years of rendering optimization
- Existing Figma files will not import
Why people still pay — and what that teaches you
collaboration: Figma's hardest part is invisible: multiplayer editing built on CRDTs (conflict-free replicated data types, data structures that merge simultaneous edits without conflicts) running on servers tuned for millions of live sessions. The builder takeaway is that 'just sync the shapes' is a distributed-systems problem wearing a design tool's clothes.
network-effects: Figma wins because the file everyone else already opens is the file everyone keeps opening; leaving means moving your whole team at once. The builder takeaway is that shared-file gravity beats feature checklists, so personal tools are wisest aimed at workflows no crowd has settled.
execution-polish: A decade of canvas engineering sits under Figma's feel: instant edits and smooth zooming on enormous files. The builder takeaway is that performance work is cumulative and unglamorous, users read it as quality, and shipping an honest 80 percent, clearly labeled, is a respectable outcome.
Stretch goals
- Add keyboard shortcuts: Delete removes, Ctrl/Cmd+D duplicates, arrow keys nudge 1px and Shift+arrow nudges 10px
- Feel the multiplayer wall firsthand: sync two browser tabs with Yjs over a 30-line WebSocket server and watch conflict handling get complicated
- Add a minimal component system: mark a shape as a symbol and let copies inherit its style edits
All steps done — did it work?
Congratulations. Tell someone what you built.
About Figma
Figma costs $20/month. They pay because entire product/design teams coordinate inside the file, not because drawing rectangles is hard.
Sources & further reading
- Penpot — The serious open-source design tool; its repo shows what real multiplayer design software actually takes.
- Excalidraw — Readable source for a collaborative whiteboard; a great reference for canvas interaction code.
- Quant-UX — Open-source prototyping and user-testing tool; useful for comparing scope decisions against your own build.
Finished alternatives (if you'd rather not build)
- Lunacy — A polished free desktop UI editor with components, prototypes, cloud documents, and native apps; collaboration limits are small-team sized.
- Penpot — The real open-source answer: browser UI design, components, tokens, prototypes, comments, and multiplayer without pretending a whiteboard is Figma.
- Quant-UX — Browser prototypes, collaboration, user tests, and analytics in one product; weaker than Figma for pixel-perfect design systems.
Keep building
New lessons and honest build notes, by email. No spam, one-click out.
Signups open when the site goes live.