3dtours b24bd78ddd web: draw the picture's own distribution behind the tone curve, and let the card be dragged
Setting a point on the curve was guesswork: the graph showed the mapping but
nothing about the picture it was mapping, so you placed a point where the tones
"probably" were. The graph now draws the picture's own histogram behind the
grid, and the panel can be dragged off the photo it is editing — the two halves
of the same complaint, that the card was describing a picture you could not look
at while you used it.

The histogram is not a second measurement. `ToneCurvePanel` takes the same
`previewUrl` the stage already renders and reads it through `readHistogram`, the
function the HISTOGRAM overlay beside it uses: one 320px sample, luminance bins
on the RGB tab and the channel's own bins on an R, G or B tab, so the shape
follows the tab the way the line does. The bins become one filled path in the
graph's own square, scaled to its own tallest bucket and closed along the floor,
and it is the SVG's first child — grid and curve draw over it, so the graph
reads as curve on distribution rather than two lines crossing. Nothing new is
rendered, sampled or cached: the panel reads the frame that is already there.

It is read from the render, which is post-curve, so the band shifts as the curve
moves. That is Lightroom's behaviour, not an accident, and it is the honest one:
the point of the picture is what you are looking at. A percentile or log scale
would show a shadow-heavy frame better than a linear max does, and the overlay
beside it does not have one either, so the two agree.

The drag is the panel's own head. `pos` is the card's position in the layer's
coordinates (null until first moved), and the first position is materialised
from `offsetLeft/offsetTop`, which is exactly the CSS bottom-left the card sits
at before anyone touches it — so the default layout costs no code and the card
carries no second positioning system. It is bounded by the STAGE, not the photo:
the card may sit off the photo, that is the point of moving it, but never off
the canvas the stage clips at 8px. Window `resize` and a `ResizeObserver` on the
stage re-clamp an existing position, because the stage can shrink under a parked
card and `overflow: hidden` would hide it with no way to reach it.

One real bug, found by the probe rather than by reading: with the head as the
handle, `setPointerCapture` retargets the click that follows, so the close
button in that same head never fired — pressing it started a drag and swallowed
the click. `panStart` now returns early when the pointer went down on a button.
The pre-existing `Histogram` overlay carries the same latent pattern; it has no
interactive children in its chrome, so it was left alone.

Verified:
  tone-curve-probe.cjs (extended, scratchpad) — the rebuilt app at
    http://localhost:8090, 42 PASS, 0 FAIL, no page errors. New checks: the
    graph draws the picture's own distribution and it is the graph's first child
    (`curve-hist`); the drawn band matches a histogram binned independently in
    the page (256 buckets, worst deviation 0.00px); the distribution piles where
    the curve put the tones (peak 128/255 after the black lift, against 9-246
    before it); the card is dragged by its head (729,280 -> 689,190, the exact
    delta); the drag bends no curve and drops no point; the card cannot be
    dragged out of the stage (clamped to stage bounds); it is pulled back in
    when the viewport shrinks to 900x640 (card 636,239 240x291 inside stage
    269,109 615x429); the close button still takes the graph off the photo.
  tone-curve-math.cjs — unchanged, 11/11.
  Regressions against the rebuilt app, 0 fail: landing-test.cjs 172,
    pro-gate-test.cjs 27, award-column-probe.cjs 18, otp-code-probe.cjs 10;
    backend npm test 180 passed, 0 failed.
  web tsc --noEmit clean.

ponytail: the histogram is read from the render, so it is post-curve and the
band moves with the curve; read it pre-curve by exposing pass 3e's input if the
feedback ever misleads. The card's position is component state, so it resets to
bottom-left when the panel closes — persisting it across a close is a key on the
recipe, add it when someone asks for the card to stay put. Percentile and log
scaling are not implemented: linear max, the same as the overlay beside it.
2026-09-23 20:18:46 +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%