Commit Graph

40 Commits

Author SHA1 Message Date
3dtours 8308b3e65f web: move WHITE/BLACK onto the WB tab as per-channel points
LIGHT already spends its slope budget on the tone knees, so the two end
points were doing nothing a tone knob could not. On WB they act on each
channel's own distance from the end: the darker channel of a shadow and
the brighter channel of a highlight move most, which neutralises a cast
at the toe and the shoulder. Both shuffles stay cubic in the channel
value, so every channel's curve is still monotonic (>= 0.46).
2026-09-18 14:35:48 +07:00
3dtours 89beb5f160 web: put the traffic screen in the admin frame
installTracking() beacons one view per page load and one click per control that
carries a data-key, so every existing button is already counted. The new STATS
pane reads it back: a 7/30/90-day range, the three totals, an SVG timeline and
eight proportional bar lists (pages, clicked features, country, region, city,
browser, system, device).
2026-09-18 14:23:25 +07:00
3dtours 1c4cdf8af1 web: put the white and black points on the LIGHT tab
WHITE (whites) and BLACK (blacks) get the same slider rows as HIGHLIGHT
and SHADOW, so the tone curve's two ends are editable on the stage, not
only typed into the CREATE form.
2026-09-18 12:49:07 +07:00
3dtours 0627f8dd91 web: keep a saved photo's look in EXIF and its own row
EXPORT no longer burns the caption strip: the pixels stay the photo's own
and the look travels as metadata — ImageDescription (0x010e) for the tag,
UserComment (0x9286, ASCII header) for the recipe JSON.

SAVE PHOTO now stores the look with the frame (photos.recipe) and the
uploader's consent for the community film strip (photos.consent, PATCH
/api/photos/:id for the owner). The landing reel skips non-consented frames,
and a new MY PHOTOS tab lists the account's saves, reopens one with the
settings it was stored with, and carries the two consent switches.
2026-09-18 12:49:05 +07:00
3dtours cf4b01d4b3 web: mark the selected recipe in FAVORITED
The list was all one colour, so an open recipe was invisible in it. The entry
the stage is showing now reads accent (name, border and background, the same
`.chip.on` the rest of the app uses). The match is id AND name: a preset that
was merely filed under a new name shares the id but not the name, and must not
light up.
2026-09-18 12:30:38 +07:00
3dtours f17cb08a63 web: fold the create form's groups and add the white/black point rows
The CREATE RECIPES form had grown into one long scroll. The six categorical
groups (SIMULATION, DYNAMIC RANGE, GRAIN EFFECT, COLOR CHROME EFFECT, COLOR
CHROME EFFECT BLUE, WHITE BALANCE) are now native <details> folds — the browser
keeps the open flag, so no state and no library.

The two tone-curve ENDS join the numeric grid: EXPOSURE (the matrix gain the IQ
tab already drives), EV (the old EXPOSURE COMP. row, renamed to the phone's
word), WHITE and BLACK. WHITE/BLACK are new ColorAdjustments fields, applied in
TONE_SKSL as cubic end-weights rather than another smoothstep knee — the HL/SH
knees already spend the slope budget, and the cubic keeps the curve monotonic
for every combination (derivative >= 0.46), so a brighter input can still never
come out darker. Both are optional, so stored recipes keep working.
2026-09-18 12:30:36 +07:00
3dtours 567f650bbb web: accent the brand and add the studio's quick gestures
- the brand "Cam", the avatar and the signed-in name follow the theme accent
- double-click a slider track resets that parameter; double-click the photo
  toggles 1:1 and the whole photo on the stage
- `*`, or a recipe chip dragged onto the star, files the look under FAVORITED
- RESET under CREATE clears the draft form, and the button now sits under a
  rule at the foot of the column
2026-09-18 12:09:06 +07:00
3dtours 0f52572f20 web: restate the pricing plans as Lite and Pro with store buttons 2026-09-18 11:37:25 +07:00
3dtours 8259bd468d web: move the section jumps to their own strip and grow the Theme menu
The six section links leave the top bar for a thin shelf under it, each a
bordered pill at the page's normal text size, so the bar itself stays a
row of actions and the jumps still read as buttons.

The Theme menu gains the workspace's colour groups — the landing palette
now takes its hue from the accent tokens — an Auto mode that follows the
OS scheme live, and a dropdown in place of the three font chips.
2026-09-18 11:30:12 +07:00
3dtours e437946ee0 web: share the landing page to social networks with a real preview card
Open Graph and Twitter Card meta point at a 1200x630 cover shot of the
hero, and the final CTA grows a share row: Facebook, X, LinkedIn and
Telegram each open with this origin filled in, plus a native share sheet
that falls back to copying the link. The top bar now leads with the web
studio pill next to the store CTA, with the theme, language and account
controls trailing them.
2026-09-18 11:17:45 +07:00
3dtours 86a93f46be web: bulk-select accounts in the admin table and link the studio from the top bar
The users table grows a checkbox column with a select-all box in its head,
and three controls above it: SELECT ALL, SELECT NONE and DELETE SELECTED
naming the count. An admin account is the API's own privilege source, so it
gets no box and select-all skips it; the picks clear once the deletes land.

The landing top bar gains a RecipesCam web studio button between the account
slot and Download RecipesCam. It hides with the rest of the wide row under
1160px, where the burger sheet already offers the studio.
2026-09-18 11:11:43 +07:00
3dtours b4d5d2926b web: give each member a photo folder and burn the strip into the export
Every member gets /photos — their own uploads, counted against a 12-photo
cap, each card showing the tagline and the technical line the studio would
print. The studio gains SAVE PHOTO n/12 in the top bar: it renders the full
resolution look, stores the strip (tag/title/meta) with the upload so the
landing reel frames it the same way, and refuses past the cap.

EXPORT now burns that strip into the file: the amber #TAG over the photo's
top-left plus a dark caption band below carrying the recipe name and the
ISO / grain / warmth line. The live preview stays clean, and the saved
upload stays clean too — the reel draws its own frame from the stored
labels, so a burned band would tag the tag twice.

Admins manage any photo through DELETE /api/photos/:id; members only their
own. The users table's photo counts stay in step with the folder.
2026-09-18 10:56:26 +07:00
3dtours 43d86b4b6f web: moderate accounts, accept 12MB uploads, put SAVE under CREATE
- /admin User account rows gain BLOCK/UNBLOCK, REMOVE/RESTORE and DELETE.
  Blocked = cannot sign in (sessions swept), removed = hidden from the strip
  and cannot sign in, both reversible; DELETE drops the account with its
  photos and recipes and unlinks the files. An allowlisted account is never
  a target, so an admin cannot moderate or delete itself.
- Photo uploads move from a 3MB API cap / 4m nginx cap to 12MB / 16m, and
  the browser shrinks an oversized still before sending it (2048px JPEG,
  avatars 512px) so the declared type still matches the sniffed bytes.
- The studio SAVE leaves the top bar and sits under the CREATE RECIPES tab,
  labelled SAVE RECIPES.
2026-09-18 10:33:35 +07:00
3dtours 8e6c1493e8 web: grade the colour matrix before the tone pass
A SkPaint runs its shader BEFORE its colourFilter, so setting the exposure
matrix on the same paint as the tone shader landed the gain after the tone
pass: HIGHLIGHT -10 rolled a bright pixel back to 0.78, +EXPOSURE then
multiplied it by 1.2 and +0.15 and it clamped back to 1.0 — the HIGHLIGHT
slider looked dead the moment exposure went up.

The matrix now renders into its own image and the tone/cinema chain samples
that. Measured on the real engine (HIGHLIGHT -10 first, then EXPOSURE +10):
top end stays 0.780 (was 1.000), midtone 0.502 -> 0.722.
2026-09-18 10:33:32 +07:00
3dtours 40177cb63f web: the studio header carries the mark and the page name too
The landing, /admin and /profile all show the icon + RecipesCam wordmark
with the page name beside it; the studio still showed a bare "RecipesCam
web" text. Same pair now, so the phone header says where the visitor is.
2026-09-18 09:03:35 +07:00
3dtours 6bbf77860b web: account avatars, member /profile, framed admin panel
- an account can carry a picture: POST /api/auth/avatar (raw bytes,
  sniffed, replaces and unlinks the old file) and the public
  GET /api/users/:id/avatar. It rides wherever the account is named —
  the landing chip, the studio TopBar, the profile form.
- new /profile page for members, sharing one Profile form (picture,
  email, password) with the admin drawer.
- /admin is now one bordered frame whose left column is
  Profile / User account / Pictures / Close. Pictures lists every
  photo in the system with the slot that shows it; User account lists
  each account's name, email, picture and contribution count.
- account control opens a menu: Admin page + Log out for an admin,
  Profile + Log out for a member.
2026-09-18 08:19:36 +07:00
3dtours 2917c034ed Sign in is the default: the landing names the account, and an admin lands on /admin 2026-09-18 08:02:57 +07:00
3dtours 7b79e49c20 Photo slots + admin page: place any upload in the strip or a live slot, sign up in place, brand links home 2026-09-18 07:55:54 +07:00
3dtours bd2f08aaf7 Panel chips: name the picked value on the right edge
Strip chips (D.RANGE, CROP, COLOR CHROME, PHOTO STYLE, ...) now carry their
pick in the chip's .val column instead of only glowing amber, and the two
chips that baked the value into the label (WB TEMP, FRAME ROTATE) use the
same right-edge slot.
2026-09-18 07:24:05 +07:00
3dtours f13fc37b5a feat(panel): cascading columns, WB colour swatches, stronger HDF glow
Layout
- the panel is a cascade of columns: the rail's tabs, the tab's chips, the
  open chip's sub-chips, then the ruler. A child column no longer hides the
  column it came from (TEMP -> COLOR TEMP keeps TEMP visible); chips stack one
  per row instead of wrapping
- FRAME's WATERMARK opens its own column, so the frame chips stay put
- CREATE RECIPES gets the wide column its two-up form needs

WB colour swatches
- the ruler draws a colour box under the slider that follows the value:
  COLOR TEMP is the Kelvin colour (Tanner Helland), TINT runs green -10 ->
  neutral 0 -> magenta +10

HDF EFFECT
- knee 0.55..0.85 -> 0.45..0.75, blur 0.004+0.015n -> 0.006+0.024n of the
  width, screen alpha 0.15+0.35n -> 0.28+0.52n: a wide halo on the highlights
  instead of a hairline glow. Web copy of toneShader only — the phone keeps
  its own tuning.

Tabs
- rail order is PRESETS, FAVORITED, WB, LIGHT, FX, FRAME, CREATE RECIPES
2026-09-18 06:42:38 +07:00
3dtours ffdefd2c9c feat(photos): community film strip uploads + admin moderation
Backend
- photos table + upload storage under DATA_DIR/uploads (magic-byte sniffing,
  no multipart dep, SVG rejected, wx exclusive writes)
- POST/GET /api/photos, GET /api/photos/:id/file with nosniff + sandboxed CSP
- admin routes (ADMIN_EMAILS allowlist): list, delete one, clear all
- identity-keyed rate limits (login 20/15m, signup 5/h, upload 60/h)
- cookie gains Secure when the request is https (via trustProxy)
- /api/auth/me now 200 {user:null} instead of 401 when signed out

Frontend
- landing strip section: signed-in users upload straight from the reel,
  guests get a /app?auth=1 link
- /admin page: grid of uploads with delete + clear all
- nginx: nosniff / X-Frame-Options / Referrer-Policy, forward
  X-Forwarded-Proto so the API can mark cookies Secure behind TLS

Tests: docker/backend test/security.mjs (45 checks)
2026-09-17 22:35:12 +07:00
3dtours 9ba2667c6c Hang the wall frame landscape too, beside the portrait one 2026-09-17 21:58:18 +07:00
3dtours f1174b8c57 Let the web demo use HDF EFFECT instead of greying it out 2026-09-17 21:49:22 +07:00
3dtours 3af5a315e2 Offer a compare-with-original split, and drop the render-time readout 2026-09-17 21:39:07 +07:00
3dtours 99be7e96bd Size the preview copy to what the stage actually paints
The copy was fixed at 1600px, so a hidpi screen was already stretching it at
1:1 and every wheel notch made it worse. The stage now reports the size it is
painting at — the contain-fit times the pixel ratio times the zoom — and the
app re-cuts the source copy to match, quantised and capped at 3200px, which is
the largest copy the grade can still afford. The wheel keeps its instant
transform; the sharper copy lands once the gesture stops.
2026-09-17 21:17:54 +07:00
3dtours 8747b1686b Tint the landing nav from the theme, not from a dark constant
The fixed bar sat on a hardcoded rgba(9, 10, 15, 0.72), so switching the
page to light left it dark under dark text. --lp-nav now carries the tint in
both palettes.
2026-09-17 20:57:02 +07:00
3dtours 014b3e5a4b Zoom and pan the preview with the wheel, and step a parameter from its ruler
Cursor over the image: the wheel zooms about the pointer, and a drag pans,
both clamped to the contain-fit of the loaded bitmap so the photo can never
be lost off-screen. Cursor over an open parameter ruler: the wheel moves that
parameter by one step, clamped to its min/max. While the crop frame is being
dragged the wheel keeps resizing the crop instead, and APPLY puts the view
back on the fit.
2026-09-17 20:57:00 +07:00
3dtours 6e8a600a82 Re-derive the preview source at the crop's own resolution
A committed crop kept only its share of the 1600px whole-photo copy, so the
stage then showed a 2x upscale and the photo read as broken. While a crop is
live the source is remade 1/f larger (f = the crop's longest side as a
fraction of the photo's), capped so the decode stays bounded; UNDO/CANCEL
and a new photo put the 1600px copy back.
2026-09-17 20:34:08 +07:00
3dtours 751beb52c1 Add a draggable crop frame behind APPLY, whole-look undo, and recipe import 2026-09-17 20:14:21 +07:00
3dtours cc313f2ea9 Enlarge the landing logo and put RecipesCam back on one word
.lp-logo is a flex row, so the bare text node "Recipes" and the <em>Cam</em>
were two flex items and the 6px gap landed between them: the wordmark read
"Recipes Cam". They now live in one span, so the gap only separates the icon
from the name.

Along with it: mark 22px -> 30px (24px under 680px), wordmark 18px -> 24px
(19px on phones), gap 6px -> 8px. Colours untouched — Cam is still the amber
accent, and the mark keeps its rounded corners.
2026-09-17 20:05:33 +07:00
3dtours cc88471256 Offer the three proposed font groups in Themes
The theme menu now carries a fourth choice beside light/dark and the accent:
the font pairing. All three pairings are free for commercial use (SIL OFL),
have a Vietnamese subset, and are self-hosted — the landing still makes no
CDN request.

- styles/fonts.css: 22 @font-face blocks for Plus Jakarta Sans, Inter,
  JetBrains Mono, Fraunces, Be Vietnam Pro, Courier Prime and Space Mono,
  vietnamese/latin-ext/latin subsets only, under public/assets/fonts.
- styles/tokens.css: [data-fonts="studio|editorial|native"] sets --font,
  --font-heading and --mono. Studio (Plus Jakarta Sans + Inter + JetBrains
  Mono) is the default.
- theme/: FontSetId + FONT_SETS, persisted as rc.fonts, applied as
  <html data-fonts> next to data-theme and data-accent.
- TopBar and the landing nav both get the picker; on the landing the theme
  tool now opens a small popover (light/dark + font group) instead of
  toggling on click.
- landing.css: --lp-display/--lp-mono now resolve to the chosen group, so the
  picker retypes the whole page. Syne.woff2 goes with its @font-face.
2026-09-17 19:20:23 +07:00
3dtours 51e572b9ce Persist the studio session across reloads
The working photo and every knob the workspace holds now survive a reload,
for guests as much as for signed-in users:

- engine/session.ts: the knobs go to localStorage (rc.studio.v1) as small
  JSON; the photo goes to IndexedDB, because a 12MP JPEG does not fit in
  localStorage. Both fail soft (private mode, quota) — the studio still works,
  it just forgets.
- App.tsx: state seeds from the stored snapshot synchronously, so the first
  paint already holds the user's settings; the boot effect pulls the photo
  back and adopts it with keepGeo, so the restored params are not clobbered
  by the photo's own EXIF.

Recipes a guest creates with SAVE RECIPE stay session-only, as asked — they
are still gone on reload (create-test asserts it).
2026-09-17 19:12:29 +07:00
3dtours 1446e852b1 Apply the CREATE form's knobs to the photo in realtime
Every row now feeds the open render as it is typed, so a recipe's effect is
visible before it is saved. The draft is not an undo step: SAVE/EXPORT stays the
only thing that commits it, and merely opening the tab changes nothing.
2026-09-17 19:05:59 +07:00
3dtours a684b178b6 Use the app icon as the web favicon and landing logo
RecipesCamIcon.png (scaled to 256px) becomes favicon + apple-touch-icon and
replaces the drawn camera svg in front of the landing wordmark, which now sits
a touch closer to it.
2026-09-17 18:59:41 +07:00
3dtours d31d945827 Add the CREATE RECIPES tab to the web app
The phone's RecipeCreateModal becomes a rail tab with the same rows, seeding
from the look on screen and clamping the same way. SAVE RECIPE applies the new
look, lists it under RECIPES and, when signed in, stores it on the account; a
guest's copy stays in memory and goes away with the page. Signed-in users can
also export the recipe as the app's encrypted .recipe file (shared/utils
/recipeShare.ts vendored byte-identical from the RN project).
2026-09-17 18:56:17 +07:00
3dtours f2e5e9abc9 Give the landing the app's theme and language switches
The blueprint page was dark-only and English-only. It now carries the same
two controls the workspace TopBar has, in the nav's top-right cluster: ◐ flips
light/dark (a paper palette for the same funnel — surfaces and ink flip, the
amber/red accents stay) and VI/EN flips the language, with the whole page of
copy, the FAQ, the pricing tables and the VIP badge all following it. Amber
text switches to a darker #a16207 on the light theme so it keeps ~4.9:1 on
white.

The register account is back as the old landing had it: a "ĐĂNG KÝ" button
pointing at /app?auth=1, and that URL now actually opens the auth dialog on
its sign-up tab (AuthModal takes an initialMode). The nav also collapses to
the hamburger below 1160px now, and the logo/tools shrink below 680px, so the
row still fits a 360px phone.
2026-09-17 18:40:05 +07:00
3dtours 340f0374fc Rebuild the web landing page to the blueprint spec
Ten dark cinematic sections: fixed glass nav with a hamburger sheet, hero
with the badge/dual CTA/stats bar, a pausable 35mm film-strip marquee, the
live preset tester (5 stocks, HUD, spec bars), the three-knob custom recipe
simulator, the 6-card feature grid, the QR sharing showcase, free-vs-Pro
pricing, reviews + FAQ accordion and the final CTA/footer.

The page owns its palette and its .lp-* styles, so it renders the same in
either workspace theme, and it pulls no CDN: Syne is bundled and the six
sample negatives are vendored (see docker/README.md). Store buttons raise a
toast instead of the old alert(). The now-dead landing CSS and the unused
land.* dictionary keys are gone.
2026-09-17 18:26:18 +07:00
3dtours dace19aec4 Match the phone's chip-strip panel on the web
The chips column was a stack of labelled sections; the phone opens one row at
a time. Port that shape: tapping a tool tab shows the tab's chips, tapping a
param chip opens its ruler above them, tapping a group chip opens its options
strip, and either closes the other. RESET leads the row (amber while dirty)
and PHOTO STYLE / RECIPES / WATERMARK are strips instead of inline chips.
2026-09-17 18:11:18 +07:00
3dtours c00c605dd7 Fix header popover hanging off-screen on narrow viewports
The header wraps under 860px, so a menu-wrap can land at the start of a row.
`.popover` is right-anchored to its button, so the theme/language/account menu
rendered at x=-280 of a 390px screen — half of it off the left edge and its
buttons unclickable. Anchor the popover to the full-width header on narrow
screens instead.

Measured at 390x844 before/after: popover x=-280 → x=56, w=326, fully inside.
2026-09-17 17:50:50 +07:00
3dtours 8c6e7930db Add self-contained docker/ stack for the web UI
`docker/` now holds the whole web build — frontend (Vite + React + CanvasKit),
backend (Fastify + SQLite) and the compose file — so the folder can be moved to
another machine and run without the React Native project:

    cd docker && cp .env.example .env && docker compose up -d --build

Only `${WEB_PORT:-8090}` is published; nginx serves the SPA and proxies /api to
the `api` container over Docker's DNS. Photos never reach the server.

The shared render code is vendored into `docker/frontend/shared/` and aliased to
a CanvasKit shim, so the app's own frameUtils/toneShader/jpegDpi run unchanged.

Fix the all-black render on GPU surfaces: `MakeWebGLCanvasSurface` creates a
separate WebGL context per call, and a texture from one context cannot be
sampled by a surface on another — so any pass that drew a snapshot onto a second
surface (output sharpen, screen sharpen, polaroid/wallframe cards) came out
solid black, while the raster fallback was correct. Use one shared
GrDirectContext + MakeRenderTarget instead.

Verified in headless Chromium against the running stack: 12MP JPEG in, preview
mean=120.5 sd=60.5, export 2048x1536 mean=107.2 sd=62.1, JFIF density 300/300,
EXIF present, no console errors; health/signup/login/me/recipes all 2xx through
the nginx proxy.
2026-09-17 17:43:03 +07:00