Lasso painting in the browser. You draw closed loops, each one becomes a filled shape, and they stack in the order you paint them. No brushes, no strokes: the lasso is the tool.
Everything is rendered with three.js on a WebGL canvas, so the artwork is real geometry rather than pixels. That is why a reload restores your drawing exactly rather than a flattened image of it, and why the eyedropper can read colour straight out of the frame buffer.
This build has no backend and no accounts. Drawings are kept in the browser's localStorage on the device that made them, which has two consequences worth knowing before you rely on it:
- Nothing syncs. A sketch made on a tablet stays on that tablet.
- Storage is not permanent. iOS Safari evicts localStorage for any site you have not visited in seven days, and the quota is around 5MB.
Use Back up in the portfolio toolbar to download everything as a single JSON file, and Restore to load it back or move it to another device. Restore merges, so it will not wipe work already on the device.
| Lasso | L |
| Eyedropper | I, or hold Alt / right mouse button |
| Pan | H, or hold Space / middle mouse button |
| Zoom | Z plus click, or Cmd / Ctrl with + and - |
| Zoom to 100% | Cmd / Ctrl + 0 |
| Undo last shape | Cmd / Ctrl + Z |
| Show or hide the palette | Shift + P |
| Export as an image | Cmd / Ctrl + E |
| Cancel the current lasso | Esc |
On a tablet, drag to lasso, one finger to pan, two to zoom, and long press to sample a colour.
yarn
yarn devTo reach it from another device on the same network, yarn dev --host and open the printed
network address.
yarn buildPUBLIC_BASE_PATH sets the asset prefix, which a project site served from a subdirectory
needs. The deploy workflow sets it from the repository name automatically.
yarn lint && yarn typecheck must pass. yarn test runs vitest in watch mode, so use
yarn vitest run anywhere that needs to exit.
Willow previously stored canvases in Supabase, behind magic link and Discord sign in. That
project no longer exists, and nothing that defined it was ever in version control: not the
table, not the four Postgres functions the client called, not the storage bucket. Rather than
reconstruct it, the eleven calls it served are implemented against localStorage in
src/DataSource/LocalBackend.ts.
Every repository function in src/Feature/*/Api dispatches on one flag, so restoring a real
backend means writing a second module with the same eleven functions. No component, and none
of the rendering code, needs to change.