Files
latent/PROPOSAL.md
Fredrik Johansson 5a5c0da8a6
All checks were successful
Docker / build-and-push (push) Successful in 3m29s
Initial implementation of latent: film-effects editor + develop-later server
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>
2026-07-16 15:19:48 +02:00

6.5 KiB
Raw Blame History

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:

  1. 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).
  2. Delayed — edit, then "send to develop": uploads the result to server storage, assigns a develop_at time 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.

"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 }. If delayed, server picks develop_at randomly within a configured range (e.g. env-configurable DEVELOP_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_at is null and the link resolves right away (that's the "optional share link" path off instant download).
  • GET /i/:id — serves the image if develop_at is 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 exact develop_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 past expires_at regardless 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. 248 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.