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.
This commit is contained in:
2026-09-17 17:43:03 +07:00
parent 608901bd0f
commit 8c6e7930db
57 changed files with 9359 additions and 2 deletions
+33
View File
@@ -0,0 +1,33 @@
import { initSkia, hasGpu } from './skiaShim';
import type { RenderAssets } from './exportEngine';
// One CanvasKit instance per page, one copy of the three render assets. The wasm
// file is served from /wasm/ (scripts/copy-wasm.mjs puts it there at build time);
// the fonts and the wall-frame artwork are in public/assets.
let assets: RenderAssets | null = null;
async function buf(url: string): Promise<ArrayBuffer> {
const res = await fetch(url);
if (!res.ok) throw new Error(`fetch ${url}: ${res.status}`);
return res.arrayBuffer();
}
// The engine is only usable on a WebGL-backed surface (see skiaShim.ts), so the
// app has to know whether it got one: reporting "no WebGL" beats silently
// spending minutes per photo on the raster fallback.
export function probeGpu(): boolean {
return hasGpu();
}
export async function loadEngine(): Promise<{ assets: RenderAssets; gpu: boolean }> {
await initSkia({ locateFile: (file) => `/wasm/${file}` });
if (!assets) {
const [textFont, emojiFont, wallframePng] = await Promise.all([
buf('/assets/Cousine-Regular.ttf'),
buf('/assets/NotoEmoji-GPS.ttf'),
buf('/assets/wallframe.png'),
]);
assets = { textFont, emojiFont, wallframe: new Uint8Array(wallframePng) };
}
return { assets, gpu: probeGpu() };
}