react-native-webrtc-kaleidoscope 2.7.2 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +269 -198
  2. package/package.json +3 -1
package/README.md CHANGED
@@ -1,20 +1,36 @@
1
1
  <p align="center">
2
- <img src="./docs/kaleidoscope-logo.png" alt="react-native-webrtc-kaleidoscope logo" width="180" />
2
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/">
3
+ <img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/evidence/issue-verification/evidence/2026-06-14-readme-demo/showcase.gif" alt="One person held in frame by the live segmentation mask while the background cycles through a parametric blur, painted worlds, and animated generative shaders" width="720" />
4
+ </a>
3
5
  </p>
4
6
 
5
7
  <p align="center">
6
- <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/">
7
- <img src="https://img.shields.io/badge/▶%20Live%20demo-blur%20yourself%2C%20swap%20the%20room-8b5cf6?style=for-the-badge" alt="Live demo" />
8
- </a>
8
+ <sub><b>This is the real camera, not a still pasted over a video.</b> The live segmentation mask holds one person while shipped presets swap in behind. Every icon below is a live preset, in the order the loop plays them; click one to run it on your own camera.</sub>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=blur-medium" title="Blur">🌫️</a> &nbsp;
13
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=kaleidoscope-mandala" title="Kaleidoscope mandala">🔮</a> &nbsp;
14
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=wizard-tower" title="Wizard's tower">🧙</a> &nbsp;
15
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=outrun-classic" title="Outrun grid">🌆</a> &nbsp;
16
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=observation-deck" title="Observation deck">🛸</a> &nbsp;
17
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=data-mesh-cobalt" title="Cobalt data-mesh">🔷</a> &nbsp;
18
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=fairy-grotto" title="Fairy grotto">🧚</a> &nbsp;
19
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simianlights-hearth" title="Simianlights hearth">🪔</a> &nbsp;
20
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simiancraft-dark" title="Simiancraft">🐒</a>
21
+ &nbsp; <sub><a href="#presets">+ dozens more</a></sub>
9
22
  </p>
10
23
 
11
24
  <p align="center">
12
- <sub>Needs a Chromium-based browser (Chrome or Edge) and webcam permission; Safari and Firefox fall back to the unprocessed track.</sub>
25
+ <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/">
26
+ <img src="https://img.shields.io/badge/▶%20Live%20demo-blur%20yourself%2C%20swap%20the%20room-8b5cf6?style=for-the-badge" alt="Live demo" />
27
+ </a>
13
28
  </p>
14
29
 
15
- # react-native-webrtc-kaleidoscope
30
+ <h1>
31
+ <img src="./docs/kaleidoscope-logo-thumb.webp" alt="kaleidoscope logo" width="36" />&nbsp; react-native-webrtc-kaleidoscope
32
+ </h1>
16
33
 
17
- [![status: alpha](https://img.shields.io/badge/status-alpha-orange)](#status)
18
34
  [![npm version](https://img.shields.io/npm/v/react-native-webrtc-kaleidoscope?color=cb3837&logo=npm)](https://www.npmjs.com/package/react-native-webrtc-kaleidoscope)
19
35
  [![Types: included](https://img.shields.io/npm/types/react-native-webrtc-kaleidoscope?color=3178c6&logo=typescript)](https://www.npmjs.com/package/react-native-webrtc-kaleidoscope)
20
36
  [![CI](https://github.com/simiancraft/react-native-webrtc-kaleidoscope/actions/workflows/ci.yml/badge.svg)](https://github.com/simiancraft/react-native-webrtc-kaleidoscope/actions/workflows/ci.yml)
@@ -22,140 +38,120 @@
22
38
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/simiancraft/react-native-webrtc-kaleidoscope/badge)](https://securityscorecards.dev/viewer/?uri=github.com/simiancraft/react-native-webrtc-kaleidoscope)
23
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
24
40
 
25
- <p align="center">
26
- <code>kaleidoscope</code> &nbsp;•&nbsp; <code>transform</code> &nbsp;•&nbsp; <code>mask</code>
27
- </p>
41
+ > **Blur yourself, swap the room; live, on the device.** Real-time background blur and replacement for React Native and web video calls: bundled images, animated generative shaders, or painted worlds, each stenciled to the person by an on-device segmentation mask. Works with `react-native-webrtc` and LiveKit on Android, iOS, and Chromium browsers; managed-Expo-friendly.
28
42
 
29
- > Creative, shader-based video effects for React Native video calls: blur or replace a person's background in a teleconference. Works with `react-native-webrtc` and LiveKit, managed-Expo-friendly.
43
+ Every other turnkey option we could find is a feature welded to one vendor's calling SDK (Stream, Agora, 100ms, and the rest). This one attaches to `react-native-webrtc` instead, so it rides whatever stack you already run, LiveKit included. And where those vendors ship blur and a static image, this paints animated, generative-shader backgrounds and whole worlds.
30
44
 
31
- **Bind a track once, then drive it with three verbs.** `kaleidoscope` swaps the background effect (blur, a still image, or a procedural shader), `transform` reorients the frame, and `mask` tunes the segmentation edge. The effects you can command are a typed preset book you declare in your own project; at `expo prebuild`, only the assets you actually reference ship in your native bundle.
45
+ What you get:
32
46
 
33
- ## Status
47
+ - **Four simple functions.** `bindKaleidoscope(track, { presets })` hands back [`kaleidoscope`, `transform`, `mask`, and `dispose`](#the-four-verbs); that is the whole runtime API.
48
+ - **Agent-first setup.** Point a coding agent at [`llms.txt`](./llms.txt) and it [installs the package, writes the config plugin, and gets an effect on screen](#with-an-agent) without you babysitting it.
49
+ - **Turnkey implementation.** [Drop-in components](#quick-start), a picker, a live editor, and a persistence provider, render [dozens of presets](#presets) over your camera; wire a callback, ship it.
50
+ - **Cost and tooling.** Every shader carries a [measured GPU cost](#performance) you can read before you ship, and the [bench, meter, and thumbnail tools](#authoring-tooling) come in the box.
51
+ - **Won't bloat your binary.** Per-asset subpath exports and `sideEffects: false` mean you [ship only the presets you reference](#only-ship-what-you-use); web tree-shakes and native bundles just the assets your book names.
52
+ - **Built for extension.** A new shader is one folder that codegens to every platform, [clear by construction](#architecture); remix the compositor to [build your own worlds](#make-your-own-presets).
34
53
 
35
- **Active development; not yet production-ready.** Published to npm (the badge shows the current version, which tracks release automation rather than maturity). The [live web demo](https://simiancraft.github.io/react-native-webrtc-kaleidoscope/) runs every effect in the browser; native is exercised via the dev-client demo in `demo/`.
54
+ ## Presets
36
55
 
37
- ### What works today
56
+ The demo book ships the gallery below; **every tile is a live link**, so click one and it opens on your own camera. Bring your own with a few lines (see [Make your own presets](#make-your-own-presets)).
38
57
 
39
- - **`kaleidoscope`**: the art axis. Blur, a bundled background image (ten themed backgrounds plus a calibration grid, or your own assets), or a procedural shader, commanded by preset name. The generative shaders that ship are `plasma`, `clouds`, `godrays`, `fireflies`, `nebula`, `simianlights`, `anamorphic-lensflare`, `light-beams-and-motes`, and `corporate-blobs`.
40
- - **`transform`**: absolute flips and rotation (snapped to 90°), reapplied as full state each call.
41
- - **`mask`**: the segmentation edge (hardness, threshold) shared by every art effect.
42
- - **Tree-shaking**: declare a preset book; only the assets you reference ship in your native bundle (web tree-shakes by import).
43
- - **Drop-in UI (optional)**: a preset-driven `PresetBookMenu` (the menu) under `react-native-webrtc-kaleidoscope/preset-book-menu`, plus a headless live-editor kit (`PresetControlPanel`, mask and transform controls, a theme provider) under `react-native-webrtc-kaleidoscope/preset-control-panel`. Controlled and NativeWind-ready. See [Drop-in UI](#drop-in-ui-optional).
58
+ <!-- PRESET-WAFFLE:START -->
44
59
 
45
- | Platform | Transform | Blur | Background replacement | Notes |
46
- |---|---|---|---|---|
47
- | Web (Chrome / Edge) | | | | MediaStreamTrackProcessor + MediaPipe Selfie Segmentation (WASM, CDN) |
48
- | Android (API 24+) | | ✓ | ✓ | OpenGL ES 3.0 + MediaPipe Selfie Segmentation (Tasks) |
49
- | iOS (≥ 15) | | | | Metal + MediaPipe Selfie Segmentation (Tasks), verified on device. Older A11 devices (iPhone X) run at a lower frame rate |
50
- | Safari / Firefox | n/a | n/a | n/a | No Insertable Streams; the effects throw a clear capability error |
60
+ <table cellspacing="0" cellpadding="2">
61
+ <tr><td align="right" valign="middle"><sub><b>Blur</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=blur-low" title="Low"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/blur-low.thumb.webp" alt="Low" title="Low" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=blur-medium" title="Medium"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/blur-medium.thumb.webp" alt="Medium" title="Medium" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=blur-high" title="High"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/blur-high.thumb.webp" alt="High" title="High" width="58" /></a></td></tr>
62
+ <tr><td align="right" valign="middle"><sub><b>Wizard Tower</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=wizard-tower" title="Wizard Tower"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/wizard-tower/wizard-tower.thumb.webp" alt="Wizard Tower" title="Wizard Tower" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=wizard-tower-night" title="Night"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/wizard-tower-night/wizard-tower-night.thumb.webp" alt="Night" title="Night" width="58" /></a></td></tr>
63
+ <tr><td align="right" valign="middle"><sub><b>Spaceship</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=observation-deck" title="Observation Deck"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/observation-deck/observation-deck.thumb.webp" alt="Observation Deck" title="Observation Deck" width="58" /></a></td></tr>
64
+ <tr><td align="right" valign="middle"><sub><b>Fairy Cave</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=fairy-cave" title="Fairy Cave"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/fairy-cave/fairy-cave.thumb.webp" alt="Fairy Cave" title="Fairy Cave" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=fairy-grotto" title="Grotto"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/fairy-grotto/fairy-grotto.thumb.webp" alt="Grotto" title="Grotto" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=fairy-hollow" title="Hollow"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/fairy-hollow/fairy-hollow.thumb.webp" alt="Hollow" title="Hollow" width="58" /></a></td></tr>
65
+ <tr><td align="right" valign="middle"><sub><b>Ocean</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=underwater" title="Underwater"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/underwater/underwater.thumb.webp" alt="Underwater" title="Underwater" width="58" /></a></td></tr>
66
+ <tr><td align="right" valign="middle"><sub><b>Corporate</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=corporate-blobs" title="Blobs"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/corporate-blobs/corporate-blobs.thumb.webp" alt="Blobs" title="Blobs" width="58" /></a></td></tr>
67
+ <tr><td align="right" valign="middle"><sub><b>Simiancraft</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simiancraft-light" title="Simiancraft Light"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/simiancraft-light.thumb.webp" alt="Simiancraft Light" title="Simiancraft Light" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simiancraft-dark" title="Simiancraft Dark"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/simiancraft-dark.thumb.webp" alt="Simiancraft Dark" title="Simiancraft Dark" width="58" /></a></td></tr>
68
+ <tr><td align="right" valign="middle"><sub><b>Office</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=office-dark" title="Dark Office"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/office-dark.thumb.webp" alt="Dark Office" title="Dark Office" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=office-light" title="Light Office"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/office-light.thumb.webp" alt="Light Office" title="Light Office" width="58" /></a></td></tr>
69
+ <tr><td align="right" valign="middle"><sub><b>Nature</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=landscape-light" title="Nature Light"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/landscape-light.thumb.webp" alt="Nature Light" title="Nature Light" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=landscape-dark" title="Nature Dark"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/landscape-dark.thumb.webp" alt="Nature Dark" title="Nature Dark" width="58" /></a></td></tr>
70
+ <tr><td align="right" valign="middle"><sub><b>Home</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=home-light" title="Home Light"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/home-light.thumb.webp" alt="Home Light" title="Home Light" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=home-dark" title="Home Dark"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/home-dark.thumb.webp" alt="Home Dark" title="Home Dark" width="58" /></a></td></tr>
71
+ <tr><td align="right" valign="middle"><sub><b>Sci-Fi</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=sci-fi-light" title="Landscape"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/sci-fi-light.thumb.webp" alt="Landscape" title="Landscape" width="58" /></a></td></tr>
72
+ <tr><td align="right" valign="middle"><sub><b>Ocean</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=oceanscape-dark" title="Underwater"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/oceanscape-dark.thumb.webp" alt="Underwater" title="Underwater" width="58" /></a></td></tr>
73
+ <tr><td align="right" valign="middle"><sub><b>Debug</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=debug-resolutions" title="Resolutions"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/debug-resolutions.thumb.webp" alt="Resolutions" title="Resolutions" width="58" /></a></td></tr>
74
+ <tr><td align="right" valign="middle"><sub><b>Sky</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=clouds" title="Daytime"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/clouds/clouds.thumb.webp" alt="Daytime" title="Daytime" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=clouds-dawn" title="Dawn"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/clouds-dawn.thumb.webp" alt="Dawn" title="Dawn" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=clouds-dusk" title="Dusk"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/clouds-dusk.thumb.webp" alt="Dusk" title="Dusk" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=clouds-night" title="Night"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/clouds-night.thumb.webp" alt="Night" title="Night" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=clouds-otherworld" title="Otherworld"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/clouds-otherworld.thumb.webp" alt="Otherworld" title="Otherworld" width="58" /></a></td></tr>
75
+ <tr><td align="right" valign="middle"><sub><b>Plasma</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=plasma-ocean" title="Ocean"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/plasma-ocean.thumb.webp" alt="Ocean" title="Ocean" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=plasma-sunset" title="Sunset"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/plasma-sunset.thumb.webp" alt="Sunset" title="Sunset" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=plasma-mint" title="Mint"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/plasma-mint.thumb.webp" alt="Mint" title="Mint" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=plasma-fast" title="Fast"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/plasma-fast.thumb.webp" alt="Fast" title="Fast" width="58" /></a></td></tr>
76
+ <tr><td align="right" valign="middle"><sub><b>Kaleidoscope</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=kaleidoscope-stained-glass" title="Stained Glass"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/kaleidoscope-stained-glass.thumb.webp" alt="Stained Glass" title="Stained Glass" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=kaleidoscope-mandala" title="Mandala"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/kaleidoscope-mandala.thumb.webp" alt="Mandala" title="Mandala" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=kaleidoscope-prism" title="Prism"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/kaleidoscope-prism.thumb.webp" alt="Prism" title="Prism" width="58" /></a></td></tr>
77
+ <tr><td align="right" valign="middle"><sub><b>Neo-Memphis</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=neo-memphis-jazz-cup" title="Jazz Cup"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/neo-memphis-jazz-cup.thumb.webp" alt="Jazz Cup" title="Jazz Cup" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=neo-memphis-bauhaus" title="Bauhaus"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/neo-memphis-bauhaus.thumb.webp" alt="Bauhaus" title="Bauhaus" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=neo-memphis-confetti" title="Confetti"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/neo-memphis-confetti.thumb.webp" alt="Confetti" title="Confetti" width="58" /></a></td></tr>
78
+ <tr><td align="right" valign="middle"><sub><b>Halftone</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=halftone-boardroom" title="Boardroom"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/halftone-boardroom.thumb.webp" alt="Boardroom" title="Boardroom" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=halftone-press" title="Press"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/halftone-press.thumb.webp" alt="Press" title="Press" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=halftone-ripple" title="Ripple"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/halftone-ripple.thumb.webp" alt="Ripple" title="Ripple" width="58" /></a></td></tr>
79
+ <tr><td align="right" valign="middle"><sub><b>Aurora</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=aurora-corporate-silk" title="Corporate Silk"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/aurora-corporate-silk.thumb.webp" alt="Corporate Silk" title="Corporate Silk" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=aurora-dusk" title="Dusk"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/aurora-dusk.thumb.webp" alt="Dusk" title="Dusk" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=aurora-polar" title="Polar"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/aurora-polar.thumb.webp" alt="Polar" title="Polar" width="58" /></a></td></tr>
80
+ <tr><td align="right" valign="middle"><sub><b>Outrun</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=outrun-classic" title="Classic"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/outrun-classic.thumb.webp" alt="Classic" title="Classic" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=outrun-miami" title="Miami"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/outrun-miami.thumb.webp" alt="Miami" title="Miami" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=outrun-circuit" title="Circuit"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/outrun-circuit.thumb.webp" alt="Circuit" title="Circuit" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=outrun-acid" title="Acid"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/outrun-acid.thumb.webp" alt="Acid" title="Acid" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=outrun-vapor" title="Vapor"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/outrun-vapor.thumb.webp" alt="Vapor" title="Vapor" width="58" /></a></td></tr>
81
+ <tr><td align="right" valign="middle"><sub><b>Nebula</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=nebula" title="Nebula"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/nebula/nebula.thumb.webp" alt="Nebula" title="Nebula" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=nebula-ember" title="Ember"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/nebula-ember.thumb.webp" alt="Ember" title="Ember" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=nebula-drift" title="Drift"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/nebula-drift.thumb.webp" alt="Drift" title="Drift" width="58" /></a></td></tr>
82
+ <tr><td align="right" valign="middle"><sub><b>Simianlights</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simianlights" title="Simianlights"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/catalog/composites/simianlights/simianlights.thumb.webp" alt="Simianlights" title="Simianlights" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simianlights-glacier" title="Glacier"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/simianlights-glacier.thumb.webp" alt="Glacier" title="Glacier" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=simianlights-hearth" title="Hearth"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/simianlights-hearth.thumb.webp" alt="Hearth" title="Hearth" width="58" /></a></td></tr>
83
+ <tr><td align="right" valign="middle"><sub><b>Interior</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=interior-home" title="Home"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/interior-home.thumb.webp" alt="Home" title="Home" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=interior-office" title="Office"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/interior-office.thumb.webp" alt="Office" title="Office" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=interior-ab-shaft" title="A/B 1-shaft"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/interior-ab-shaft.thumb.webp" alt="A/B 1-shaft" title="A/B 1-shaft" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=interior-ab-3beam" title="A/B 3-beam"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/interior-ab-3beam.thumb.webp" alt="A/B 3-beam" title="A/B 3-beam" width="58" /></a></td></tr>
84
+ <tr><td align="right" valign="middle"><sub><b>Data-Mesh</b></sub></td><td valign="middle"><a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=data-mesh-datafield" title="Datafield"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/data-mesh-datafield.thumb.webp" alt="Datafield" title="Datafield" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=data-mesh-boardroom" title="Boardroom"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/data-mesh-boardroom.thumb.webp" alt="Boardroom" title="Boardroom" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=data-mesh-acid" title="Acid"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/data-mesh-acid.thumb.webp" alt="Acid" title="Acid" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=data-mesh-cobalt" title="Cobalt"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/data-mesh-cobalt.thumb.webp" alt="Cobalt" title="Cobalt" width="58" /></a> <a href="https://simiancraft.github.io/react-native-webrtc-kaleidoscope/?preset=data-mesh-slate" title="Slate"><img src="https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/demo/assets/thumbnails/data-mesh-slate.thumb.webp" alt="Slate" title="Slate" width="58" /></a></td></tr>
85
+ </table>
51
86
 
52
- ### Coming soon
87
+ <sub>63 presets across 4 families (Effects, Worlds, Backgrounds, Shaders); click any to open it live, or <a href="#make-your-own-presets">make your own</a> in a few lines.</sub>
53
88
 
54
- - **Animated image backgrounds**: a bundled still that moves (beyond the procedural shaders, which already render behind the person today). Same composite path; the new piece is a per-effect background producer for animated images.
89
+ <!-- PRESET-WAFFLE:END -->
55
90
 
56
- ## Install
91
+ <sub>Runs on Android, iOS, and Chromium browsers (Chrome, Edge), against either `react-native-webrtc` or LiveKit. Platform specifics and the Safari/Firefox fallback are in [Platform support](#platform-support).</sub>
57
92
 
58
- ```sh
59
- bun add react-native-webrtc react-native-webrtc-kaleidoscope
60
- ```
93
+ ## Quick start
61
94
 
62
- `react-native-webrtc` is a peer dependency. Install it explicitly.
95
+ Two paths to a working integration: hand it to a coding agent, or wire it yourself. Either way the shape is the same: install, add the config plugin, declare a preset book, bind a track.
63
96
 
64
- ### Using LiveKit?
97
+ ### With an agent
65
98
 
66
- If your project uses `@livekit/react-native` it pulls in `@livekit/react-native-webrtc`, a fork of upstream `react-native-webrtc` that preserves the same `videoEffects` native classes and the `_setVideoEffects` JS API. Kaleidoscope works against either fork; the Android Gradle script picks whichever one your autolinking surfaced.
99
+ Point your coding agent (Claude Code, Cursor, Copilot, …) at [`llms.txt`](./llms.txt). It is written for exactly this: a top-to-bottom integration guide that installs the package, writes the config-plugin entry, provisions a runnable preset book, and gets an effect on screen, with a six-file starting set lifted from the working `demo/`.
67
100
 
68
- ```sh
69
- bun add @livekit/react-native @livekit/react-native-webrtc react-native-webrtc-kaleidoscope
70
101
  ```
71
-
72
- Pick one fork. Installing both upstream `react-native-webrtc` and `@livekit/react-native-webrtc` in the same app will cause native class collisions; that's the consumer's problem to resolve.
73
-
74
- **Native wiring.** `@livekit/react-native` hands you a `LocalVideoTrack`; bind effects to its underlying `MediaStreamTrack`:
75
-
76
- ```ts
77
- import { bindKaleidoscope } from 'react-native-webrtc-kaleidoscope';
78
- import { presets } from './kaleidoscope.preset-book';
79
-
80
- const { kaleidoscope } = bindKaleidoscope(localCameraTrack.mediaStreamTrack, { presets });
81
- kaleidoscope('blur-soft');
102
+ Read https://raw.githubusercontent.com/simiancraft/react-native-webrtc-kaleidoscope/main/llms.txt
103
+ and integrate react-native-webrtc-kaleidoscope into this Expo app: add the config
104
+ plugin, create a starter preset book, and show the PresetBookMenu over my camera track.
82
105
  ```
83
106
 
84
- **Web wiring.** On web, LiveKit owns the `RTCRtpSender`, so you cannot swap the track yourself; go through LiveKit's processor API instead. The opt-in `/livekit` subpath ships a ready-made processor (it needs `livekit-client`, which a LiveKit app already has):
107
+ ### Manually
85
108
 
86
- ```ts
87
- import { KaleidoscopeProcessor } from 'react-native-webrtc-kaleidoscope/livekit';
88
-
89
- await localVideoTrack.setProcessor(new KaleidoscopeProcessor(['blur']), true);
90
- ```
91
-
92
- The `true` second argument to `setProcessor` shows the processed stream in your local preview. The processor tears down its Insertable-Streams pipeline on camera flip (`restart`) and unpublish (`destroy`), so repeated flips do not leak generators.
93
-
94
- To tune the segmentation mask edge on the processor path, use `setMaskTuning`; it is the processor-path twin of the binding's `mask` verb. The mask edge is page-shared state every kaleidoscope pipeline reads per frame, so the write reaches every active processor on the next frame with no rebuild:
95
-
96
- ```ts
97
- import { setMaskTuning } from 'react-native-webrtc-kaleidoscope/livekit';
98
-
99
- setMaskTuning({ hardness: 0.2, threshold: 0.85 });
109
+ ```sh
110
+ bun add react-native-webrtc react-native-webrtc-kaleidoscope
100
111
  ```
101
112
 
102
- ## Configure
103
-
104
- Add the config plugin to `app.config.ts`:
113
+ `react-native-webrtc` is a peer dependency; install it explicitly. (Using LiveKit instead? See [Using LiveKit](#using-livekit).) Add the config plugin to `app.config.ts`, then rebuild native code:
105
114
 
106
115
  ```ts
107
- export default {
108
- expo: {
109
- plugins: ['react-native-webrtc-kaleidoscope'],
110
- },
111
- };
116
+ export default { expo: { plugins: ['react-native-webrtc-kaleidoscope'] } };
112
117
  ```
113
118
 
114
- (`react-native-webrtc` 124.x does not ship a config plugin upstream; do not list it in `plugins`. If you are on a fork that adds one, add it explicitly.)
115
-
116
- Then rebuild native code:
117
-
118
119
  ```sh
119
120
  bunx expo prebuild
120
121
  ```
121
122
 
122
- ## Use
123
-
124
- First declare a **preset book** in your project: a flat catalog of **composites**, the only things you can command. A composite is `{ name, taxonomy, thumbnail?, layers }`, where `taxonomy` is the picker's grouping path, root first (`[group]` or `[group, category]`, e.g. `['Backgrounds', 'Office']`); everything is a layer stack, painted back to front. A layer is `{ id, shader, target?, blend? }` plus the shader's fields: `image` takes a `source`; `direct` samples the ingest-normalized (upright, non-mirrored) camera frame for its target (`target: 'subject'` is the masked person, `target: 'background'` the full frame); `blur` and the generative shaders (`plasma`, `clouds`, …) take `uniforms`. `target` defaults to `'background'` (fullscreen); `'subject'` stencils to the segmented person. Each layer's `id` is unique within its composite. Declare the book `as const satisfies PresetBook` for per-layer typing.
123
+ Declare a **preset book**: a flat catalog of the effects you can command. A rudimentary one is three entries:
125
124
 
126
125
  ```ts
127
126
  // kaleidoscope.preset-book.ts
128
- import type { PresetBook } from 'react-native-webrtc-kaleidoscope';
127
+ import type { KaleidoscopePresetBook } from 'react-native-webrtc-kaleidoscope';
129
128
  import { officeDark } from 'react-native-webrtc-kaleidoscope/images/office/office-dark';
130
- // Packaged composites ship ready to use; import and spread them in.
131
129
  import { wizardTower } from 'react-native-webrtc-kaleidoscope/composites/wizard-tower';
132
130
 
133
131
  export const presets = {
134
- // Replace your background with an image, you composited over it.
135
- 'office-dark': {
136
- name: 'Dark Office',
137
- taxonomy: ['Backgrounds', 'Office'],
138
- thumbnail: officeDark,
132
+ 'blur-soft': {
133
+ name: 'Soft blur',
134
+ taxonomy: ['Effects', 'Blur'],
139
135
  layers: [
140
- { id: 'office-dark', shader: 'image', source: officeDark },
136
+ { id: 'bg', shader: 'blur', target: 'background', uniforms: { sigma: 5 } },
141
137
  { id: 'you', shader: 'direct', target: 'subject' },
142
138
  ],
143
139
  },
144
- // Blur the background, stay sharp.
145
- 'blur-heavy': {
146
- name: 'Heavy',
147
- taxonomy: ['Effects', 'Blur'],
140
+ 'office-dark': {
141
+ name: 'Dark office',
142
+ taxonomy: ['Backgrounds', 'Office'],
143
+ thumbnail: officeDark,
148
144
  layers: [
149
- { id: 'bg', shader: 'blur', target: 'background', uniforms: { sigma: 7 } },
145
+ { id: 'office', shader: 'image', source: officeDark },
150
146
  { id: 'you', shader: 'direct', target: 'subject' },
151
147
  ],
152
148
  },
153
- // A packaged multi-layer composite (clouds + a cut-out image + you).
149
+ // A packaged multi-layer world, imported and spread in.
154
150
  'wizard-tower': wizardTower,
155
- } as const satisfies PresetBook;
151
+ } as const satisfies KaleidoscopePresetBook;
156
152
  ```
157
153
 
158
- Then bind a track once and drive it with the three verbs:
154
+ Bind a track once and drive it:
159
155
 
160
156
  ```ts
161
157
  import { mediaDevices } from 'react-native-webrtc';
@@ -165,63 +161,150 @@ import { presets } from './kaleidoscope.preset-book';
165
161
  const stream = await mediaDevices.getUserMedia({ video: true });
166
162
  const [track] = stream.getVideoTracks();
167
163
 
168
- const { kaleidoscope, transform, mask } = bindKaleidoscope(track, {
164
+ const { kaleidoscope, dispose } = bindKaleidoscope(track, {
169
165
  presets,
170
- // Web rebuilds the pipeline per command and yields a NEW track; read it here
171
- // (attach to <video> or replaceTrack). Native mutates the bound track in place.
172
- onTrack: (out) => {
173
- /* setPreviewTrack(out) */
174
- },
166
+ // Web yields a NEW track per command; read it here. Native mutates in place.
167
+ onTrack: (out) => {/* setPreviewTrack(out) */},
175
168
  });
176
169
 
177
- // kaleidoscope, the art axis. Pass a preset id (autocompletes from your book):
178
- kaleidoscope('wizard-tower');
179
- // Override a layer's uniforms live, addressed by layer id, while the patched
180
- // preset is active (merged, no pipeline rebuild). `shader` types the uniforms:
181
- kaleidoscope('blur-heavy', [{ id: 'bg', shader: 'blur', uniforms: { sigma: 5 } }]);
182
- kaleidoscope(null); // clear the art
170
+ kaleidoscope('wizard-tower'); // autocompletes from your book
171
+ // call dispose() on unmount to release the track
172
+ ```
173
+
174
+ For a ready-made gallery, drop in the picker; it reads your book directly:
175
+
176
+ ```tsx
177
+ import { PresetBookMenu } from 'react-native-webrtc-kaleidoscope/preset-book-menu';
178
+ import { presets } from './kaleidoscope.preset-book';
179
+
180
+ <PresetBookMenu presets={presets} value={art} onSelect={setArt} />;
181
+ // route onSelect into kaleidoscope() and the picker is wired.
182
+ ```
183
+
184
+ Want the selection, live tweaks, and mask edge to survive a reload? Wrap your app in the [persistence provider](#persistence). Want the styled UI and a live tuning panel? See [Drop-in UI](#drop-in-ui).
185
+
186
+ ### Using LiveKit
187
+
188
+ If your project uses `@livekit/react-native` it pulls in `@livekit/react-native-webrtc`, a fork that preserves the same `videoEffects` native classes and `_setVideoEffects` JS API. Kaleidoscope works against either fork; the Android Gradle script picks whichever one autolinking surfaced. Pick **one** fork, never both, or the native classes collide.
189
+
190
+ ```sh
191
+ bun add @livekit/react-native @livekit/react-native-webrtc react-native-webrtc-kaleidoscope
192
+ ```
193
+
194
+ On native, `@livekit/react-native` hands you a `LocalVideoTrack`; bind to its underlying `MediaStreamTrack`:
195
+
196
+ ```ts
197
+ const { kaleidoscope } = bindKaleidoscope(localCameraTrack.mediaStreamTrack, { presets });
198
+ kaleidoscope('blur-soft');
199
+ ```
200
+
201
+ On web, LiveKit owns the `RTCRtpSender`, so you cannot swap the track yourself; go through LiveKit's processor API. The opt-in `/livekit` subpath ships a ready-made processor (it needs `livekit-client`, which a LiveKit app already has):
202
+
203
+ ```ts
204
+ import { KaleidoscopeProcessor, setMaskTuning } from 'react-native-webrtc-kaleidoscope/livekit';
205
+
206
+ // A processor effect is a `composite` spec: the same layer stack a preset projects into.
207
+ const composite = {
208
+ name: 'composite',
209
+ layers: [
210
+ { id: 'bg', shader: 'blur', uniforms: { sigma: 8 } },
211
+ { id: 'you', shader: 'direct', target: 'subject' },
212
+ ],
213
+ };
214
+ await localVideoTrack.setProcessor(
215
+ new KaleidoscopeProcessor([composite]),
216
+ true,
217
+ );
218
+ setMaskTuning({ hardness: 0.2, threshold: 0.85 }); // the processor-path twin of `mask`
219
+ ```
220
+
221
+ The processor takes the same effect inputs as the core API: a `composite` spec (its layer stack), or a bare transform name like `'flip-x'`; not a preset-book id. The `true` shows the processed stream in your local preview. It tears down its Insertable-Streams pipeline on camera flip (`restart`) and unpublish (`destroy`), so repeated flips do not leak generators.
222
+
223
+ ## Concepts
224
+
225
+ The vocabulary, in the order you meet it.
226
+
227
+ | Term | What it is |
228
+ |---|---|
229
+ | **Preset book** | The file you author (`kaleidoscope.preset-book.ts`): a flat, typed map of the effects your app can command. Your point of entry; everything hangs off it. Declare `as const satisfies KaleidoscopePresetBook` for per-layer typing and id autocomplete. |
230
+ | **Preset** | One named entry in the book: `{ name, taxonomy, thumbnail?, layers, controls? }`. What `kaleidoscope(id)` applies. `taxonomy` is the picker's grouping path (`[group, category]`). |
231
+ | **Layer** | One entry in a preset's stack, painted back to front, addressed by a unique `id`. Three fields shape it:<br>&bull; **shader**: what it draws (`image`, `direct` the camera, `blur`, or a generative shader like `plasma` or `clouds`).<br>&bull; **target**: where it lands, `background` (fullscreen) or `subject` (stenciled to the person).<br>&bull; **blend**: how it stacks, opaque, `normal` (alpha-over), or `additive`. |
232
+ | **Composite** | What a preset becomes at runtime: the layer stack rendered into the frame. One registered native effect; "one effect" is a composite with a single layer. |
233
+ | **Patch** | A partial uniform override addressed by a layer `id`, merged over the baked values live with no rebuild. The lever the live editor and persistence ride on. |
234
+ | **Controls** | The editor component a preset supplies (`controls?`) so its tunable uniforms get sliders in the live panel. |
235
+
236
+ `direct` + `subject` is the masked person; `direct` + `background` is the raw camera frame.
237
+
238
+ ## The four verbs
239
+
240
+ <p align="center">
241
+ <code>kaleidoscope</code> &nbsp;•&nbsp; <code>transform</code> &nbsp;•&nbsp; <code>mask</code> &nbsp;•&nbsp; <code>dispose</code>
242
+ </p>
243
+
244
+ `bindKaleidoscope(track, { presets })` returns four functions (plus the live `track`). That is the whole runtime API.
245
+
246
+ | Verb | What it does |
247
+ |---|---|
248
+ | **`kaleidoscope(id, patches?)`** | Swap the background. Pass a preset id; optionally patch a layer's uniforms live, addressed by `id`. Pass `null` to clear. |
249
+ | **`transform(state?)`** | Absolute flip and rotate, snapped to 90°. Every call is the full state from identity; call bare to reset. |
250
+ | **`mask(edge)`** | Tune the one segmentation edge shared by every effect: `hardness` and `threshold`, both `0..1`. |
251
+ | **`dispose()`** | Tear down the pipeline and release the bound track. Call on unmount. |
252
+
253
+ ```ts
254
+ const { kaleidoscope, transform, mask, dispose } =
255
+ bindKaleidoscope(track, { presets, onTrack });
256
+
257
+ kaleidoscope('wizard-tower'); // a preset id
258
+ kaleidoscope('blur-soft', [ // patch a layer live, by id
259
+ { id: 'bg', shader: 'blur', uniforms: { sigma: 9 } },
260
+ ]);
261
+ kaleidoscope(null); // clear the art
183
262
 
184
- // transform, absolute geometry. Every call is the full state from identity;
185
- // re-pass what you want to keep. rotate snaps to the nearest 90°.
186
263
  transform({ flip: { x: true }, rotate: 90 });
187
- transform(); // reset to identity
264
+ transform(); // reset to identity
188
265
 
189
- // mask, the segmentation edge shared by every art effect. Both required, 0..1.
190
266
  mask({ hardness: 0.5, threshold: 0.5 });
267
+ dispose(); // on unmount
191
268
  ```
192
269
 
193
- That is the whole runtime surface: `kaleidoscope`, `transform`, `mask`.
270
+ Many uniforms are normalized `0..1`; others (`sigma`, scales, counts) carry natural units, and JSDoc documents each range. `mask` defaults to `0.5 / 0.5`; nudge it to match your camera and lighting.
194
271
 
195
- Numeric shader uniforms are normalized 0..1 by convention where practical; JSDoc documents each option's expected range as an IntelliSense hint (ranges are not enforced at runtime; validate in your own layer if you forward them to end users).
272
+ ## Make your own presets
196
273
 
197
- **Tuning note:** all three platforms run MediaPipe selfie segmentation (Tasks Image Segmenter on native, the Selfie Segmentation Solution on web), so the mask edge that suits one may differ slightly from another. `mask({ hardness, threshold })` defaults to `0.5 / 0.5`; nudge it to match your camera and lighting.
274
+ A preset is a composition: **every preset is a back-to-front stack of N layers**, and the compositor does not care what produces a layer's texture, which is exactly what makes it extensible. To author one, stack layers in the order you want them painted, lowest first, the masked person (`{ shader: 'direct', target: 'subject' }`) usually last so it sits on top.
198
275
 
199
- ## Drop-in UI (optional)
276
+ ```ts
277
+ // A generative shader behind the person, with an additive glow layer on top of it.
278
+ 'aurora-night': {
279
+ name: 'Aurora night',
280
+ taxonomy: ['Shaders', 'Aurora'],
281
+ layers: [
282
+ { id: 'sky', shader: 'clouds', target: 'background', uniforms: { coverage: 0.4 } },
283
+ { id: 'glow', shader: 'godrays', target: 'background', blend: 'additive' },
284
+ { id: 'you', shader: 'direct', target: 'subject' },
285
+ ],
286
+ },
287
+ ```
200
288
 
201
- Build your own controls against the three verbs, or import a ready-made, headless picker from `react-native-webrtc-kaleidoscope/preset-book-menu` that reads your preset book directly:
289
+ The demo book's [`wolf-cave`](./demo/kaleidoscope.preset-book.ts) is a runnable example of a custom composite (a bundled image plus the masked person). It is demo-owned, its image not shipped in the package, which is the point: it shows a consumer adding their own background.
202
290
 
203
- ```tsx
204
- import { useEffect, useState } from 'react';
205
- import { PresetBookMenu } from 'react-native-webrtc-kaleidoscope/preset-book-menu';
206
- import { presets } from './kaleidoscope.preset-book';
291
+ - **Bundled images** ship as tree-shakeable `image` layers, filed by category and imported per image (`import { officeDark } from 'react-native-webrtc-kaleidoscope/images/office/office-dark'`). On web a `source` can also be any image URL or data URI; native resolves bundled ids only. See [`catalog/images/README.md`](./catalog/images/README.md).
292
+ - **New shaders** drop a single `.frag` + typed `.ts` into `catalog/shaders/<name>/`; `bun run build:shaders` codegens the web and Android sources and transpiles the iOS Metal. The canonical upright frame and the mask stencil come for free; you write zero orientation code. See [`catalog/shaders/README.md`](./catalog/shaders/README.md).
293
+ - **Packaged composites** (the Worlds) live in `catalog/composites/<name>/` behind a `./composites/<name>` subpath export; import and spread one into your book.
207
294
 
208
- // `kaleidoscope` is the verb returned by bindKaleidoscope(track, { presets }).
209
- function BackgroundControls({ kaleidoscope }) {
210
- const [art, setArt] = useState<keyof typeof presets | null>(null);
211
- useEffect(() => {
212
- if (art) kaleidoscope(art);
213
- else kaleidoscope(null);
214
- }, [art, kaleidoscope]);
295
+ After adding a preset to the demo book, regenerate its thumbnail and this README's gallery: `bun run thumbs && bun run gen:waffle` (see [Authoring tooling](#authoring-tooling)).
215
296
 
216
- return <PresetBookMenu presets={presets} value={art} onSelect={setArt} />;
217
- }
218
- ```
297
+ ## Drop-in UI
298
+
299
+ Build your own controls against the four verbs, or import the headless, controlled components. All are presentational: they emit a selection or a patch, you apply it.
219
300
 
220
- `PresetBookMenu` is a two-level browser driven by each composite's `taxonomy`: a tab row across the top, one tab per **group** (`taxonomy[0]`, e.g. Effects, Worlds, Backgrounds, Shaders), and a left-hand menu of **categories** (`taxonomy[1]`) under the active group; the tile grid is filtered by both. A flat (depth-1) group shows no category menu. Every preset renders as a uniform tile: a wallpaper when the composite has a `thumbnail`, a recessed button of the same footprint when it does not, so a thumbnail-less preset never breaks the grid. The same pieces are exported as standalone primitives (`PresetGrid`, `PresetTile`, plus the `usePresetBookMenu` hook and `PresetBookMenuLayout`), so you can lay out your own. Selection is controlled (`value` + `onSelect(id)`, narrowed to your book's keys); the components are presentational: they emit the selected id, you apply it via `kaleidoscope`.
301
+ ### The picker
302
+
303
+ `PresetBookMenu` (from `react-native-webrtc-kaleidoscope/preset-book-menu`) is a two-level browser driven by each preset's `taxonomy`: a tab row across the top, one tab per **group** (`taxonomy[0]`), and a left-hand menu of **categories** (`taxonomy[1]`) under the active group; the tile grid filters by both. A flat (depth-1) group shows no category menu. Every preset renders as a uniform tile: a wallpaper when it has a `thumbnail`, a recessed button of the same footprint when it does not, so a thumbnail-less preset never breaks the grid. The same pieces ship as standalone primitives (`PresetGrid`, `PresetTile`, the `usePresetBookMenu` hook, `PresetBookMenuLayout`) for custom layouts.
221
304
 
222
305
  **Styling, three tiers.** Sensible defaults out of the box; override with an RN `style` prop, a `className` prop, or a `renderTile` render-prop slot for full control.
223
306
 
224
- **NativeWind-ready.** The components accept `className`. To turn it on, import the opt-in registration once in your NativeWind interop setup (`nativewind` is an optional peer dependency; the core `./preset-book-menu` import never pulls it in):
307
+ **NativeWind-ready.** The components accept `className`. Turn it on by importing the opt-in registration once (`nativewind` is an optional peer; the core `./preset-book-menu` import never pulls it in):
225
308
 
226
309
  ```ts
227
310
  import { registerKaleidoscopeNativeWind } from 'react-native-webrtc-kaleidoscope/nativewind';
@@ -240,7 +323,6 @@ import {
240
323
  TransformControlPanel,
241
324
  } from 'react-native-webrtc-kaleidoscope/preset-control-panel';
242
325
 
243
- // `controls` is the object from bindKaleidoscope(track, { presets }).
244
326
  <KaleidoscopeThemeProvider>
245
327
  <PresetControlPanel presets={presets} value={art} onPatch={(p) => controls.kaleidoscope(art, [p])} />
246
328
  <MaskControlPanel hardness={h} threshold={t} onChange={setMask} />
@@ -248,28 +330,19 @@ import {
248
330
  </KaleidoscopeThemeProvider>
249
331
  ```
250
332
 
251
- Each preset supplies its editor as a `controls` component on the book entry. The packaged composites export theirs at `react-native-webrtc-kaleidoscope/composites/<name>/controls`; for your own presets, compose `CompositeLayerControlPanel` over a shader's control descriptor (or `makeControls` for a custom widget). See [catalog/shaders/README.md](./catalog/shaders/README.md).
252
-
253
- Like the picker, the editor is controlled and presentational: it emits patches and you apply them. `KaleidoscopeThemeProvider` themes every control at once (a slot bank; `style` works everywhere, `className` via the same opt-in NativeWind interop). The sliders need `@react-native-community/slider` (an optional peer; a native module, so installing it needs a dev-client rebuild).
333
+ Each preset supplies its editor as a `controls` component on the book entry; packaged composites export theirs at `react-native-webrtc-kaleidoscope/composites/<name>/controls`. For your own presets, compose `CompositeLayerControlPanel` over a shader's control descriptor (or `makeControls` for a custom widget). `KaleidoscopeThemeProvider` themes every control at once. The sliders need `@react-native-community/slider` (an optional peer; a native module, so it needs a dev-client rebuild). Live per-layer tuning runs on web today; on native the editor renders while the live per-layer uniform channel is in progress. Mask and transform are live on every platform.
254
334
 
255
- Live per-layer tuning runs on web today; on native the editor renders but the live per-layer uniform channel is in progress. Mask and transform are live on every platform.
335
+ ### Persistence
256
336
 
257
- ### Persistence (the selection that survives a reload)
258
-
259
- `react-native-webrtc-kaleidoscope/persistence` ships a provider + hook that keep the person's selection across launches: the last applied preset id, the per-layer uniform patches they dialed in through the control panels (kept per preset, so tweaks to several presets all survive), and the mask edge.
337
+ `react-native-webrtc-kaleidoscope/persistence` ships a provider + hook that keep the person's selection across launches: the last applied preset id, the per-layer uniform patches they dialed in (kept per preset), and the mask edge.
260
338
 
261
339
  ```tsx
262
340
  // App root:
263
341
  import { KaleidoscopeStateProvider } from 'react-native-webrtc-kaleidoscope/persistence';
264
- import { presets } from './kaleidoscope.preset-book';
265
-
266
- <KaleidoscopeStateProvider presets={presets}>
267
- <App />
268
- </KaleidoscopeStateProvider>;
342
+ <KaleidoscopeStateProvider presets={presets}><App /></KaleidoscopeStateProvider>;
269
343
 
270
344
  // In the screen that binds the track:
271
345
  import { useKaleidoscopeState } from 'react-native-webrtc-kaleidoscope/persistence';
272
-
273
346
  const { hydrated, presetId, mask, setPreset, setMask, setPatch, patchesFor, reset } =
274
347
  useKaleidoscopeState<typeof presets>();
275
348
 
@@ -280,89 +353,87 @@ useEffect(() => {
280
353
  }, [hydrated, controls, presetId]);
281
354
  ```
282
355
 
283
- Route the picker's `onSelect` into `setPreset`, the editor's `onPatch` into `setPatch(presetId, patch)` (and apply the live patch as usual), and the mask panel into `setMask`; every write persists. Pass the editor `patches={patches[presetId]}` so restored tweaks appear in the sliders, and mount it after `hydrated` (the forms seed at mount). A stored preset that no longer exists in your book reads as "none" instead of crashing the picker.
356
+ Route the picker's `onSelect` into `setPreset`, the editor's `onPatch` into `setPatch(presetId, patch)`, and the mask panel into `setMask`; every write persists. The default store is [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage) (an optional peer; localStorage-backed on web). Back it with anything else (MMKV, a server) by passing a `{ load, save }` pair as the `store` prop; the stored shape is versioned and parses tolerantly, so a malformed payload reads as empty rather than throwing.
284
357
 
285
- The default store is [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage) (an optional peer; install it alongside the library when you use this subpath; on web it is localStorage-backed). To back it with something else (MMKV, a server), pass any `{ load, save }` pair as the `store` prop; the stored shape is versioned and parses tolerantly, so a malformed payload reads as empty rather than throwing.
358
+ ## Performance
286
359
 
287
- ## Worlds
360
+ The product is the masked-background composite, and its cost is something you can see, not guess.
288
361
 
289
- Packaged composites: a multi-layer stack (a generative shader or a cut-out image, the masked person on top), imported and spread into your book (e.g. `import { wizardTower } from 'react-native-webrtc-kaleidoscope/composites/wizard-tower'`). They carry their own `taxonomy: ['Worlds', <group>]`, so the menu groups them under the Worlds tab.
362
+ - **Annotated shader cost.** Each generative shader's `.ts` carries a measured GPU cost annotation (relative to `plasma` as the cheap baseline), so you know what a preset spends before you ship it.
363
+ - **One resolution knob.** Raw shader compute scales with output resolution, handled by the resolution tier (`targetShortSide`), not by per-effect orientation tricks. Drop the tier on weak GPUs; the mask and composite logic are unchanged.
364
+ - **Bounded work per frame.** Compositing is per-layer through a single mask stencil; a new shader inherits the pipeline's frame budget rather than adding a pass of its own.
290
365
 
291
- | Wizard Tower | Observation Deck | Fairy Cave |
292
- |---|---|---|
293
- | <img src="composites/wizard-tower/wizard-tower.thumb.webp" width="220" alt="wizard-tower" /> | <img src="composites/observation-deck/observation-deck.thumb.webp" width="220" alt="observation-deck" /> | <img src="composites/fairy-cave/fairy-cave.thumb.webp" width="220" alt="fairy-cave" /> |
294
- | Underwater | Nebula | Simianlights |
295
- | <img src="composites/underwater/underwater.thumb.webp" width="220" alt="underwater" /> | <img src="composites/nebula/nebula.thumb.webp" width="220" alt="nebula" /> | <img src="composites/simianlights/simianlights.thumb.webp" width="220" alt="simianlights" /> |
296
- | Corporate Blobs | | |
297
- | <img src="composites/corporate-blobs/corporate-blobs.thumb.webp" width="220" alt="corporate-blobs" /> | | |
366
+ ## Authoring tooling
298
367
 
299
- The `clouds` (Sky) composite also ships; its preview tile is pending.
368
+ The kit ships the same tools used to build it. All regenerate from the command line.
300
369
 
301
- ## Background presets
370
+ | Command | What it does |
371
+ |---|---|
372
+ | `bun run bench:shader` | SPIR-V weighted op-cost bench for a shader (good for no-loop shaders; rank by the meter for loop-bound ones). |
373
+ | `bun run shader:view` | WebGL2 A/B viewer with a live GPU-time meter for tuning a shader against the camera. |
374
+ | `bun run thumbs` | Render a `320×180` WebP thumbnail per preset in a book (the gallery tiles and picker wallpapers). |
375
+ | `bun run gen:waffle` | Regenerate this README's [preset gallery](#presets) from the demo book + thumbnails. Run after adding a preset; `--check` gates staleness. |
302
376
 
303
- The bundled backgrounds ship as `image` layers, filed by category and imported per image (e.g. `import { officeDark } from 'react-native-webrtc-kaleidoscope/images/office/office-dark'`). On web an image can also be any image URL or data URI; native resolves bundled image ids only.
377
+ ## Only ship what you use
304
378
 
305
- | Category | Light | Dark |
306
- |---|---|---|
307
- | Office | <img src="images/office/office-light.thumb.webp" width="220" alt="office-light" /> | <img src="images/office/office-dark.thumb.webp" width="220" alt="office-dark" /> |
308
- | Home | <img src="images/home/home-light.thumb.webp" width="220" alt="home-light" /> | <img src="images/home/home-dark.thumb.webp" width="220" alt="home-dark" /> |
309
- | Nature | <img src="images/nature/landscape-light.thumb.webp" width="220" alt="landscape-light" /> | <img src="images/nature/landscape-dark.thumb.webp" width="220" alt="landscape-dark" /> |
310
- | Sci-Fi | <img src="images/sci-fi/sci-fi-light.thumb.webp" width="220" alt="sci-fi-light" /> | |
311
- | Underwater | | <img src="images/underwater/oceanscape-dark.thumb.webp" width="220" alt="oceanscape-dark" /> |
312
- | Simiancraft | <img src="images/simiancraft/simiancraft-light.thumb.webp" width="220" alt="simiancraft-light" /> | <img src="images/simiancraft/simiancraft-dark.thumb.webp" width="220" alt="simiancraft-dark" /> |
379
+ Install size and bundle size are different numbers, and you pay for the second.
313
380
 
314
- The `simiancraft` category also ships two transparent brand images, `simiancraft-light-transparency` and `simiancraft-dark-transparency` (alpha preserved). Plus **`debug-resolutions`**, a viewport/resolution calibration grid for verifying background cover-fit:
381
+ - **Per-asset subpath exports.** Each bundled image and packaged composite is its own file behind its own subpath (`./images/<category>/<leaf>`, `./composites/<name>`), and the package sets `sideEffects: false`. A web bundler drops every preset you do not import.
382
+ - **Native ships only what your book references.** Metro does not tree-shake, so an unused preset is simply never imported; and `expo prebuild` copies only the assets your preset book actually names into the native bundle. Declare ten rooms, reference two, ship two.
383
+ - **Assets are WebP.** Backgrounds are 720p WebP; each platform decodes it natively (BitmapFactory on Android, ImageIO / MTKTextureLoader on iOS), so a full image set is a couple of megabytes, not sixteen.
315
384
 
316
- <img src="images/debug/debug-resolutions.thumb.webp" width="220" alt="debug-resolutions" />
385
+ ## For LLMs and agents
317
386
 
318
- See [`catalog/images/README.md`](./catalog/images/README.md) for the folder layout, the two image formats, and how to add one.
387
+ Feeding this repo into Claude / Cursor / Copilot, or shipping it into an app with an agent? Read [`llms.txt`](./llms.txt) first: the same scope as this README in a denser, parseable shape, with a copy-paste starting fileset that runs on all three platforms. It is the file to hand an agent for a hands-off integration (see [Quick start → With an agent](#with-an-agent)).
319
388
 
320
- ## Web and native differences
321
-
322
- The API surface is the same across platforms, but the runtimes differ in ways worth knowing before you wire effects in:
389
+ ## Architecture
323
390
 
324
- - **Output track.** On web each `kaleidoscope`/`transform` command rebuilds the Insertable-Streams pipeline and yields a NEW `MediaStreamTrack`, surfaced via `onTrack`; on native the bound track is mutated in place. `mask` updates the segmentation edge the running composite reads each frame, with no rebuild on either platform.
325
- - **Image source.** An `image` layer's `source` is a bundled preset name on native (the upstream `_setVideoEffects` registry is keyed by flat strings, not URIs), but on web it accepts either a preset name or an arbitrary image URL or data URI.
326
- - **Background presets ship as tree-shakeable files.** The bundled backgrounds (see [Background presets](#background-presets)) are importable per image: `import { officeDark } from 'react-native-webrtc-kaleidoscope/images/office/office-dark'`. Each image is its own file behind its own subpath export, and the package sets `sideEffects: false`, so an unused preset is dropped by web bundlers; since Metro doesn't tree-shake, it is simply never imported on native. Web resolves the bundled WebP to a URL; native loads its own bundled copy by name. Web also still accepts an arbitrary image URL or data URI. See [`catalog/images/README.md`](./catalog/images/README.md).
327
- - **Segmentation model on web.** The web compositor loads MediaPipe Selfie Segmentation from the jsdelivr CDN (`cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation`) on first use. A strict Content-Security-Policy must allow that origin for `script-src`, `connect-src`, and the WASM fetch, and the effects do not work offline. The `transform` ops need no model.
328
- - **Browser support on web.** Effects use Insertable Streams (`MediaStreamTrackProcessor` and `MediaStreamTrackGenerator`), which ship in Chromium-based browsers (Chrome, Edge); Safari and Firefox lack the API, so the effects throw a clear capability error and the demo falls back to the unprocessed track.
329
- - **Android revokes the camera ~60 s into the background.** Android 11+ disables camera access for backgrounded apps by device policy (`ERROR_CAMERA_DISABLED`); `react-native-webrtc` logs the event but never restarts capture, so after a long background the preview stays black on resume and no effect or preset change can recover it (the frame source is dead, not the pipeline). Re-acquire `getUserMedia` when the app returns from the background; the demo's [`use-loopback-stream.ts`](./demo/src/use-loopback-stream.ts) shows the `AppState` pattern, and effects re-bind to the new track through the normal verbs.
391
+ Every effect is a **layer in one compositor**: a bundled `image`, a `direct` passthrough (the masked person or the raw camera), a camera-sampling `blur`, or a generative shader, composited back to front with per-layer blend. There is one registered native effect, `composite`; its layer stack is delivered out of band and reconciled each command. Adding a background source is adding a layer kind, not a new effect, which is why [a new shader](#make-your-own-presets) reaches all three platforms from one folder.
330
392
 
331
- ## What this isn't
393
+ Canonical assets live in three root, folder-per-item directories, out of the TypeScript build path:
332
394
 
333
- - **Not a fork of `react-native-webrtc`.** A thin layer over its undocumented `_setVideoEffects` registry on native, and `MediaStreamTrackProcessor` on web. Install alongside `react-native-webrtc`.
334
- - **Not a managed cloud SaaS.** Effects run locally on the device; the track stays peer-to-peer. No service, no API key, no per-minute billing.
335
- - **Not a face-filter SDK.** Effects are background segmentation and frame transforms, not facial AR.
336
- - **Not a streaming protocol replacement.** The transformed track plugs into the consumer's existing `RTCPeerConnection` pipeline.
395
+ - `catalog/shaders/<name>/`: each shader's `.frag` plus its typed `.ts` (uniforms + control descriptor). All share one vertex stage; `bun run build:shaders` codegens the web and Android sources and transpiles the iOS Metal.
396
+ - `catalog/images/<category>/`: images filed by category; each is a `<leaf>.webp`, its `<leaf>.thumb.webp`, and the `<leaf>.ts` / `<leaf>.web.ts` loader pair, behind a subpath export.
397
+ - `catalog/composites/<name>/`: each packaged composite, behind a `./composites/<name>` subpath export.
337
398
 
338
- ## Architecture
399
+ The code spans the platform surfaces: `src/` (JS facade + shared types), `web-driver/` (WebGL2 pipeline), `android/` (OpenGL ES 3.0), and `ios/` (Metal). Orientation is normalized exactly once at the ingest, so effects do zero orientation work. The full contract, including the texture-orientation convention and the mask buffer-ownership rule, is in [`PATTERNS.md`](./PATTERNS.md).
339
400
 
340
- The canonical assets live in three root, folder-per-item directories, out of the TypeScript build path; the build and the prebuild copy read from them:
401
+ ## Platform support
341
402
 
342
- - `catalog/shaders/<name>/`: each shader's `.frag` plus its typed `.ts` (uniforms + control descriptor). All shaders share one vertex stage, `catalog/shaders/_shared/passthrough.vert`; there is no per-shader `.vert`. `catalog/shaders/_shared/` also holds the per-layer composite frags (`composite-camera.frag`, `composite-image.frag`, `composite-subject.frag`, `composite-masked.frag`, `composite-blit.frag`) and `transform.frag`. `bun run build:shaders` codegens the web and Android sources and transpiles the iOS Metal from these. See [catalog/shaders/README.md](./catalog/shaders/README.md) to add or extend a shader.
343
- - `catalog/images/<category>/`: images filed by category, several per folder. Each image is a quad: `<leaf>.webp`, its `<leaf>.thumb.webp`, and the `<leaf>.ts` / `<leaf>.web.ts` loader pair, behind a `./images/<category>/<leaf>` subpath export.
344
- - `catalog/composites/<name>/`: each packaged composite (a `KaleidoscopePreset`), behind a `./composites/<name>` subpath export.
403
+ | Platform | Transform | Blur | Background replacement | Notes |
404
+ |---|---|---|---|---|
405
+ | Web (Chrome / Edge) | | | | MediaStreamTrackProcessor + MediaPipe Selfie Segmentation (WASM, CDN) |
406
+ | Android (API 24+) | ✓ | ✓ | ✓ | OpenGL ES 3.0 + MediaPipe Selfie Segmentation (Tasks) |
407
+ | iOS (≥ 15) | ✓ | ✓ | ✓ | Metal + MediaPipe Selfie Segmentation (Tasks), verified on device. Older A11 devices (iPhone X) run at a lower frame rate |
408
+ | Safari / Firefox | n/a | n/a | n/a | No Insertable Streams; the effects throw a clear capability error and the demo falls back to the unprocessed track |
345
409
 
346
- The code lives across the platform surfaces:
410
+ A few runtime differences worth knowing before you wire effects in:
347
411
 
348
- - `src/`: JS facade and shared types. `bindKaleidoscope` returns the `kaleidoscope` / `transform` / `mask` verbs; the preset-book types live in `src/kaleidoscope/`.
349
- - `web-driver/`: WebGL2 pipeline. MediaPipe segmentation + the layered compositor (`web-driver/effects/composite.ts`).
350
- - `android/`: OpenGL ES 3.0 pipeline. MediaPipe Tasks segmentation + the layered compositor (`effects/CompositeFactory.kt`); codegen lands in `gpu/ShadersGenerated.kt`, the hand-written layer GLSL in `effects/LayerShaders.kt`.
351
- - `ios/`: Metal pipeline (Swift) with MediaPipe Tasks segmentation (`selfie_segmenter.tflite`, the same model Android bundles); the canonical GLSL transpiles to Metal via `scripts/build-shaders.ts`.
412
+ - **Output track.** On web each `kaleidoscope` / `transform` command rebuilds the Insertable-Streams pipeline and yields a NEW `MediaStreamTrack` via `onTrack`; on native the bound track is mutated in place. `mask` updates the running composite with no rebuild on either platform.
413
+ - **Segmentation model on web.** The web compositor loads MediaPipe Selfie Segmentation from the jsDelivr CDN on first use. A strict Content-Security-Policy must allow that origin for `script-src`, `connect-src`, and the WASM fetch, and the effects do not work offline. `transform` needs no model.
414
+ - **Android revokes the camera ~60 s into the background.** Android 11+ disables camera access for backgrounded apps by device policy; `react-native-webrtc` logs it but never restarts capture, so after a long background the preview stays black on resume. Re-acquire `getUserMedia` when the app returns from the background; the demo's [`use-loopback-stream.ts`](./demo/src/use-loopback-stream.ts) shows the `AppState` pattern, and effects re-bind to the new track through the normal verbs.
352
415
 
353
- Every effect is a LAYER in one compositor: an `image` image, a `direct` passthrough (the masked person, or the raw camera), a camera-sampling `blur`, or a generative shader, composited back to front with per-layer blend. There is one registered native effect, `composite`; its layer stack is delivered out of band and reconciled each command.
416
+ ## What this isn't
354
417
 
355
- See [`PATTERNS.md`](./PATTERNS.md) for the file-layout conventions, texture-orientation contract, and recipe for adding new effects, shaders, presets, or tunable parameters.
418
+ - **Not a fork of `react-native-webrtc`.** A thin layer over its undocumented `_setVideoEffects` registry on native, and `MediaStreamTrackProcessor` on web. Install alongside it.
419
+ - **Not a managed cloud SaaS.** Effects run locally on the device; the track stays peer-to-peer. No service, no API key, no per-minute billing.
420
+ - **Not a face-filter SDK.** Effects are background segmentation and frame transforms, not facial AR.
421
+ - **Not a streaming protocol replacement.** The transformed track plugs into your existing `RTCPeerConnection` pipeline.
356
422
 
357
423
  ## Reference
358
424
 
425
+ - [CHANGELOG.md](./CHANGELOG.md): release history (semantic-release, Conventional Commits).
359
426
  - [CONTRIBUTING.md](./CONTRIBUTING.md): setup, scripts, commit conventions.
360
- - [AGENTS.md](./AGENTS.md): agent and contributor orientation.
361
- - [PATTERNS.md](./PATTERNS.md): codebase conventions and how-to-extend.
427
+ - [AGENTS.md](./AGENTS.md): contributor and agent orientation for working on the repo.
428
+ - [PATTERNS.md](./PATTERNS.md): codebase conventions, the orientation contract, and how to extend.
362
429
  - [catalog/shaders/README.md](./catalog/shaders/README.md): adding and extending shaders.
430
+ - [catalog/images/README.md](./catalog/images/README.md): the image folder layout and formats.
431
+ - [llms.txt](./llms.txt): dense, agent-oriented integration guide.
363
432
  - [SECURITY.md](./SECURITY.md): security policy and reporting.
364
433
  - [NOTICE.md](./NOTICE.md): third-party attributions.
365
434
 
366
435
  ---
367
436
 
368
437
  MIT licensed. © 2026 Jesse Harlin / [Simiancraft](https://github.com/simiancraft).
438
+
439
+ <p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-webrtc-kaleidoscope",
3
- "version": "2.7.2",
3
+ "version": "2.7.4",
4
4
  "description": "Live video effects (blur, background replacement, generative backgrounds, flip/rotate) for react-native-webrtc, packaged as a managed-Expo-friendly Expo Module. Working on web, Android, and iOS. Active development.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -587,6 +587,8 @@
587
587
  "bench:shader": "bun run scripts/shader-cost.ts",
588
588
  "shader:view": "bun run scripts/shader-view.ts",
589
589
  "thumbs": "bun run build && bun tools/thumbnails/make-thumbnails.ts --book demo/kaleidoscope.preset-book.ts --out demo/assets/thumbnails --repo",
590
+ "gen:waffle": "bun run scripts/gen-preset-waffle.ts",
591
+ "check:waffle": "bun run scripts/gen-preset-waffle.ts --check",
590
592
  "demo": "bun run build && cd demo && bun run start",
591
593
  "demo:wsl": "bun run build && cd demo && bun run start:wsl",
592
594
  "demo:ios": "bun run build && cd demo && bun run ios",