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

136 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
### 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 }`. 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.