Files
RecipesCam/docker
3dtours fa0b2cb719 fix(heal): one drag is one repair, and the band at its rim is the model's
A press has to mean what it lands on. A drag is a run of spots and the run
has a spine, so a press within half a brush radius of that spine takes the
drag and dragging moves it, while a press in the outer half of the spots —
where the run is not, and a new repair is wanted — lays a stroke against it.
Before, a brush near a repaired one was read as a drag OF it: the second
stroke was folded into the first and the first moved under the user. Spots
carry the drag they came from (stroke), older recipes still answer to the
old radius, and the outline of a repaired run is drawn idle so it can be
taken hold of (app.css).

A repair already made is part of the photo it was made in. The renderer now
solves one drag at a time, in the order they were drawn, each against the
frame the drags before it have already left — so a stroke laid beside a
repaired one invents its pixels from a frame that no longer holds what the
first took away, and the repair the user has already accepted does not move
when they brush next to it. Measured (probe-seam.mjs, a second stroke laid
2.5 brush radii from the first, counted in levels over the first drag's own
circles): 0 at the worst pixel, 0 of 19189 pixels moved by more than two.

The ring the first repair left behind was the fill's and not the model's.
The model's pixels were laid through the mask's own ramp, so the outer
quarter of every spot — where the mask still weighs the fill in at up to
full strength — was the fill's, and the fill is the light of the WHOLE box
it was built in: a stroke a third of the frame long is filled by blurs up to
sixteen brush radii wide, so what it brings there is the box's average and
not the rim's. The model is the repair wherever it has one, so it now owns
the spot out to 0.92 of its radius (HEAL_PATCH_STOPS) and the fill only
meets the photo in the sliver the mask itself fades over. Measured with the
same probe: 0.76 levels of mean change over the repair where it was 1.86,
8.5 at the worst pixel where it was 14.2, and the edge strength at the rim
after the repair 0.92 where the photo's own edges around it are 0.5 (1.52
before) — the band that was 8 to 11 levels dark is gone.

matchToRim goes with them: the relaxation it ran over every crop measures
nothing the band above does not already do, and the probe's numbers are
identical with it removed.
2026-10-15 15:32:59 +07:00
..
…

RecipesCam web — self-contained stack

A Docker-hosted web build of RecipesCam. Everything it needs is in this folder: move it to another machine, run two commands, and the app is up. It does not need the React Native project around it.

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

What runs where

Service Image Role
frontend nginx:1.27-alpine (built by frontend/Dockerfile) Static SPA + /api/ reverse proxy
api node:22-slim (built by backend/Dockerfile) Accounts + saved recipes, SQLite on ./data

frontend resolves api through Docker's embedded DNS and proxies /api/* to it — that is why the API container is named api and why it is not published on the host. Only ${WEB_PORT:-8090} is exposed.

Photos never leave the browser. The CanvasKit render pipeline (grade, frame, watermarks, JPEG encode) runs in the visitor's tab; the API only stores recipes as JSON.

Layout

docker-compose.yml      the stack
.env.example            WEB_PORT
data/                   SQLite (created on first run, gitignored)
backend/                Fastify + better-sqlite3 API, own Dockerfile
frontend/               Vite + React + CanvasKit SPA, own Dockerfile + nginx.conf
  shared/               vendored copies of the app's types + utils (see below)
  src/engine/           skiaShim.ts (CanvasKit) + exportEngine.ts (render pipeline)
                        + session.ts (localStorage/IndexedDB studio persistence)

Vendored files

frontend/shared/{types/index.ts,utils/*.ts} are byte-identical copies of src/types/index.ts and ten src/utils/*.ts files from the React Native project (@shopify/react-native-skia is aliased to src/engine/skiaShim.ts in vite.config.ts + tsconfig.json, so those files compile unchanged):

cinemaShader colorUtils defaultRecipes exifWrite frameUtils jpegDpi paramDefs recipeShare skiaImage toneShader.

The landing page needs no CDN: frontend/public/assets/fonts/*.woff2 are the seven self-hosted faces behind the three font groups the Themes menu offers (Plus Jakarta Sans / Inter / JetBrains Mono, Fraunces / Be Vietnam Pro / Courier Prime, Be Vietnam Pro / Space Mono — all SIL OFL, pulled from Google Fonts, vietnamese + latin + latin-ext subsets), and frontend/public/assets/samples/s*.jpg are the six placeholder negatives the film strip, preset tester and QR card show (swap them for real graded stills whenever we have them). Its one foreign request is the QR image from api.qrserver.com, which degrades to an empty slot offline.

When the app changes one of them, copy it back in — the renderer is only "parity" for as long as these stay in sync:

cd <repo>/docker/frontend/shared/utils
cp <repo>/src/utils/<name>.ts .

Operations

docker compose logs -f api        # API log
docker compose restart api        # after backend/src changes (rebuild: --build)
docker compose down               # stop; ./data survives

Backup is the ./data folder — that is the whole database.

Checks

curl -s http://localhost:8090/api/health          # {"ok":true}
curl -sI http://localhost:8090/                   # 200, index.html

Then open the UI, drop a photo in, and confirm the preview shows the picture and EXPORT downloads a JPEG that opens. The preview going solid black while the export still reports a plausible size is the one failure mode worth knowing: it means the CanvasKit GPU surfaces lost their shared GrDirectContext (see frontend/src/engine/skiaShim.ts), and with no GPU the raster fallback renders the same pipeline correctly, just slower.