Virtual Camera Bender is a web-based still-image editor for creating circuit-bent compact digital camera aesthetics from input images, with a CLI companion that applies the same cameras to video.
The project is inspired by early 2000s consumer digital cameras pushed into failure states: false color, clipped highlights, vertical readout smear, dense sensor noise, posterized contours, and occasional memory or scanline corruption. The goal is artistic plausibility, not physically exact simulation — with one growing exception: the physics rail modules (IR cut, CCD clock, AFE bend, rail sag, bus bend, master clock, address bus) physically simulate real camera mods and circuit-level bends on the camera's optical IR filter, charge-transfer clocks, analog front end, supply rail, ADC data bus, master oscillator, and frame-buffer memory.
The still-image editor is working: image upload (click or drag-and-drop), live preview, 53 built-in camera presets with live thumbnails, macro and per-module controls, classic editing adjustments, family/global/per-module randomizers, undo/redo, A/B comparison, JSON preset save/load, original-resolution export (PNG/WebP/JPEG), and a headless CLI renderer that shares the same engine. A separate Preset Lab page can batch-generate random presets against a folder of test images and export selected results as paste-ready JS.
Video is supported image-first: load a clip in the app to design the look on a contact sheet of its frames (no in-app playback or encoding), tune temporal behavior (locked/hold/flicker seeds, parameter drift, frame ghosting), then render with the CLI (render-video, ffmpeg-backed, parallel workers).
Repository layout:
index.html,styles.css: static web app shell (no build step).preset-lab.html: batch preset generator and review surface for finding stronger built-in candidates.src/engine-core.js: the pure image-processing pipeline, shared by the web app, the render worker, and the CLI.src/render-worker.js: Web Worker that runs the pipeline off the main thread.src/presets.js: preset schema defaults, macro-to-pipeline mapping, control definitions.src/built-in-presets.js: the built-in camera preset collection.src/randomize.js: random modes, family randomizers, and per-module randomizers.src/app.js: UI wiring.src/temporal.js: video temporal scheduling — per-frame seed modes and parameter drift, shared by app and CLI.src/cli.js,src/dev-server.js: headless still/video renderer (ffmpeg-backed) and static dev server.src/video-worker.js: Node worker thread used byrender-videofor parallel frame rendering.docs/PROJECT_SPEC.md: product, aesthetic, technical, and roadmap spec.docs/PRESET_FORMAT.md: the preset JSON structure and module reference.
Local source/reference image folders are intentionally ignored by Git. Keep personal test photos in test-images/ and unlicensed visual references in reference-images/.
Start a local static server from the repository root:
npm run serveThen open http://localhost:8787.
Preset Lab is served from the same static server at http://localhost:8787/preset-lab.html.
The still-image pipeline currently chains these modules (all preset-controlled):
physics rail (one shared raw round trip: inverse ISP → 12-bit Bayer raw → enabled circuit sims in signal order → forward ISP), currently IR cut (filter-removal mod: glowing pink-white foliage, dark liquid skies, soft infrared haze) → CCD clock (charge-transfer faults: melt drips, row stalls, shear bands, bloom spikes) → AFE bend (analog oscillator injection, gain wobble, CDS ghosting) → rail sag (supply brownout: breathing exposure bands, latch-up color bands, static and dropout rows) → bus bend (per-clock ADC data-bus bend circuit, with soft series-resistance shorts and a common effects bus) → master clock (reclocked capture: stretch, skew, vertical roll, sync tears, shredded scanlines) → address bus (SDRAM address-line faults: mirrored tiles, interleaved rows, ping-ponged halves, band tearing) → then cheap camera (downscale, lens blur, bit crush + dither, sharpen) → sync fault (frame-wrap tears, rolling-shutter wobble) → bayer fault (wrong-phase demosaic checkerboards) → buffer ghost (stale-frame blocks, self or loaded second image) → chroma shift → exposure fault → color bend (hue rotate, channel swap/invert, solarize) → contour rings → false color (9 palettes, posterized or smooth gradient map) → gradient wash (positional rainbow fields) → edge burn → pixel sort → vertical smear → sensor noise → memory fault (interlace, block shift, row repeat, scanline dropout) → DCT crunch (JPEG quantization, DC hue drift, AC scramble, block stutter, chroma subsampling) → OSD overlay (datestamp and HUD burn-in) → basic adjustments (plain brightness, contrast, saturation, temperature, tint, gamma, shadows, highlights).
-
Web Worker rendering — processing runs off the main thread, so the UI stays responsive during heavy renders and full-resolution exports.
-
Undo / Redo — every preset switch, randomize, reroll, and control tweak is tracked (up to 60 steps).
-
A/B split compare — toggle Split A/B and drag the divider across the image; hold
Cfor a quick full-frame flash of the original. -
Per-module randomize, reset, solo & bypass — each module group in the right-side panels has a dice button (re-roll only that module's parameters, keeping the seed), an
Rreset button (restore schema defaults), anSsolo button, and a lamp button (quick enable/disable). The dice respects the Randomize mode selected in the left panel. -
Panel-level off/reset — each module panel has
ALL OFFandRESET ALLactions for quick comparison or returning a whole panel to schema defaults. -
Classic Edit panel — ordinary photo adjustments at the end of the chain, kept separate from emulation modules and left untouched by macro/global/family randomize.
-
Preset Lab — separate batch UI for folder-based test-image sweeps, keep/reject review, name/description/tag editing, and JS export of selected presets.
-
Live preset thumbnails — the preset list previews every built-in camera on the currently loaded image.
-
Ghost image — the Buffer Ghost module blends a stale frame into blocks of the image: a shifted copy of the photo by default, or any second image via the
LOADbutton in its Stylized Circuit panel (--ghost <image>in the CLI). -
Video frames sheet — loading a video opens a 25-frame contact sheet rendered through the current camera (
Vor the Frames button); clicking a frame makes it the working image, carrying its temporal seed so the preview matches the final render of that exact frame. -
Temporal panel — visible when a video is loaded: seed mode (locked / hold / flicker), hold length, parameter drift amount/speed, and ghost lag (feeds Buffer Ghost the frame N back for real stale-buffer trails). Saved in the preset; the Copy Render Command button emits the matching CLI line.
R— global randomizeG— camera galleryV— video frames sheet (when a video is loaded)C(hold) — view original imageS— toggle A/B split compareCmd/Ctrl+Z/Shift+Cmd/Ctrl+Z— undo / redo?— in-app help (signal path, randomize modes, module buttons)- Reroll button — new seed, same settings
Stills and video render headless (requires ffmpeg on PATH):
node src/cli.js render input.jpg output.png --builtin "IR Bloom"
node src/cli.js render input.jpg output.png --preset my-camera.vcb-preset.json --set pipeline.pixelSort.strength=0.9
node src/cli.js render input.jpg output.png --builtin "Double Buffer" --ghost other-frame.jpg
node src/cli.js render-video clip.mp4 bent.mp4 --preset my-camera.vcb-preset.json
node src/cli.js render-video clip.mp4 test.mp4 --builtin "Codec Rot" --quality small --start 4 --duration 2 --max-dimension 960
node src/cli.js list-presetsrender-video processes every frame through the engine in parallel worker threads (--jobs, default cores−1), preserves the source frame rate, stream-copies audio, and encodes H.264. The default --quality balanced uses CRF 23 for practical output sizes; use small (CRF 28), high (CRF 18), or exact --crf <n> when needed. --preset-json '<json>' accepts a complete preset without a separate file, which is what the app's Copy Render Command action emits. Temporal behavior comes from the preset's temporal block and can be overridden inline, e.g. --set temporal.mode=hold --set temporal.driftAmount=0.4. Frame indices are deterministic: the same input, preset, and options produce byte-identical output regardless of --jobs.
The first milestone (still-image web app with upload, preview, presets, randomizers, macro + advanced controls, JSON preset save/load, and original-resolution export) is complete, the CLI renderer arrived early, and video mode (contact-sheet design in the app, render-video in the CLI with temporal seed modes, drift, and frame ghosting) shipped with Phase 4. Next up:
- batch image processing through the CLI
- more effect families targeting the camera's digital brain (JPEG/DCT corruption, OSD/datestamp burn-in, sync tear + rolling-shutter wobble, Bayer/demosaic faults, and stale-buffer ghosting are done): amp glow, dead columns, purple fringing, AWB/AE hunting bands, and generational recompression — full implementation notes in
docs/PROJECT_SPEC.mdunder "Phase 5: New Effect Families"
The interface prioritizes usability while borrowing from old digital camera menus: compact controls, mode labels, preset slots, simple macro settings, and deeper configuration panels when needed.
The engine keeps the image-processing pipeline (src/engine-core.js) separate from the UI, so the same preset format drives the web app, the CLI renderer, and a future video renderer.
