Measure the wall. Arrange the art. Print the plan. Bang the nails in the right place.
Hang is a wall arrangement planner that runs entirely in your browser. Describe a real wall — its size, its colour, the radiator and the light switch you can't cover — then drag your framed pieces around until the arrangement looks right. When it does, print three A4 sheets and take them to the wall.
No server, no accounts, and your photographs never leave the machine.
- Describe the wall. Width, height and colour in millimetres, drawn to scale.
- Block out what you can't cover. Doors, radiators, sockets, a sloped ceiling — dragged into place, so an awkward wall can be described properly.
- Add the artwork. Artwork size, mount and frame (each with its own colour), an optional photograph with a pan-and-zoom crop, and a hook position measured down from the top of the frame.
- Arrange it. Drag pieces onto the wall with snapping to edges, centres and matching gaps. Nothing may overlap another piece, a blocked area or the wall edge — the geometry won't let you plan something you can't hang.
- Print it. Three A4 landscape sheets: the measured hanging plan, the hook positions, and a colour preview. Print to PDF and take it to the wall.
- Real arranging tools. Box-select or shift-click for multi-select, then align and distribute PowerPoint-style, drag the set together, or nudge it 1 mm at a time with the arrow keys. Full undo/redo.
- A reserve strip. Drag a piece off the wall and it waits below instead of vanishing.
- Hook positions, not guesses. The plan sheet gives ordinate dimensions from a single datum, so one mis-measurement doesn't cascade down the wall.
- Save and load. One
.jsonfile carrying the whole plan, image thumbnails included, so a plan travels between machines. - Autosave. The wall is written to
localStorageas you work. - An offline single file.
dist/hang-standalone.htmlis the entire planner — stylesheet, modules, thumbnails and web fonts inlined. Download it once and it works forever, which is handy when the wall you are measuring is nowhere near the wifi.
The site is plain HTML, CSS and ES modules — there is nothing to install and no
framework to learn. It does need to be served over HTTP (ES modules do not load
from file://):
npm run serve # http://localhost:8080, serves src/ directlyEdit anything under src/ and reload. That is the whole development loop.
npm run build # writes dist/
npm run preview # build, then serve dist/
npm test # drives the built site in Chromium (needs Playwright)npm test needs Playwright available (npm i -D playwright && npx playwright install chromium); without it the script says so and exits cleanly.
There is no bundler. scripts/build.mjs copies src/ to dist/ and adds one
extra artefact: the standalone HTML file described above. (Font embedding is
best-effort: if Google Fonts can't be reached at build time the file falls back
to the system stack and everything else still works.)
.github/workflows/deploy.yml builds and publishes on every push to main.
One-time setup in the repository: Settings → Pages → Build and deployment →
Source: GitHub Actions. No branch to create and no gh-pages to maintain.
All asset paths are relative, so the site works at a user site
(you.github.io) or a project site (you.github.io/repo/) without
configuration.
Pull requests run the same build and smoke test without deploying.
src/
index.html the shell
css/organic.css the Organic design system — tokens and components
css/app.css app chrome, scrollbars, and the print rules for A4
js/dom.js ~200-line keyed renderer: h(), createRenderer()
js/geometry.js rectangles, overlap and fit tests, in wall millimetres
js/labels.js label wrapping and shrink-to-fit
js/plan.js measurement model and the three printable sheets
js/view.js the markup
js/app.js state, history, drag, snapping, selection, file I/O
js/example-prints.js baked thumbnails for the Example wall (PLACEHOLDER — see below)
scripts/ build, dev server, smoke test — all dependency-free
docs/ the README hero
.github/workflows/ build, test, deploy to Pages
Two conventions worth knowing before editing:
- Millimetres everywhere. A piece's
x/yis the top-left corner of its frame;w/hare the artwork alone, so the framed rectangle grows by the mount plus the frame on each side (geometry.js: outer()). Only the render layer multiplies byscale()to reach pixels. - Every document change goes through
commit(). That is what captures undo history and writeslocalStorage;setState()alone is for view state (selection, menus, toasts) that history should ignore. A drag opens a transaction so the whole gesture folds into one undo step.
project/Wall Planner.dc.html— the prototype, and the visual reference this implementation matches.project/_ds/organic-*/— the Organic design system it is built on;src/css/organic.cssis a copy of itsstyles.css.
The prototype ran on a React-backed template runtime. This implementation is plain modules with its own small renderer, but the geometry, layout rules and markup are ported faithfully — the label-fitting engine, snapping maths and plan drawing are the prototype's, line for line.
The Example wall uses Banksy prints at their real published sheet sizes. Those images are in copyright and are here for the sake of a demonstration layout.
src/js/example-prints.js is a placeholder. This repository was rebuilt from the
session transcript after the original working copy was lost, and that file — 132 KB
of base64 thumbnails — was the one thing too large to recover.
To restore it, copy example-prints.js from the Claude Design handoff bundle over
src/js/example-prints.js. Nothing needs to change in the code.
Until then the Example wall still loads with the correct sizes, positions, frames and labels; the seven frames are simply empty. Two smoke-test checks skip themselves and say so, and turn back into real assertions once the file is in place.
MIT © 2026 Andy Roberts.