3dtours 7e47a153b8 web: make EXPOSURE, EV and HIGHLIGHT mean what Lightroom means
A stop is a multiplier on light, so EXPOSURE and EV stop living in the sRGB
colour matrix and get a linear-light pass of their own (EXPOSURE_SKSL:
linearise, `C * 2^EV`, re-encode). The matrix keeps CONTRAST: a gain on encoded
values is what made +1 EV land at x1.5 instead of x2. Measured on the neutral
PROVIA sim: EV +1 = x2.011, EV +2 = x3.999, still unclipped at 239.

The pass sits between the matrix and the tone shader, and the tone / cinema /
curve / glow / halation children all sample through it, so HIGHLIGHT finally
sees the value exposure produced instead of the one before it. Recovery keeps
`L + strength * mask * (1 - L)` over `smoothstep(0.50,1.00,luma)`, and the
colour comes back as `color * (luma_new / luma)`: a blown white stays white
(255 -> 255 at -10, 255 at +10), a 0.8 grey loses 33 luma, the midtones beside
it do not move.

AUTO is the histogram the LIGHT tab already draws: weighted mean luminance
(guard 0.001), target 0.48, `log2(0.48 / avg)` clamped to +-2.5 EV, handed to
the same knob. A 0.251 grey asks for EV 0.9 and lands at mean 83.0 against the
83.3 predicted, idempotent on a second press. A stock's own bias rides the same
pass (`SIM_EXPOSURE_BIAS_EV`, VIVID +0.25 EV) and cancels against the knob, so
-1 EXPOSURE on VIVID returns the CLASSIC rendering (measured 0.4149 vs 0.4177).

ponytail: the phone app's `src/utils/colorUtils.ts` keeps the old math, so the
two copies have to move together; recipes saved before this commit (EXPOSURE 2,
HIGHLIGHT +-1) render under the new stop semantics.

Verified on the rebuilt container (BASE=http://localhost:8090):
- web-exposure-probe.cjs 20 PASS / 0 FAIL (neutral 128 -> 128, EV +1 ratio
  2.011, EV +2 ratio 3.999, EXPOSURE +10 ratio 5.62 / -10 ratio 0.172, AUTO
  EV 0.9, HIGHLIGHT -10 on a 204 grey 204 -> 171, white 255 -> 255, no console
  errors)
- sim-exposure-test.cjs 9 PASS / 0 FAIL (classic 0.4149, vivid 0.4531, knob -1
  returning 0.4177, bias 0.0382)
- regression suite, 28 probes: mask 53/0, brush-edit 35/0, heal-idle 23/0,
  heal-zoom-drag 28/0, sims 31/0, sim-vivid 9/0, white-black 4/0, temp-swatch
  33/0, tone-curve clean, compare 25/0, create 52/0, wb-preset 33/0, zoom 25/0,
  save-recent 25/0, web-smoke 9/0 (its export step was stale — EXPORT opens a
  size picker now). panel-test 4 FAIL, histogram-wb 1 FAIL, studio-save-hl and
  progate timeouts, landing-test 6 FAIL ($0.99 pricing) are pre-existing.
- npx tsc --noEmit clean.
2026-09-25 17:44:28 +07:00
2026-07-17 09:33:24 +07:00
2026-09-16 18:11:17 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00
2026-07-17 09:33:24 +07:00

RecipesCam

RecipesCam is a camera and photo-editing app built around recipes — reusable looks that carry a film simulation plus a full set of adjustments. You shoot or open a photo, dial in a look, and keep it as a recipe you can apply again, share as a .recipe file, or save to your account.

It ships twice from one repository: a React Native (Expo) app for iOS and Android, and a self-contained web build under docker/ that runs the same render pipeline in the browser.

What it does

  • Shoot with a recipe applied. Live viewfinder, GPS-tagged captures, and the recipe baked into the saved file.
  • Film simulations. Built-in looks — PROVIPES, VELVIPES, CLASSIC CHRIPES, CLASSIC NEGIPES, ASTIPES, ETERNIPES, ACRIPES, B&W HIGH CONTRAST and the LC STREETLIFE pair — each with its own grain and tone curve.
  • The full adjustment set. Exposure, contrast, highlights and shadows, saturation, colour temperature, clarity, grain, and an HSL mixer with a colour picker that samples straight off the photo.
  • Geometry. Crop to a fixed ratio or free-form, quarter turns, and a straighten ruler, plus printed frames (classic border, retro instant, wall frame portrait/landscape).
  • Finishing. Watermark and GPS stamp, EXIF carried through the export, JPEG written with a proper 300 DPI JFIF header.
  • On-device upscaling. A Real-ESRGAN pass runs locally when an export asks for more pixels than the source has — no server sees the photo.
  • Recipes. Save, favourite, export and import .recipe files; the web build keeps them in your account, the phone build also keeps them on device.

The two builds

Build Where Stack
iOS / Android repo root Expo + React Native, @shopify/react-native-skia for the render pipeline, NativeWind for styling
Web docker/ Vite + React, CanvasKit (canvaskit-wasm) for the same pipeline, Fastify + SQLite API for accounts and recipes

The render engine is shared by design: the web build compiles the app's own src/utils/* and type definitions unchanged, with @shopify/react-native-skia aliased to a CanvasKit shim (docker/frontend/src/engine/skiaShim.ts). A look looks the same on both because it is the same code.

Running the web build

cd docker
cp .env.example .env
docker compose up -d --build
# → http://localhost:8090

Photos never leave the browser: grading, framing, watermarking and JPEG encoding all run in the visitor's tab; the API only stores accounts and recipe JSON. See docker/README.md for the layout and the proxy setup.

Running the app

npm install
npx expo start          # Expo Go / dev client
npx expo run:android    # or run:ios for a native build

Repository layout

App.tsx, src/           the Expo app: screens, tool rail, viewfinder, shaders
docker/                 the web build (frontend + API + compose file)
  frontend/shared/      vendored copies of the app's types and utils
  frontend/src/engine/  CanvasKit shim, export engine, super-resolution
docs/                   privacy policy
THIRD_PARTY_NOTICES.md  licences of the bundled fonts, models and libraries

Licence

See LICENSE and THIRD_PARTY_NOTICES.md.

S
Description
No description provided
Readme 6.6 GiB
Languages
TypeScript 92.9%
Kotlin 5.7%
JavaScript 1.3%