A web application for giving digital images a filmic look by simulating the characteristics of 135 (35mm) film photography.
Source Image: A digital image uploaded or selected by the user. The input to processing. Format-agnostic (JPEG, PNG, WebP, etc.).
Grain: A texture overlay applied during processing, sourced from a Grain Texture, to simulate film grain. Controlled by a single intensity parameter (scalar).
Grain Texture: An image file — typically a scan of blank film — used as the grain overlay pattern. Provided as a static asset, not uploaded by the user per session.
Vignette: Parametric edge darkening applied during processing. Controlled by intensity (scalar: how dark the edges become) and feather (scalar: how gradually the darkening transitions from center to edge).
Halation: A reddish glow around bright highlights, simulating light penetrating the film emulsion and reflecting off the camera pressure plate back into the red-sensitive layer. Controlled by intensity (scalar: opacity of the glow), spread (scalar: how far the glow bleeds), and threshold (scalar: minimum luminance required to trigger the glow).
Render: The output image after processing. A Source Image has one Manual Render and zero-to-many AI Takes (both are Renders). Displayed as a preview in the browser and available for download.
Preview Render: A Render downscaled to 1200px on its longest dimension for display in the Film Strip. Generated lazily after each parameter change.
Full Render: A Render at the Source Image's original resolution. Generated on demand (download or magnifying glass inspection).
Manual Render: The existing deterministic Render produced by Halation → Film Tint → Vignette → Grain. A Source Image has exactly one Manual Render, distinct from any AI Takes. Avoid: Default render, manual output
AI Take: A single AI-generated film grain rendering of a Source Image, produced by gpt-5.4 via a one-button action
in the Edit View. Immutable — once generated, it is not re-processable. Stored as Preview and Full Render in Convex file
storage. A Source Image can have multiple AI Takes, each in a separate Convex table. Avoid: AI Render, Generated
Render
Takes (/takes): The top-level route showing all AI Takes across all Source Images, displayed as rows grouped by
Source Image. Accessible from the root navigation alongside the Gallery. Avoid: Generated, AI Renders, Contact Sheet
Process (verb): The act of transforming a Source Image into a Render by applying Halation, Film Tint, Vignette, and Grain.
Avoid: Transform, filter, generate, "135ify" as a verb
Film Stock: A named tint preset (e.g., Natural, Golden Hour, Seabreeze, Faded, Whisper) that applies a specific colour shift to the Source Image during processing.
Avoid: Film preset, tint preset
Film Strip: The horizontal row of rendered images, styled with sprocket-hole decoration, through which the user navigates Source Images and their Renders. Always rendered in the root layout.
Avoid: Carousel, gallery, film strip
Edit View: The route-based overlay at /gallery/$imageId where a user adjusts processing parameters for a Source
Image. Replaces the former EditSheet component. Editing components live in src/features/image/.
Avoid: EditSheet, edit modal
Account: An authenticated user identity, linked to a Gallery. Required for server-side persistence of Source Images and processing parameters.
Avoid: User, profile
Gallery: The persistent collection of a user's saved Source Images and their processing parameters, stored
server-side at /gallery. Accessible only when authenticated. Each Source Image in the Gallery is identified by a
unique document ID and can be loaded back into the Film Strip for re-editing.
Avoid: History, library, collection
Session: The authenticated state of a user within a browser tab. Determines whether auto-save is active and whether the Gallery is accessible. Sessions are managed by Convex Auth and are independent across browser tabs.
FileRecord: What the storage layer persists for each Source Image: identity (id, fileName, sourceUrl) and processing parameters (ProcessParams). Returned by the Storage Facade. Does not include ephemeral render state.
Avoid: FileWithState (use FileRecord for storage-layer concerns)
RenderState: Ephemeral client-side processing state for a Source Image: render URL, processing flag, and error message. Applied per AI Take as well — each AI Take has its own RenderState. Not persisted server-side. Managed by a separate Zustand store.
Avoid: Mixing into FileRecord or Storage Facade
Storage Facade: The interface (StorageContextValue) that both persistence implementations (Convex, Zustand) conform to. Provides file CRUD (add, remove) and parameter updates. Does not manage render state.
Aavoid: StorageProvider, storage layer
AI Provider: Abstraction wrapping the model API used for AI Take generation. In this epic, the provider reads the user's client-side OpenAI API key from localStorage for a BYO-key flow. Future: server-side platform key via subscription. Avoid: AI service, model service
- Each Source Image carries its own independent processing parameters and Render.
- A Source Image is processed once to produce a Render.
- Grain is applied using a Grain Texture as its pattern source.
- Vignette is applied independently; Grain and Vignette compose into the Render.
- Halation is derived from bright regions of the Source Image and composited into the Render.
- Processing order: Halation (derived from source), Film Tint, Vignette, Grain (final).
- An Account owns a Gallery containing multiple Source Images and their parameters.
- A Session determines whether an Account's Gallery is accessible and whether auto-save is active.
- Source Images in the Gallery are re-processed client-side on load using stored parameters; Render output is not persisted server-side.
- A FileRecord is composed with a RenderState at the UI layer to produce the full view model for a Source Image.
- The Storage Facade is independent of render state management; composition happens in FileProvider.
- The Film Strip is always rendered in the root layout. The Edit View opens as a Sheet overlay on top of it.
- A Source Image can have many AI Takes. Deleting a Source Image cascade-deletes its AI Takes.
- AI Takes are read/written via direct Convex queries and mutations (no Storage Facade involvement).
- AI Takes are generated fire-and-forget: the Convex action always completes regardless of navigation.
- AI generation uses a fixed prompt. The user has no prompt input.
- On completion, a success toast and a notification dot on the Takes nav link signal the result.
- If the generated image exceeds the user's storage quota, a popup offers immediate download before discard.
Dev: "When I process a Source Image, does the Grain apply before or after the Vignette?" Domain expert: "Apply Vignette first, then Grain on top. The film grain should sit over the darkened edges, not underneath them." Dev: "And can the user provide their own Grain Texture?" Domain expert: "Not in MVP. There's one fixed Grain Texture. The only control is intensity."