Skip to content

Repository files navigation

Photoshoot

A photobooth for the browser and desktop.

Open your camera, choose an effect and capture a single photo, a four-shot strip or a short video. Photoshoot combines a live WebGL2 viewfinder, on-device face effects, five visual themes and a local gallery.

Open the web app · Product site · Case study · Build locally

The web app is live. This repository includes the browser build and Electron desktop source. For Windows, build from source using npm run build:win; no downloadable release is currently published.

Camera processing and captures stay on the device. The app was designed and built by Yousof Selim, including the interface, Electron/WebGL2 implementation, effects and deployment.

Highlights

  • 🎥 Live viewfinder rendered through a real WebGL2 pipeline (not a raw video tag)
  • 17 real‑time GLSL effects — Sepia, B&W, Plastic Camera, Comic Book, Color Pencil, Glow, Thermal, X‑Ray, Bulge, Dent, Twirl, Squeeze, Mirror, Light Tunnel, Fish Eye, Stretch, Pop Art (distortions have a draggable center)
  • 🙂 8 face‑tracked “fun” effects — Bug Out, Chipmunk, Frog, Dizzy, Blockhead, Nose Twirl, Lovestruck, Space Alien — powered by an on‑device MediaPipe face mesh (nothing uploaded)
  • 📸 Single photo, 4‑shot vertical strip (composited with borders, paper texture & footer), and video capture
  • 🪄 Classic background replacement with six original procedural backdrops
  • 🎨 Five polished built‑in themes — Studio (Photo Booth‑style light), Studio Dark, Classic Metal, Retro Film, and Darkroom (warm paper · safelight red)
  • 🔌 Secure community theme + filter import — zero‑trust; filters are clamped numeric grades + an optional validated LUT, never code
  • 🔊 Original sound design synthesized at runtime (no audio files at all)
  • 🖼 Local gallery with preview, open file, show in folder, delete
  • 🛡 Secure Electron architecture — context isolation, sandbox, strict CSP, camera‑only permissions, local‑only processing
  • Performance‑mindedrequestVideoFrameCallback, reused GPU textures/programs, optional FPS/debug overlay

Requirements

  • Windows 10/11 (the target platform). Development also runs on macOS/Linux.
  • Node.js 18+ and npm.
  • A webcam, and a GPU/driver with WebGL2 support (virtually all modern machines).

Getting started

npm install      # install dev dependencies (the app itself has zero runtime deps)
npm run dev      # build once, then launch the app

Other scripts:

npm start            # launch the already‑built app (electron .)
npm run build:app    # bundle main/preload/renderer into dist/ (no packaging)
npm run watch        # rebuild on change (then run npm start in another terminal)
npm run typecheck    # strict TypeScript type checking (no emit)

Building a Windows app

npm run build        # production bundle + electron-builder (installer + portable)
npm run build:win    # explicitly target Windows
npm run build:dir    # unpacked build (fast, for smoke‑testing packaging)

Output is written to release/:

  • Photoshoot-<version>-x64.exe — NSIS installer
  • Photoshoot-<version>-portable.exe — portable single‑file build

Build the Windows installer on Windows for a native, signed‑ready result. Cross‑building Windows targets from macOS/Linux requires extra tooling (Wine) and is not recommended.

The app icon (build/icon.png) is generated, original art. You can regenerate it with node build/make-icon.mjs.

The download link on the website points at GitHub Releases. To publish a Windows build, run npm run build:win on Windows, then attach the release/*.exe files to a GitHub Release.


Web app & website

Photoshoot also runs entirely in the browser — the same renderer, with a small browser shim (src/web/shim.ts) replacing the Electron bridge: captures live in IndexedDB, settings/themes in localStorage, and "export" becomes a download. Camera processing stays on‑device; nothing is uploaded.

npm run build:web     # builds the landing page + web app into web-dist/
npm run deploy        # builds web-dist/ and deploys to Firebase Hosting

web-dist/ is a plain static site:

  • web-dist/index.html — the marketing landing page (web/landing/)
  • web-dist/app/ — the web app (shim + renderer + styles)

Live: the site is hosted on Firebase Hosting at https://photoshoot-yeegz.web.app (app at /app/). Hosting config is in firebase.json / .firebaserc; deploy with firebase login && npm run deploy. Because the web app uses getUserMedia, it must be served over HTTPS (Firebase Hosting, GitHub Pages, or localhost all qualify).


Where photos are saved

All captures are written to:

%USERPROFILE%\Pictures\Photoshoot

(Pictures/Photoshoot). Files are named like Photoshoot_single_2026-06-13_22-04-31.png. Photos are PNG or JPG (your choice in Settings); strips are a single composited image; videos are .webm.

The in‑app gallery stores small thumbnails and metadata in the app's user‑data folder. The full‑resolution files live only in Pictures/Photoshoot.


How the sounds were made

There are no audio files in this project. Every sound — the shutter click, countdown ticks, capture flash shimmer, the 4‑shot completion chime, the photo‑drop "plip", button taps, theme switch, and error tone — is synthesized at runtime with the Web Audio API from oscillators and filtered noise (see src/renderer/sound.ts).

This recreates the feeling and timing of a tactile photobooth while guaranteeing the audio is 100% original and contains nothing extracted or copied from Apple or anyone else. Volume and a global mute live in Settings and persist locally. If audio can't initialize, the app continues silently.


Effects & the rendering pipeline

The live preview is a full WebGL2 pipeline (see src/renderer/gl):

  1. The webcam stream feeds a hidden <video> element.
  2. Frames drive the loop via requestVideoFrameCallback (falling back to requestAnimationFrame), uploading each frame to a reused GPU texture.
  3. A per‑effect GLSL fragment shader renders the processed image to the canvas.
  4. Captures read the canvas directly, so the saved image exactly matches the live preview (effect + mirror + background included).

Resources (programs, textures, the quad VAO) are created once and reused; nothing is allocated per frame. The Effects menu shows live preview tiles via a second small renderer that uploads the frame once and draws every effect from it.

Enable Settings → Advanced → Performance overlay to see FPS, resolution, active effect, and the processing backend.


Background replacement

Choose Backgrounds, pick an original backdrop, then follow the prompts: step out of frame so Photoshoot can capture a reference, then step back in. Each frame is compared against the reference at low resolution (the one intentional CPU pixel loop) to build a soft matte; the GPU composites you over the chosen backdrop, and effects still apply on top. Tune edge tolerance in Settings.

Backdrops (Dreamy Clouds, Retro Dots, Space Horizon, Mountain Sunset, Underwater, Stage Lights) are all drawn procedurally with Canvas2D — no copyrighted images.


Themes

Five built‑in themes ship with the app and are switchable in Settings:

  • Studio — bright, neutral, Photo Booth‑style light (the default on first launch)
  • Studio Dark — clean macOS‑style dark mode
  • Classic Metal — brushed‑aluminium hardware nostalgia
  • Retro Film — warm vintage photo‑print palette
  • Darkroom — warm photo‑paper chrome with a deep safelight‑red shutter

Your selected theme persists locally and is restored on restart. Themes are built on a shared vocabulary of design tokens (colors, spacing, radius, shadow, font, textures), so they restyle the entire UI — not just a color swap.

Importing community themes (and the security model)

Photoshoot can import community‑made themes, and treats them as fully untrusted using a zero‑trust model.

A theme "package" is a folder containing a theme.json manifest and optional image files beside it. In Settings → Appearance → Import Theme…, choose the theme.json. Example manifest:

{
  "schemaVersion": 1,
  "name": "Sunset Booth",
  "author": "Jane Doe",
  "base": "modern",
  "tokens": {
    "--app-bg": "#1a1030",
    "--accent": "#ff8a5b",
    "--accent-2": "#ffd23f",
    "--radius": "20px",
    "--shadow": "0 18px 50px rgba(0,0,0,0.5)"
  },
  "textures": {
    "--app-bg-texture": "backdrop.png"
  }
}

Imported themes can only ever set validated design tokens. They are parsed as data — never executed. The validator (shared by the main process and the renderer, run twice) enforces:

  • A strict whitelist of allowed token names and per‑token value types (color / length / shadow / font / texture).
  • Forbidden anywhere: javascript:, expression(), url() to remote/file resources, @import, HTML/<script>, http(s)://, file:, CSS escapes, ;{} injection, event handlers — rejected.
  • Image assets must be plain leaf filenames beside the manifest (no .., no absolute paths, no traversal), within strict size limits, and are verified by magic bytes (PNG/JPEG/WebP) — not by extension. Valid images are inlined as data: URIs; the renderer never touches the file system.
  • Tokens are applied via the CSSOM (element.style.setProperty), so a strict CSP with no unsafe-inline is preserved. Invalid tokens are skipped with a warning; the rest still apply.

Imported themes live in a controlled app‑data folder. Settings provides Remove imported theme and Reset to Studio.

No software can honestly promise zero exploits, but this design eliminates the obvious paths: no code execution, no remote/network resources, no file access, no HTML injection, no CSP escape.


Effects & community filters

Effects are organized in a Photo Booth‑style grid (Settings → Effects), paged with dots. Distortion effects (Bulge, Dent, Twirl, Squeeze, Fish Eye, Stretch, Light Tunnel) have a draggable center — click‑drag on the preview to move where the warp is anchored. The fun‑face effects use an on‑device MediaPipe face mesh (the model + WASM are bundled and served locally over a private app:// origin in Electron; nothing is ever uploaded), and fall back to the plain image when no face is detected or the mesh can't load.

Custom community filters (and the security model)

Beyond themes, Photoshoot can import community filters — and treats them, like themes, as fully untrusted data, never code. A filter is a filter.json manifest of clamped numeric color grades plus an optional LUT image. In Settings → Custom Filters → Import Filter…, choose the filter.json (select the LUT alongside it on the web). Example manifest:

{
  "schemaVersion": 1,
  "name": "Golden Hour",
  "author": "Jane Doe",
  "params": {
    "brightness": 0.05,
    "contrast": 1.15,
    "saturation": 1.25,
    "temperature": 0.4,
    "tint": -0.05,
    "gamma": 1.0,
    "fade": 0.15,
    "vignette": 0.3,
    "grain": 0.08,
    "hue": 0,
    "lutAmount": 1.0
  },
  "lut": "golden-hour.png"
}

The imported filter appears as a tile in the Effects grid. The validator (shared by the main process, the web app, and the renderer) enforces:

  • Every parameter is coerced to a finite number and hard‑clamped to a fixed range. Unknown keys are ignored; NaN/Infinity fall back to the default. The full, fixed vocabulary is the eleven grades above — nothing else is read.
  • The optional LUT must be a real PNG of exactly 512×512 (a 64‑level cube), checked by magic bytes and the PNG IHDR dimensions — not the extension. Its reference must be a plain leaf filename beside the manifest (no .., no absolute paths). Valid LUTs are inlined as a data: URI.
  • Filters are applied by one fixed, audited GLSL shader that only reads those numbers and (optionally) samples the LUT texture. No part of a manifest ever becomes shader source, JavaScript, CSS, or HTML. The worst a malicious filter can do is make an ugly picture.

A documented example manifest lives in examples/filters/. Imported filters live in a controlled app‑data folder; Settings provides Remove per filter.


Privacy

Photoshoot is built to keep everything on your device:

  • Camera frames never leave your computer. There is no upload, no cloud, no network calls of any kind. The Electron session even cancels unexpected network requests.
  • No analytics, tracking, or telemetry.
  • Captures are saved only to Pictures/Photoshoot.
  • Settings, gallery metadata, and imported themes are stored locally in the app's user‑data folder.

Security architecture: contextIsolation on, nodeIntegration off, sandboxed renderer, a tiny audited preload bridge (the only IPC surface), strict Content‑Security‑Policy, camera‑only permission handler, window‑open and navigation guards, and all file system access funneled through the main process with path‑traversal and save‑folder containment checks.


Troubleshooting

Camera permission (Windows). If you see "Camera access blocked", open Settings → Privacy & security → Camera, ensure camera access is on and that desktop apps are allowed, then click Try Again in Photoshoot.

"No camera found" / "Camera in use". Connect a webcam, or close other apps (Teams, Zoom, etc.) that may be holding the camera, then retry.

macOS during development. The first launch triggers the system camera prompt; allow it. If denied, re‑enable under System Settings → Privacy & Security → Camera.

WebGL unavailable. Photoshoot needs WebGL2. Update your GPU drivers. If running in a VM or over remote desktop, hardware acceleration may be disabled.

No sound. Check the Volume slider and Mute toggle in Settings. Audio unlocks on your first interaction with the window (per browser autoplay policy).


Project structure

src/
  main/        Electron main process (window, IPC, storage, theme import, paths)
  preload/     The single context-bridge API surface
  shared/      IPC contract + the theme validation schema (no Node/DOM)
  renderer/    UI, camera, sound, capture, gallery, settings, themes
    gl/        WebGL core, shaders, effect registry, renderer
    styles/    base / layout / components / themes CSS
build/         App icon + generator
esbuild.mjs    Build pipeline (bundles all three targets)

Limitations & future ideas

  • Background replacement is the classic reference‑frame technique; it works best with a still background and even lighting. A future ML/segmentation backend could remove that requirement.
  • Video is saved as WebM (capped at 60s) without audio.
  • Strip layout is a single vertical 4‑frame design; configurable layouts could follow.
  • Effect "amount" is fixed per effect today; per‑effect sliders would be a nice addition.

License

MIT. All bundled assets, art, and sounds are original to Photoshoot.

No Apple branding, logos, icons, graphics, layouts, code, or sounds are used.

About

A local-first browser and Electron photobooth with WebGL2 effects, on-device face tracking, photo strips and video capture.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages