01 / Project summary
A real canvas tool, not only a visual mockup.
A browser drawing editor needs predictable tool state, durable history, safe imports, and export paths that work without a backend. I built Web Paint with the Canvas 2D API, vanilla JavaScript state, pointer events, typed-array flood fill, capped ImageData history, PNG import/export, localStorage save/load, canvas resizing, zoom, and a desktop-inspired toolbar that still works at narrow widths.
Technical problem
Canvas tools have to coordinate mutable pixels, active tool state, pointer input, history, imports, exports, zoom, and resize behavior.
Constraints
The editor runs with no framework or backend, so browser APIs carry file handling, storage, clipboard, and fullscreen behavior.
Approach
I used a focused vanilla JavaScript state model, Canvas 2D drawing paths, pointer capture, typed-array flood fill, and capped ImageData snapshots.
Tradeoff
ImageData history is straightforward and reliable for a small editor, but it becomes memory-heavy on large canvases.
Next improvement
The next pass would add command or dirty-region history, worker-backed persistence, selection movement, richer shortcuts, and better nonvisual output.
Why this matters
It makes a browser-only drawing tool feel usable.
Tool problem
Lightweight drawing and markup tasks still need predictable tools, undo/redo, import, export, zoom, and mobile controls without forcing users into installed desktop software.
Useful approach
The Canvas 2D API, pointer events, focused tool state, bounded ImageData history, and browser file/clipboard/storage APIs make the editor usable without a backend.
Employer signal
The project shows UI state modeling, DOM event handling, browser API integration, performance tradeoffs, mobile layout, and labeled controls.
Production readiness
The editor works; larger documents need a more robust model.
Already solid
The tool has working drawing modes, pointer input, undo/redo snapshots, import/export, local save/load, zoom, resize controls, visible status updates, and responsive tool panels.
Prototype-only
History uses full ImageData snapshots. Canvas content has no semantic model, text placement uses a prompt, and the editor lacks layers, document names, and full shortcuts.
Next engineering work
A larger editor would need memory-aware history, autosave recovery, selection movement, richer shortcuts, workers for heavy operations, browser testing, and nonvisual descriptions.
Security and privacy
The tool keeps drawings local. Cloud save or sharing would require file validation, privacy controls, retention rules, upload limits, and abuse handling.
02 / What I built
The full editor surface and interaction model.
- Implemented Canvas drawing modes for pencil, eraser, shapes, fill, text, selection, and preview rendering.
- Built undo/redo snapshots, image import validation, PNG export, local save/load, zoom, and canvas resizing.
- Managed tool settings, colors, brush size, shape choices, selection bounds, drawing drafts, pointer capture, and status output in vanilla JavaScript.
- Added paste, copy, cut, fullscreen, canvas-size controls, mobile-friendly control panels, and mobile instruction copy.
- Included guardrails for MIME type, file size, pixel count, bounded canvas dimensions, requestAnimationFrame-based drawing, and large-canvas history limits.
External pieces are browser APIs: Canvas 2D, File input, Clipboard, Fullscreen, object URLs, and LocalStorage. The editor logic and UI wiring are vanilla JavaScript in this repo.
03 / Tool list
Editor features map to drawing tasks.
| Area | Tools | Implementation detail |
|---|---|---|
| Drawing | Pencil, brush, eraser, text, fill bucket. | Pointer events convert screen coordinates into canvas coordinates; brush settings map to stroke style, line width, caps, joins, and fill color. |
| Shapes | Line, rectangle, rounded rectangle, circle, triangle, right triangle, star, polygon. | Shape drawing uses a saved draft image so dragging previews the current geometry without permanently painting every intermediate frame. |
| Color and size | Palette swatches, active color preview, 2px, 6px, 12px, and 20px brush controls. | UI buttons update shared state and active button classes so toolbar and drawing engine stay in sync. |
| Canvas operations | New canvas, erase canvas, width/height controls, drag resize handle, zoom, fullscreen. | Canvas dimensions are clamped from 64 to 4096 pixels; resize preserves overlapping pixels and resets history after the new canvas is committed. |
| File and clipboard | Open image, paste image/text, copy selection, cut selection, save PNG, local save. | File APIs, ClipboardItem, object URLs, canvas blobs, LocalStorage, and PNG data URLs support open, paste, copy, export, and local-save actions. |
04 / Canvas rendering model
Pointer input is converted into canvas-space drawing.
-
Capture pointer
pointerdownsets pointer capture, stores the current canvas rectangle, and calculates the starting canvas-space coordinate. - Pick behavior by tool Fill, text, selection, freehand drawing, and shape drawing take different paths instead of trying to force all tools through one handler.
- Render freehand strokes Pencil, brush, and eraser draw line segments from the previous point to the latest point, using the current brush configuration.
- Preview shapes from a draft Shape drawing stores an ImageData draft at pointer start, restores it during drag, then renders the current geometry on top.
- Commit a history snapshot On pointer release, the final canvas pixels are saved into bounded undo history so future edits can restore a known image state.
Continuous pointer movement is throttled through requestAnimationFrame with a pending point, which keeps drawing fluid under pen/mouse input while avoiding unnecessary repeated work during high-frequency pointer events.
05 / Tool state management
A focused state object coordinates the editor.
Active mode
activeTool and activeShape decide whether the next pointer action draws freehand, fills an area, places text, creates a selection, or previews a shape.
Drawing session
isDrawing, lastPoint, startPoint, draft, pendingPoint, and frameRequest separate the live drag from committed canvas state.
Selection and view
selectionRect, selectionStart, zoom, and cached canvas rectangles support selection overlays, clipboard operations, and scaled coordinate conversion.
The important choice is that the canvas pixels remain the source of truth for the image, while JavaScript state describes the current interaction. That keeps the implementation understandable without a component framework.
function setTool(tool) {
if (tool !== "select") clearSelection();
state.activeTool = tool;
state.activeShape = "";
toolStatus.textContent = `Tool: ${titleCase(tool)}`;
syncActiveControls();
}
This is the bridge between the DOM toolbar and the canvas input handlers.
06 / Undo and redo strategy
History stores bounded ImageData snapshots.
Undo and redo are implemented with an array of ImageData snapshots and a history index. Before saving a new edit, the app slices away any redo states after the current index, pushes the latest pixels, and caps the stack at 24 snapshots. Restoring a state uses putImageData.
This direct snapshot model works for a small editor because every tool produces a final pixel state. The tradeoff is memory: large canvases make each snapshot expensive. A production version would likely move toward command history, dirty-region snapshots, compression, or worker-backed persistence for bigger documents.
function saveHistory() {
state.history = state.history.slice(0, state.historyIndex + 1);
state.history.push(context.getImageData(0, 0, canvas.width, canvas.height));
if (state.history.length > maxHistory) state.history.shift();
state.historyIndex = state.history.length - 1;
}
function restoreHistory(index) {
const snapshot = state.history[index];
if (!snapshot) return;
context.putImageData(snapshot, 0, 0);
}
The simple snapshot model fits the current editor, while the surrounding case study names the memory tradeoff.
07 / Import and export flow
Browser APIs make the tool work without a backend.
- Open image The hidden file input accepts common image types. JavaScript validates MIME type and file size before loading the image through an object URL.
- Validate dimensions Imported images with no dimensions or more than 48 million pixels are rejected before they are drawn onto the canvas.
- Fit to canvas The image is scaled to fit the current canvas while preserving aspect ratio, centered on a white background, then committed to history.
-
Clipboard flow
Copy and cut export either the full canvas or the current selection with
toBlobandClipboardItem, with download fallback when clipboard writing is unavailable. -
Export and local save
Save downloads a PNG. Local save stores a PNG data URL in
localStorage, enough for this browser tool but not a full document system.
08 / Keyboard, mouse, and touch
Pointer events keep the drawing model device-agnostic.
Mouse and pen
Canvas drawing and resizing use pointer events, pointer capture, and canvas-space coordinate mapping so drag behavior remains stable while the user is interacting with the canvas.
Touch devices
The canvas sets touch-action: none, allowing touch drawing without the browser treating the gesture as page scroll or zoom inside the drawing surface.
Keyboard access
Toolbar controls are native buttons and numeric inputs. Width and height fields commit on Enter, but richer shortcuts such as Ctrl+Z, Ctrl+Y, tool hotkeys, and selection movement are still future work.
09 / Nonvisual limits and improvements
The shell is keyboard-friendly, but canvas content needs a deeper strategy.
The interface uses semantic buttons, labels, visible focus states, ARIA labels for icon-only controls, and descriptive toolbar groups. The canvas itself has a label, but the drawing content is still visual pixel data with no equivalent structured representation.
Current support
Native buttons and inputs support keyboard focus, tool groups are labeled, color swatches announce their value, and selection/resize controls have names.
Known limitations
The app does not yet expose the drawn image as semantic content, lacks full keyboard drawing commands, and uses a prompt for text placement rather than an inline text tool.
Next improvements
Add keyboard shortcuts, a command palette, stronger status announcements, optional high-contrast toolbar mode, and an exportable text description field for drawings.
10 / Performance considerations
Large canvases make memory and pixel work the main constraints.
Web Paint already clamps canvases to 4096 by 4096 pixels, caps imports by file size and pixel count, runs flood fill with typed arrays, schedules resize preview work through requestAnimationFrame, and limits history to 24 snapshots.
The biggest limitation is that undo/redo stores full ImageData snapshots. At large dimensions, every snapshot can consume significant memory, and operations such as flood fill or full-canvas history save can become expensive. A production version would add memory-aware history limits, offscreen canvas or worker processing, region-based invalidation, progressive import feedback, and stronger handling for low-memory mobile devices.
11 / Testing and QA notes
QA focuses on canvas state, browser APIs, and mobile input.
Automated tests
No automated suite exists yet. Current confidence comes from manual checks of drawing tools, state transitions, import/export paths, and responsive controls.
Manual checklist
Check every tool mode, shape preview/commit, undo/redo, reset/new canvas, resize, zoom, fullscreen, image import, paste, copy/cut selection, PNG export, local save/load, and status messages.
Browser and device checks
Test desktop mouse/keyboard, mobile touch, tablet widths, high-DPI screens, clipboard permissions, fullscreen availability, focus visibility, and reduced-motion behavior.
Edge cases
Exercise invalid files, huge images, blocked clipboard writes, unavailable LocalStorage, canvas limits, rapid tool switching mid-drag, empty selections, and large-canvas history limits.
Known limitations
There are no regression tests for pixel output, pointer sequences, memory pressure, or cross-browser clipboard behavior. Canvas content also needs a nonvisual strategy.
12 / Deployment and run notes
Web Paint runs as a browser tool.
Hosted/run location
The live tool is hosted on GitHub Pages at zacbatten.me/paint.html. It runs entirely in the browser from HTML, CSS, and paint.js, with no backend service.
Environment/config
No environment variables are required. User settings and local saves rely on browser state such as localStorage; imported files are handled in the active browser session.
External APIs/services
There are no external APIs for drawing. The implementation depends on browser APIs: Canvas 2D, Pointer Events, File input, Clipboard, Fullscreen, object URLs, and LocalStorage.
Local development
Clone the portfolio repository, run python -m http.server 8000 from the repo root, and open http://localhost:8000/paint.html. A local server is preferred because clipboard, file, and fullscreen behavior can differ from direct file:// access.
Secrets and privacy
The current tool does not upload drawings or use secrets. If sharing or cloud save were added, it would need authentication, upload validation, storage quotas, data retention rules, and privacy controls.
13 / Screenshots and captures
Screenshots from the live browser tool.
14 / What I would improve next
The next pass would make it more editor-like.
- Add real keyboard shortcuts for undo, redo, save, tool selection, zoom, and canvas movement.
- Support moving and resizing a selected region after it is created, rather than only copy/cut/export behavior.
- Replace prompt-based text placement with an inline text box and editable text commit flow.
- Add a better autosave/reopen story, including restored local drawings and explicit document naming.
- Improve memory behavior for large canvases with region-based history or a command-style undo stack.
- Add short GIFs for import/export, undo/redo, and shape preview after the interaction polish is finalized.