{"_id":"@amplib/photography","_rev":"2-34a4815f642f04e60d27a541d84ffaef","name":"@amplib/photography","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@amplib/photography","version":"0.1.0","author":{"name":"Another Machine"},"license":"Apache-2.0","_id":"@amplib/photography@0.1.0","maintainers":[{"name":"jakealbaugh","email":"jake.albaugh@gmail.com"}],"homepage":"https://amplib.app/photography","bugs":{"url":"https://github.com/another-machine/public-library/issues"},"dist":{"shasum":"512500a5e8ff45b33210889e82304735393fdf4e","tarball":"https://registry.npmjs.org/@amplib/photography/-/photography-0.1.0.tgz","fileCount":6,"integrity":"sha512-1Ki4LpO6/aIAbSELQ8JJOwPP0F9bRom2yDpPvYZokfFSDm2kYyQ03J0w/RbKU6GHNhoiydVpYSR6jmStr2uLRA==","signatures":[{"sig":"MEUCIBR3ocKH0ou60mcbtf8K0lmpAhQxCK4l3q6grTPIPYetAiEAwKvFkkaxT/zkC5K2rWqrhm2HcO86pQVHXbwFXoS0hxE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":134707},"main":"dist/index.js","tsup":{"dts":true,"clean":true,"entry":["src/index.ts"],"format":"esm","minify":false,"sourcemap":true,"splitting":false},"type":"module","types":"dist/index.d.ts","gitHead":"698fc6679d04f3c6e45060e59738c29e54310054","scripts":{"build":"tsup","watch":"tsup --watch","prepack":"npm run build"},"_npmUser":{"name":"jakealbaugh","email":"jake.albaugh@gmail.com"},"repository":{"url":"git+https://github.com/another-machine/public-library.git","type":"git","directory":"packages/amplib-photography"},"_npmVersion":"10.2.4","description":"Burst exposure and film-style development for a camera on the GPU: stack real frames into one photograph, then develop it.","directories":{},"_nodeVersion":"20.11.0","dependencies":{"@amplib/devices":"^0.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/photography_0.1.0_1786995077661_0.25720365300378","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@amplib/photography","description":"Burst exposure and film-style development for a camera on the GPU: stack real frames into one photograph, then develop it.","version":"0.1.1","main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"homepage":"https://amplib.app/photography","repository":{"type":"git","url":"git+https://github.com/another-machine/public-library.git","directory":"packages/amplib-photography"},"scripts":{"build":"tsup","watch":"tsup --watch","prepack":"npm run build"},"dependencies":{"@amplib/devices":"^0.2.0"},"author":{"name":"Another Machine"},"license":"Apache-2.0","type":"module","_id":"@amplib/photography@0.1.1","gitHead":"7c4cd7606a827479dfb8f810e24bd308f7c24fcb","bugs":{"url":"https://github.com/another-machine/public-library/issues"},"_nodeVersion":"20.11.0","_npmVersion":"10.2.4","dist":{"integrity":"sha512-zU3i3DIiUh5mt8uQ/qNIJVHbvR/JU46+SIwhTR+PXaqtdANSxzDHmvM5k57NuGbiyvkBP0RfU7X2DrgceNrbSA==","shasum":"68e3f84c1a7844fa361d28ac6d0b014a35e98de0","tarball":"https://registry.npmjs.org/@amplib/photography/-/photography-0.1.1.tgz","fileCount":8,"unpackedSize":267296,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBxdnTdUFNJe4gGMoqndQ8AxSX2Ef8ujKB6e1P0rySpKAiEAifwIZCDACGWPQuJk1oOHICSOJWWx6av3t6fcvwNCQAc="}]},"_npmUser":{"name":"jakealbaugh","email":"jake.albaugh@gmail.com"},"directories":{},"maintainers":[{"name":"jakealbaugh","email":"jake.albaugh@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/photography_0.1.1_1787004055454_0.33934402552777776"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-17T19:31:17.451Z","modified":"2026-08-17T22:00:56.932Z","0.1.0":"2026-08-17T19:31:17.806Z","0.1.1":"2026-08-17T22:00:55.777Z"},"bugs":{"url":"https://github.com/another-machine/public-library/issues"},"author":{"name":"Another Machine"},"license":"Apache-2.0","homepage":"https://amplib.app/photography","repository":{"type":"git","url":"git+https://github.com/another-machine/public-library.git","directory":"packages/amplib-photography"},"description":"Burst exposure and film-style development for a camera on the GPU: stack real frames into one photograph, then develop it.","maintainers":[{"name":"jakealbaugh","email":"jake.albaugh@gmail.com"}],"readme":"# @amplib/photography\n\nA long exposure a browser cannot take, assembled from the frames it can read —\nthen developed.\n\n```ts\nimport { Camera, Darkroom, defaultParams } from \"@amplib/photography\";\n\nconst camera = new Camera({ facingMode: \"environment\" });\nconst darkroom = new Darkroom(canvas);\nconst params = defaultParams();\n\nawait camera.start(); // prompts for the camera\nawait darkroom.expose(camera.video, {\n  frames: 8, // ≈267ms at 30fps\n  stack: \"mean\",\n  mirror: camera.mirrored,\n  keepNegative: true, // hold the burst so frames/stack can restack\n});\n\ndarkroom.develop(params); // params include trail — develop-time, not capture\ndarkroom.restack({ frames: 4 }); // the shutter closing earlier, after the fact\nconst blob = await darkroom.toBlob();\n```\n\n## Modules\n\n| Module            | Description                                                          |\n| ----------------- | -------------------------------------------------------------------- |\n| `Camera`          | A camera to photograph with — viewfinder, granted frame rate, shutter |\n| `Darkroom`        | Stacks frames into an exposure, then develops it                     |\n| `SCHEMA`          | Every parameter described, including when it stops applying          |\n| `defaultParams`   | The documented defaults, as one object                               |\n| `inertReason`     | Why a parameter is being ignored right now, or null                  |\n\n## Design\n\n**Shutter speed is a frame count.** `getUserMedia` exposes no exposure-time\ncontrol, so the only long exposure available is an assembled one: read N\nconsecutive frames and add them together. Eight frames at 30fps is 267ms of\nmovement and 267ms of light, and because sensor noise is uncorrelated between\nframes while the signal is not, it also arrives about a third cleaner. `Camera`\nreports `shutterMs(frames)` because that is the number a photographer wants,\nand `fps` is read back from the track rather than assumed — asking a camera to\nrun at 15fps is a request, and Safari in particular declines it.\n\n**Two ways to stack, and they are different photographs.** `mean` sums frames by\nweight, so a moving subject spreads its light over everywhere it went and dims\nin proportion — motion blur. `max` keeps the brightest value each pixel ever\nreached, so a moving light holds full intensity along its entire path — a light\ntrail. Neither is a post-process of the other.\n\n**Trail is a develop parameter, because the math allows it.** The trail weight\nis linear in a frame's position along the burst, so the accumulator keeps two\nmoments — the weighted sum and the position-weighted sum — and any trail value\nis a per-pixel mix of the two at resolve time. Nothing about the trail is\nburned into the capture: it drags live like every other develop slider, and\nthe negative range is free — `-1` makes trails lead instead of follow, a\nmirrored ramp the burned-in weighting could never express.\n\n**The negative is optional, because it costs real memory.** With\n`keepNegative: true` the burst itself stays on the GPU — 4 bytes per pixel per\nframe — and `restack({ frames?, stack? })` re-accumulates from it: fewer\nframes is the shutter closing earlier, and `mean`/`max` swap on the same\nlight. Only the light is unrepeatable. The next expose frees it.\n\n**Exposing and developing share one GL context, because they must.** The\naccumulation stays on the GPU as a float texture and the develop chain samples\nit directly. Handing it between two contexts would cost a full readback per\nshot and, worse, would clip everything above 1.0 — which is exactly the range\nhalation is made of, since a neon sign clips flat in an 8-bit camera frame and\nhas to be pushed back up before it can bloom.\n\n**Every kernel is a fraction of the image, not a count of texels.** That is what\nmakes `develop(params, 0.5)` a preview rather than a different picture: the\nwhole chain runs at half size and the result is the same photograph, smaller. A\nUI can render at half resolution while a slider moves and commit to full\nresolution on release, and the two agree. `toBlob` and `toDataURL` re-develop at\nfull resolution first if the last render was a preview, because saving the\nhalf-size version is never what was meant.\n\n**Parameters carry their own applicability.** `trail` weights later frames more\nheavily, which means nothing under `max` — there is no per-frame weight there to\nbias. Left as a plain options object that control stays live and does nothing,\nwhich reads as a bug in the renderer rather than a property of the mode. So\n`inert` sits next to the parameter it constrains, returns the reason as a string\nmeant to be shown, and both a UI and a headless caller consult the same rule.\nThe same mechanism covers the halation tints when halation is off and the split\nhues when split is zero.\n\n**The camera comes from @amplib/devices.** `CameraStream` already solves\nacquiring, enumerating, cycling, and — the part that bites — stopping the\nprevious tracks so the indicator light goes out. `Camera` adds only what a\nphotograph needs and a stream has no opinion about: an element to sample,\nmirroring for a front-facing lens, the granted frame rate, and shutter time.\n\n**A burst that cannot finish honestly does not finish.** `expose` rejects with an\n`AbortError` rather than returning a torn image if the source changes size\nmid-burst (a device turn, which would otherwise stitch two orientations into one\nframe), if its `signal` aborts, or if the page is hidden — a backgrounded tab\nstops producing frames, and stacking the same stale one eight times yields\nsomething that looks like a photograph instead of like a failure.\n`requestVideoFrameCallback` is raced against a timeout for the same reason: a\nstalled stream never fires it, and an unraced await would hang with the shutter\nopen and no way back.\n\n**Rotation is renegotiated, not corrected.** Uploading the video element captures\nexactly the frame the viewfinder shows, so the two cannot disagree. What breaks\non a device turn is a platform that keeps handing out landscape frames while the\ndevice is portrait — visible in the preview too — so `Camera` restarts the\nstream rather than rotating after the fact. The rotation direction cannot be\nderived reliably from the orientation angle alone, and a wrong guess is worse\nthan a restart.\n\n## Stacking stills\n\n`exposeFrames` takes an array of anything the GPU can upload, for a burst that is\nnot coming from a live camera — bracketed stills, or frames pulled out of a\ndecoded video.\n\n```ts\ndarkroom.exposeFrames([img1, img2, img3], { frames: 3, stack: \"max\" });\n```\n\n## Requirements\n\nWebGL2, and a secure context for the camera. Without\n`EXT_color_buffer_float` the pipeline still runs on 8-bit targets and\n`darkroom.floatTargets` is false — highlights then clip at 1.0 and halation\nloses most of its bite.\n","readmeFilename":"README.md"}