136 lines
6.5 KiB
Markdown
136 lines
6.5 KiB
Markdown
|
|
# 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. 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.
|