Skip to content
st-patrickPublic

About

An open-source PureRef for the browser: an infinite wall for reference images. No account, no upload, works offline.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Tack

An open-source PureRef for the browser. An infinite wall you drop images on, arrange, crop, rotate, pack tight, annotate, and keep. Nothing leaves your computer: boards live in the browser's IndexedDB, the app works offline once it has loaded, and there is no account and no server.

Live: https://projects.patrickreinbold.com/tack/ — installable as a PWA. Source: https://github.com/st-patrick/tack — MIT. Landing page: landing.html. Contact: bugs and feature requests in issues, or email tack@patrickreinbold.com — it reaches one person, and that person reads it. Support: if Tack saved you a licence fee, you can put something back.

Tack with a packed board

What it does

  • Images in by drag and drop (from the desktop or another browser tab), by paste (a screenshot on the clipboard, an image URL, or plain text becomes a note), or through the file picker. Original bytes are kept — nothing is re-encoded.
  • Arrange: move with edge/centre snapping to neighbours, scale about the opposite corner (or the centre with Alt), rotate with a knob (Shift snaps to 15°), crop by dragging edges, flip, greyscale, opacity, z-order, duplicate, nudge with the arrow keys. Multi-select by marquee or Shift-click; group scale and rotate.
  • Pack (Ctrl P) tidies everything — or the selection — into a tight rectangle shaped like your window; Ctrl Shift P first normalises heights.
  • Notes: double-click empty space, type. They wrap, scale and rotate like images.
  • Boards: any number of them, switch from the toolbar. A board can be saved as one .tack file with the images inside, and opened again anywhere. Export the board or a selection as a PNG at full resolution.
  • Undo/redo for everything, autosave 400 ms after the last change, and on tab switch.
  • Light and dark, keyboard-first, touch pinch/pan on tablets.

Run it

It is static files. Serve the folder with anything:

npx serve .          # or python -m http.server, or open index.html over http

There is no build step and no dependency. The source is ES modules:

src/scene.js    the board: items, camera, selection, undo (pure data)
src/render.js   canvas drawing, handle geometry, PNG export
src/input.js    pointer + keyboard gestures, snapping, pinch
src/store.js    IndexedDB (boards + original image blobs)
src/images.js   decode cache with a ≤1024px mip for zoomed-out drawing
src/pack.js     shelf packing ("Optimize")
src/files.js    drops, pastes, URLs, .tack export/import
src/app.js      wiring: boards, autosave, menus, note editing
sw.js           offline shell cache

Test

test/walk.mjs drives the app in headless Chromium: drops synthetic images, packs, drags, scales, rotates, crops, flips, marquee-selects, group scales, writes a note, reloads to check persistence, saves and reopens a .tack file, exports a PNG, toggles theme, and checks that nothing logged an error. It also reads the saved file with its own ZIP reader (mimetype first, original bytes, sources, a byte-identical second save), opens a board in the older JSON form, refuses one from a newer format version, tells a wheel notch from a two-finger scroll from a pinch, and reloads with the network off. Playwright is the only (dev) dependency, and it lives in test/ so the app itself stays dependency-free.

cd test && npm install
npm run serve      # serves the repo root on :8460
npm run walk       # in a second terminal; screenshots land in test/shots/
npm run walk:live  # the same walk against the deployed site
npm run stale      # only the deploy-reaches-an-open-tab test (own server, port 8461)
npm run fixture    # save a board with the app for ../desktop/tests/fixtures/ (needs npm run serve)

A walkthrough run opts itself out of the visit counter, so it can be pointed at the live site freely.

test/health.mjs asks a different question — is what people can reach intact? It follows the page instead of a list: every src/href, the module import graph, the service worker's precache list, the manifest and its icons, each checked for status, content type and being what it claims to be. It fails if the app imports a module that sw.js does not precache, and it compares every deployable file with this checkout ("drift").

npm run health                     # the live site; exits 1 on a failure
node health.mjs --strict           # after a deploy: drift fails too
node health.mjs http://localhost:8460/

File format

.tack is a ZIP: a mimetype entry first (application/x-tack-board), a board.json manifest, and the imported files under images/, byte for byte as they arrived and named by their content. Rename a board to .zip and the pictures are there. The manifest is { app: "tack", v: 1, name, cam, items[], blobs{ id: "images/<id>.<ext>" } }. Item fields: kind (image|note), x y (centre, world px), scale, rot (radians), flipX flipY gray opacity z, crop {l t r b} as fractions of the natural size, nw nh natural pixels, blobId, source (where the picture came from; never fetched); notes carry text w.

Boards saved before 28 August 2026 were a single JSON document with the images inlined as data: URLs. Those still open. The format is specified in docs/BOARD-FORMAT.md, which is public domain. That file is a copy of the one kept with the desktop app; npm run health fails when the two differ.

What comes next

The backlog is a ranked list of concrete steps, each with the files it touches and what "done" means. It is kept with the working notes rather than in the repo; ask if you want to know what is planned.

2026-10-02 — health check, walkthrough refresh, two fixes, a deploy

An unattended run.

  • The live site was a month behind the repo. The three commits of 3 September (image sources, two-finger pan, Shift as "finer") had never been deployed. They are live now, with the two fixes below.
  • test/health.mjs is new (005a850), and found the above on its first run. See Test.
  • test/walk.mjs grew from 40 checks to 63 (4bf4921) and is green against the live site.
  • Shift+wheel zoomed in the tests and crept sideways in Chrome, which delivers that gesture with its axes swapped (68453b0). Verified with a synthetic event shaped like Chrome's; not with a physical mouse.
  • A board with a higher format version is refused with a message rather than opened as if it were version 1 (795a171).
  • This README's file-format section described the pre-August JSON form; it now describes the ZIP.

To take one of the two fixes back: git revert 68453b0 or git revert 795a171, then node ../deploy.mjs and node test/health.mjs --strict. To take the 3 September behaviour off the live site as well, revert d1477c4, a53d881 and b73b264 the same way. Do not git checkout an old index.html or landing.html over the working copy: both carry an uncommitted analytics line that is live and belongs to another session.

2026-10-09 — boards are read the way the format says

An unattended run. Four rules from BOARD-FORMAT.md that the reader did not follow:

  • Defaults. An image item may leave out scale, rot, crop, opacity, z and the flags. One that did used to blank the board (10e3e77).
  • Unknown kinds. An item that is neither an image nor a note is kept, not drawn, and written back exactly as it came (0509b76). It cannot be selected or moved, and the status line does not count it.
  • Preserve what you do not understand. Manifest fields, blobs entries and archive entries this app has no use for now survive an open-and-save, including a trip through IndexedDB (3663be1).
  • A board is a board under any name. Drop my-board.zip and it opens; isBoardFile reads the mimetype entry instead of the extension (4372e9b).

test/fixtures/sparse.tack is a board with all four in it, built by test/fixtures/make-sparse.mjs; the walkthrough has ten checks on it (73 in all). The desktop app reads the same fixture in its own tests. The service worker is tack-v6 and no longer trusts the HTTP cache (08073a2), because this change renames what the modules export.

To take it back: git revert the commits above, bump VERSION in sw.js, node ../deploy.mjs, node test/health.mjs --strict.

2026-10-09 — three ways to lose work, closed

  • Undo after a board was deleted. Delete an image, let the autosave run, delete some other board, press Ctrl+Z: the image came back as a grey rectangle, its bytes collected. store.vacuum(keep) now spares every blob the open board or its undo/redo steps can reach (4bf350e).
  • A deploy that never arrived. The host serves modules with a week's max-age. The service worker already fetched round the HTTP cache; it now also hands the page responses marked no-cache, because the tab's own memory cache was answering reloads from the old copy. tack-v7 (e3d81ae). test/serve.mjs --max-age=604800 behaves like the host, and test/stale.mjs edits a module under a running copy and reloads twice; npm run walk runs it after the walkthrough. health.mjs prints the cache-control of index.html, sw.js and src/app.js.
  • Storage the browser may clear. After the first image the app asks navigator.storage.persist(), once per visit. The Boards panel shows what the site stores and the answer: "38 MB · protected", or "38 MB · not protected — save a .tack to be safe" (b483997). The size is the browser's figure for the whole site, not for Tack's boards alone.

The walkthrough is 83 checks, plus 5 in stale.mjs.

To take any of it back: git revert that commit, bump VERSION in sw.js, node ../deploy.mjs, node test/health.mjs --strict.

2026-10-09 — two tabs, the spec, and a deploy that checks itself

  • The same board in two tabs. Each save used to write the whole board and tell nobody, so the tab that saved last won. Now a save is announced on a BroadcastChannel, and it only goes through if the stored board is still the one this tab last saw. A tab with nothing unsaved shows the other tab's change within about half a second. A tab with changes of its own stops autosaving and asks: This board was changed in another tab — load that version, keep mine as a copy, or overwrite with mine (12645ed, tack-v8).
  • The format spec is here. docs/BOARD-FORMAT.md is a copy of the desktop app's; npm run health fails when the two differ (b618daa).
  • Fixtures both ways. npm run fixture saves a board with the real app and hands it to the desktop tests; test/fixtures/written-by-the-desktop-app.tack is the desktop writer's output, and the walkthrough opens it (77a3619).
  • Deploys are verified. ../deploy.mjs sends one file per call, only what differs, then fetches every file back and re-sends any that is wrong. deploy.json is no longer uploaded (e49546f for the health check's side).

The walkthrough is 101 checks, plus 5 in stale.mjs.

To take any of it back: git revert that commit, bump VERSION in sw.js, node ../deploy.mjs, node test/health.mjs --strict. The old deploy script is ../deploy.mjs.bak-2026-10-09.

2026-10-09 — a finger, and "Open with Tack"

Written by an autonomous run while Patrick was away.

  • Touch. Hold a finger still on an image, a note or the empty board for half a second and the menu the right mouse button opens appears — which is where Delete, Duplicate, flip, greyscale and crop live when there is no keyboard. After a touch the corner handles are larger and can be taken from 22px away; the next mouse press makes them small again. Tested with synthetic touch events only, not yet on a tablet.
  • Files. An installed Tack (Chrome, Edge) that is handed a .tack file by the system opens it, and the "Open board" picker lists .zip files too, since a board renamed to .zip is still a board.
  • Deploying. node ../deploy.mjs compares the cache number in the sw.js the site serves with the one in this checkout and stops when this one is lower — the live site was published from fleet/2026-10-09, and a deploy from an unmerged main would have rolled it back. --older overrides. Bump VERSION in sw.js with every change to the app.
  • The walkthrough has 113 checks (+ 5 in stale.mjs).

Licence

MIT.

About

An open-source PureRef for the browser: an infinite wall for reference images. No account, no upload, works offline.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages