Skip to content

feat: Sketch & Annotation Canvas — phase 1 (annotate media images) (#2036)#2082

Merged
atomantic merged 5 commits into
mainfrom
claim/issue-2036
Jul 2, 2026
Merged

feat: Sketch & Annotation Canvas — phase 1 (annotate media images) (#2036)#2082
atomantic merged 5 commits into
mainfrom
claim/issue-2036

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Sketch & Annotation Canvas — Phase 1 (Refs #2036)

Ships the first phase of the Sketch & Annotation Canvas: a lightweight drawing surface to mark up already-generated images.

What shipped (Phase 1)

  • Canvas overlay — plain <canvas> stroke layer over the target image with draw/erase, per-stroke color + brush size, undo, and clear. No new heavy deps — a ~120-line pure stroke model/renderer (client/src/lib/sketchCanvas.js) drives it.
  • Pointer/touchpointerdown/move/up handle mouse, pen, AND touch; touch-action: none so drawing doesn't scroll. Verified layout/behavior at 360px width.
  • Deep-linkable route/media/annotate/:mediaKey (mediaKey is the URL source of truth), with a stale/unsupported-key fallback and a bare /media/annotate landing. Registered in NAV_COMMANDS (⌘K + voice: "annotate"/"sketch").
  • Entry points — an "Annotate" action on Media History and Collection-detail image cards navigates to the id'd route.
  • Persistence — new Zod-validated GET/PUT /api/media/sketches/:key saves a per-image sidecar under data/media-sketches/ (JSON strokes in natural-pixel space + flattened PNG; retrievable at /png). Follows the docs/STORAGE.md file-sidecar classification (media-adjacent, non-relational) using atomicWrite/tryReadFile. Empty strokes remove the sidecar.
  • Export — "Export" flattens image + strokes to a PNG client-side and downloads it (works even before a save).

Out of scope (remain open on #2036)

  • Phase 2 — feed the markup back into img2img re-render.
  • Phase 3 — blank-canvas storyboard.

Umbrella issue stays open (Refs, not Closes); I'll comment there on what shipped/remains.

Tests

  • server/services/mediaSketches.test.js — save/load round-trip, PNG decode/persist, empty-strokes removal, sanitize/clamp, non-image + invalid-key rejection.
  • server/routes/mediaSketches.test.js — GET/PUT//png HTTP contract incl. 400 (malformed) and 404 (no export).
  • client/src/lib/sketchCanvas.test.js — stroke model (create/append/undo/clamp) + renderer (polyline vs dot, erase compositing, skip-invalid).
  • Full client suite green (3135). Server: all touched-file suites green; the DB-backed integration suites fail only because no portos_test Postgres is provisioned in this environment (unrelated).

Test plan

  1. Media History (or a Collection) → an image card → "Annotate" (pencil).
  2. Draw strokes, switch color/size, erase, undo — Save.
  3. Reload the page → strokes restored.
  4. Export → flattened PNG downloads.
  5. Repeat on a 360px-wide viewport with touch — drawing works, page doesn't scroll.

https://claude.ai/code/session_01KSyAbxEjURcLPhugG8MZCw

atomantic added 5 commits July 2, 2026 12:25
New GET/PUT /api/media/sketches/:key persists freehand annotation strokes
(JSON vectors + flattened PNG) as a per-image file sidecar under
data/media-sketches/, Zod-validated and image-only. Phase 1 of #2036.

Claude-Session: https://claude.ai/code/session_01KSyAbxEjURcLPhugG8MZCw
Pure stroke model/renderer (client/src/lib/sketchCanvas.js), a pointer-event
<canvas> overlay component (mouse/pen/touch, draw/erase/undo, color+size), and
the deep-linkable /media/annotate/:mediaKey page with save + client-side PNG
export. Phase 1 of #2036.

Claude-Session: https://claude.ai/code/session_01KSyAbxEjURcLPhugG8MZCw
Adds the /media/annotate route (+ bare index), NAV_COMMANDS entry, MediaCard
"Annotate" action wired from Media History and Collection detail, the client
API wrappers, and a changelog entry. Phase 1 of #2036.

Claude-Session: https://claude.ai/code/session_01KSyAbxEjURcLPhugG8MZCw
…2036)

Review fixes: (1) track a single active pointerId so a second finger/palm
can't hijack the in-progress stroke on touch; (2) reset strokes/dims/imageError
synchronously when the :mediaKey route param changes, since React Router reuses
the component instance across param changes (no remount).

Claude-Session: https://claude.ai/code/session_01KSyAbxEjURcLPhugG8MZCw
Review fix (codex): a re-save carrying strokes but no PNG left the prior
<id>.png on disk while the JSON recorded hasPng:false, so getSketchPng/`/png`
kept streaming the stale export. Now unlink the PNG in that branch; regression
test covers save-with-PNG then save-without.

Claude-Session: https://claude.ai/code/session_01KSyAbxEjURcLPhugG8MZCw
@atomantic atomantic merged commit 9bc76ec into main Jul 2, 2026
2 checks passed
@atomantic atomantic deleted the claim/issue-2036 branch July 2, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant