Canvas-based client-side editor (grain, vignette, halation, procedural light leaks, color-cast presets) plus a small Express/SQLite server for the two paths that need persistence: instant share links and delayed "send to develop" links with a randomized reveal time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.5 KiB
Proposal: latent — a phone-photo editor with a "develop it later" option
Status: proposed, not built. Self-contained — written so a fresh agent with no prior conversation context can pick this up and implement it without anything explained first. No repo exists yet; this file is the seed of one.
Motivation
A small, fun webapp: upload a photo from your phone, apply film-style manipulation (grain, light leaks, vignette, halation, color-cast) in the browser, and either download it immediately or — the silly, on-brand part — send it off to "develop," which holds it server-side behind a randomized delay (hours to days) before the link actually resolves to the image. Getting your photos back later, like real film, on purpose, for no practical reason except that it's a fun mechanic.
Named latent after the latent image — the real photographic term for
the picture that already exists on exposed film but is invisible until
developed. Both halves of this app map onto that word directly.
Two independent finishing paths off one editor, not two separate apps:
- Instant — edit, then download directly, entirely client-side, no server touched. Optionally generate a share link for the edited image (explicit user action — "generate share link" — not automatic).
- Delayed — edit, then "send to develop": uploads the result to
server storage, assigns a
develop_attime chosen randomly within a configured range, and gives back a link that won't resolve to the actual image until that time passes.
Architecture
Static/client-heavy frontend (canvas-based editing, same "no build step,
plain HTML/CSS/JS" family as typo/flit/wisp's frontend) plus a small
single-process server for the two paths that need persistence: share
links and delayed development. Reuses the shape already validated by
wisp — blob + SQLite metadata + a background TTL-style sweep — rather
than inventing a new storage pattern. No accounts, no auth: the link
itself is the credential, same as wisp.
Unlike wisp, this does not need end-to-end encryption — none of
this is sensitive, and the entire point is a link other people can open.
That actually makes the backend simpler than wisp despite doing more:
no client-side crypto, no key-in-fragment handling, just an opaque random
ID per stored image.
Editing (client-side, no server involved until "generate link" or
"send to develop" is pressed)
All via <canvas> — pixel manipulation, no ML, no WASM needed for a first
pass:
- Grain — random noise overlay, opacity/size adjustable
- Light leaks — a handful of pre-baked semi-transparent PNG overlays (warm streaks/blooms near an edge), randomized placement/rotation per application so repeat use doesn't look identical
- Vignette — radial darken toward the edges
- Halation — bloom/glow around bright areas (blur a thresholded bright-pixel mask, screen-blend back over the original)
- Color cast / white balance shift — a few presets (warm/expired-film amber, cool/cross-processed teal, faded/low-contrast) rather than raw sliders for every channel — keep the UI to "pick a vibe," not a full Lightroom clone
- Adjustments should be non-destructive within a session (recompute from the original + a param object, not chained lossy operations) so dragging a slider back to zero actually gets you back to the original
Server
Single process, SQLite for metadata (same pattern as galr/stash),
images as blobs on local disk (same pattern as wisp).
Table images:
| column | type | notes |
|---|---|---|
id |
text PK | random opaque ID, used in the URL |
blob_path |
text | where the file lives on disk |
mime |
text | |
created_at |
integer | |
develop_at |
integer, nullable | null = instant share (no delay gate); set = "send to develop" path |
expires_at |
integer | TTL sweep target — don't keep images forever regardless of path |
Endpoints:
POST /api/upload— body: image bytes +{ delayed: boolean }. Ifdelayed, server picksdevelop_atrandomly within a configured range (e.g. env-configurableDEVELOP_MIN_HOURS/DEVELOP_MAX_HOURS) and returns the link immediately — the whole point is you get the link before it resolves, so you can send it to someone and both of you wait. If not delayed,develop_atis null and the link resolves right away (that's the "optional share link" path off instant download).GET /i/:id— serves the image ifdevelop_atis null or already passed; otherwise a small "still developing, check back in ~Nh" page (server can compute and show a rough remaining time without revealing the exactdevelop_at, to keep a little mystery — exact behavior is an open question below).- TTL sweep (interval-based, same "deliberately dumb, not push-based"
precedent as
keep watch/wisp's sweep) deletes blobs + rows pastexpires_atregardless of whether they were ever viewed.
No auth on any endpoint — this is a public, no-accounts toy. Rate-limit
/api/upload (basic IP-based throttle) to avoid it becoming an open file
host.
Open questions
- Exact delay range — "hours to days" is the pitch; needs an actual default (e.g. 2–48 hours?) and whether it's configurable per-upload or fixed server-side via env.
- What the "still developing" page shows — exact countdown vs. vague "come back later" vs. a tiny animated placeholder (fogged/blank photo silhouette) for flavor. Worth spending a little design effort here since it's the whole gimmick.
- Filter presets vs. raw controls — start with presets-only (faster to ship, more "fun toy" than "editing tool") and add fine controls later if it turns out sliders are actually wanted.
- Mobile upload flow specifics —
<input type="file" capture>for direct camera access vs. just a normal file picker; probably both, file picker as the fallback. - Image size limits / re-encoding — phone camera photos can be large; decide whether to downscale/re-encode on upload (bandwidth + storage) or store as-is up to some cap.
- Retention for the instant/non-delayed path — does a same-length TTL apply, or does it expire faster than the delayed path since it's a much more casual "just give me a link" flow?
Non-goals
- No accounts, no galleries, no "your uploads" history — every image
stands alone via its own link, consistent with
wisp's throwaway philosophy. - No e2e encryption — deliberately out of scope, see Architecture.
- No collaborative/multi-user editing — one uploader, one result.