@viji-dev/core 0.16.0 → 0.17.0

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.
package/dist/docs-api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const docsApi = {
2
2
  "version": "1.1.0",
3
- "coreVersion": "0.16.0",
4
- "generatedAt": "2026-08-26T17:41:23.222Z",
3
+ "coreVersion": "0.17.0",
4
+ "generatedAt": "2026-09-12T17:25:53.411Z",
5
5
  "navigation": [
6
6
  {
7
7
  "id": "getting-started",
@@ -126,6 +126,11 @@ export const docsApi = {
126
126
  "title": "Color",
127
127
  "path": "native/parameters/color"
128
128
  },
129
+ {
130
+ "id": "native-param-gradient",
131
+ "title": "Gradient",
132
+ "path": "native/parameters/gradient"
133
+ },
129
134
  {
130
135
  "id": "native-param-toggle",
131
136
  "title": "Toggle",
@@ -388,6 +393,11 @@ export const docsApi = {
388
393
  "title": "Color",
389
394
  "path": "p5/parameters/color"
390
395
  },
396
+ {
397
+ "id": "p5-param-gradient",
398
+ "title": "Gradient",
399
+ "path": "p5/parameters/gradient"
400
+ },
391
401
  {
392
402
  "id": "p5-param-toggle",
393
403
  "title": "Toggle",
@@ -640,6 +650,11 @@ export const docsApi = {
640
650
  "title": "Color",
641
651
  "path": "shader/parameters/color"
642
652
  },
653
+ {
654
+ "id": "shader-param-gradient",
655
+ "title": "Gradient",
656
+ "path": "shader/parameters/gradient"
657
+ },
643
658
  {
644
659
  "id": "shader-param-toggle",
645
660
  "title": "Toggle",
@@ -884,7 +899,7 @@ export const docsApi = {
884
899
  },
885
900
  {
886
901
  "type": "text",
887
- "markdown": "A few things to notice:\n\n- **[`viji.slider()`](/native/parameters/slider)** creates a UI slider the user can adjust. You define it once at the top level and read `.value` inside `render()`.\n- **[`viji.deltaTime`](/native/timing#vijideltatime-frame-delta)** is the time since the last frame in seconds. Use it with an accumulator (`angle +=`) for smooth, frame-rate-independent animation that doesn't jump when you change parameters.\n- **[`viji.width`](/native/canvas-context#dimension-properties) / [`viji.height`](/native/canvas-context#dimension-properties)** keep your scene resolution-agnostic.\n- **`render(viji)`** is called every frame. This is where you draw.\n\n## What You Can Access\n\nEverything is available through the `viji` object:\n\n| Category | What It Gives You |\n|----------|------------------|\n| **Canvas** | [`viji.canvas`](/native/canvas-context#the-canvas), [`viji.width`](/native/canvas-context#dimension-properties), [`viji.height`](/native/canvas-context#dimension-properties) |\n| **Timing** | [`viji.time`](/native/timing#vijitime-absolute-time), [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta), [`viji.frameCount`](/native/timing#vijiframecount), [`viji.fps`](/native/timing#vijifps-target-frame-rate) |\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), [`viji.color()`](/native/parameters/color), [`viji.toggle()`](/native/parameters/toggle), [`viji.select()`](/native/parameters/select), [`viji.number()`](/native/parameters/number), [`viji.text()`](/native/parameters/text), [`viji.image()`](/native/parameters/image), [`viji.button()`](/native/parameters/button), [`viji.coordinate()`](/native/parameters/coordinate) |\n| **Audio** | [Volume, bands, beat, spectral, FFT & waveform](/native/audio#api-overview) |\n| **Video & CV** | [Video frames, face detection, hand tracking, pose estimation, body segmentation](/native/video#api-overview) |\n| **Interaction** | [Unified pointer](/native/pointer), [mouse buttons & wheel](/native/mouse), [keyboard state](/native/keyboard), [multi-touch with pressure & velocity](/native/touch) |\n| **Sensors** | [Accelerometer, gyroscope, device orientation](/native/sensors#api-reference) |\n\n## Three Ways to Create\n\nViji supports three rendering modes:\n\n| Renderer | Best For | Entry Point |\n|----------|----------|-------------|\n| **Native** | Full control with Canvas 2D, WebGL, or Three.js | `render(viji)` |\n| **P5.js** | Artists familiar with Processing / P5.js | `render(viji, p5)` |\n| **Shader** | GPU effects, raymarching, generative patterns | `void main()` in GLSL |\n\nAll three share the same audio, video, parameter, and interaction APIs. See [Renderers Overview](../renderers-overview/) for how each works.\n\n## Next Steps\n\n- [Renderers Overview](../renderers-overview/): how to choose and use each renderer\n- [Best Practices](../best-practices/): essential patterns for reliable, performant scenes\n- [Common Mistakes](../common-mistakes/): pitfalls to avoid\n- [Native Quick Start](/native/quickstart): build with JavaScript and full canvas control\n- [P5 Quick Start](/p5/quickstart): build with the familiar P5.js API\n- [Shader Quick Start](/shader/quickstart): build with GLSL fragment shaders"
902
+ "markdown": "A few things to notice:\n\n- **[`viji.slider()`](/native/parameters/slider)** creates a UI slider the user can adjust. You define it once at the top level and read `.value` inside `render()`.\n- **[`viji.deltaTime`](/native/timing#vijideltatime-frame-delta)** is the time since the last frame in seconds. Use it with an accumulator (`angle +=`) for smooth, frame-rate-independent animation that doesn't jump when you change parameters.\n- **[`viji.width`](/native/canvas-context#dimension-properties) / [`viji.height`](/native/canvas-context#dimension-properties)** keep your scene resolution-agnostic.\n- **`render(viji)`** is called every frame. This is where you draw.\n\n## What You Can Access\n\nEverything is available through the `viji` object:\n\n| Category | What It Gives You |\n|----------|------------------|\n| **Canvas** | [`viji.canvas`](/native/canvas-context#the-canvas), [`viji.width`](/native/canvas-context#dimension-properties), [`viji.height`](/native/canvas-context#dimension-properties) |\n| **Timing** | [`viji.time`](/native/timing#vijitime-absolute-time), [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta), [`viji.frameCount`](/native/timing#vijiframecount), [`viji.fps`](/native/timing#vijifps-target-frame-rate) |\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), [`viji.color()`](/native/parameters/color), [`viji.gradient()`](/native/parameters/gradient), [`viji.toggle()`](/native/parameters/toggle), [`viji.select()`](/native/parameters/select), [`viji.number()`](/native/parameters/number), [`viji.text()`](/native/parameters/text), [`viji.image()`](/native/parameters/image), [`viji.button()`](/native/parameters/button), [`viji.coordinate()`](/native/parameters/coordinate) |\n| **Audio** | [Volume, bands, beat, spectral, FFT & waveform](/native/audio#api-overview) |\n| **Video & CV** | [Video frames, face detection, hand tracking, pose estimation, body segmentation](/native/video#api-overview) |\n| **Interaction** | [Unified pointer](/native/pointer), [mouse buttons & wheel](/native/mouse), [keyboard state](/native/keyboard), [multi-touch with pressure & velocity](/native/touch) |\n| **Sensors** | [Accelerometer, gyroscope, device orientation](/native/sensors#api-reference) |\n\n## Three Ways to Create\n\nViji supports three rendering modes:\n\n| Renderer | Best For | Entry Point |\n|----------|----------|-------------|\n| **Native** | Full control with Canvas 2D, WebGL, or Three.js | `render(viji)` |\n| **P5.js** | Artists familiar with Processing / P5.js | `render(viji, p5)` |\n| **Shader** | GPU effects, raymarching, generative patterns | `void main()` in GLSL |\n\nAll three share the same audio, video, parameter, and interaction APIs. See [Renderers Overview](../renderers-overview/) for how each works.\n\n## Next Steps\n\n- [Renderers Overview](../renderers-overview/): how to choose and use each renderer\n- [Best Practices](../best-practices/): essential patterns for reliable, performant scenes\n- [Common Mistakes](../common-mistakes/): pitfalls to avoid\n- [Native Quick Start](/native/quickstart): build with JavaScript and full canvas control\n- [P5 Quick Start](/p5/quickstart): build with the familiar P5.js API\n- [Shader Quick Start](/shader/quickstart): build with GLSL fragment shaders"
888
903
  }
889
904
  ]
890
905
  },
@@ -1297,13 +1312,13 @@ export const docsApi = {
1297
1312
  "content": [
1298
1313
  {
1299
1314
  "type": "text",
1300
- "markdown": "# Create Your First Scene\n\nNew to Viji? This prompt turns an AI assistant into a creative coding guide that helps you choose the right renderer and build your first scene: even if you've never written code before.\n\n## How It Works\n\n1. Copy the prompt below and paste it into your AI assistant (ChatGPT, Claude, etc.).\n2. Describe what you want to create: as simple or detailed as you like.\n3. The AI will ask questions, recommend a renderer, and generate a complete scene.\n\n## Renderer Quick Comparison\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| **Language** | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js | GLSL (GPU fragment shader) |\n| **Best for** | Full control, Three.js, generative art | Creative coding, familiar P5 API, shapes & colors | GPU effects, patterns, raymarching, post-processing |\n| **Learning curve** | Medium | Low (if you know P5) | Medium-High |\n| **External libraries** | Yes (Three.js, etc.) | P5.js built-in | No |\n| **3D support** | Yes (WebGL, Three.js) | Yes (via `// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n\n## The Prompt\n\n````\nYou are a creative coding assistant for the Viji platform. Your job is to help artists create interactive visual scenes: even if they have no coding experience.\n\n## YOUR BEHAVIOR\n\n1. Ask the artist what they want to create. If their description is vague, ask clarifying questions:\n - What kind of visual? (patterns, shapes, particles, video effects, 3D, etc.)\n - Should it react to audio/music?\n - Should it use a camera/video?\n - Should it respond to mouse/touch/device tilt?\n - What mood or style? (abstract, organic, geometric, glitchy, minimal, etc.)\n2. Recommend a renderer based on what the artist wants to achieve. Coding experience is NOT a selection criterion: with your help, every renderer is accessible to artists at every level. Choose the renderer whose strengths best fit the goal:\n - **Per-pixel GPU effects** (raymarching, fractals, distance fields, full-screen post-processing, generative patterns that read every pixel) → recommend **Shader**.\n - **3D scenes with Three.js, custom WebGL or WebGPU, pixel-perfect Canvas 2D, or any external library via `await import()`** → recommend **Native**.\n - **Classic creative-coding shapes** (`p5.ellipse`, `p5.line`, `p5.bezier`), **color modes, transforms, particle systems with simple drawing**, or **an existing P5 sketch the artist already has** → recommend **P5**.\n - Audio reactivity, camera/CV reactivity, and parameters work in every renderer: pick based on the visual style, not on data sources.\n - You may briefly mention that Shader (GLSL) and Native + Three.js have a steeper conceptual learning curve, but never use this as the primary selection criterion. The artist asked for help; help them with the renderer that fits the goal.\n3. Generate a complete, working scene with parameters for everything the artist might want to adjust.\n4. Explain what the code does in simple terms.\n5. Suggest ways to iterate and improve, and invite the artist to ask for changes.\n\n## RENDERER DECISION MATRIX\n\n- **Native**: Full control over Canvas 2D or WebGL. Supports `await import()` for external libraries like Three.js. Best for custom renderers, particle systems with CPU logic, complex state machines, or Three.js 3D scenes.\n- **P5**: Uses P5.js v1.9.4 with familiar `setup()`/`render()` pattern. Best for creative coding with shapes, colors, transforms, text. Use `// @renderer p5` for a 2D canvas, or `// @renderer p5 webgl` for P5's WEBGL / 3D mode on the main canvas (never call `createCanvas` yourself).\n- **Shader**: GLSL fragment shader on a GPU fullscreen quad. Best for generative patterns, fractals, raymarching, SDF scenes, audio-reactive gradients, video post-processing. Extremely fast: runs entirely on the GPU.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules, tables, and templates in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact API names, types, and uniform availability for the renderer you recommend.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (e.g., specific P5.js function syntax, advanced CV data structures, behavior nuances, or full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference (all auto-injected uniforms with types and descriptions): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 types (large file: only fetch when verifying P5 syntax): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- P5.js v1.9.4 reference (HTML): https://p5js.org/reference/\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## VIJI ARCHITECTURE (all renderers)\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is NO DOM access.\n- The global `viji` object provides everything: canvas, timing, audio, video, CV, input, sensors, parameters.\n- **Top-level code** runs once (parameters, state, imports).\n- **`render(viji)` / `render(viji, p5)` / `void main()`** runs every frame.\n- `fetch()` is available. `window`, `document`, `Image()` are NOT.\n\n## SCENE STRUCTURE PER RENDERER\n\n### Native\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx...\n}\n```\n\n### P5\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.circle(), p5.rect(), etc. (all need p5. prefix)\n}\n```\n\n### Shader (GLSL)\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## PARAMETERS (all renderers)\n\nArtists control scenes through parameters: declared once, shown as UI controls.\n\n**Native/P5 syntax** (top-level):\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // → .value: number\nviji.color(default, { label }) // → .value: '#rrggbb', .rgb: {r,g,b} 0..255, .hsb: {h:0..360,s/b:0..100}\nviji.toggle(default, { label }) // → .value: boolean\nviji.select(default, { options: [...], label }) // → .value: string|number\nviji.number(default, { min, max, step, label }) // → .value: number\nviji.text(default, { label, maxLength }) // → .value: string\nviji.image(null, { label }) // → .value: ImageBitmap|null\nviji.button({ label }) // → .value: boolean (1 frame)\nviji.coordinate(defaultValue, { step, label, description, group, category }) // → .value: { x, y } // both -1 to 1\n```\n\n**Shader syntax** (comment directives):\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 → uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 → uniform vec3 name;\n// @viji-toggle:name label:\"Toggle\" default:false → uniform bool name;\n// @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\"] → uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 → uniform float name;\n// @viji-image:name label:\"Texture\" → uniform sampler2D name;\n// @viji-button:name label:\"Reset\" → uniform bool name;\n// @viji-coordinate:name label:\"Label\" default:[0.0,0.0] → uniform vec2 name; // both -1 to 1\n// @viji-accumulator:name rate:speed → uniform float name;\n```\n\n## AUDIO: `viji.audio` / Shader uniforms\n\nALWAYS check `isConnected` / `u_audioVolume > 0.0` before using audio data.\n\nKey members (Native/P5):\n- `isConnected`\n- `volume.current`, `volume.peak`, `volume.smoothed` (smoothed = 200ms decay envelope of `current`)\n- `bands.low`, `bands.lowMid`, `bands.mid`, `bands.highMid`, `bands.high` (instant band energies, 0-1)\n- `bands.lowSmoothed`, `bands.lowMidSmoothed`, `bands.midSmoothed`, `bands.highMidSmoothed`, `bands.highSmoothed` (each is a 150ms decay envelope of the matching instant band, sibling property; not nested)\n- `beat.kick`, `beat.snare`, `beat.hat`, `beat.any` (instant energies, 0-1)\n- `beat.kickSmoothed`, `beat.snareSmoothed`, `beat.hatSmoothed`, `beat.anySmoothed` (500ms decay envelopes, sibling properties)\n- `beat.triggers.kick`, `beat.triggers.snare`, `beat.triggers.hat`, `beat.triggers.any` (boolean, true for one frame on the corresponding beat)\n- `beat.bpm`, `beat.confidence`, `beat.isLocked`\n- `spectral.brightness`, `spectral.flatness`\n- `getFrequencyData()` -> `Uint8Array`, `getWaveform()` -> `Float32Array`\n\nKey shader uniforms: `u_audioVolume`, `u_audioLow`-`u_audioHigh`, `u_audioKick`-`u_audioAny`, `u_audioKickTrigger`-`u_audioAnyTrigger`, `u_audioBPM`, `u_audioBrightness`, `u_audioFlatness`, `u_audioFFT`, `u_audioWaveform`. Smoothed scalar variants exist as `u_audioVolumeSmoothed`, `u_audioLowSmoothed`-`u_audioHighSmoothed`, `u_audioKickSmoothed`-`u_audioAnySmoothed`.\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` exposing the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` (and each `*Smoothed` sibling), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as above. **No** beat detection, BPM, triggers, or events: those exist only on the main `viji.audio`.\n\n## VIDEO & CV: `viji.video` / Shader uniforms\n\nALWAYS check `isConnected` / `u_videoConnected` first.\n\nKey members (Native/P5):\n- `isConnected`, `currentFrame`, `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`\n- CV toggle: `cv.enableFaceDetection(bool)`, `cv.enableFaceMesh(bool)`, `cv.enableEmotionDetection(bool)`, `cv.enableHandTracking(bool)`, `cv.enablePoseDetection(bool)`, `cv.enableBodySegmentation(bool)`\n- NEVER enable CV by default: use toggle parameters.\n- Data: `faces[]` (FaceData), `hands[]` (HandData), `pose` (PoseData|null), `segmentation` (SegmentationData|null)\n\nShader CV activation: use `// @viji-cv:<featureToken>` directives at the top of the shader. Bare form (`// @viji-cv:faceDetection`) is always-on, no UI parameter. Toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) synthesizes a host-side toggle parameter keyed by the feature token, plus a `bool <featureToken>` shader uniform mirroring the toggle. Six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\nKey shader uniforms: `u_video`, `u_videoResolution`, `u_videoConnected`, `u_faceCount`, `u_face0*`, `u_handCount`, `u_leftHand*`, `u_rightHand*`, `u_poseDetected`, `u_pose*Position`, `u_segmentationMask`.\n\n## INPUT\n\n**Pointer** (unified): `viji.pointer.x/y`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas` / Shader: `u_pointer`, `u_pointerDown`, `u_pointerWasPressed`\n**Mouse**: `viji.mouse.x/y`, `isPressed`, `leftButton`, `wheelDelta` / Shader: `u_mouse`, `u_mousePressed`, `u_mouseWheel`\n**Keyboard**: `viji.keyboard.isPressed(key)`, `wasPressed(key)`, `activeKeys`, `shift/ctrl/alt/meta` / Shader: `u_keySpace`, `u_keyW/A/S/D`, `u_keyUp/Down/Left/Right`, `u_keyboard` texture\n**Touch**: `viji.touches.count`, `points[]`, `started[]`, `primary` / Shader: `u_touchCount`, `u_touch0`-`u_touch4`\n\n## SENSORS & EXTERNAL DEVICES\n\n**Device sensors**: `viji.device.motion` (acceleration, rotationRate), `viji.device.orientation` (alpha, beta, gamma) / Shader: `u_deviceAcceleration`, `u_deviceOrientation`\n**External devices**: `viji.devices[]` (each entry: `id`, `name`, `motion`, `orientation`, `video`, `audio`) / Shader: `u_device0`-`u_device7` camera textures, plus per-device sensor and connection uniforms. Device audio is exposed as `u_audioStream{i}*` scalar uniforms (volume, bands, spectral; no beat/BPM/FFT/waveform: those are main audio only).\n\n## CRITICAL RULES (all renderers)\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`. `fetch()` IS available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` / `main()`.\n3. ALWAYS use `viji.width`/`viji.height` or `u_resolution`: NEVER hardcode pixel sizes.\n4. ALWAYS use `viji.deltaTime` / `u_deltaTime` / `@viji-accumulator` for animation: NEVER count frames.\n5. NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`), it causes animation jumps when the parameter changes. Use a `deltaTime` accumulator instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter, give each speed its own accumulator.\n6. NEVER allocate objects/arrays inside `render()`: pre-allocate at top level.\n7. ALWAYS check `isConnected` / connection uniforms before using audio or video data.\n8. NEVER enable CV features by default: use toggle parameters.\n9. ALWAYS set `category` on input-dependent parameters: `category: 'audio'` for audio controls, `category: 'video'` for video/CV controls, `category: 'interaction'` for mouse/keyboard/touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input.\n10. In P5: ALWAYS prefix every P5 function/constant with `p5.`. NEVER use `createCanvas()`.\n11. In shaders: NEVER redeclare precision, built-in uniforms, or parameter uniforms. NEVER use `u_` prefix for parameter names.\n\n## FOR ADVANCED FEATURES\n\nWhen the artist needs the full API surface (detailed CV data structures, all 52 face blendshapes, full external-device interfaces, backbuffer/feedback in shaders, host-supplied additional sources `viji.videoStreams[]` / `viji.audioStreams[]` used by Viji's compositor), use the renderer-specific prompts:\n- **Native**: Use the \"Prompt: Native Scenes\" page for the complete API reference\n- **P5**: Use the \"Prompt: P5 Scenes\" page for the complete API reference + P5 mapping\n- **Shader**: Use the \"Prompt: Shader Scenes\" page for all 270+ uniforms and directive details\n\nNow help the artist create their Viji scene. Start by asking what they want to build.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. Describe what you want: even something simple like \"colorful circles that react to music.\"\n4. The AI will guide you through choosing a renderer and building a scene.\n\n> [!TIP]\n> Don't worry about technical details: the AI will handle those. Focus on describing what you want to **see** and **feel**. Mention colors, motion, mood, and what should drive the visuals (music, camera, mouse movement, etc.).\n\n## Next Steps\n\nOnce you've created your first scene and want more control, use the full renderer-specific prompts:\n\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): exhaustive Native API prompt\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): exhaustive P5 API prompt\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt): exhaustive Shader API prompt\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n\n## Related\n\n- [Overview](/getting-started/overview): what Viji is and how it works\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
1315
+ "markdown": "# Create Your First Scene\n\nNew to Viji? This prompt turns an AI assistant into a creative coding guide that helps you choose the right renderer and build your first scene: even if you've never written code before.\n\n## How It Works\n\n1. Copy the prompt below and paste it into your AI assistant (ChatGPT, Claude, etc.).\n2. Describe what you want to create: as simple or detailed as you like.\n3. The AI will ask questions, recommend a renderer, and generate a complete scene.\n\n## Renderer Quick Comparison\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| **Language** | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js | GLSL (GPU fragment shader) |\n| **Best for** | Full control, Three.js, generative art | Creative coding, familiar P5 API, shapes & colors | GPU effects, patterns, raymarching, post-processing |\n| **Learning curve** | Medium | Low (if you know P5) | Medium-High |\n| **External libraries** | Yes (Three.js, etc.) | P5.js built-in | No |\n| **3D support** | Yes (WebGL, Three.js) | Yes (via `// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n\n## The Prompt\n\n````\nYou are a creative coding assistant for the Viji platform. Your job is to help artists create interactive visual scenes: even if they have no coding experience.\n\n## YOUR BEHAVIOR\n\n1. Ask the artist what they want to create. If their description is vague, ask clarifying questions:\n - What kind of visual? (patterns, shapes, particles, video effects, 3D, etc.)\n - Should it react to audio/music?\n - Should it use a camera/video?\n - Should it respond to mouse/touch/device tilt?\n - What mood or style? (abstract, organic, geometric, glitchy, minimal, etc.)\n2. Recommend a renderer based on what the artist wants to achieve. Coding experience is NOT a selection criterion: with your help, every renderer is accessible to artists at every level. Choose the renderer whose strengths best fit the goal:\n - **Per-pixel GPU effects** (raymarching, fractals, distance fields, full-screen post-processing, generative patterns that read every pixel) → recommend **Shader**.\n - **3D scenes with Three.js, custom WebGL or WebGPU, pixel-perfect Canvas 2D, or any external library via `await import()`** → recommend **Native**.\n - **Classic creative-coding shapes** (`p5.ellipse`, `p5.line`, `p5.bezier`), **color modes, transforms, particle systems with simple drawing**, or **an existing P5 sketch the artist already has** → recommend **P5**.\n - Audio reactivity, camera/CV reactivity, and parameters work in every renderer: pick based on the visual style, not on data sources.\n - You may briefly mention that Shader (GLSL) and Native + Three.js have a steeper conceptual learning curve, but never use this as the primary selection criterion. The artist asked for help; help them with the renderer that fits the goal.\n3. Generate a complete, working scene with parameters for everything the artist might want to adjust.\n4. Explain what the code does in simple terms.\n5. Suggest ways to iterate and improve, and invite the artist to ask for changes.\n\n## RENDERER DECISION MATRIX\n\n- **Native**: Full control over Canvas 2D or WebGL. Supports `await import()` for external libraries like Three.js. Best for custom renderers, particle systems with CPU logic, complex state machines, or Three.js 3D scenes.\n- **P5**: Uses P5.js v1.9.4 with familiar `setup()`/`render()` pattern. Best for creative coding with shapes, colors, transforms, text. Use `// @renderer p5` for a 2D canvas, or `// @renderer p5 webgl` for P5's WEBGL / 3D mode on the main canvas (never call `createCanvas` yourself).\n- **Shader**: GLSL fragment shader on a GPU fullscreen quad. Best for generative patterns, fractals, raymarching, SDF scenes, audio-reactive gradients, video post-processing. Extremely fast: runs entirely on the GPU.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules, tables, and templates in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact API names, types, and uniform availability for the renderer you recommend.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (e.g., specific P5.js function syntax, advanced CV data structures, behavior nuances, or full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference (all auto-injected uniforms with types and descriptions): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 types (large file: only fetch when verifying P5 syntax): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- P5.js v1.9.4 reference (HTML): https://p5js.org/reference/\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## VIJI ARCHITECTURE (all renderers)\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is NO DOM access.\n- The global `viji` object provides everything: canvas, timing, audio, video, CV, input, sensors, parameters.\n- **Top-level code** runs once (parameters, state, imports).\n- **`render(viji)` / `render(viji, p5)` / `void main()`** runs every frame.\n- `fetch()` is available. `window`, `document`, `Image()` are NOT.\n\n## SCENE STRUCTURE PER RENDERER\n\n### Native\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx...\n}\n```\n\n### P5\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.circle(), p5.rect(), etc. (all need p5. prefix)\n}\n```\n\n### Shader (GLSL)\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## PARAMETERS (all renderers)\n\nArtists control scenes through parameters: declared once, shown as UI controls.\n\n**Native/P5 syntax** (top-level):\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // → .value: number\nviji.color(default, { label, alpha }) // → .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb, .hsb, .alpha\nviji.gradient([{ color, position }], { label, minStops, maxStops, alpha }) // → .value: sorted stops, .at(t): hex at t // default required\nviji.toggle(default, { label }) // → .value: boolean\nviji.select(default, { options: [...], label }) // → .value: string|number\nviji.number(default, { min, max, step, label }) // → .value: number\nviji.text(default, { label, maxLength }) // → .value: string\nviji.image(null, { label }) // → .value: ImageBitmap|null\nviji.button({ label }) // → .value: boolean (1 frame)\nviji.coordinate(defaultValue, { step, label, description, group, category }) // → .value: { x, y } // both -1 to 1\n```\n\n**Shader syntax** (comment directives):\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 → uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 → uniform vec3 name; (alpha:true → vec4)\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] → vec3 name(float t) sampler // stops are #hex@position; alpha:true → vec4\n// @viji-toggle:name label:\"Toggle\" default:false → uniform bool name;\n// @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\"] → uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 → uniform float name;\n// @viji-image:name label:\"Texture\" → uniform sampler2D name;\n// @viji-button:name label:\"Reset\" → uniform bool name;\n// @viji-coordinate:name label:\"Label\" default:[0.0,0.0] → uniform vec2 name; // both -1 to 1\n// @viji-accumulator:name rate:speed → uniform float name;\n```\n\n## AUDIO: `viji.audio` / Shader uniforms\n\nALWAYS check `isConnected` / `u_audioVolume > 0.0` before using audio data.\n\nKey members (Native/P5):\n- `isConnected`\n- `volume.current`, `volume.peak`, `volume.smoothed` (smoothed = 200ms decay envelope of `current`)\n- `bands.low`, `bands.lowMid`, `bands.mid`, `bands.highMid`, `bands.high` (instant band energies, 0-1)\n- `bands.lowSmoothed`, `bands.lowMidSmoothed`, `bands.midSmoothed`, `bands.highMidSmoothed`, `bands.highSmoothed` (each is a 150ms decay envelope of the matching instant band, sibling property; not nested)\n- `beat.kick`, `beat.snare`, `beat.hat`, `beat.any` (instant energies, 0-1)\n- `beat.kickSmoothed`, `beat.snareSmoothed`, `beat.hatSmoothed`, `beat.anySmoothed` (500ms decay envelopes, sibling properties)\n- `beat.triggers.kick`, `beat.triggers.snare`, `beat.triggers.hat`, `beat.triggers.any` (boolean, true for one frame on the corresponding beat)\n- `beat.bpm`, `beat.confidence`, `beat.isLocked`\n- `spectral.brightness`, `spectral.flatness`\n- `getFrequencyData()` -> `Uint8Array`, `getWaveform()` -> `Float32Array`\n\nKey shader uniforms: `u_audioVolume`, `u_audioLow`-`u_audioHigh`, `u_audioKick`-`u_audioAny`, `u_audioKickTrigger`-`u_audioAnyTrigger`, `u_audioBPM`, `u_audioBrightness`, `u_audioFlatness`, `u_audioFFT`, `u_audioWaveform`. Smoothed scalar variants exist as `u_audioVolumeSmoothed`, `u_audioLowSmoothed`-`u_audioHighSmoothed`, `u_audioKickSmoothed`-`u_audioAnySmoothed`.\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` exposing the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` (and each `*Smoothed` sibling), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as above. **No** beat detection, BPM, triggers, or events: those exist only on the main `viji.audio`.\n\n## VIDEO & CV: `viji.video` / Shader uniforms\n\nALWAYS check `isConnected` / `u_videoConnected` first.\n\nKey members (Native/P5):\n- `isConnected`, `currentFrame`, `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`\n- CV toggle: `cv.enableFaceDetection(bool)`, `cv.enableFaceMesh(bool)`, `cv.enableEmotionDetection(bool)`, `cv.enableHandTracking(bool)`, `cv.enablePoseDetection(bool)`, `cv.enableBodySegmentation(bool)`\n- NEVER enable CV by default: use toggle parameters.\n- Data: `faces[]` (FaceData), `hands[]` (HandData), `pose` (PoseData|null), `segmentation` (SegmentationData|null)\n\nShader CV activation: use `// @viji-cv:<featureToken>` directives at the top of the shader. Bare form (`// @viji-cv:faceDetection`) is always-on, no UI parameter. Toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) synthesizes a host-side toggle parameter keyed by the feature token, plus a `bool <featureToken>` shader uniform mirroring the toggle. Six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\nKey shader uniforms: `u_video`, `u_videoResolution`, `u_videoConnected`, `u_faceCount`, `u_face0*`, `u_handCount`, `u_leftHand*`, `u_rightHand*`, `u_poseDetected`, `u_pose*Position`, `u_segmentationMask`.\n\n## INPUT\n\n**Pointer** (unified): `viji.pointer.x/y`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas` / Shader: `u_pointer`, `u_pointerDown`, `u_pointerWasPressed`\n**Mouse**: `viji.mouse.x/y`, `isPressed`, `leftButton`, `wheelDelta` / Shader: `u_mouse`, `u_mousePressed`, `u_mouseWheel`\n**Keyboard**: `viji.keyboard.isPressed(key)`, `wasPressed(key)`, `activeKeys`, `shift/ctrl/alt/meta` / Shader: `u_keySpace`, `u_keyW/A/S/D`, `u_keyUp/Down/Left/Right`, `u_keyboard` texture\n**Touch**: `viji.touches.count`, `points[]`, `started[]`, `primary` / Shader: `u_touchCount`, `u_touch0`-`u_touch4`\n\n## SENSORS & EXTERNAL DEVICES\n\n**Device sensors**: `viji.device.motion` (acceleration, rotationRate), `viji.device.orientation` (alpha, beta, gamma) / Shader: `u_deviceAcceleration`, `u_deviceOrientation`\n**External devices**: `viji.devices[]` (each entry: `id`, `name`, `motion`, `orientation`, `video`, `audio`) / Shader: `u_device0`-`u_device7` camera textures, plus per-device sensor and connection uniforms. Device audio is exposed as `u_audioStream{i}*` scalar uniforms (volume, bands, spectral; no beat/BPM/FFT/waveform: those are main audio only).\n\n## CRITICAL RULES (all renderers)\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`. `fetch()` IS available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` / `main()`.\n3. ALWAYS use `viji.width`/`viji.height` or `u_resolution`: NEVER hardcode pixel sizes.\n4. ALWAYS use `viji.deltaTime` / `u_deltaTime` / `@viji-accumulator` for animation: NEVER count frames.\n5. NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`), it causes animation jumps when the parameter changes. Use a `deltaTime` accumulator instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter, give each speed its own accumulator.\n6. NEVER allocate objects/arrays inside `render()`: pre-allocate at top level.\n7. ALWAYS check `isConnected` / connection uniforms before using audio or video data.\n8. NEVER enable CV features by default: use toggle parameters.\n9. ALWAYS set `category` on input-dependent parameters: `category: 'audio'` for audio controls, `category: 'video'` for video/CV controls, `category: 'interaction'` for mouse/keyboard/touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input.\n10. In P5: ALWAYS prefix every P5 function/constant with `p5.`. NEVER use `createCanvas()`.\n11. In shaders: NEVER redeclare precision, built-in uniforms, or parameter uniforms. NEVER use `u_` prefix for parameter names.\n\n## FOR ADVANCED FEATURES\n\nWhen the artist needs the full API surface (detailed CV data structures, all 52 face blendshapes, full external-device interfaces, backbuffer/feedback in shaders, host-supplied additional sources `viji.videoStreams[]` / `viji.audioStreams[]` used by Viji's compositor), use the renderer-specific prompts:\n- **Native**: Use the \"Prompt: Native Scenes\" page for the complete API reference\n- **P5**: Use the \"Prompt: P5 Scenes\" page for the complete API reference + P5 mapping\n- **Shader**: Use the \"Prompt: Shader Scenes\" page for all 270+ uniforms and directive details\n\nNow help the artist create their Viji scene. Start by asking what they want to build.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. Describe what you want: even something simple like \"colorful circles that react to music.\"\n4. The AI will guide you through choosing a renderer and building a scene.\n\n> [!TIP]\n> Don't worry about technical details: the AI will handle those. Focus on describing what you want to **see** and **feel**. Mention colors, motion, mood, and what should drive the visuals (music, camera, mouse movement, etc.).\n\n## Next Steps\n\nOnce you've created your first scene and want more control, use the full renderer-specific prompts:\n\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): exhaustive Native API prompt\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): exhaustive P5 API prompt\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt): exhaustive Shader API prompt\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n\n## Related\n\n- [Overview](/getting-started/overview): what Viji is and how it works\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
1301
1316
  }
1302
1317
  ]
1303
1318
  },
1304
1319
  "ai-prompt-native": {
1305
1320
  "id": "ai-prompt-native",
1306
- "title": "ai-prompt-native",
1321
+ "title": "\"Prompt: Native Scenes\"",
1307
1322
  "description": "A complete, self-contained AI prompt for generating Viji native scenes from scratch: covers the entire API surface.",
1308
1323
  "headings": [
1309
1324
  {
@@ -1325,7 +1340,7 @@ export const docsApi = {
1325
1340
  "content": [
1326
1341
  {
1327
1342
  "type": "text",
1328
- "markdown": "# Prompt: Native Scenes\r\n\r\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are generating a Viji native scene: a creative visual that runs inside an OffscreenCanvas Web Worker.\r\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\r\n\r\n## YOUR BEHAVIOR\r\n\r\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples of useful questions: \"Should this react to audio or stay purely visual?\", \"Should it use the camera or only mouse input?\", \"Roughly how dense / how minimal do you want it?\". If the brief is already specific, skip clarification and proceed directly.\r\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust (speed, density, colors, mode toggles).\r\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\r\n4. **Iterate.** Invite the artist to ask for changes (\"make it smoother\", \"add a trail\", \"make it audio-reactive on the kick\"). Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\r\n\r\n## REFERENCE (source of truth)\r\n\r\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\r\n\r\n**If you have web/file access:**\r\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact API names and types.\r\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, behavior nuances, full examples).\r\n\r\n**If you do NOT have web/file access:**\r\n- Use only the API surface explicitly named in this prompt.\r\n- Never invent property, method, or uniform names from memory.\r\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\r\n\r\n**Tier 1 (always consult when accessible):**\r\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\r\n\r\n**Tier 2 (consult when needed):**\r\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\r\n\r\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\r\n\r\n## ARCHITECTURE\r\n\r\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\r\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\r\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\r\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\r\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\r\n\r\n## RULES\r\n\r\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\r\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\r\n ```javascript\r\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\n function render(viji) { /* use speed.value */ }\r\n ```\r\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Use them instead of parsing hex by hand. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\r\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\r\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\r\n - `viji.time`: elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\r\n - `viji.deltaTime`: seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\r\n6. NEVER multiply `viji.time` by a parameter for animation speed: it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\r\n ```javascript\r\n // WRONG: jumps when speed changes:\r\n const t = viji.time * speed.value;\r\n // RIGHT: smooth:\r\n let phase = 0; // top level\r\n phase += speed.value * viji.deltaTime; // in render()\r\n ```\r\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\r\n ```javascript\r\n // WRONG: jumps when rotSpeed changes:\r\n const rot = phase * rotSpeed.value;\r\n // RIGHT: give it its own accumulator:\r\n let rotPhase = 0; // top level\r\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\r\n ```\r\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\r\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\r\n - `viji.useContext('2d')`: Canvas 2D\r\n - `viji.useContext('webgl')`: WebGL 1\r\n - `viji.useContext('webgl2')`: WebGL 2\r\n - `viji.useContext('webgpu')`: WebGPU\r\n Calling a different type after the first returns `null`.\r\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\r\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\r\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\r\n ```javascript\r\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\r\n // In render:\r\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\r\n else await viji.video.cv.enableFaceDetection(false);\r\n ```\r\n12. Be mindful of WebGL context limits: each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\r\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\r\n ```javascript\r\n // Right: creative-strength sliders with the matching category.\r\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\r\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\r\n\r\n // Wrong: scene-level on/off toggle for an input the host already gates.\r\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\r\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\r\n ```\r\n14. ALWAYS type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so errors can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\r\n ```javascript\r\n function render(viji: VijiAPI) { ... }\r\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\r\n ```\r\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/native/api-reference) lists every `viji` member with its exact type.\r\n15. For external libraries, use dynamic import with a pinned version:\r\n ```javascript\r\n const THREE = await import('https://esm.sh/three@0.160.0');\r\n ```\r\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\r\n\r\n## COMPLETE API REFERENCE\r\n\r\n### Canvas & Context\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\r\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\r\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\r\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\r\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Get WebGPU context (null when unsupported) |\r\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\r\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\r\n| `viji.gpu` | `GPUCanvasContext` | Shortcut (after useContext('webgpu')) |\r\n| `viji.width` | `number` | Current canvas width in pixels |\r\n| `viji.height` | `number` | Current canvas height in pixels |\r\n\r\n### Timing\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `viji.time` | `number` | Seconds since scene start |\r\n| `viji.deltaTime` | `number` | Seconds since last frame |\r\n| `viji.frameCount` | `number` | Total frames rendered |\r\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\r\n\r\n### Parameters\r\n\r\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\r\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\r\n\r\n```javascript\r\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\r\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\r\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\r\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\r\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\r\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\r\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\r\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\r\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\r\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\r\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\r\n```\r\n\r\n### Audio: `viji.audio`\r\n\r\nALWAYS check `viji.audio.isConnected` first.\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `isConnected` | `boolean` | Whether audio source is active |\r\n| `volume.current` | `number` | RMS volume 0-1 |\r\n| `volume.peak` | `number` | Peak amplitude 0-1 |\r\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\r\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\r\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\r\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\r\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\r\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\r\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\r\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\r\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\r\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\r\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\r\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\r\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\r\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\r\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\r\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\r\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\r\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\r\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\r\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\r\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\r\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\r\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\r\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\r\n\r\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\r\n\r\n### Video: `viji.video`\r\n\r\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `isConnected` | `boolean` | Whether video source is active |\r\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\r\n| `frameWidth` | `number` | Frame width in pixels |\r\n| `frameHeight` | `number` | Frame height in pixels |\r\n| `frameRate` | `number` | Video frame rate |\r\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\r\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\r\n\r\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results (`faces`, `hands`, `pose`, `segmentation`) live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\r\n\r\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds. Define this `videoFit` helper at module scope and use it for every video / CV scene:\r\n\r\n```javascript\r\nfunction videoFit(viji, mode = 'cover') {\r\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\r\n const w = viji.width, h = viji.height;\r\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\r\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\r\n const dw = vw * scale, dh = vh * scale;\r\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\r\n}\r\n\r\nconst v = videoFit(viji); // 'cover' (default) or 'contain'\r\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\r\n\r\n// CV coords are normalized 0-1 to the source video frame, not the canvas.\r\n// Map them through v to align with the fitted video:\r\nviji.video.cv.faces.forEach(face => {\r\n const bx = v.x + face.bounds.x * v.width;\r\n const by = v.y + face.bounds.y * v.height;\r\n const bw = face.bounds.width * v.width;\r\n const bh = face.bounds.height * v.height;\r\n ctx.strokeRect(bx, by, bw, bh);\r\n});\r\n```\r\n\r\nDefault to `'cover'` for live camera feeds (fills the canvas, edges cropped). Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible (full frame shown with letterbox bars). Stretching directly to `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\r\n\r\n**`currentFrame` vs `analysedFrame`.** Default to `currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\r\n\r\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\r\n\r\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need (4+ on low-end hardware risks context-limit failures).\r\n\r\n```javascript\r\n// Verbs return Promise<void>. await is optional. Safe to call from module\r\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\r\n// (opt-in CV). Idempotent + reference-counted.\r\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\r\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\r\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\r\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\r\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\r\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\r\nviji.video.cv.getActiveFeatures(); // CVFeature[]\r\nviji.video.cv.isProcessing(); // boolean\r\n```\r\n\r\nCV-paired outputs (also on `viji.video.cv`):\r\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\r\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\r\n\r\n**`viji.video.cv.faces: FaceData[]`**\r\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\r\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\r\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\r\n- `confidence: number | null` (0..1) — populated by face detection\r\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\r\n- `headPose: {pitch, yaw, roll} | null` (degrees) — populated by face mesh\r\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\r\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\r\n\r\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\r\n\r\n**`viji.video.cv.hands: HandData[]`**\r\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\r\n\r\n**`viji.video.cv.pose: PoseData | null`**\r\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\r\n\r\n**`viji.video.cv.segmentation: SegmentationData | null`**\r\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\r\n\r\n### Input: Pointer (unified mouse/touch): `viji.pointer`\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `x`, `y` | `number` | Position in pixels |\r\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\r\n| `isDown` | `boolean` | True if pressed/touching |\r\n| `wasPressed` | `boolean` | True on press frame |\r\n| `wasReleased` | `boolean` | True on release frame |\r\n| `isInCanvas` | `boolean` | True if inside canvas |\r\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\r\n\r\n### Input: Mouse: `viji.mouse`\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `x`, `y` | `number` | Position in pixels |\r\n| `isInCanvas` | `boolean` | Inside canvas bounds |\r\n| `isPressed` | `boolean` | Any button pressed |\r\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\r\n| `deltaX`, `deltaY` | `number` | Movement delta |\r\n| `wheelDelta` | `number` | Scroll wheel delta |\r\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\r\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\r\n\r\n### Input: Keyboard: `viji.keyboard`\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `isPressed(key)` | `boolean` | True while key is held |\r\n| `wasPressed(key)` | `boolean` | True on key-down frame |\r\n| `wasReleased(key)` | `boolean` | True on key-up frame |\r\n| `activeKeys` | `Set<string>` | Currently held keys |\r\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\r\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\r\n| `lastKeyPressed` | `string` | Most recent key-down |\r\n| `lastKeyReleased` | `string` | Most recent key-up |\r\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\r\n\r\n### Input: Touch: `viji.touches`\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `count` | `number` | Active touch count |\r\n| `points` | `TouchPoint[]` | All active touches |\r\n| `started` | `TouchPoint[]` | Touches started this frame |\r\n| `moved` | `TouchPoint[]` | Touches moved this frame |\r\n| `ended` | `TouchPoint[]` | Touches ended this frame |\r\n| `primary` | `TouchPoint\\|null` | First active touch |\r\n\r\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\r\n\r\n### Device Sensors: `viji.device`\r\n\r\n| Member | Type | Description |\r\n|--------|------|-------------|\r\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\r\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\r\n\r\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\r\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\r\n\r\n### External Devices: `viji.devices`\r\n\r\nArray of connected external devices. Each `DeviceState`:\r\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\r\n\r\n### Streams: `viji.videoStreams`\r\n\r\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\r\n\r\n### Streams: `viji.audioStreams`\r\n\r\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\r\n\r\n### External Libraries\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\r\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\r\n```\r\n\r\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\r\n\r\n## BEST PRACTICES\r\n\r\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\r\n2. Guard audio/video with `isConnected` checks.\r\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\r\n4. For CV, use toggle parameters: never enable by default.\r\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\r\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\r\n\r\n## TEMPLATE\r\n\r\n```javascript\r\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n angle += speed.value * viji.deltaTime;\r\n\r\n ctx.fillStyle = bgColor.value;\r\n ctx.fillRect(0, 0, viji.width, viji.height);\r\n\r\n const cx = viji.width / 2;\r\n const cy = viji.height / 2;\r\n const radius = Math.min(viji.width, viji.height) * 0.3;\r\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\r\n const n = Math.floor(count.value);\r\n\r\n for (let i = 0; i < n; i++) {\r\n const a = angle + (i / n) * Math.PI * 2;\r\n const x = cx + Math.cos(a) * radius;\r\n const y = cy + Math.sin(a) * radius;\r\n const hue = (i / n) * 360;\r\n ctx.beginPath();\r\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\r\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\r\n ctx.fill();\r\n }\r\n}\r\n```\r\n\r\nNow help the artist build a Viji native scene based on their description below.\r\n\r\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\r\n\r\nWhen you generate the scene:\r\n- Follow every rule in this prompt.\r\n- Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\r\n- Output the scene code in a single fenced code block.\r\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\r\n- Invite the artist to ask for changes.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\r\n3. After the prompt, describe the scene you want: be as specific as you like.\r\n4. The AI will return a complete Viji native scene.\r\n\r\n> [!TIP]\r\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\r\n\r\n## Related\r\n\r\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\r\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\r\n- [Native Quick Start](/native/quickstart): your first Viji native scene\r\n- [Native API Reference](/native/api-reference): full API reference\r\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\r\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
1343
+ "markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene: a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples of useful questions: \"Should this react to audio or stay purely visual?\", \"Should it use the camera or only mouse input?\", \"Roughly how dense / how minimal do you want it?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust (speed, density, colors, mode toggles).\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"make it smoother\", \"add a trail\", \"make it audio-reactive on the kick\"). Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact API names and types.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, behavior nuances, full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Use them instead of parsing hex by hand. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time`: elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\n - `viji.deltaTime`: seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER multiply `viji.time` by a parameter for animation speed: it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\n ```javascript\n // WRONG: jumps when speed changes:\n const t = viji.time * speed.value;\n // RIGHT: smooth:\n let phase = 0; // top level\n phase += speed.value * viji.deltaTime; // in render()\n ```\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\n ```javascript\n // WRONG: jumps when rotSpeed changes:\n const rot = phase * rotSpeed.value;\n // RIGHT: give it its own accumulator:\n let rotPhase = 0; // top level\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\n ```\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')`: Canvas 2D\n - `viji.useContext('webgl')`: WebGL 1\n - `viji.useContext('webgl2')`: WebGL 2\n - `viji.useContext('webgpu')`: WebGPU\n Calling a different type after the first returns `null`.\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n12. Be mindful of WebGL context limits: each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n14. ALWAYS type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so errors can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\n ```javascript\n function render(viji: VijiAPI) { ... }\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/native/api-reference) lists every `viji` member with its exact type.\n15. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Get WebGPU context (null when unsupported) |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.gpu` | `GPUCanvasContext` | Shortcut (after useContext('webgpu')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results (`faces`, `hands`, `pose`, `segmentation`) live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds. Define this `videoFit` helper at module scope and use it for every video / CV scene:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (default) or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0-1 to the source video frame, not the canvas.\n// Map them through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live camera feeds (fills the canvas, edges cropped). Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible (full frame shown with letterbox bars). Stretching directly to `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n\n**`currentFrame` vs `analysedFrame`.** Default to `currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need (4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\n- `confidence: number | null` (0..1) — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\n- `headPose: {pitch, yaw, roll} | null` (degrees) — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow help the artist build a Viji native scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want: be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Native Quick Start](/native/quickstart): your first Viji native scene\n- [Native API Reference](/native/api-reference): full API reference\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
1329
1344
  }
1330
1345
  ]
1331
1346
  },
@@ -1353,7 +1368,7 @@ export const docsApi = {
1353
1368
  "content": [
1354
1369
  {
1355
1370
  "type": "text",
1356
- "markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene: a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js v1.9.4.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D canvas or WEBGL / 3D mode?\", \"Should it use the camera or only mouse input?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes. Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or P5 function names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete docs (every Viji page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene's needs.\n2. ALWAYS use `render(viji, p5)`: not `draw()`. ALWAYS use `setup(viji, p5)`: not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255; matches `colorMode(RGB, 255)`) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100; matches `colorMode(HSB, 360, 100, 100)`); prefer those over parsing hex. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed, it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications, never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG: jumps: const t = viji.time * speed.value;\n // WRONG: nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only: `p5.drawingContext` is WebGL, not Canvas 2D. **Always preserve the source aspect ratio with the `videoFit` helper.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n ```javascript\n function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // 'cover' (default) or 'contain'\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n // CV coords (face.bounds, hand.landmarks, etc.) are normalized 0-1 to the\n // SOURCE video frame, not the canvas. Map through v: x = v.x + pt.x * v.width.\n ```\n Default to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. Stretching with `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)`: Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context: never use Canvas 2D-only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default: use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes: the canvas is managed by P5.\n32. ALWAYS type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/p5/api-reference) lists every `viji` member with its exact type.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5 using the `videoFit` helper (see Drawing & Canvas section above) to preserve source aspect: `const v = videoFit(viji); p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);`. Never `(0, 0, viji.width, viji.height)` unless distortion is intentional.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need.\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`, not on `viji.video` directly):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` | Remove: canvas is provided |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow help the artist build a Viji P5 scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5): convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart): your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference): full API reference\n- [Drawing with P5](/p5/drawing): Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/): full P5.js documentation"
1371
+ "markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene: a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js v1.9.4.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D canvas or WEBGL / 3D mode?\", \"Should it use the camera or only mouse input?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes. Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or P5 function names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete docs (every Viji page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene's needs.\n2. ALWAYS use `render(viji, p5)`: not `draw()`. ALWAYS use `setup(viji, p5)`: not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255; matches `colorMode(RGB, 255)`) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100; matches `colorMode(HSB, 360, 100, 100)`); prefer those over parsing hex. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed, it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications, never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG: jumps: const t = viji.time * speed.value;\n // WRONG: nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only: `p5.drawingContext` is WebGL, not Canvas 2D. **Always preserve the source aspect ratio with the `videoFit` helper.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n ```javascript\n function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // 'cover' (default) or 'contain'\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n // CV coords (face.bounds, hand.landmarks, etc.) are normalized 0-1 to the\n // SOURCE video frame, not the canvas. Map through v: x = v.x + pt.x * v.width.\n ```\n Default to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. Stretching with `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)`: Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context: never use Canvas 2D-only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default: use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes: the canvas is managed by P5.\n32. ALWAYS type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/p5/api-reference) lists every `viji` member with its exact type.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5 using the `videoFit` helper (see Drawing & Canvas section above) to preserve source aspect: `const v = videoFit(viji); p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);`. Never `(0, 0, viji.width, viji.height)` unless distortion is intentional.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need.\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`, not on `viji.video` directly):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` | Remove: canvas is provided |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow help the artist build a Viji P5 scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5): convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart): your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference): full API reference\n- [Drawing with P5](/p5/drawing): Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/): full P5.js documentation"
1357
1372
  }
1358
1373
  ]
1359
1374
  },
@@ -1381,7 +1396,7 @@ export const docsApi = {
1381
1396
  "content": [
1382
1397
  {
1383
1398
  "type": "text",
1384
- "markdown": "# Prompt: Shader Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the shader effect you want. The prompt gives the AI everything it needs about Viji's shader renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji GLSL shader scene: a fragment shader that runs on a fullscreen quad inside a Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working GLSL code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D pattern or raymarched 3D?\", \"Hard geometric or soft organic feel?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable shader that follows every rule in this prompt. Include `@viji-*` parameter directives for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the shader works, which uniforms and parameters it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"more chaotic\", \"warmer palette\", \"make the kick punch harder\"). Treat each follow-up as a refinement: keep the working shader as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV uniforms, behavior nuances, full shader examples).\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt.\n- Never invent uniform names or directive names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Viji renders a **fullscreen quad**. Your shader defines the color of every pixel.\n- Viji **auto-injects** `precision mediump float;` and ALL uniform declarations: both built-in uniforms and parameter uniforms from `@viji-*` directives.\n- You write only helper functions and `void main() { ... }`.\n- **GLSL ES 1.00** by default. Add `#version 300 es` as the very first line for ES 3.00.\n- ES 3.00 requires `out vec4 fragColor;` (before `main`) and `fragColor = ...` instead of `gl_FragColor`.\n- ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader uses `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the first line (or after `#version 300 es` if using ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. NEVER use the `u_` prefix for your own parameter names: it is reserved for built-in uniforms. Name parameters descriptively: `speed`, `colorMix`, `intensity`.\n6. `@viji-*` parameter directives ONLY work with `//` comments. NEVER use `/* */` for directives.\n7. ALWAYS use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation: this prevents jumps when sliders change:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase); // smooth, no jumps\n ```\n The same applies to **nested** multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators:\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame), just reference it in code: Viji auto-detects and enables it.\n9. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n10. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls. This lets the host UI hide irrelevant controls when that input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level \"Audio Reactive\" / \"Show Video\" toggle just duplicates the host switch.\n ```glsl\n // Right: creative-strength sliders with the matching category.\n // @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n // @viji-slider:videoMix label:\"Video Mix\" default:0.6 min:0 max:1 group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 min:0 max:1 group:interaction category:interaction\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true category:audio\n // // @viji-toggle:showVideo label:\"Show Video\" default:true category:video\n ```\n\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Computer vision uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero unless the matching feature is activated. Use bare form for always-on or toggleable form to expose a host-side toggle.\n ```glsl\n // Bare: pipeline runs whenever a video stream is connected. No UI parameter.\n // @viji-cv:faceDetection\n // @viji-cv:handTracking\n\n // Toggleable: synthesizes a host-side toggle parameter and a `bool <featureToken>`\n // shader uniform. Slot becomes both the parameter key and the GLSL uniform name.\n // @viji-cv:faceDetection label:\"Face Detection\" default:false group:cv category:video\n ```\n The six valid tokens (JS-API parity): `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are always available: do NOT declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `u_audioSnare` | `float` | Snare energy curve 0-1, 300ms decay |\n| `u_audioHat` | `float` | Hi-hat energy curve 0-1, 300ms decay |\n| `u_audioAny` | `float` | Any-beat energy curve 0-1, 300ms decay |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `u_audioKickTrigger` | `bool` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `u_audioSnareTrigger` | `bool` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `u_audioHatTrigger` | `bool` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `u_audioAnyTrigger` | `bool` | True for exactly one frame on any beat, then auto-resets |\n| `u_audioBPM` | `float` | `0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0-1 |\n| `u_audioIsLocked` | `bool` | True on stable tempo lock |\n| `u_audioBrightness` | `float` | Spectral brightness 0-1 |\n| `u_audioFlatness` | `float` | Spectral flatness 0-1 |\n\n### Audio: Textures\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioFFT` | `sampler2D` | FFT frequency spectrum (1024 bins, 0-255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (2048 samples, -1 to 1) |\n\n**Note:** `u_audioFFT` / `u_audioWaveform` apply only to the main audio source. Additional streams (host `audioStreams` and device audio) use `u_audioStream{i}*` float/bool uniforms only: see \"Streams (Compositor)\" → \"Audio streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Just-arrived video frame texture |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with the CV uniforms (`u_face*`, `u_hand*`, `u_pose*`, `u_segmentationMask`) |\n| `u_videoAnalysedAvailable` | `bool` | True after the first CV result lands |\n| `u_videoResolution` | `vec2` | Video frame size in pixels (shared between `u_video` and `u_videoAnalysed`) |\n| `u_videoFrameRate` | `float` | Video frame rate |\n| `u_videoConnected` | `bool` | True if video source is active |\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with the canvas's normalized UV stretches the video and misaligns CV uniforms. Define this `vijiVideoUV` helper at the top of the shader and use it for every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, crops video edges)\n// 0 = contain (fits video, letterboxes canvas)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// Pair with vijiVideoUV: true when videoUV is inside the fitted region.\n// Use to paint a flat bar color in contain mode's letterbox area instead of\n// sampling u_video at out-of-range UVs (which returns the CLAMP_TO_EDGE\n// edge column stretched).\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // 1 = cover (default), 0 = contain\n vec4 video = texture2D(u_video, videoUV);\n // Bar color vec3(17.0/255.0) matches the native/p5 #111 canvas background.\n vec3 col = vijiInVideo(videoUV) ? video.rgb : vec3(17.0 / 255.0);\n // CV uniforms (u_face0Center, etc.) share the same bottom-up coordinate\n // convention as videoUV — compare them against videoUV directly, no flip:\n float faceDist = length(videoUV - u_face0Center);\n // ...\n}\n```\n\nDefault to mode `1` (cover) for live camera shaders. Use mode `0` (contain) for CV-overlay shaders where features near frame edges must stay visible. Sampling `texture2D(u_video, uv)` directly is allowed only when distortion is intentional.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when the shader reads pixels from the displayed frame at CV-derived positions (compositing `u_segmentationMask` onto the body, sampling skin under face landmarks, warping the face along its mesh, texture-mapped face filters). For shaders that consume CV uniforms without sampling at CV positions (head-pose-driven color shifts, generative geometry from `u_face*` / `u_hand*` / `u_pose*`), keep `u_video`: `u_videoAnalysed` only advances when MediaPipe completes an inference, so the displayed video stutters or holds between inferences.\n\nWhen `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` to fall back during the brief startup window before the first CV result lands:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration with gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | Orientation (alpha, beta, gamma) degrees |\n| `u_deviceOrientationAbsolute` | `bool` | True if using magnetometer |\n\n### External Devices\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceCount` | `int` | Number of device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | Number of external devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Device camera resolutions |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Device connection status |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration w/ gravity |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n**Note:** device audio (when an external device provides an audio source) is exposed as `u_audioStream{i}*` scalar uniforms: same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0-7). There are NO per-device or per-stream FFT/waveform textures; only the main audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0-1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0-1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0-1 |\n\n(`i` = 0…7.) **Lightweight scalars only**: **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0-1)\n// `default:` accepts: #rrggbb, #rgb, vec3(r,g,b) in 0..1, rgb(r,g,b) in 0..255,\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:invert label:\"Invert\" default:false\n// → uniform bool invert;\n\n// @viji-select:mode label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index)\n\n// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// → uniform float count;\n\n// @viji-image:tex label:\"Texture\"\n// → uniform sampler2D tex;\n\n// @viji-button:reset label:\"Reset\"\n// → uniform bool reset; (true for one frame on press)\n\n// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]\n// → uniform vec2 origin; (default uses [x,y] array; both components -1 to 1)\n\n// @viji-accumulator:phase rate:speed\n// → uniform float phase; (CPU-side: += speed × deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow help the artist build a Viji shader scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the shader:\n- Follow every rule in this prompt.\n- Use `// @renderer shader` as the first line. Do NOT declare precision or uniforms. Use `@viji-accumulator` for parameter-driven animation. Use `@viji-slider/color/toggle` for artist controls.\n- Output the GLSL code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the shader works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the shader effect you want.\n4. The AI will return a complete Viji shader scene.\n\n> [!TIP]\n> For better results, describe the visual effect you want (patterns, colors, motion), mention data sources (audio, video, mouse), and what controls the user should have. If you have existing Shadertoy shaders to convert, use the [Convert: Shadertoy](/ai-prompts/convert-shadertoy) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: Shadertoy](/ai-prompts/convert-shadertoy): convert existing Shadertoy shaders to Viji\n- [Shader Quick Start](/shader/quickstart): your first Viji shader\n- [Shader API Reference](/shader/api-reference): full uniform reference\n- [Backbuffer & Feedback](/shader/backbuffer): previous-frame feedback effects\n- [Shadertoy Compatibility](/shader/shadertoy): compatibility layer for Shadertoy code"
1399
+ "markdown": "# Prompt: Shader Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the shader effect you want. The prompt gives the AI everything it needs about Viji's shader renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji GLSL shader scene: a fragment shader that runs on a fullscreen quad inside a Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working GLSL code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D pattern or raymarched 3D?\", \"Hard geometric or soft organic feel?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable shader that follows every rule in this prompt. Include `@viji-*` parameter directives for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the shader works, which uniforms and parameters it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"more chaotic\", \"warmer palette\", \"make the kick punch harder\"). Treat each follow-up as a refinement: keep the working shader as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV uniforms, behavior nuances, full shader examples).\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt.\n- Never invent uniform names or directive names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Viji renders a **fullscreen quad**. Your shader defines the color of every pixel.\n- Viji **auto-injects** `precision mediump float;` and ALL uniform declarations: both built-in uniforms and parameter uniforms from `@viji-*` directives.\n- You write only helper functions and `void main() { ... }`.\n- **GLSL ES 1.00** by default. Add `#version 300 es` as the very first line for ES 3.00.\n- ES 3.00 requires `out vec4 fragColor;` (before `main`) and `fragColor = ...` instead of `gl_FragColor`.\n- ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader uses `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the first line (or after `#version 300 es` if using ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. NEVER use the `u_` prefix for your own parameter names: it is reserved for built-in uniforms. Name parameters descriptively: `speed`, `colorMix`, `intensity`.\n6. `@viji-*` parameter directives ONLY work with `//` comments. NEVER use `/* */` for directives.\n7. ALWAYS use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation: this prevents jumps when sliders change:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase); // smooth, no jumps\n ```\n The same applies to **nested** multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators:\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame), just reference it in code: Viji auto-detects and enables it.\n9. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n10. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls. This lets the host UI hide irrelevant controls when that input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level \"Audio Reactive\" / \"Show Video\" toggle just duplicates the host switch.\n ```glsl\n // Right: creative-strength sliders with the matching category.\n // @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n // @viji-slider:videoMix label:\"Video Mix\" default:0.6 min:0 max:1 group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 min:0 max:1 group:interaction category:interaction\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true category:audio\n // // @viji-toggle:showVideo label:\"Show Video\" default:true category:video\n ```\n\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Computer vision uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero unless the matching feature is activated. Use bare form for always-on or toggleable form to expose a host-side toggle.\n ```glsl\n // Bare: pipeline runs whenever a video stream is connected. No UI parameter.\n // @viji-cv:faceDetection\n // @viji-cv:handTracking\n\n // Toggleable: synthesizes a host-side toggle parameter and a `bool <featureToken>`\n // shader uniform. Slot becomes both the parameter key and the GLSL uniform name.\n // @viji-cv:faceDetection label:\"Face Detection\" default:false group:cv category:video\n ```\n The six valid tokens (JS-API parity): `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are always available: do NOT declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `u_audioSnare` | `float` | Snare energy curve 0-1, 300ms decay |\n| `u_audioHat` | `float` | Hi-hat energy curve 0-1, 300ms decay |\n| `u_audioAny` | `float` | Any-beat energy curve 0-1, 300ms decay |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `u_audioKickTrigger` | `bool` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `u_audioSnareTrigger` | `bool` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `u_audioHatTrigger` | `bool` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `u_audioAnyTrigger` | `bool` | True for exactly one frame on any beat, then auto-resets |\n| `u_audioBPM` | `float` | `0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0-1 |\n| `u_audioIsLocked` | `bool` | True on stable tempo lock |\n| `u_audioBrightness` | `float` | Spectral brightness 0-1 |\n| `u_audioFlatness` | `float` | Spectral flatness 0-1 |\n\n### Audio: Textures\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioFFT` | `sampler2D` | FFT frequency spectrum (1024 bins, 0-255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (2048 samples, -1 to 1) |\n\n**Note:** `u_audioFFT` / `u_audioWaveform` apply only to the main audio source. Additional streams (host `audioStreams` and device audio) use `u_audioStream{i}*` float/bool uniforms only: see \"Streams (Compositor)\" → \"Audio streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Just-arrived video frame texture |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with the CV uniforms (`u_face*`, `u_hand*`, `u_pose*`, `u_segmentationMask`) |\n| `u_videoAnalysedAvailable` | `bool` | True after the first CV result lands |\n| `u_videoResolution` | `vec2` | Video frame size in pixels (shared between `u_video` and `u_videoAnalysed`) |\n| `u_videoFrameRate` | `float` | Video frame rate |\n| `u_videoConnected` | `bool` | True if video source is active |\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with the canvas's normalized UV stretches the video and misaligns CV uniforms. Define this `vijiVideoUV` helper at the top of the shader and use it for every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, crops video edges)\n// 0 = contain (fits video, letterboxes canvas)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// Pair with vijiVideoUV: true when videoUV is inside the fitted region.\n// Use to paint a flat bar color in contain mode's letterbox area instead of\n// sampling u_video at out-of-range UVs (which returns the CLAMP_TO_EDGE\n// edge column stretched).\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // 1 = cover (default), 0 = contain\n vec4 video = texture2D(u_video, videoUV);\n // Bar color vec3(17.0/255.0) matches the native/p5 #111 canvas background.\n vec3 col = vijiInVideo(videoUV) ? video.rgb : vec3(17.0 / 255.0);\n // CV uniforms (u_face0Center, etc.) share the same bottom-up coordinate\n // convention as videoUV — compare them against videoUV directly, no flip:\n float faceDist = length(videoUV - u_face0Center);\n // ...\n}\n```\n\nDefault to mode `1` (cover) for live camera shaders. Use mode `0` (contain) for CV-overlay shaders where features near frame edges must stay visible. Sampling `texture2D(u_video, uv)` directly is allowed only when distortion is intentional.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when the shader reads pixels from the displayed frame at CV-derived positions (compositing `u_segmentationMask` onto the body, sampling skin under face landmarks, warping the face along its mesh, texture-mapped face filters). For shaders that consume CV uniforms without sampling at CV positions (head-pose-driven color shifts, generative geometry from `u_face*` / `u_hand*` / `u_pose*`), keep `u_video`: `u_videoAnalysed` only advances when MediaPipe completes an inference, so the displayed video stutters or holds between inferences.\n\nWhen `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` to fall back during the brief startup window before the first CV result lands:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration with gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | Orientation (alpha, beta, gamma) degrees |\n| `u_deviceOrientationAbsolute` | `bool` | True if using magnetometer |\n\n### External Devices\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceCount` | `int` | Number of device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | Number of external devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Device camera resolutions |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Device connection status |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration w/ gravity |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n**Note:** device audio (when an external device provides an audio source) is exposed as `u_audioStream{i}*` scalar uniforms: same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0-7). There are NO per-device or per-stream FFT/waveform textures; only the main audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0-1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0-1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0-1 |\n\n(`i` = 0…7.) **Lightweight scalars only**: **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// (alpha:true -> uniform vec4 tint; an 8-digit default without alpha:true is an error)\n// @viji-gradient:ramp label:\"Ramp\" default:[#ff0000@0,#0000ff@1] minStops:2 maxStops:8\n// -> vec3 ramp(float t) sampler (vec4 with alpha:true) over uniform arrays ramp_colors[N], ramp_positions[N], ramp_count\n// stops are #hex@position (hex only), default required; call ramp(t) in main(), never declare the uniforms\n// → uniform vec3 tint; (RGB 0-1)\n// `default:` accepts: #rrggbb, #rgb, vec3(r,g,b) in 0..1, rgb(r,g,b) in 0..255,\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:invert label:\"Invert\" default:false\n// → uniform bool invert;\n\n// @viji-select:mode label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index)\n\n// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// → uniform float count;\n\n// @viji-image:tex label:\"Texture\"\n// → uniform sampler2D tex;\n\n// @viji-button:reset label:\"Reset\"\n// → uniform bool reset; (true for one frame on press)\n\n// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]\n// → uniform vec2 origin; (default uses [x,y] array; both components -1 to 1)\n\n// @viji-accumulator:phase rate:speed\n// → uniform float phase; (CPU-side: += speed × deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow help the artist build a Viji shader scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the shader:\n- Follow every rule in this prompt.\n- Use `// @renderer shader` as the first line. Do NOT declare precision or uniforms. Use `@viji-accumulator` for parameter-driven animation. Use `@viji-slider/color/toggle` for artist controls.\n- Output the GLSL code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the shader works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the shader effect you want.\n4. The AI will return a complete Viji shader scene.\n\n> [!TIP]\n> For better results, describe the visual effect you want (patterns, colors, motion), mention data sources (audio, video, mouse), and what controls the user should have. If you have existing Shadertoy shaders to convert, use the [Convert: Shadertoy](/ai-prompts/convert-shadertoy) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: Shadertoy](/ai-prompts/convert-shadertoy): convert existing Shadertoy shaders to Viji\n- [Shader Quick Start](/shader/quickstart): your first Viji shader\n- [Shader API Reference](/shader/api-reference): full uniform reference\n- [Backbuffer & Feedback](/shader/backbuffer): previous-frame feedback effects\n- [Shadertoy Compatibility](/shader/shadertoy): compatibility layer for Shadertoy code"
1385
1400
  }
1386
1401
  ]
1387
1402
  },
@@ -1487,7 +1502,7 @@ export const docsApi = {
1487
1502
  "content": [
1488
1503
  {
1489
1504
  "type": "text",
1490
- "markdown": "# Convert: P5 Sketches to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the P5.js sketch you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji-P5 scene.\n\n## The Prompt\n\n````\nYou are converting a standard P5.js sketch into a Viji-P5 scene.\nViji scenes run inside an OffscreenCanvas Web Worker using P5.js v1.9.4. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source sketch is incomplete (missing `setup` or `draw`), uses libraries you cannot identify (`p5.sound`, custom `loadX` calls, third-party add-ons), or relies on `index.html` HTML elements, ask the artist for the missing pieces or for permission to drop them before generating code. If the sketch is self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji-P5 scene that follows every rule in this prompt. Preserve the artist's visual intent and parameter ranges; replace only the Viji-incompatible parts.\n3. **Explain.** After the code block, give a short summary of the key changes you made (renamed `draw` to `render`, added `p5.` prefix, replaced `mouseX` with `viji.pointer.x`, removed `loadImage`, etc.). Flag any features you had to drop or simplify because they are incompatible with the Viji worker environment.\n4. **Iterate.** Invite the artist to ask for refinements (\"the colors look off\", \"the animation is too fast\", \"I want a slider for the speed\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common conversion mappings, but they do NOT cover the full Viji or P5 API surface. If anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax for any call this prompt does not list.\n- ON DEMAND: fetch from Tier-2 resources when the source sketch uses a Viji-side feature this prompt does not map (advanced CV data, device sensors, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard P5.js v1.x API for direct ports.\n- Never invent Viji property, method, or P5 function names from memory.\n- If the source sketch uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- Companion prompt for any Viji feature this conversion prompt does not cover: https://unpkg.com/@viji-dev/core/dist/docs-api.js (search for \"p5-prompt\")\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS set the first line from the sketch's canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or 3D primitives on the main canvas. NEVER keep `createCanvas()`: Viji creates the canvas.\n2. ALWAYS rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it doesn't exist, do NOT add one.\n4. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`\n This applies to ALL P5 functions and constants without exception.\n5. NEVER call `createCanvas()`. The canvas is created and managed by Viji. WEBGL is selected only with `// @renderer p5 webgl`, not with `createCanvas(..., p5.WEBGL)`.\n6. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in an async `setup()` for data.\n7. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Instead, check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` (works for both mouse and touch) or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` (works for both mouse and touch) or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: use `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. These are host-level concerns.\n9. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n10. NEVER use `createCapture()` or `createVideo()`. Use `viji.video.*` instead.\n11. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n12. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n13. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\n ```javascript\n // CORRECT\n const size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\n function render(viji, p5) { p5.circle(0, 0, size.value); }\n\n // WRONG: creates a new parameter every frame\n function render(viji, p5) { const size = viji.slider(50, { ... }); }\n ```\n14. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100) for direct use with `colorMode(RGB, 255)` / `colorMode(HSB, 360, 100, 100)`.\n15. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n16. ALWAYS use `viji.deltaTime` for frame-rate-independent animation. Replace `frameCount * 0.01` patterns with a deltaTime accumulator:\n ```javascript\n let angle = 0;\n function render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n }\n ```\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes jumps when the parameter changes. Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n19. For image parameters displayed with P5, use `photo.p5` (not `photo.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n20. ALWAYS type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## API MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` / `createCanvas(w, h, WEBGL)` | Remove: use `// @renderer p5` or `// @renderer p5 webgl` |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host uses `captureFrame()` |\n\nThe mapping above covers the most common direct ports. The complete Viji API surface is below: use it for any feature the source sketch reaches for that this table does not list (audio analysis, video frames, CV data, touch, device sensors, etc.).\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical to the Native renderer (same object, same types). Access it inside `setup(viji, p5)` and `render(viji, p5)`.\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n`viji.useContext()` is NOT available in P5: the canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\nCommon P5 sound conversions: `new p5.AudioIn() ... mic.getLevel()` → `viji.audio.volume.current`; `fft.analyze()` → `viji.audio.getFrequencyData()`; `p5.Amplitude` → `viji.audio.volume.smoothed`.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\nReplace `createCapture(VIDEO)` with `viji.video.currentFrame`. **Always preserve the source aspect ratio with `videoFit`.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (live cameras, default) or 'contain' (CV overlays)\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0-1 to the source frame; map through v:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n // ...\n});\n```\n\nUse `p5.image(...)` in both 2D and WEBGL P5 modes, or `p5.drawingContext.drawImage(...)` in 2D only.\n\nDefault to `viji.video.currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\nP5 `touchStarted()` / `touchMoved()` / `touchEnded()` callbacks do not fire. Read `viji.touches.started` / `.moved` / `.ended` inside `render()` instead.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## P5-SPECIFIC GOTCHAS\n\nThese behaviors are different from running P5 in a browser tab:\n\n- **Fonts:** `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is NOT available.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes (`// @renderer p5 webgl`) it is a WebGL context: never use Canvas-2D-only APIs on it; use P5 3D drawing or `p5.image()` for textures and video.\n- **`viji.useContext()`** is NOT available in P5: the canvas and 2D context are managed by P5 internally.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(angle * 30 % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nNow convert the P5.js sketch I provide.\n\nIf the sketch is incomplete or uses features that are incompatible with the Viji worker environment, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule and mapping above. The **API MAPPING** table covers the most common direct ports; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any feature the source sketch reaches for that the mapping table does not list (audio analysis, video frames, CV data, touch, device sensors). For any P5 call you are unsure about, consult the p5.js v1.x reference linked in **REFERENCE**. The canonical companion generation prompt is `p5-prompt` (in the `docs-api.js` bundle).\n- Output the Viji-P5 scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify.\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the P5.js sketch you want to convert.\n4. The AI will return a Viji-compatible scene.\n\nFor a detailed human-readable guide, see [Converting P5 Sketches](/p5/converting-sketches#step-by-step).\n\n## Related\n\n- [Converting P5 Sketches](/p5/converting-sketches#step-by-step): step-by-step manual conversion guide\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): AI prompt for creating new P5 scenes from scratch\n- [P5 Quick Start](/p5/quickstart): your first Viji-P5 scene"
1505
+ "markdown": "# Convert: P5 Sketches to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the P5.js sketch you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji-P5 scene.\n\n## The Prompt\n\n````\nYou are converting a standard P5.js sketch into a Viji-P5 scene.\nViji scenes run inside an OffscreenCanvas Web Worker using P5.js v1.9.4. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source sketch is incomplete (missing `setup` or `draw`), uses libraries you cannot identify (`p5.sound`, custom `loadX` calls, third-party add-ons), or relies on `index.html` HTML elements, ask the artist for the missing pieces or for permission to drop them before generating code. If the sketch is self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji-P5 scene that follows every rule in this prompt. Preserve the artist's visual intent and parameter ranges; replace only the Viji-incompatible parts.\n3. **Explain.** After the code block, give a short summary of the key changes you made (renamed `draw` to `render`, added `p5.` prefix, replaced `mouseX` with `viji.pointer.x`, removed `loadImage`, etc.). Flag any features you had to drop or simplify because they are incompatible with the Viji worker environment.\n4. **Iterate.** Invite the artist to ask for refinements (\"the colors look off\", \"the animation is too fast\", \"I want a slider for the speed\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common conversion mappings, but they do NOT cover the full Viji or P5 API surface. If anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax for any call this prompt does not list.\n- ON DEMAND: fetch from Tier-2 resources when the source sketch uses a Viji-side feature this prompt does not map (advanced CV data, device sensors, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard P5.js v1.x API for direct ports.\n- Never invent Viji property, method, or P5 function names from memory.\n- If the source sketch uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- Companion prompt for any Viji feature this conversion prompt does not cover: https://unpkg.com/@viji-dev/core/dist/docs-api.js (search for \"p5-prompt\")\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS set the first line from the sketch's canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or 3D primitives on the main canvas. NEVER keep `createCanvas()`: Viji creates the canvas.\n2. ALWAYS rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it doesn't exist, do NOT add one.\n4. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`\n This applies to ALL P5 functions and constants without exception.\n5. NEVER call `createCanvas()`. The canvas is created and managed by Viji. WEBGL is selected only with `// @renderer p5 webgl`, not with `createCanvas(..., p5.WEBGL)`.\n6. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in an async `setup()` for data.\n7. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Instead, check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` (works for both mouse and touch) or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` (works for both mouse and touch) or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: use `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. These are host-level concerns.\n9. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n10. NEVER use `createCapture()` or `createVideo()`. Use `viji.video.*` instead.\n11. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n12. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n13. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\n ```javascript\n // CORRECT\n const size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\n function render(viji, p5) { p5.circle(0, 0, size.value); }\n\n // WRONG: creates a new parameter every frame\n function render(viji, p5) { const size = viji.slider(50, { ... }); }\n ```\n14. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100) for direct use with `colorMode(RGB, 255)` / `colorMode(HSB, 360, 100, 100)`.\n15. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n16. ALWAYS use `viji.deltaTime` for frame-rate-independent animation. Replace `frameCount * 0.01` patterns with a deltaTime accumulator:\n ```javascript\n let angle = 0;\n function render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n }\n ```\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes jumps when the parameter changes. Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n19. For image parameters displayed with P5, use `photo.p5` (not `photo.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n20. ALWAYS type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## API MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` / `createCanvas(w, h, WEBGL)` | Remove: use `// @renderer p5` or `// @renderer p5 webgl` |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host uses `captureFrame()` |\n\nThe mapping above covers the most common direct ports. The complete Viji API surface is below: use it for any feature the source sketch reaches for that this table does not list (audio analysis, video frames, CV data, touch, device sensors, etc.).\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical to the Native renderer (same object, same types). Access it inside `setup(viji, p5)` and `render(viji, p5)`.\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n`viji.useContext()` is NOT available in P5: the canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\nCommon P5 sound conversions: `new p5.AudioIn() ... mic.getLevel()` → `viji.audio.volume.current`; `fft.analyze()` → `viji.audio.getFrequencyData()`; `p5.Amplitude` → `viji.audio.volume.smoothed`.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\nReplace `createCapture(VIDEO)` with `viji.video.currentFrame`. **Always preserve the source aspect ratio with `videoFit`.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (live cameras, default) or 'contain' (CV overlays)\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0-1 to the source frame; map through v:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n // ...\n});\n```\n\nUse `p5.image(...)` in both 2D and WEBGL P5 modes, or `p5.drawingContext.drawImage(...)` in 2D only.\n\nDefault to `viji.video.currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\nP5 `touchStarted()` / `touchMoved()` / `touchEnded()` callbacks do not fire. Read `viji.touches.started` / `.moved` / `.ended` inside `render()` instead.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## P5-SPECIFIC GOTCHAS\n\nThese behaviors are different from running P5 in a browser tab:\n\n- **Fonts:** `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is NOT available.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes (`// @renderer p5 webgl`) it is a WebGL context: never use Canvas-2D-only APIs on it; use P5 3D drawing or `p5.image()` for textures and video.\n- **`viji.useContext()`** is NOT available in P5: the canvas and 2D context are managed by P5 internally.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(angle * 30 % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nNow convert the P5.js sketch I provide.\n\nIf the sketch is incomplete or uses features that are incompatible with the Viji worker environment, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule and mapping above. The **API MAPPING** table covers the most common direct ports; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any feature the source sketch reaches for that the mapping table does not list (audio analysis, video frames, CV data, touch, device sensors). For any P5 call you are unsure about, consult the p5.js v1.x reference linked in **REFERENCE**. The canonical companion generation prompt is `p5-prompt` (in the `docs-api.js` bundle).\n- Output the Viji-P5 scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify.\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the P5.js sketch you want to convert.\n4. The AI will return a Viji-compatible scene.\n\nFor a detailed human-readable guide, see [Converting P5 Sketches](/p5/converting-sketches#step-by-step).\n\n## Related\n\n- [Converting P5 Sketches](/p5/converting-sketches#step-by-step): step-by-step manual conversion guide\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): AI prompt for creating new P5 scenes from scratch\n- [P5 Quick Start](/p5/quickstart): your first Viji-P5 scene"
1491
1506
  }
1492
1507
  ]
1493
1508
  },
@@ -1515,7 +1530,7 @@ export const docsApi = {
1515
1530
  "content": [
1516
1531
  {
1517
1532
  "type": "text",
1518
- "markdown": "# Convert: Shadertoy to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the Shadertoy shader you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji shader scene.\n\n## The Prompt\n\n````\nYou are converting a Shadertoy shader into a Viji shader scene.\nViji runs GLSL fragment shaders with automatic uniform injection. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source shader is incomplete (missing `mainImage`), depends on multi-buffer pipelines, cube maps, 3D textures, or sound shaders that Viji does not support, ask the artist whether to drop, simplify, or skip that part before generating code. If the source is a self-contained `mainImage` (single-pass, with at most one Buffer for feedback), skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji shader that follows every rule in this prompt. Replace Shadertoy uniforms with Viji equivalents using the mapping table; convert `iTime * speed` patterns to `@viji-accumulator`; declare any artist-controllable parameters with `@viji-*` directives.\n3. **Explain.** After the code block, give a short summary of the key changes (e.g., \"replaced `iResolution` with `u_resolution`, swapped `iChannel0` for a `@viji-image` parameter, converted `iTime * speed` to a `@viji-accumulator`, used `backbuffer` for the Buffer A feedback\"). Flag any features you had to drop or simplify (multi-buffer pipelines, `iChannelTime`, `iChannelResolution`, etc.).\n4. **Iterate.** Invite the artist to ask for refinements (\"expose the color as a slider\", \"swap the texture for the camera\", \"make it audio-reactive on the kick\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The mapping tables in this prompt focus on the most common Shadertoy → Viji conversions, but they do NOT cover the full Viji uniform surface. If anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability for any data the source shader reaches for (audio, video, CV, sensors, backbuffer).\n- ON DEMAND: fetch from the Tier-2 resource when the source shader uses a Viji-side feature this prompt does not map (advanced CV uniforms, full shader examples) or when you need a behavior nuance.\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt and the standard Shadertoy uniform set for direct ports.\n- Never invent Viji uniform or directive names from memory.\n- If the source uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Companion prompt for any Viji uniform or directive this conversion prompt does not cover (search for \"shader-prompt\" in the docs above).\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the very first line (or after `#version 300 es` if using GLSL ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. Convert the `mainImage` signature to a standard `void main()`:\n - Replace `fragCoord` with `gl_FragCoord.xy`\n - Replace `fragColor` with `gl_FragColor`\n - Remove the `mainImage` wrapper entirely.\n6. ALWAYS replace Shadertoy uniforms with Viji equivalents using this mapping:\n\n | Shadertoy | Viji | Notes |\n |---|---|---|\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. For `.z` (aspect ratio), use `u_resolution.x / u_resolution.y`. |\n | `iResolution.x`, `.y` | `u_resolution.x`, `.y` | Direct match. |\n | `iTime` | `u_time` | Elapsed seconds. |\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\n | `iFrame` | `u_frame` | Frame counter (`int`). |\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\n | `iMouse.z` | Approximate with `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click-origin. |\n | `iMouse.w` | Approximate with `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click-origin. |\n | `iChannel0`-`3` (Texture / Image) | Declare `@viji-image` parameters | See rule 7 (static-image case). |\n | `iChannel0`-`3` (FFT mode in Shadertoy) | `u_audioFFT` | Auto-injected `sampler2D`, 1024 bins, 0-255. NEVER declare. |\n | `iChannel0`-`3` (Wave / Sound) | `u_audioWaveform` | Auto-injected `sampler2D`, time-domain (-1 to 1). NEVER declare. |\n | `iChannel0`-`3` (Microphone) | `u_audioFFT` and/or `u_audioWaveform` | Viji exposes the active audio source through the main audio uniforms whether it is mic, file, or stream. |\n | `iChannel0`-`3` (Keyboard) | `u_keyboard` | See rule 8. |\n | `iChannel0`-`3` (Cubemap) | Not supported | Only single-face 2D textures available. |\n | `iChannelResolution` | Not available | Track dimensions manually if needed. |\n | `iChannelTime` | Not available | Per-channel time is not tracked. |\n | `iDate` | Not available | Use `u_time` for elapsed time. |\n | `iSampleRate` | Not available | Not applicable. |\n\n7. For `iChannel` textures set to a **static image** in Shadertoy (NOT FFT, Wave/Sound, Microphone, Keyboard, or Cubemap), declare `@viji-image` parameters:\n ```glsl\n // @viji-image:channel0 label:\"Texture 1\"\n // @viji-image:channel1 label:\"Texture 2\"\n ```\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (or `texture(channel0, uv)` in ES 3.00).\n For `iChannel` slots set to **FFT / Sound / Music / Microphone** in Shadertoy: do NOT declare an `@viji-image`. Sample the auto-injected `u_audioFFT` and `u_audioWaveform` `sampler2D` uniforms directly: `texture2D(u_audioFFT, vec2(freq, 0.0)).r` for spectrum, `texture2D(u_audioWaveform, vec2(t, 0.0)).r` for waveform.\n\n **Shadertoy's two-row audio channel:** in `Sound` / `Music` / `Microphone` mode, Shadertoy encodes BOTH the FFT and the waveform in a single iChannel texture: row 0 (`y ≈ 0.0`, top) is the FFT spectrum, row 1 (`y ≈ 0.5+`, bottom) is the time-domain waveform. When converting:\n - `texture(iChannelN, vec2(uv.x, 0.0)).r` (sampling row 0) → `texture2D(u_audioFFT, vec2(uv.x, 0.0)).r`\n - `texture(iChannelN, vec2(uv.x, 0.5)).r` or `vec2(uv.x, 0.75)` (sampling row 1) → `texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r`\n - When unsure which row a sample is reading, prefer `u_audioFFT` for visualizations that look like a spectrum and `u_audioWaveform` for visualizations that look like an oscilloscope.\n\n8. If an `iChannel` is set to **Keyboard** input in Shadertoy, replace it with `u_keyboard`:\n ```glsl\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\n ```\n `u_keyboard` is a built-in `sampler2D` (256x3). Row 0 = held, Row 1 = pressed this frame, Row 2 = toggle. Do NOT declare it: it is auto-injected.\n\n9. For `iResolution` used as `vec3`, replace with:\n ```glsl\n vec3(u_resolution, u_resolution.x / u_resolution.y)\n ```\n Or refactor to use `u_resolution` as `vec2` directly: most code only needs `.xy`.\n\n10. If the shader uses `iTime` multiplied by a speed factor, ALWAYS use an accumulator instead of `u_time * speed` to prevent animation jumps when the slider changes:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n ```\n Then replace `iTime` with `phase`.\n The same applies to nested multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators.\n\n11. If adding artist-controllable parameters, ALWAYS use `// @viji-*` directives:\n ```glsl\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\n // @viji-color:name label:\"Label\" default:#ff6600\n // @viji-toggle:name label:\"Label\" default:true\n // @viji-select:name label:\"Label\" default:0 options:[\"A\",\"B\",\"C\"]\n // @viji-image:name label:\"Label\"\n // @viji-button:name label:\"Label\"\n // @viji-coordinate:uniformName label:\"Label\" default:[x,y] // vec2 uniform, both -1 to 1; keys: default (required), label (required), step, description, group, category\n // @viji-accumulator:name rate:source\n ```\n NEVER use the `u_` prefix for parameter names: it is reserved for built-in uniforms.\n\n12. Parameter directives ONLY work with `//` comments. NEVER use `/* */` for `@viji-*` directives.\n\n13. If the shader uses `#version 300 es`:\n - Keep it as the very first line (before `// @renderer shader`).\n - Replace `gl_FragColor = ...` with `out vec4 fragColor;` (declared before `main`) and `fragColor = ...`.\n - Replace `texture2D()` with `texture()`.\n - Default (no `#version`) uses GLSL ES 1.00 for maximum compatibility.\n\n14. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n15. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls.\n\n16. For feedback effects that used Shadertoy's Buffer tabs, use Viji's backbuffer:\n ```glsl\n vec4 prev = texture2D(backbuffer, uv);\n ```\n `backbuffer` is auto-detected and enabled. This replaces simple Buffer A patterns.\n Viji does NOT support multi-buffer pipelines (Buffer A feeding Buffer B). Only single-pass feedback is available.\n\n17. UNSUPPORTED Shadertoy features: if the source shader uses any of these, warn the user:\n - **Multi-buffer pipelines** (Buffer A→B→C→D): only single `backbuffer` available.\n - **CubeMap buffer** (`samplerCube`): not supported.\n - **3D textures** (`sampler3D`): not supported.\n - **`iChannelTime`**: not available.\n - **`iChannelResolution`**: not available.\n - **Sound output buffer**: not supported.\n - **`mainVR()`**: not supported.\n - **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are auto-injected: do NOT declare them. The conversion-rules table above covers the direct Shadertoy mappings; this reference is the full Viji surface for any feature the source shader reaches for that the mapping table does not list.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `u_audioSnare` | `float` | Snare energy curve 0-1, 300ms decay |\n| `u_audioHat` | `float` | Hi-hat energy curve 0-1, 300ms decay |\n| `u_audioAny` | `float` | Any-beat energy curve 0-1, 300ms decay |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `u_audioKickTrigger` | `bool` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `u_audioSnareTrigger` | `bool` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `u_audioHatTrigger` | `bool` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `u_audioAnyTrigger` | `bool` | True for exactly one frame on any beat, then auto-resets |\n| `u_audioBPM` | `float` | `0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0-1 |\n| `u_audioIsLocked` | `bool` | True on stable tempo lock |\n| `u_audioBrightness` | `float` | Spectral brightness 0-1 |\n| `u_audioFlatness` | `float` | Spectral flatness 0-1 |\n\n### Audio: Textures\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioFFT` | `sampler2D` | FFT frequency spectrum (1024 bins, 0-255). Sample with `texture2D(u_audioFFT, vec2(freq, 0.0)).r`. |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (2048 samples, -1 to 1). Sample with `texture2D(u_audioWaveform, vec2(t, 0.0)).r`. |\n\n**Note:** `u_audioFFT` / `u_audioWaveform` apply only to the main audio source. Additional streams (host `audioStreams` and device audio) use `u_audioStream{i}*` float / bool uniforms only: see Streams below.\n\n### Video\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Current video frame texture |\n| `u_videoResolution` | `vec2` | Video frame size in pixels |\n| `u_videoFrameRate` | `float` | Video frame rate |\n| `u_videoConnected` | `bool` | True if video source is active |\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n`u_deviceAcceleration` (vec3, m/s² without gravity), `u_deviceAccelerationGravity` (vec3, m/s² with gravity), `u_deviceRotationRate` (vec3, deg/s), `u_deviceOrientation` (vec3, alpha/beta/gamma in degrees), `u_deviceOrientationAbsolute` (bool, true if magnetometer-based).\n\n### External Devices\n\n`u_deviceCount` (int, video-providing devices 0-8), `u_externalDeviceCount` (int, total external devices 0-8), `u_device0` - `u_device7` (sampler2D, device camera textures), `u_device0Resolution` - `u_device7Resolution` (vec2), `u_device0Connected` - `u_device7Connected` (bool), `u_device0Acceleration` - `u_device7Acceleration` (vec3), `u_device0AccelerationGravity` - `u_device7AccelerationGravity` (vec3), `u_device0RotationRate` - `u_device7RotationRate` (vec3), `u_device0Orientation` - `u_device7Orientation` (vec3). Device audio uses the same `u_audioStream{i}*` scalar uniforms as the streams below; there are NO per-device FFT or waveform textures.\n\n### Streams (Compositor)\n\n`u_videoStreamCount` (int, 0-8), `u_videoStream0` - `u_videoStream7` (sampler2D), `u_videoStream0Resolution` - `u_videoStream7Resolution` (vec2), `u_videoStream0Connected` - `u_videoStream7Connected` (bool). Streams are host-provided video sources used internally by the compositor.\n\n`u_audioStreamCount` (int, 0-8), and per-slot `u_audioStream{i}Connected` (bool), `u_audioStream{i}Volume` (float), `u_audioStream{i}Low` - `u_audioStream{i}High` (float band energies: `Low`, `LowMid`, `Mid`, `HighMid`, `High`), `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` (float). **Lightweight scalars only**: NO `u_audioFFT` / `u_audioWaveform` per stream, and NO beat / BPM / trigger uniforms (those remain main-audio only).\n\n### Backbuffer\n\n`backbuffer` (sampler2D, no `u_` prefix): previous frame, auto-enabled when referenced. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize. Sample with `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER TYPE → UNIFORM MAPPING\n\n| Directive | GLSL Type | Example |\n|---|---|---|\n| `@viji-slider` | `uniform float` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\n| `@viji-number` | `uniform float` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\n| `@viji-color` | `uniform vec3` | `// @viji-color:tint label:\"Tint\" default:#00ffcc` |\n| `@viji-toggle` | `uniform bool` | `// @viji-toggle:invert label:\"Invert\" default:false` |\n| `@viji-select` | `uniform int` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"A\",\"B\"]` |\n| `@viji-image` | `uniform sampler2D` | `// @viji-image:tex label:\"Texture\"` |\n| `@viji-button` | `uniform bool` | `// @viji-button:trigger label:\"Trigger\"` |\n| `@viji-coordinate` | `uniform vec2` | `// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]` (both -1 to 1; `label` and `default:[x,y]` required; optional: `step`, `description`, `group`, `category`) |\n| `@viji-accumulator` | `uniform float` | `// @viji-accumulator:phase rate:speed` |\n\n## CONVERSION TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n // ... converted shader logic ...\n // Use `phase` instead of `iTime`\n // Use `u_resolution` instead of `iResolution.xy`\n // Use `gl_FragCoord.xy` instead of `fragCoord`\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow convert the Shadertoy shader I provide.\n\nIf the source is incomplete or relies on Shadertoy features that Viji does not support (multi-buffer pipelines, cube maps, 3D textures, sound shaders, `mainVR()`), ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule and mapping above. Convert directly to native Viji uniforms; do NOT use a `#define` compatibility layer.\n- The conversion-rules mapping table covers the direct Shadertoy ports; the **COMPLETE UNIFORM REFERENCE** above lists the full Viji uniform surface for any feature the source shader reaches for that the mapping table does not list (CV face/hands/pose/segmentation, device sensors, external devices, streams, audio textures, pointer, touch). For details not in this prompt, consult the **REFERENCE** links: the canonical companion generation prompt is `shader-prompt` (in the `docs-api.js` bundle).\n- Output the Viji shader code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify.\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the Shadertoy shader code (the `mainImage` function).\n4. The AI will return a native Viji shader scene.\n\n> [!TIP]\n> This prompt converts to **native Viji uniforms** (no `#define` compatibility layer). For a quick-and-dirty conversion using `#define` macros, see the compatibility header in [Shadertoy Compatibility](/shader/shadertoy).\n\n## Related\n\n- [Shadertoy Compatibility](/shader/shadertoy): manual conversion guide with compatibility layer\n- [Accumulator](/shader/parameters/accumulator): how accumulators prevent animation jumps\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt): AI prompt for creating new shaders from scratch\n- [Shader Quick Start](/shader/quickstart): your first Viji shader"
1533
+ "markdown": "# Convert: Shadertoy to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the Shadertoy shader you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji shader scene.\n\n## The Prompt\n\n````\nYou are converting a Shadertoy shader into a Viji shader scene.\nViji runs GLSL fragment shaders with automatic uniform injection. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source shader is incomplete (missing `mainImage`), depends on multi-buffer pipelines, cube maps, 3D textures, or sound shaders that Viji does not support, ask the artist whether to drop, simplify, or skip that part before generating code. If the source is a self-contained `mainImage` (single-pass, with at most one Buffer for feedback), skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji shader that follows every rule in this prompt. Replace Shadertoy uniforms with Viji equivalents using the mapping table; convert `iTime * speed` patterns to `@viji-accumulator`; declare any artist-controllable parameters with `@viji-*` directives.\n3. **Explain.** After the code block, give a short summary of the key changes (e.g., \"replaced `iResolution` with `u_resolution`, swapped `iChannel0` for a `@viji-image` parameter, converted `iTime * speed` to a `@viji-accumulator`, used `backbuffer` for the Buffer A feedback\"). Flag any features you had to drop or simplify (multi-buffer pipelines, `iChannelTime`, `iChannelResolution`, etc.).\n4. **Iterate.** Invite the artist to ask for refinements (\"expose the color as a slider\", \"swap the texture for the camera\", \"make it audio-reactive on the kick\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The mapping tables in this prompt focus on the most common Shadertoy → Viji conversions, but they do NOT cover the full Viji uniform surface. If anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability for any data the source shader reaches for (audio, video, CV, sensors, backbuffer).\n- ON DEMAND: fetch from the Tier-2 resource when the source shader uses a Viji-side feature this prompt does not map (advanced CV uniforms, full shader examples) or when you need a behavior nuance.\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt and the standard Shadertoy uniform set for direct ports.\n- Never invent Viji uniform or directive names from memory.\n- If the source uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Companion prompt for any Viji uniform or directive this conversion prompt does not cover (search for \"shader-prompt\" in the docs above).\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the very first line (or after `#version 300 es` if using GLSL ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. Convert the `mainImage` signature to a standard `void main()`:\n - Replace `fragCoord` with `gl_FragCoord.xy`\n - Replace `fragColor` with `gl_FragColor`\n - Remove the `mainImage` wrapper entirely.\n6. ALWAYS replace Shadertoy uniforms with Viji equivalents using this mapping:\n\n | Shadertoy | Viji | Notes |\n |---|---|---|\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. For `.z` (aspect ratio), use `u_resolution.x / u_resolution.y`. |\n | `iResolution.x`, `.y` | `u_resolution.x`, `.y` | Direct match. |\n | `iTime` | `u_time` | Elapsed seconds. |\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\n | `iFrame` | `u_frame` | Frame counter (`int`). |\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\n | `iMouse.z` | Approximate with `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click-origin. |\n | `iMouse.w` | Approximate with `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click-origin. |\n | `iChannel0`-`3` (Texture / Image) | Declare `@viji-image` parameters | See rule 7 (static-image case). |\n | `iChannel0`-`3` (FFT mode in Shadertoy) | `u_audioFFT` | Auto-injected `sampler2D`, 1024 bins, 0-255. NEVER declare. |\n | `iChannel0`-`3` (Wave / Sound) | `u_audioWaveform` | Auto-injected `sampler2D`, time-domain (-1 to 1). NEVER declare. |\n | `iChannel0`-`3` (Microphone) | `u_audioFFT` and/or `u_audioWaveform` | Viji exposes the active audio source through the main audio uniforms whether it is mic, file, or stream. |\n | `iChannel0`-`3` (Keyboard) | `u_keyboard` | See rule 8. |\n | `iChannel0`-`3` (Cubemap) | Not supported | Only single-face 2D textures available. |\n | `iChannelResolution` | Not available | Track dimensions manually if needed. |\n | `iChannelTime` | Not available | Per-channel time is not tracked. |\n | `iDate` | Not available | Use `u_time` for elapsed time. |\n | `iSampleRate` | Not available | Not applicable. |\n\n7. For `iChannel` textures set to a **static image** in Shadertoy (NOT FFT, Wave/Sound, Microphone, Keyboard, or Cubemap), declare `@viji-image` parameters:\n ```glsl\n // @viji-image:channel0 label:\"Texture 1\"\n // @viji-image:channel1 label:\"Texture 2\"\n ```\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (or `texture(channel0, uv)` in ES 3.00).\n For `iChannel` slots set to **FFT / Sound / Music / Microphone** in Shadertoy: do NOT declare an `@viji-image`. Sample the auto-injected `u_audioFFT` and `u_audioWaveform` `sampler2D` uniforms directly: `texture2D(u_audioFFT, vec2(freq, 0.0)).r` for spectrum, `texture2D(u_audioWaveform, vec2(t, 0.0)).r` for waveform.\n\n **Shadertoy's two-row audio channel:** in `Sound` / `Music` / `Microphone` mode, Shadertoy encodes BOTH the FFT and the waveform in a single iChannel texture: row 0 (`y ≈ 0.0`, top) is the FFT spectrum, row 1 (`y ≈ 0.5+`, bottom) is the time-domain waveform. When converting:\n - `texture(iChannelN, vec2(uv.x, 0.0)).r` (sampling row 0) → `texture2D(u_audioFFT, vec2(uv.x, 0.0)).r`\n - `texture(iChannelN, vec2(uv.x, 0.5)).r` or `vec2(uv.x, 0.75)` (sampling row 1) → `texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r`\n - When unsure which row a sample is reading, prefer `u_audioFFT` for visualizations that look like a spectrum and `u_audioWaveform` for visualizations that look like an oscilloscope.\n\n8. If an `iChannel` is set to **Keyboard** input in Shadertoy, replace it with `u_keyboard`:\n ```glsl\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\n ```\n `u_keyboard` is a built-in `sampler2D` (256x3). Row 0 = held, Row 1 = pressed this frame, Row 2 = toggle. Do NOT declare it: it is auto-injected.\n\n9. For `iResolution` used as `vec3`, replace with:\n ```glsl\n vec3(u_resolution, u_resolution.x / u_resolution.y)\n ```\n Or refactor to use `u_resolution` as `vec2` directly: most code only needs `.xy`.\n\n10. If the shader uses `iTime` multiplied by a speed factor, ALWAYS use an accumulator instead of `u_time * speed` to prevent animation jumps when the slider changes:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n ```\n Then replace `iTime` with `phase`.\n The same applies to nested multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators.\n\n11. If adding artist-controllable parameters, ALWAYS use `// @viji-*` directives:\n ```glsl\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\n // @viji-color:name label:\"Label\" default:#ff6600 // vec3 uniform; add alpha:true for a vec4 (then #rrggbbaa / rgba() defaults are allowed)\n // @viji-gradient:uniformName label:\"Label\" default:[#ff0000@0,#0000ff@1] // injected `vec3 uniformName(float t)` sampler (vec4 with alpha:true); stops are #hex@position; keys: default (required), label, minStops, maxStops, alpha, description, group, category\n // @viji-toggle:name label:\"Label\" default:true\n // @viji-select:name label:\"Label\" default:0 options:[\"A\",\"B\",\"C\"]\n // @viji-image:name label:\"Label\"\n // @viji-button:name label:\"Label\"\n // @viji-coordinate:uniformName label:\"Label\" default:[x,y] // vec2 uniform, both -1 to 1; keys: default (required), label (required), step, description, group, category\n // @viji-accumulator:name rate:source\n ```\n NEVER use the `u_` prefix for parameter names: it is reserved for built-in uniforms.\n\n12. Parameter directives ONLY work with `//` comments. NEVER use `/* */` for `@viji-*` directives.\n\n13. If the shader uses `#version 300 es`:\n - Keep it as the very first line (before `// @renderer shader`).\n - Replace `gl_FragColor = ...` with `out vec4 fragColor;` (declared before `main`) and `fragColor = ...`.\n - Replace `texture2D()` with `texture()`.\n - Default (no `#version`) uses GLSL ES 1.00 for maximum compatibility.\n\n14. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n15. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls.\n\n16. For feedback effects that used Shadertoy's Buffer tabs, use Viji's backbuffer:\n ```glsl\n vec4 prev = texture2D(backbuffer, uv);\n ```\n `backbuffer` is auto-detected and enabled. This replaces simple Buffer A patterns.\n Viji does NOT support multi-buffer pipelines (Buffer A feeding Buffer B). Only single-pass feedback is available.\n\n17. UNSUPPORTED Shadertoy features: if the source shader uses any of these, warn the user:\n - **Multi-buffer pipelines** (Buffer A→B→C→D): only single `backbuffer` available.\n - **CubeMap buffer** (`samplerCube`): not supported.\n - **3D textures** (`sampler3D`): not supported.\n - **`iChannelTime`**: not available.\n - **`iChannelResolution`**: not available.\n - **Sound output buffer**: not supported.\n - **`mainVR()`**: not supported.\n - **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are auto-injected: do NOT declare them. The conversion-rules table above covers the direct Shadertoy mappings; this reference is the full Viji surface for any feature the source shader reaches for that the mapping table does not list.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `u_audioSnare` | `float` | Snare energy curve 0-1, 300ms decay |\n| `u_audioHat` | `float` | Hi-hat energy curve 0-1, 300ms decay |\n| `u_audioAny` | `float` | Any-beat energy curve 0-1, 300ms decay |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `u_audioKickTrigger` | `bool` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `u_audioSnareTrigger` | `bool` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `u_audioHatTrigger` | `bool` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `u_audioAnyTrigger` | `bool` | True for exactly one frame on any beat, then auto-resets |\n| `u_audioBPM` | `float` | `0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0-1 |\n| `u_audioIsLocked` | `bool` | True on stable tempo lock |\n| `u_audioBrightness` | `float` | Spectral brightness 0-1 |\n| `u_audioFlatness` | `float` | Spectral flatness 0-1 |\n\n### Audio: Textures\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioFFT` | `sampler2D` | FFT frequency spectrum (1024 bins, 0-255). Sample with `texture2D(u_audioFFT, vec2(freq, 0.0)).r`. |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (2048 samples, -1 to 1). Sample with `texture2D(u_audioWaveform, vec2(t, 0.0)).r`. |\n\n**Note:** `u_audioFFT` / `u_audioWaveform` apply only to the main audio source. Additional streams (host `audioStreams` and device audio) use `u_audioStream{i}*` float / bool uniforms only: see Streams below.\n\n### Video\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Current video frame texture |\n| `u_videoResolution` | `vec2` | Video frame size in pixels |\n| `u_videoFrameRate` | `float` | Video frame rate |\n| `u_videoConnected` | `bool` | True if video source is active |\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n`u_deviceAcceleration` (vec3, m/s² without gravity), `u_deviceAccelerationGravity` (vec3, m/s² with gravity), `u_deviceRotationRate` (vec3, deg/s), `u_deviceOrientation` (vec3, alpha/beta/gamma in degrees), `u_deviceOrientationAbsolute` (bool, true if magnetometer-based).\n\n### External Devices\n\n`u_deviceCount` (int, video-providing devices 0-8), `u_externalDeviceCount` (int, total external devices 0-8), `u_device0` - `u_device7` (sampler2D, device camera textures), `u_device0Resolution` - `u_device7Resolution` (vec2), `u_device0Connected` - `u_device7Connected` (bool), `u_device0Acceleration` - `u_device7Acceleration` (vec3), `u_device0AccelerationGravity` - `u_device7AccelerationGravity` (vec3), `u_device0RotationRate` - `u_device7RotationRate` (vec3), `u_device0Orientation` - `u_device7Orientation` (vec3). Device audio uses the same `u_audioStream{i}*` scalar uniforms as the streams below; there are NO per-device FFT or waveform textures.\n\n### Streams (Compositor)\n\n`u_videoStreamCount` (int, 0-8), `u_videoStream0` - `u_videoStream7` (sampler2D), `u_videoStream0Resolution` - `u_videoStream7Resolution` (vec2), `u_videoStream0Connected` - `u_videoStream7Connected` (bool). Streams are host-provided video sources used internally by the compositor.\n\n`u_audioStreamCount` (int, 0-8), and per-slot `u_audioStream{i}Connected` (bool), `u_audioStream{i}Volume` (float), `u_audioStream{i}Low` - `u_audioStream{i}High` (float band energies: `Low`, `LowMid`, `Mid`, `HighMid`, `High`), `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` (float). **Lightweight scalars only**: NO `u_audioFFT` / `u_audioWaveform` per stream, and NO beat / BPM / trigger uniforms (those remain main-audio only).\n\n### Backbuffer\n\n`backbuffer` (sampler2D, no `u_` prefix): previous frame, auto-enabled when referenced. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize. Sample with `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER TYPE → UNIFORM MAPPING\n\n| Directive | GLSL Type | Example |\n|---|---|---|\n| `@viji-slider` | `uniform float` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\n| `@viji-number` | `uniform float` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\n| `@viji-color` | `uniform vec3`, or `uniform vec4` with `alpha:true` | `// @viji-color:tint label:\"Tint\" default:#00ffcc` (`alpha:true` allows `#rrggbbaa` / `rgba()` defaults) |\n| `@viji-gradient` | arrays + `vec3 name(float t)` (or `vec4` with `alpha:true`) | `// @viji-gradient:ramp label:\"Ramp\" default:[#ff0000@0,#0000ff@1]` (stops are `#hex@position`; `default` required; optional: `minStops`, `maxStops`, `alpha`, `description`, `group`, `category`; sample with `ramp(t)`) |\n| `@viji-toggle` | `uniform bool` | `// @viji-toggle:invert label:\"Invert\" default:false` |\n| `@viji-select` | `uniform int` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"A\",\"B\"]` |\n| `@viji-image` | `uniform sampler2D` | `// @viji-image:tex label:\"Texture\"` |\n| `@viji-button` | `uniform bool` | `// @viji-button:trigger label:\"Trigger\"` |\n| `@viji-coordinate` | `uniform vec2` | `// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]` (both -1 to 1; `label` and `default:[x,y]` required; optional: `step`, `description`, `group`, `category`) |\n| `@viji-accumulator` | `uniform float` | `// @viji-accumulator:phase rate:speed` |\n\n## CONVERSION TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n // ... converted shader logic ...\n // Use `phase` instead of `iTime`\n // Use `u_resolution` instead of `iResolution.xy`\n // Use `gl_FragCoord.xy` instead of `fragCoord`\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow convert the Shadertoy shader I provide.\n\nIf the source is incomplete or relies on Shadertoy features that Viji does not support (multi-buffer pipelines, cube maps, 3D textures, sound shaders, `mainVR()`), ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule and mapping above. Convert directly to native Viji uniforms; do NOT use a `#define` compatibility layer.\n- The conversion-rules mapping table covers the direct Shadertoy ports; the **COMPLETE UNIFORM REFERENCE** above lists the full Viji uniform surface for any feature the source shader reaches for that the mapping table does not list (CV face/hands/pose/segmentation, device sensors, external devices, streams, audio textures, pointer, touch). For details not in this prompt, consult the **REFERENCE** links: the canonical companion generation prompt is `shader-prompt` (in the `docs-api.js` bundle).\n- Output the Viji shader code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify.\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the Shadertoy shader code (the `mainImage` function).\n4. The AI will return a native Viji shader scene.\n\n> [!TIP]\n> This prompt converts to **native Viji uniforms** (no `#define` compatibility layer). For a quick-and-dirty conversion using `#define` macros, see the compatibility header in [Shadertoy Compatibility](/shader/shadertoy).\n\n## Related\n\n- [Shadertoy Compatibility](/shader/shadertoy): manual conversion guide with compatibility layer\n- [Accumulator](/shader/parameters/accumulator): how accumulators prevent animation jumps\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt): AI prompt for creating new shaders from scratch\n- [Shader Quick Start](/shader/quickstart): your first Viji shader"
1519
1534
  }
1520
1535
  ]
1521
1536
  },
@@ -1543,7 +1558,7 @@ export const docsApi = {
1543
1558
  "content": [
1544
1559
  {
1545
1560
  "type": "text",
1546
- "markdown": "# Convert: Three.js to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the Three.js code you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji native scene with Three.js.\n\n## The Prompt\n\n````\nYou are converting a standalone Three.js application into a Viji native scene.\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source code is incomplete (missing init or render loop), uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js), or relies on DOM elements outside the canvas, ask the artist for the missing pieces or for permission to drop the framework wrapper before generating code. If the code is plain Three.js and self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji native scene that follows every rule in this prompt. Preserve the artist's visual intent, scene graph, and material setup; replace only the Viji-incompatible parts (window/document access, `requestAnimationFrame`, `THREE.Clock`, DOM event listeners).\n3. **Explain.** After the code block, give a short summary of the key changes you made (e.g., \"wrapped scene init in top-level code, moved per-frame logic into `render(viji)`, replaced `clock.getDelta()` with `viji.deltaTime`, replaced mouse listeners with `viji.pointer`\"). Flag any features you had to drop or simplify (e.g., `OrbitControls`, postprocessing that depends on DOM events).\n4. **Iterate.** Invite the artist to ask for refinements (\"add a slider for camera distance\", \"make the cube react to audio\", \"swap the material for a wireframe\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common Three.js → Viji mappings, but they do NOT cover the full Viji API surface. If anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resource. Use it to verify exact Viji API names, parameter types, and any Viji feature the source code may reach for (audio, video, CV, sensors, parameters).\n- ON DEMAND: fetch from the Tier-2 resource when the source uses a Viji-side feature this prompt does not map (advanced CV data structures, device sensors, full Viji examples) or when you need authoritative TypeScript signatures.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard Three.js API for direct ports.\n- Never invent Viji property or method names from memory.\n- If the source uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Companion prompt for any Viji feature this conversion prompt does not cover (search for \"native-prompt\" in the docs above).\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS import Three.js dynamically at the top level using `await import()`:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n NEVER use `<script>` tags, `require()`, or static `import` statements.\n ALWAYS pin the version number in the URL.\n\n2. ALWAYS use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n ALWAYS pass `false` as the third argument to `setSize()`: this prevents Three.js from setting CSS styles, which would fail in the worker.\n\n3. NEVER use `requestAnimationFrame()`. Viji controls the render loop. Write all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. ALWAYS handle resize by checking `viji.width` / `viji.height` in `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n renderer.render(scene, camera);\n }\n ```\n\n5. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n\n6. NEVER use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. Replace hardcoded values with Viji parameters declared at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const color = viji.color('#049ef4', { label: 'Color' });\n ```\n NEVER declare parameters inside `render()`.\n ALWAYS read via `.value`: `speed.value`, `color.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Three.js color objects can take `.rgb` directly, e.g. `material.color.setRGB(c.rgb.r/255, c.rgb.g/255, c.rgb.b/255)`, or just keep using `material.color.set(c.value)`.\n\n8. ALWAYS use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n NEVER use `clock.getDelta()` or `Date.now()`. Remove any `THREE.Clock` usage.\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes animation jumps when the parameter changes. Same for nested multiplications: never multiply an accumulator by another parameter; give each speed its own accumulator.\n\n9. Replace mouse/keyboard event listeners with Viji APIs:\n - `event.clientX` → `viji.pointer.x` (works for both mouse and touch) or `viji.mouse.x`\n - `event.clientY` → `viji.pointer.y` or `viji.mouse.y`\n - Mouse buttons → `viji.mouse.leftButton`, `viji.mouse.rightButton`\n - Key presses → `viji.keyboard.isPressed('keyName')`\n\n10. `OrbitControls` and other controls that depend on DOM events will NOT work in the worker.\n For camera interaction, read `viji.pointer` (handles both mouse and touch) and update the camera manually.\n\n11. For Three.js addons, import from the examples directory:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n ALWAYS use the same Three.js version for addons as for the main library.\n\n12. For textures from file inputs, use Viji's image parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n // In render():\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n ```\n\n13. For video textures, use `viji.video`:\n ```javascript\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n ```\n\n14. NEVER allocate new objects inside `render()`. Pre-create vectors, colors, and materials at the top level.\n\n15. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n\n16. Remove any `window.addEventListener('resize', ...)`: resize is handled in `render()` (see rule 4).\n\n17. Remove any CSS, HTML, or DOM manipulation code. Viji scenes produce only canvas output.\n\n18. ALWAYS type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical across all renderers (same object, same types). The mapping table above covers the most common Three.js → Viji ports; the reference below is the full surface for any Viji-side feature the source code reaches for (audio analysis, video frames, CV data, touch, device sensors).\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (pass to Three.js renderer) |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\nWhen using Three.js, do NOT call `viji.useContext()`: pass `viji.canvas` to the `THREE.WebGLRenderer({ canvas: viji.canvas })` and Three.js manages its own GL context.\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. For Three.js video textures, see rule 13.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## TEMPLATE\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 1, 3);\ncamera.lookAt(0, 0, 0);\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshStandardMaterial({ color: color.value });\nconst mesh = new THREE.Mesh(geometry, material);\nscene.add(mesh);\n\nscene.add(new THREE.DirectionalLight(0xffffff, 1.5));\nscene.add(new THREE.AmbientLight(0x404040));\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n mesh.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n```\n\nNow convert the Three.js code I provide.\n\nIf the source is incomplete or uses a framework that needs to be unwrapped, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule above. The conversion-specific rules (1-17) cover the Three.js → Viji mappings most artists need; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any Viji-side feature the source code reaches for (audio, video, CV, touch, device sensors, parameters beyond the basics). For details not in this prompt, consult the **REFERENCE** links: the canonical companion generation prompt is `native-prompt` (in the `docs-api.js` bundle).\n- Output the Viji scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify (controls, postprocessing, framework wrappers).\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the Three.js code you want to convert.\n4. The AI will return a Viji-compatible native scene.\n\n> [!NOTE]\n> This prompt handles standard Three.js scenes. If the original code uses a framework (React Three Fiber, Drei, etc.), you may need to manually extract the Three.js scene setup first.\n\n## Related\n\n- [External Libraries](/native/external-libraries): detailed guide for using Three.js and other libraries in Viji\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): AI prompt for creating new native scenes from scratch\n- [Native Quick Start](/native/quickstart): your first Viji native scene"
1561
+ "markdown": "# Convert: Three.js to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the Three.js code you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji native scene with Three.js.\n\n## The Prompt\n\n````\nYou are converting a standalone Three.js application into a Viji native scene.\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source code is incomplete (missing init or render loop), uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js), or relies on DOM elements outside the canvas, ask the artist for the missing pieces or for permission to drop the framework wrapper before generating code. If the code is plain Three.js and self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji native scene that follows every rule in this prompt. Preserve the artist's visual intent, scene graph, and material setup; replace only the Viji-incompatible parts (window/document access, `requestAnimationFrame`, `THREE.Clock`, DOM event listeners).\n3. **Explain.** After the code block, give a short summary of the key changes you made (e.g., \"wrapped scene init in top-level code, moved per-frame logic into `render(viji)`, replaced `clock.getDelta()` with `viji.deltaTime`, replaced mouse listeners with `viji.pointer`\"). Flag any features you had to drop or simplify (e.g., `OrbitControls`, postprocessing that depends on DOM events).\n4. **Iterate.** Invite the artist to ask for refinements (\"add a slider for camera distance\", \"make the cube react to audio\", \"swap the material for a wireframe\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common Three.js → Viji mappings, but they do NOT cover the full Viji API surface. If anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resource. Use it to verify exact Viji API names, parameter types, and any Viji feature the source code may reach for (audio, video, CV, sensors, parameters).\n- ON DEMAND: fetch from the Tier-2 resource when the source uses a Viji-side feature this prompt does not map (advanced CV data structures, device sensors, full Viji examples) or when you need authoritative TypeScript signatures.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard Three.js API for direct ports.\n- Never invent Viji property or method names from memory.\n- If the source uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Companion prompt for any Viji feature this conversion prompt does not cover (search for \"native-prompt\" in the docs above).\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS import Three.js dynamically at the top level using `await import()`:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n NEVER use `<script>` tags, `require()`, or static `import` statements.\n ALWAYS pin the version number in the URL.\n\n2. ALWAYS use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n ALWAYS pass `false` as the third argument to `setSize()`: this prevents Three.js from setting CSS styles, which would fail in the worker.\n\n3. NEVER use `requestAnimationFrame()`. Viji controls the render loop. Write all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. ALWAYS handle resize by checking `viji.width` / `viji.height` in `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n renderer.render(scene, camera);\n }\n ```\n\n5. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n\n6. NEVER use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. Replace hardcoded values with Viji parameters declared at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const color = viji.color('#049ef4', { label: 'Color' });\n ```\n NEVER declare parameters inside `render()`.\n ALWAYS read via `.value`: `speed.value`, `color.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Three.js color objects can take `.rgb` directly, e.g. `material.color.setRGB(c.rgb.r/255, c.rgb.g/255, c.rgb.b/255)`, or just keep using `material.color.set(c.value)`.\n\n8. ALWAYS use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n NEVER use `clock.getDelta()` or `Date.now()`. Remove any `THREE.Clock` usage.\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes animation jumps when the parameter changes. Same for nested multiplications: never multiply an accumulator by another parameter; give each speed its own accumulator.\n\n9. Replace mouse/keyboard event listeners with Viji APIs:\n - `event.clientX` → `viji.pointer.x` (works for both mouse and touch) or `viji.mouse.x`\n - `event.clientY` → `viji.pointer.y` or `viji.mouse.y`\n - Mouse buttons → `viji.mouse.leftButton`, `viji.mouse.rightButton`\n - Key presses → `viji.keyboard.isPressed('keyName')`\n\n10. `OrbitControls` and other controls that depend on DOM events will NOT work in the worker.\n For camera interaction, read `viji.pointer` (handles both mouse and touch) and update the camera manually.\n\n11. For Three.js addons, import from the examples directory:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n ALWAYS use the same Three.js version for addons as for the main library.\n\n12. For textures from file inputs, use Viji's image parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n // In render():\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n ```\n\n13. For video textures, use `viji.video`:\n ```javascript\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n ```\n\n14. NEVER allocate new objects inside `render()`. Pre-create vectors, colors, and materials at the top level.\n\n15. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n\n16. Remove any `window.addEventListener('resize', ...)`: resize is handled in `render()` (see rule 4).\n\n17. Remove any CSS, HTML, or DOM manipulation code. Viji scenes produce only canvas output.\n\n18. ALWAYS type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical across all renderers (same object, same types). The mapping table above covers the most common Three.js → Viji ports; the reference below is the full surface for any Viji-side feature the source code reaches for (audio analysis, video frames, CV data, touch, device sensors).\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (pass to Three.js renderer) |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\nWhen using Three.js, do NOT call `viji.useContext()`: pass `viji.canvas` to the `THREE.WebGLRenderer({ canvas: viji.canvas })` and Three.js manages its own GL context.\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. For Three.js video textures, see rule 13.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## TEMPLATE\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 1, 3);\ncamera.lookAt(0, 0, 0);\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshStandardMaterial({ color: color.value });\nconst mesh = new THREE.Mesh(geometry, material);\nscene.add(mesh);\n\nscene.add(new THREE.DirectionalLight(0xffffff, 1.5));\nscene.add(new THREE.AmbientLight(0x404040));\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n mesh.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n```\n\nNow convert the Three.js code I provide.\n\nIf the source is incomplete or uses a framework that needs to be unwrapped, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule above. The conversion-specific rules (1-17) cover the Three.js → Viji mappings most artists need; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any Viji-side feature the source code reaches for (audio, video, CV, touch, device sensors, parameters beyond the basics). For details not in this prompt, consult the **REFERENCE** links: the canonical companion generation prompt is `native-prompt` (in the `docs-api.js` bundle).\n- Output the Viji scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify (controls, postprocessing, framework wrappers).\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the Three.js code you want to convert.\n4. The AI will return a Viji-compatible native scene.\n\n> [!NOTE]\n> This prompt handles standard Three.js scenes. If the original code uses a framework (React Three Fiber, Drei, etc.), you may need to manually extract the Three.js scene setup first.\n\n## Related\n\n- [External Libraries](/native/external-libraries): detailed guide for using Three.js and other libraries in Viji\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): AI prompt for creating new native scenes from scratch\n- [Native Quick Start](/native/quickstart): your first Viji native scene"
1547
1562
  }
1548
1563
  ]
1549
1564
  },
@@ -1699,7 +1714,7 @@ export const docsApi = {
1699
1714
  "content": [
1700
1715
  {
1701
1716
  "type": "text",
1702
- "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your scene functions. Use it as a quick lookup: each entry links to its dedicated documentation page for full details, examples, and patterns.\n\nNew to Viji? Start with the [Quick Start](/native/quickstart) instead.\n\nAll types named on this page are available globally in scene code: annotate variables with them directly, with no imports or aliases. TypeScript is optional; see [TypeScript Support](/native/quickstart#typescript-support).\n\n## Entry Points\n\n```javascript\n// Top-level code runs once (initialization, parameters, state, imports)\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\n\nfunction render(viji) {\n // Called every frame: this is where you draw\n}\n```\n\nThere is no `setup()` function in native scenes. All initialization goes at the top level. Top-level `await` is supported for dynamic imports.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/native/canvas-context#the-canvas) | `OffscreenCanvas` | The rendering canvas | [Canvas & Context](/native/canvas-context) |\n| [`viji.ctx`](/native/canvas-context#2d-context) | `OffscreenCanvasRenderingContext2D` | 2D context (after `useContext('2d')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.gl`](/native/canvas-context#webgl-context) | `WebGLRenderingContext \\| WebGL2RenderingContext` | WebGL context (after `useContext('webgl'\\|'webgl2')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.width`](/native/canvas-context#dimension-properties) | `number` | Canvas width in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.height`](/native/canvas-context#dimension-properties) | `number` | Canvas height in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.useContext(type)`](/native/canvas-context#choosing-a-context) | `Method` | Request a rendering context: `'2d'`, `'webgl'`, `'webgl2'` | [Canvas & Context](/native/canvas-context) |\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/native/timing#vijitime-absolute-time) | `number` | Seconds elapsed since the scene started | [Timing](/native/timing) |\n| [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | `number` | Seconds since the previous frame | [Timing](/native/timing) |\n| [`viji.frameCount`](/native/timing#vijiframecount) | `number` | Monotonically increasing frame counter | [Timing](/native/timing) |\n| [`viji.fps`](/native/timing#vijifps-target-frame-rate) | `number` | Target FPS based on the host's frame rate mode | [Timing](/native/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/native/parameters/slider) | `SliderParameter` | `number` | [Slider](/native/parameters/slider) |\n| [`viji.color(default, config)`](/native/parameters/color) | `ColorParameter` | `string` (hex) | [Color](/native/parameters/color) |\n| [`viji.toggle(default, config)`](/native/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/native/parameters/toggle) |\n| [`viji.select(default, config)`](/native/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/native/parameters/select) |\n| [`viji.number(default, config)`](/native/parameters/number) | `NumberParameter` | `number` | [Number](/native/parameters/number) |\n| [`viji.text(default, config)`](/native/parameters/text) | `TextParameter` | `string` | [Text](/native/parameters/text) |\n| [`viji.image(null, config)`](/native/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/native/parameters/image) |\n| [`viji.button(config)`](/native/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/native/parameters/button) |\n| [`viji.coordinate(default, config)`](/native/parameters/coordinate) | `CoordinateParameter` | 2D coordinate pad: `{ x: number, y: number }`, both -1 to 1 | [Coordinate](/native/parameters/coordinate) |\n\nSee [Parameters Overview](/native/parameters) for the declaration pattern, [Grouping](/native/parameters/grouping) and [Categories](/native/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/native/audio) | `boolean` | Whether an audio source is active | [Overview](/native/audio) |\n| [`viji.audio.volume.current`](/native/audio/volume#property-reference) | `number` | Current RMS volume 0-1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.peak`](/native/audio/volume#property-reference) | `number` | Peak volume 0-1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.smoothed`](/native/audio/volume#property-reference) | `number` | Smoothed volume 0-1 | [Volume](/native/audio/volume) |\n| [`viji.audio.bands.low`](/native/audio/bands#instant-bands) | `number` | Low frequency band energy (20-120 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMid`](/native/audio/bands#instant-bands) | `number` | Low-mid band energy (120-500 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.mid`](/native/audio/bands#instant-bands) | `number` | Mid band energy (500-2 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMid`](/native/audio/bands#instant-bands) | `number` | High-mid band energy (2-6 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.high`](/native/audio/bands#instant-bands) | `number` | High band energy (6-16 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed low band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed low-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed high-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed high band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.beat.kick`](/native/audio/beat#energy-curves-fast-decay) | `number` | Kick beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snare`](/native/audio/beat#energy-curves-fast-decay) | `number` | Snare beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hat`](/native/audio/beat#energy-curves-fast-decay) | `number` | Hi-hat beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.any`](/native/audio/beat#energy-curves-fast-decay) | `number` | Combined beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed kick | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed snare | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed hi-hat | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed combined | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/native/audio/beat#triggers) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/native/audio/beat#triggers) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/native/audio/beat#triggers) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/native/audio/beat#triggers) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.events`](/native/audio/beat#events) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.bpm`](/native/audio/beat#tempo) | `number` | Tracked BPM | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.confidence`](/native/audio/beat#tempo) | `number` | Beat-tracker confidence 0-1 | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.isLocked`](/native/audio/beat#tempo) | `boolean` | Whether beat tracking is locked | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.spectral.brightness`](/native/audio/spectral#property-reference) | `number` | Spectral brightness 0-1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/native/audio/spectral#property-reference) | `number` | Spectral flatness 0-1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/native/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0-255) | [Frequency Data](/native/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/native/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/native/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/native/video) | `boolean` | Whether a video source is active | [Overview](/native/video) |\n| [`viji.video.currentFrame`](/native/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/native/video/basics) |\n| [`viji.video.frameWidth`](/native/video/basics) | `number` | Frame width in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameHeight`](/native/video/basics) | `number` | Frame height in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameRate`](/native/video/basics) | `number` | Video frame rate | [Video Basics](/native/video/basics) |\n| [`viji.video.getFrameData()`](/native/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/native/video/basics) |\n| [`viji.video.cv.faces`](/native/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/native/video/face-detection) |\n| [`viji.video.cv.hands`](/native/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.cv.pose`](/native/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.cv.segmentation`](/native/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/native/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/native/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/native/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/native/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/native/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/native/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/native/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/native/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/native/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/native/video) | `() => CVFeature[]` | List of active CV features | [Overview](/native/video) |\n| [`viji.video.cv.isProcessing()`](/native/video) | `() => boolean` | Whether CV is currently processing | [Overview](/native/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/native/mouse#position) | `number` | Cursor X position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.y`](/native/mouse#position) | `number` | Cursor Y position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.isInCanvas`](/native/mouse#position) | `boolean` | Whether cursor is inside the canvas | [Mouse](/native/mouse) |\n| [`viji.mouse.isPressed`](/native/mouse#buttons) | `boolean` | Whether any button is pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.leftButton`](/native/mouse#buttons) | `boolean` | Left button state | [Mouse](/native/mouse) |\n| [`viji.mouse.rightButton`](/native/mouse#buttons) | `boolean` | Right button state | [Mouse](/native/mouse) |\n| [`viji.mouse.middleButton`](/native/mouse#buttons) | `boolean` | Middle button state | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaX`](/native/mouse#movement) | `number` | Horizontal movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaY`](/native/mouse#movement) | `number` | Vertical movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelDelta`](/native/mouse#scroll-wheel) | `number` | Scroll wheel delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelX`](/native/mouse#scroll-wheel) | `number` | Horizontal scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelY`](/native/mouse#scroll-wheel) | `number` | Vertical scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wasPressed`](/native/mouse#frame-events) | `boolean` | True for one frame when pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.wasReleased`](/native/mouse#frame-events) | `boolean` | True for one frame when released | [Mouse](/native/mouse) |\n| [`viji.mouse.wasMoved`](/native/mouse#frame-events) | `boolean` | True for one frame when moved | [Mouse](/native/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/native/keyboard#methods) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/native/keyboard#methods) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/native/keyboard#methods) | `(string) => boolean` | True for one frame when released | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.activeKeys`](/native/keyboard#properties) | `Set<string>` | All currently held keys | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/native/keyboard#properties) | `Set<string>` | Keys pressed this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/native/keyboard#properties) | `Set<string>` | Keys released this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/native/keyboard#properties) | `string` | Most recently pressed key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/native/keyboard#properties) | `string` | Most recently released key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.shift`](/native/keyboard#modifier-keys) | `boolean` | Shift key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.ctrl`](/native/keyboard#modifier-keys) | `boolean` | Ctrl/Cmd key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.alt`](/native/keyboard#modifier-keys) | `boolean` | Alt/Option key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.meta`](/native/keyboard#modifier-keys) | `boolean` | Meta/Win key state | [Keyboard](/native/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | All active touch points | [Touch](/native/touch) |\n| [`viji.touches.count`](/native/touch#touchapi-vijitouches) | `number` | Number of active touches | [Touch](/native/touch) |\n| [`viji.touches.started`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that started this frame | [Touch](/native/touch) |\n| [`viji.touches.moved`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/native/touch) |\n| [`viji.touches.ended`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/native/touch) |\n| [`viji.touches.primary`](/native/touch#touchapi-vijitouches) | `TouchPoint \\| null` | The first active touch point | [Touch](/native/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/native/pointer#api-reference) | `number` | Primary pointer X position | [Pointer](/native/pointer) |\n| [`viji.pointer.y`](/native/pointer#api-reference) | `number` | Primary pointer Y position | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaX`](/native/pointer#api-reference) | `number` | Horizontal movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaY`](/native/pointer#api-reference) | `number` | Vertical movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.isDown`](/native/pointer#api-reference) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/native/pointer) |\n| [`viji.pointer.wasPressed`](/native/pointer#api-reference) | `boolean` | True for one frame when pressed | [Pointer](/native/pointer) |\n| [`viji.pointer.wasReleased`](/native/pointer#api-reference) | `boolean` | True for one frame when released | [Pointer](/native/pointer) |\n| [`viji.pointer.isInCanvas`](/native/pointer#api-reference) | `boolean` | Whether pointer is inside the canvas | [Pointer](/native/pointer) |\n| [`viji.pointer.type`](/native/pointer#api-reference) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/native/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/native/sensors#devicemotiondata-vijidevicemotion) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/native/sensors) |\n| [`viji.device.orientation`](/native/sensors#deviceorientationdata-vijideviceorientation) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/native/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/native/external-devices#devicestate-vijidevicesi) | `DeviceState[]` | Connected external devices | [Overview](/native/external-devices) |\n| [`viji.devices[i].id`](/native/external-devices#devicestate-vijidevicesi) | `string` | Unique device identifier | [Overview](/native/external-devices) |\n| [`viji.devices[i].name`](/native/external-devices#devicestate-vijidevicesi) | `string` | User-friendly device name | [Overview](/native/external-devices) |\n| [`viji.devices[i].motion`](/native/external-devices/sensors#devicemotiondata-devicemotion) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/native/external-devices/sensors#deviceorientationdata-deviceorientation) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].video`](/native/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/native/external-devices/video) |\n| [`viji.devices[i].audio`](/native/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/native/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.videoStreams`](/native/video) | `VideoAPI[]` | Additional video streams provided by the host | [Video](/native/video) |\n| [`viji.audioStreams`](/native/audio#beyond-the-main-stream) | `AudioStreamAPI[]` | Additional audio streams provided by the host | [Audio](/native/audio) |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/native/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking: see [`viji.audio`](/native/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/native/quickstart): getting started with the Native renderer\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid\n- [P5 API Reference](/p5/api-reference): the same API in the P5 renderer\n- [Shader API Reference](/shader/api-reference): built-in uniforms for shaders"
1717
+ "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your scene functions. Use it as a quick lookup: each entry links to its dedicated documentation page for full details, examples, and patterns.\n\nNew to Viji? Start with the [Quick Start](/native/quickstart) instead.\n\nAll types named on this page are available globally in scene code: annotate variables with them directly, with no imports or aliases. TypeScript is optional; see [TypeScript Support](/native/quickstart#typescript-support).\n\n## Entry Points\n\n```javascript\n// Top-level code runs once (initialization, parameters, state, imports)\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\n\nfunction render(viji) {\n // Called every frame: this is where you draw\n}\n```\n\nThere is no `setup()` function in native scenes. All initialization goes at the top level. Top-level `await` is supported for dynamic imports.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/native/canvas-context#the-canvas) | `OffscreenCanvas` | The rendering canvas | [Canvas & Context](/native/canvas-context) |\n| [`viji.ctx`](/native/canvas-context#2d-context) | `OffscreenCanvasRenderingContext2D` | 2D context (after `useContext('2d')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.gl`](/native/canvas-context#webgl-context) | `WebGLRenderingContext \\| WebGL2RenderingContext` | WebGL context (after `useContext('webgl'\\|'webgl2')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.width`](/native/canvas-context#dimension-properties) | `number` | Canvas width in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.height`](/native/canvas-context#dimension-properties) | `number` | Canvas height in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.useContext(type)`](/native/canvas-context#choosing-a-context) | `Method` | Request a rendering context: `'2d'`, `'webgl'`, `'webgl2'` | [Canvas & Context](/native/canvas-context) |\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/native/timing#vijitime-absolute-time) | `number` | Seconds elapsed since the scene started | [Timing](/native/timing) |\n| [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | `number` | Seconds since the previous frame | [Timing](/native/timing) |\n| [`viji.frameCount`](/native/timing#vijiframecount) | `number` | Monotonically increasing frame counter | [Timing](/native/timing) |\n| [`viji.fps`](/native/timing#vijifps-target-frame-rate) | `number` | Target FPS based on the host's frame rate mode | [Timing](/native/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/native/parameters/slider) | `SliderParameter` | `number` | [Slider](/native/parameters/slider) |\n| [`viji.color(default, config)`](/native/parameters/color) | `ColorParameter` | `string` (hex; `#rrggbbaa` with `alpha`) | [Color](/native/parameters/color) |\n| [`viji.gradient(default, config)`](/native/parameters/gradient) | `GradientParameter` | Sorted `{ color, position }[]` plus `at(t)` | [Gradient](/native/parameters/gradient) |\n| [`viji.toggle(default, config)`](/native/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/native/parameters/toggle) |\n| [`viji.select(default, config)`](/native/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/native/parameters/select) |\n| [`viji.number(default, config)`](/native/parameters/number) | `NumberParameter` | `number` | [Number](/native/parameters/number) |\n| [`viji.text(default, config)`](/native/parameters/text) | `TextParameter` | `string` | [Text](/native/parameters/text) |\n| [`viji.image(null, config)`](/native/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/native/parameters/image) |\n| [`viji.button(config)`](/native/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/native/parameters/button) |\n| [`viji.coordinate(default, config)`](/native/parameters/coordinate) | `CoordinateParameter` | 2D coordinate pad: `{ x: number, y: number }`, both -1 to 1 | [Coordinate](/native/parameters/coordinate) |\n\nSee [Parameters Overview](/native/parameters) for the declaration pattern, [Grouping](/native/parameters/grouping) and [Categories](/native/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/native/audio) | `boolean` | Whether an audio source is active | [Overview](/native/audio) |\n| [`viji.audio.volume.current`](/native/audio/volume#property-reference) | `number` | Current RMS volume 0-1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.peak`](/native/audio/volume#property-reference) | `number` | Peak volume 0-1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.smoothed`](/native/audio/volume#property-reference) | `number` | Smoothed volume 0-1 | [Volume](/native/audio/volume) |\n| [`viji.audio.bands.low`](/native/audio/bands#instant-bands) | `number` | Low frequency band energy (20-120 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMid`](/native/audio/bands#instant-bands) | `number` | Low-mid band energy (120-500 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.mid`](/native/audio/bands#instant-bands) | `number` | Mid band energy (500-2 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMid`](/native/audio/bands#instant-bands) | `number` | High-mid band energy (2-6 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.high`](/native/audio/bands#instant-bands) | `number` | High band energy (6-16 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed low band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed low-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed high-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/native/audio/bands#smoothed-bands) | `number` | Smoothed high band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.beat.kick`](/native/audio/beat#energy-curves-fast-decay) | `number` | Kick beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snare`](/native/audio/beat#energy-curves-fast-decay) | `number` | Snare beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hat`](/native/audio/beat#energy-curves-fast-decay) | `number` | Hi-hat beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.any`](/native/audio/beat#energy-curves-fast-decay) | `number` | Combined beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed kick | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed snare | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed hi-hat | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/native/audio/beat#energy-curves-smoothed) | `number` | Smoothed combined | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/native/audio/beat#triggers) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/native/audio/beat#triggers) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/native/audio/beat#triggers) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/native/audio/beat#triggers) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.events`](/native/audio/beat#events) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.bpm`](/native/audio/beat#tempo) | `number` | Tracked BPM | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.confidence`](/native/audio/beat#tempo) | `number` | Beat-tracker confidence 0-1 | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.isLocked`](/native/audio/beat#tempo) | `boolean` | Whether beat tracking is locked | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.spectral.brightness`](/native/audio/spectral#property-reference) | `number` | Spectral brightness 0-1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/native/audio/spectral#property-reference) | `number` | Spectral flatness 0-1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/native/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0-255) | [Frequency Data](/native/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/native/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/native/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/native/video) | `boolean` | Whether a video source is active | [Overview](/native/video) |\n| [`viji.video.currentFrame`](/native/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/native/video/basics) |\n| [`viji.video.frameWidth`](/native/video/basics) | `number` | Frame width in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameHeight`](/native/video/basics) | `number` | Frame height in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameRate`](/native/video/basics) | `number` | Video frame rate | [Video Basics](/native/video/basics) |\n| [`viji.video.getFrameData()`](/native/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/native/video/basics) |\n| [`viji.video.cv.faces`](/native/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/native/video/face-detection) |\n| [`viji.video.cv.hands`](/native/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.cv.pose`](/native/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.cv.segmentation`](/native/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/native/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/native/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/native/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/native/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/native/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/native/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/native/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/native/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/native/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/native/video) | `() => CVFeature[]` | List of active CV features | [Overview](/native/video) |\n| [`viji.video.cv.isProcessing()`](/native/video) | `() => boolean` | Whether CV is currently processing | [Overview](/native/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/native/mouse#position) | `number` | Cursor X position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.y`](/native/mouse#position) | `number` | Cursor Y position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.isInCanvas`](/native/mouse#position) | `boolean` | Whether cursor is inside the canvas | [Mouse](/native/mouse) |\n| [`viji.mouse.isPressed`](/native/mouse#buttons) | `boolean` | Whether any button is pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.leftButton`](/native/mouse#buttons) | `boolean` | Left button state | [Mouse](/native/mouse) |\n| [`viji.mouse.rightButton`](/native/mouse#buttons) | `boolean` | Right button state | [Mouse](/native/mouse) |\n| [`viji.mouse.middleButton`](/native/mouse#buttons) | `boolean` | Middle button state | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaX`](/native/mouse#movement) | `number` | Horizontal movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaY`](/native/mouse#movement) | `number` | Vertical movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelDelta`](/native/mouse#scroll-wheel) | `number` | Scroll wheel delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelX`](/native/mouse#scroll-wheel) | `number` | Horizontal scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelY`](/native/mouse#scroll-wheel) | `number` | Vertical scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wasPressed`](/native/mouse#frame-events) | `boolean` | True for one frame when pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.wasReleased`](/native/mouse#frame-events) | `boolean` | True for one frame when released | [Mouse](/native/mouse) |\n| [`viji.mouse.wasMoved`](/native/mouse#frame-events) | `boolean` | True for one frame when moved | [Mouse](/native/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/native/keyboard#methods) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/native/keyboard#methods) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/native/keyboard#methods) | `(string) => boolean` | True for one frame when released | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.activeKeys`](/native/keyboard#properties) | `Set<string>` | All currently held keys | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/native/keyboard#properties) | `Set<string>` | Keys pressed this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/native/keyboard#properties) | `Set<string>` | Keys released this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/native/keyboard#properties) | `string` | Most recently pressed key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/native/keyboard#properties) | `string` | Most recently released key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.shift`](/native/keyboard#modifier-keys) | `boolean` | Shift key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.ctrl`](/native/keyboard#modifier-keys) | `boolean` | Ctrl/Cmd key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.alt`](/native/keyboard#modifier-keys) | `boolean` | Alt/Option key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.meta`](/native/keyboard#modifier-keys) | `boolean` | Meta/Win key state | [Keyboard](/native/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | All active touch points | [Touch](/native/touch) |\n| [`viji.touches.count`](/native/touch#touchapi-vijitouches) | `number` | Number of active touches | [Touch](/native/touch) |\n| [`viji.touches.started`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that started this frame | [Touch](/native/touch) |\n| [`viji.touches.moved`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/native/touch) |\n| [`viji.touches.ended`](/native/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/native/touch) |\n| [`viji.touches.primary`](/native/touch#touchapi-vijitouches) | `TouchPoint \\| null` | The first active touch point | [Touch](/native/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/native/pointer#api-reference) | `number` | Primary pointer X position | [Pointer](/native/pointer) |\n| [`viji.pointer.y`](/native/pointer#api-reference) | `number` | Primary pointer Y position | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaX`](/native/pointer#api-reference) | `number` | Horizontal movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaY`](/native/pointer#api-reference) | `number` | Vertical movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.isDown`](/native/pointer#api-reference) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/native/pointer) |\n| [`viji.pointer.wasPressed`](/native/pointer#api-reference) | `boolean` | True for one frame when pressed | [Pointer](/native/pointer) |\n| [`viji.pointer.wasReleased`](/native/pointer#api-reference) | `boolean` | True for one frame when released | [Pointer](/native/pointer) |\n| [`viji.pointer.isInCanvas`](/native/pointer#api-reference) | `boolean` | Whether pointer is inside the canvas | [Pointer](/native/pointer) |\n| [`viji.pointer.type`](/native/pointer#api-reference) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/native/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/native/sensors#devicemotiondata-vijidevicemotion) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/native/sensors) |\n| [`viji.device.orientation`](/native/sensors#deviceorientationdata-vijideviceorientation) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/native/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/native/external-devices#devicestate-vijidevicesi) | `DeviceState[]` | Connected external devices | [Overview](/native/external-devices) |\n| [`viji.devices[i].id`](/native/external-devices#devicestate-vijidevicesi) | `string` | Unique device identifier | [Overview](/native/external-devices) |\n| [`viji.devices[i].name`](/native/external-devices#devicestate-vijidevicesi) | `string` | User-friendly device name | [Overview](/native/external-devices) |\n| [`viji.devices[i].motion`](/native/external-devices/sensors#devicemotiondata-devicemotion) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/native/external-devices/sensors#deviceorientationdata-deviceorientation) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].video`](/native/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/native/external-devices/video) |\n| [`viji.devices[i].audio`](/native/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/native/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.videoStreams`](/native/video) | `VideoAPI[]` | Additional video streams provided by the host | [Video](/native/video) |\n| [`viji.audioStreams`](/native/audio#beyond-the-main-stream) | `AudioStreamAPI[]` | Additional audio streams provided by the host | [Audio](/native/audio) |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/native/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking: see [`viji.audio`](/native/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/native/quickstart): getting started with the Native renderer\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid\n- [P5 API Reference](/p5/api-reference): the same API in the P5 renderer\n- [Shader API Reference](/shader/api-reference): built-in uniforms for shaders"
1703
1718
  }
1704
1719
  ]
1705
1720
  },
@@ -2011,7 +2026,7 @@ export const docsApi = {
2011
2026
  "content": [
2012
2027
  {
2013
2028
  "type": "text",
2014
- "markdown": "# Parameters\n\nParameters are the primary way to give users control over your scene. You define them at the top level of your code, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Read `.value` inside `render()` to get the current state. Values update in real time as users interact with the controls.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB) |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = color.value;\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `render()`. They are registered once during initialization. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on active capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, organize parameters into logical sections and control their visibility:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [P5 Parameters](/p5/parameters): same system in the P5 renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
2029
+ "markdown": "# Parameters\n\nParameters are the primary way to give users control over your scene. You define them at the top level of your code, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Read `.value` inside `render()` to get the current state. Values update in real time as users interact with the controls.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB), optional alpha channel |\n| [Gradient](gradient/) | [`viji.gradient(default, config)`](gradient/) | `{ color, position }[]` | Color ramp editor. Sorted stops plus `at(t)` sampling. |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = color.value;\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `render()`. They are registered once during initialization. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on active capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, organize parameters into logical sections and control their visibility:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp with `at(t)` sampling\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [P5 Parameters](/p5/parameters): same system in the P5 renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
2015
2030
  }
2016
2031
  ]
2017
2032
  },
@@ -2103,6 +2118,11 @@ export const docsApi = {
2103
2118
  "level": 2,
2104
2119
  "text": "Accessing components"
2105
2120
  },
2121
+ {
2122
+ "id": "alpha",
2123
+ "level": 2,
2124
+ "text": "Alpha"
2125
+ },
2106
2126
  {
2107
2127
  "id": "related",
2108
2128
  "level": 2,
@@ -2112,7 +2132,7 @@ export const docsApi = {
2112
2132
  "content": [
2113
2133
  {
2114
2134
  "type": "text",
2115
- "markdown": "# viji.color()\n\n```\ncolor(defaultValue: ColorInput, config: ColorConfig): ColorParameter\n```\n\nCreates a color picker parameter. The host renders it as a color swatch that opens a full color picker when clicked. The returned object exposes the live value as canonical hex plus auto-derived `.rgb` and `.hsb` accessors, so artist code never has to parse hex by hand.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `ColorInput` | Yes | | Initial color. Accepts hex (3- or 6-digit), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), `'rgb(...)'` or `'hsl(...)'` strings. |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ColorParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Canonical lowercase hex `#rrggbb`. Updates in real-time. |\n| `rgb` | `{ r: number, g: number, b: number }` | Integer RGB components in `0..255`. Frozen, recomputed when `value` changes. |\n| `hsb` | `{ h: number, s: number, b: number }` | HSB components: `h` in `0..360`, `s` and `b` in `0..100`. Frozen, recomputed when `value` changes. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n`defaultValue` accepts hex strings, RGB / HSB objects, or CSS `rgb()` / `hsl()` strings; pick whichever form is most natural for the color you want. The returned object always exposes the same three accessors (`.value`, `.rgb`, `.hsb`) regardless of which form you used.\n\n```javascript\nconst bg = viji.color('#1a1a2e', { label: 'Background' }); // 6-digit hex\nconst accent = viji.color({ r: 255, g: 102, b: 0 }, { label: 'Accent', group: 'colors' }); // RGB object, 0..255\nconst tint = viji.color({ h: 180, s: 80, b: 90 }, { label: 'Tint', group: 'colors' }); // HSB object, 0..360 / 0..100\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n\n // .value: canonical hex, drop into any Canvas color property\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n // .rgb: components in 0..255, useful for rgba() with custom alpha\n const { r, g, b } = accent.rgb;\n ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.7)`;\n ctx.fillRect(viji.width * 0.1, viji.height * 0.1, viji.width * 0.4, viji.height * 0.4);\n\n // .hsb: h in 0..360, s and b in 0..100\n const { h, s, b: brightness } = tint.hsb;\n ctx.fillStyle = `hsl(${h}, ${s}%, ${brightness}%)`;\n ctx.fillRect(viji.width * 0.5, viji.height * 0.5, viji.width * 0.4, viji.height * 0.4);\n}\n```\n\nThe `.value` is always a 6-digit lowercase hex string (`#rrggbb`). You can pass it directly to `ctx.fillStyle`, `ctx.strokeStyle`, `ctx.shadowColor`, or any Canvas 2D color property.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
2135
+ "markdown": "# viji.color()\n\n```\ncolor(defaultValue: ColorInput, config: ColorConfig): ColorParameter\n```\n\nCreates a color picker parameter. The host renders it as a color swatch that opens a full color picker when clicked. The returned object exposes the live value as canonical hex plus auto-derived `.rgb` and `.hsb` accessors, so artist code never has to parse hex by hand.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `ColorInput` | Yes | | Initial color. Accepts hex (3- or 6-digit), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), `'rgb(...)'` or `'hsl(...)'` strings. |\n| `config.label` | `string` | No | parameter name | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n| `config.alpha` | `boolean` | No | `false` | Enable transparency: the value becomes `#rrggbbaa`, the picker gains an alpha slider, and alpha-bearing inputs are accepted. See [Alpha](#alpha). |\n\n## Return Value\n\nReturns a `ColorParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Canonical lowercase hex: `#rrggbb`, or `#rrggbbaa` when `alpha: true`. Updates in real-time. |\n| `rgb` | `{ r: number, g: number, b: number }` | Integer RGB components in `0..255`. Frozen, recomputed when `value` changes. |\n| `hsb` | `{ h: number, s: number, b: number }` | HSB components: `h` in `0..360`, `s` and `b` in `0..100`. Frozen, recomputed when `value` changes. |\n| `alpha` | `number` | Alpha in `0..1`. Always present: `1` on an opaque parameter, the live alpha when `alpha: true`. Recomputed with `rgb` and `hsb`. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n`defaultValue` accepts hex strings, RGB / HSB objects, or CSS `rgb()` / `hsl()` strings; pick whichever form is most natural for the color you want. The returned object always exposes the same three accessors (`.value`, `.rgb`, `.hsb`) regardless of which form you used.\n\n```javascript\nconst bg = viji.color('#1a1a2e', { label: 'Background' }); // 6-digit hex\nconst accent = viji.color({ r: 255, g: 102, b: 0 }, { label: 'Accent', group: 'colors' }); // RGB object, 0..255\nconst tint = viji.color({ h: 180, s: 80, b: 90 }, { label: 'Tint', group: 'colors' }); // HSB object, 0..360 / 0..100\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n\n // .value: canonical hex, drop into any Canvas color property\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n // .rgb: components in 0..255, useful for rgba() with custom alpha\n const { r, g, b } = accent.rgb;\n ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.7)`;\n ctx.fillRect(viji.width * 0.1, viji.height * 0.1, viji.width * 0.4, viji.height * 0.4);\n\n // .hsb: h in 0..360, s and b in 0..100\n const { h, s, b: brightness } = tint.hsb;\n ctx.fillStyle = `hsl(${h}, ${s}%, ${brightness}%)`;\n ctx.fillRect(viji.width * 0.5, viji.height * 0.5, viji.width * 0.4, viji.height * 0.4);\n}\n```\n\nThe `.value` is always a 6-digit lowercase hex string (`#rrggbb`). You can pass it directly to `ctx.fillStyle`, `ctx.strokeStyle`, `ctx.shadowColor`, or any Canvas 2D color property.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
2116
2136
  },
2117
2137
  {
2118
2138
  "type": "live-example",
@@ -2122,7 +2142,60 @@ export const docsApi = {
2122
2142
  },
2123
2143
  {
2124
2144
  "type": "text",
2125
- "markdown": "## Input forms for `defaultValue`\n\nAll five forms are equivalent and produce the same canonical hex internally:\n\n```javascript\nviji.color('#ff6600', { label: 'A' }); // 6-digit hex\nviji.color('#f60', { label: 'B' }); // 3-digit hex (auto-expanded)\nviji.color({ r: 255, g: 102, b: 0 }, { label: 'C' }); // RGB object, 0..255\nviji.color({ h: 24, s: 100, b: 100 }, { label: 'D' }); // HSB object, 0..360 / 0..100\nviji.color('rgb(255, 102, 0)', { label: 'E' }); // CSS rgb()\nviji.color('hsl(24, 100%, 50%)', { label: 'F' }); // CSS hsl()\n```\n\nThe same forms are also accepted by `core.setParameter('myColor', ...)` from the host.\n\n## Accessing components\n\nBecause `.rgb` and `.hsb` are auto-derived, you never need to parse hex manually:\n\n```javascript\nconst c = viji.color('#ff6600', { label: 'Tint' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n\n // RGB components in 0..255, perfect for rgba() with custom alpha\n const { r, g, b } = c.rgb;\n ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.5)`;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n // HSB components, useful for hue rotations or saturation effects\n const { h, s, b: brightness } = c.hsb;\n ctx.fillStyle = `hsl(${(h + 30) % 360}, ${s}%, ${brightness * 0.5}%)`;\n ctx.fillRect(0, 0, viji.width / 2, viji.height);\n}\n```\n\n`c.value`, `c.rgb`, and `c.hsb` always reflect the current picker value; they\nupdate together whenever the user changes the color.\n\n## Related\n\n- [Slider](../slider/): numeric slider parameter\n- [Toggle](../toggle/): boolean on/off parameter\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility\n- [Shader Color](/shader/parameters/color): equivalent for the Shader renderer"
2145
+ "markdown": "## Input forms for `defaultValue`\n\nAll five forms are equivalent and produce the same canonical hex internally:\n\n```javascript\nviji.color('#ff6600', { label: 'A' }); // 6-digit hex\nviji.color('#f60', { label: 'B' }); // 3-digit hex (auto-expanded)\nviji.color({ r: 255, g: 102, b: 0 }, { label: 'C' }); // RGB object, 0..255\nviji.color({ h: 24, s: 100, b: 100 }, { label: 'D' }); // HSB object, 0..360 / 0..100\nviji.color('rgb(255, 102, 0)', { label: 'E' }); // CSS rgb()\nviji.color('hsl(24, 100%, 50%)', { label: 'F' }); // CSS hsl()\n```\n\nThe same forms are also accepted by `core.setParameter('myColor', ...)` from the host.\n\n## Accessing components\n\nBecause `.rgb` and `.hsb` are auto-derived, you never need to parse hex manually:\n\n```javascript\nconst c = viji.color('#ff6600', { label: 'Tint' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n\n // RGB components in 0..255, perfect for rgba() with custom alpha\n const { r, g, b } = c.rgb;\n ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.5)`;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n // HSB components, useful for hue rotations or saturation effects\n const { h, s, b: brightness } = c.hsb;\n ctx.fillStyle = `hsl(${(h + 30) % 360}, ${s}%, ${brightness * 0.5}%)`;\n ctx.fillRect(0, 0, viji.width / 2, viji.height);\n}\n```\n\n`c.value`, `c.rgb`, and `c.hsb` always reflect the current picker value; they\nupdate together whenever the user changes the color.\n\n## Alpha\n\nPass `alpha: true` to give the color a transparency channel. The value becomes a fixed 8-digit `#rrggbbaa` (opaque inputs are padded to `ff`), the picker gains an alpha slider, and the `alpha` accessor reports the live alpha in `0..1`.\n\n```javascript\nconst glow = viji.color('#ffffff80', { label: 'Glow', alpha: true });\n// glow.value -> '#ffffff80', glow.alpha -> 0.5, glow.rgb -> { r: 255, g: 255, b: 255 }\n```\n\nOnly a color declared with `alpha: true` accepts a default that carries transparency: `#rrggbbaa`, `#rgba`, `rgba()`, `hsla()`, `vec4()`, or an object with an `a` field. Without `alpha: true`, those forms are rejected when the scene loads, even at full alpha, and the error message says what to do: add `alpha: true`, or use `rgb()`, `hsb()`, or a 6-digit hex.\n\nWithout `alpha: true` nothing changes: `value` stays `#rrggbb`, `alpha` is always `1`, and existing scenes keep working.\n\n## Related\n\n- [Slider](../slider/): numeric slider parameter\n- [Toggle](../toggle/): boolean on/off parameter\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility\n- [Gradient](../gradient/): a ramp of colors sampled with `at(t)`\n- [Shader Color](/shader/parameters/color): equivalent for the Shader renderer"
2146
+ }
2147
+ ]
2148
+ },
2149
+ "native-param-gradient": {
2150
+ "id": "native-param-gradient",
2151
+ "title": "Gradient Parameter",
2152
+ "description": "Create a color ramp editor whose stops feed canvas gradients and per-element colors.",
2153
+ "headings": [
2154
+ {
2155
+ "id": "parameters",
2156
+ "level": 2,
2157
+ "text": "Parameters"
2158
+ },
2159
+ {
2160
+ "id": "return-value",
2161
+ "level": 2,
2162
+ "text": "Return Value"
2163
+ },
2164
+ {
2165
+ "id": "usage",
2166
+ "level": 2,
2167
+ "text": "Usage"
2168
+ },
2169
+ {
2170
+ "id": "interpolation",
2171
+ "level": 2,
2172
+ "text": "Interpolation"
2173
+ },
2174
+ {
2175
+ "id": "alpha",
2176
+ "level": 2,
2177
+ "text": "Alpha"
2178
+ },
2179
+ {
2180
+ "id": "related",
2181
+ "level": 2,
2182
+ "text": "Related"
2183
+ }
2184
+ ],
2185
+ "content": [
2186
+ {
2187
+ "type": "text",
2188
+ "markdown": "# viji.gradient()\n\n```\ngradient(defaultValue: GradientInput, config: GradientConfig): GradientParameter\n```\n\nCreates a gradient parameter that the host application renders as a color ramp editor: stops can be added, moved, recolored and removed. The returned object exposes the live stop list as `value` and a sampling function `at(t)`, so scene code can feed the stops straight into a canvas gradient and pick a matching color for any element without reimplementing the interpolation.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `GradientInput` | Yes | | Initial stops: an array of `{ color, position }`. `color` accepts every form [`viji.color()`](../color/) accepts; `position` is `0..1`. Any order; normalized and sorted. |\n| `config.minStops` | `number` | No | `2` | Fewest stops the editor allows. Integer in `2..32`. |\n| `config.maxStops` | `number` | No | `8` | Most stops the editor allows. Integer in `2..32`. |\n| `config.label` | `string` | No | parameter name | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n| `config.alpha` | `boolean` | No | `false` | Per-stop transparency. When `true`, stop colors are `#rrggbbaa` and alpha-bearing inputs are accepted; when `false`, stops are `#rrggbb` and alpha-bearing inputs are rejected. |\n\n## Return Value\n\nReturns a `GradientParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `GradientValue` | Current stops, frozen and sorted ascending by `position`. Each stop is `{ color: string, position: number }` with `color` in canonical lowercase hex. Updates in real-time. |\n| `at(t)` | `(t: number) => string` | Color at `t` (clamped to `0..1`) in the same hex width as the stops. Same interpolation the canvas uses for the stops, so sampled colors match the fill. |\n| `label` | `string \\| undefined` | Display label |\n| `description` | `string` | Description text (empty string when not configured) |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\nDefine the gradient at the **top level** of your scene code, before the `render()` function. Loop over `value` to build a canvas gradient with `addColorStop`, and call `at(t)` for per-element colors.\n\n> [!NOTE]\n> Parameters must be defined at the top level, not inside `render()`. They are registered once during scene initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
2189
+ },
2190
+ {
2191
+ "type": "live-example",
2192
+ "title": "Basic Gradient",
2193
+ "sceneCode": "const ramp = viji.gradient([\n { color: '#ff6b6b', position: 0 },\n { color: '#ffd166', position: 0.5 },\n { color: '#4ecdc4', position: 1 }\n], {\n label: 'Ramp',\n description: 'Fills the band and colors each orb via at(t)',\n group: 'appearance'\n});\n\nconst orbs = viji.slider(12, {\n min: 3,\n max: 40,\n step: 1,\n label: 'Orbs',\n group: 'appearance'\n});\n\nconst speed = viji.slider(0.4, {\n min: 0,\n max: 2,\n step: 0.05,\n label: 'Speed',\n group: 'motion'\n});\n\nlet phase = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const s = Math.min(w, h);\n phase += viji.deltaTime * speed.value;\n\n ctx.fillStyle = '#1a1a2e';\n ctx.fillRect(0, 0, w, h);\n\n // The band: every stop goes straight into a canvas gradient\n const band = ctx.createLinearGradient(0, 0, w, 0);\n for (const stop of ramp.value) band.addColorStop(stop.position, stop.color);\n ctx.fillStyle = band;\n ctx.fillRect(0, h * 0.4, w, h * 0.2);\n\n // The orbs: each one samples the same ramp at its own t\n const count = orbs.value;\n for (let i = 0; i < count; i++) {\n const t = (i / (count - 1) + phase) % 1;\n const x = w * 0.08 + (w * 0.84) * (i / (count - 1));\n const y = h * 0.75 + Math.sin(phase * 6 + i) * h * 0.06;\n ctx.fillStyle = ramp.at(t);\n ctx.beginPath();\n ctx.arc(x, y, s * 0.025, 0, Math.PI * 2);\n ctx.fill();\n }\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n ctx.font = Math.round(s * 0.025) + 'px monospace';\n ctx.textAlign = 'center';\n ctx.fillText('at(0.5) = ' + ramp.at(0.5) + ' stops: ' + ramp.value.length, w / 2, h * 0.3);\n}\n",
2194
+ "sceneFile": "gradient-basic.scene.js"
2195
+ },
2196
+ {
2197
+ "type": "text",
2198
+ "markdown": "## Interpolation\n\n`at(t)` follows CSS `linear-gradient` semantics: between two stops the color is mixed in premultiplied sRGB, then rounded per channel to 8 bits. Two stops at the same position form a hard edge: everything past that position takes the later stop. Stops themselves are returned verbatim, so `at(0)` is the first stop and `at(1)` the last. When both stops of a segment are fully transparent, the color is the plain blend of their rgb values with alpha `0`.\n\nA canvas gradient built from the same stops interpolates the same way, so a shape filled with the gradient and an element colored with `at(t)` match. The ramp editor uses the same rule when you insert a stop by clicking the ramp.\n\n## Alpha\n\nWith `alpha: true` every stop carries its own transparency. Stop colors accept the alpha forms (`#rrggbbaa`, `#rgba`, `rgba()`, `hsla()`, `vec4()`, objects with `a`) and opaque inputs are padded to `ff`. `at(t)` then returns 8-digit hex, which canvas fills accept directly.\n\n```javascript\nconst fade = viji.gradient([\n { color: '#ff6b6b', position: 0 },\n { color: '#ff6b6b00', position: 1 }\n], { label: 'Fade', alpha: true });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = fade.at(0.5); // '#ff6b6b80'\n}\n```\n\nWithout `alpha: true`, a stop color that carries transparency is rejected when the scene loads. See [Color](../color/#alpha) for the accepted forms.\n\n## Related\n\n- [Color](../color/): single color with the same input grammar\n- [Slider](../slider/): numeric range, useful for driving `t`\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility based on capabilities\n- [Shader Gradient](/shader/parameters/gradient): equivalent for the Shader renderer"
2126
2199
  }
2127
2200
  ]
2128
2201
  },
@@ -4363,7 +4436,7 @@ export const docsApi = {
4363
4436
  "content": [
4364
4437
  {
4365
4438
  "type": "text",
4366
- "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your P5 scene functions. The `viji` API is identical to the [Native renderer](/native/api-reference): the difference is that P5 scenes also receive a `p5` instance as the second argument.\n\nNew to Viji P5? Start with the [Quick Start](/p5/quickstart) instead.\n\nAll types named on this page are available globally in scene code: annotate variables with them directly, with no imports or aliases. TypeScript is optional; see [TypeScript Support](/native/quickstart#typescript-support).\n\n## Entry Points\n\n```javascript\n// @renderer p5\n\nfunction setup(viji, p5) {\n // Called once when the scene starts (optional)\n}\n\nfunction render(viji, p5) {\n // Called every frame\n}\n```\n\nThe `p5` parameter is a full [P5.js](https://p5js.org/reference/) instance (v1.9.4) in instance mode. All P5 drawing methods (`p5.rect()`, `p5.fill()`, `p5.ellipse()`, etc.) are accessed through it. See [Drawing with P5](/p5/drawing) for Viji-specific drawing patterns.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/p5/canvas-resolution#vijicanvas-in-p5-scenes) | `OffscreenCanvas` | The rendering canvas | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.width`](/p5/canvas-resolution#resolution-properties) | `number` | Canvas width in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.height`](/p5/canvas-resolution#resolution-properties) | `number` | Canvas height in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n\n> [!WARNING]\n> `viji.useContext()` is **not available** in P5 scenes. The canvas and its rendering context (2D or WEBGL, depending on the directive) are managed by P5 internally. Calling `useContext()` would conflict with P5's rendering pipeline.\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/p5/timing#vijitime-oscillations--cycles) | `number` | Seconds elapsed since the scene started | [Timing](/p5/timing) |\n| [`viji.deltaTime`](/p5/timing#vijideltatime-accumulation) | `number` | Seconds since the previous frame | [Timing](/p5/timing) |\n| [`viji.frameCount`](/p5/timing#vijiframecount-vs-p5framecount) | `number` | Monotonically increasing frame counter | [Timing](/p5/timing) |\n| [`viji.fps`](/p5/timing#vijifps-target-frame-rate) | `number` | Target FPS based on the host's frame rate mode | [Timing](/p5/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/p5/parameters/slider) | `SliderParameter` | `number` | [Slider](/p5/parameters/slider) |\n| [`viji.color(default, config)`](/p5/parameters/color) | `ColorParameter` | `string` (hex) | [Color](/p5/parameters/color) |\n| [`viji.toggle(default, config)`](/p5/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/p5/parameters/toggle) |\n| [`viji.select(default, config)`](/p5/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/p5/parameters/select) |\n| [`viji.number(default, config)`](/p5/parameters/number) | `NumberParameter` | `number` | [Number](/p5/parameters/number) |\n| [`viji.text(default, config)`](/p5/parameters/text) | `TextParameter` | `string` | [Text](/p5/parameters/text) |\n| [`viji.image(null, config)`](/p5/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/p5/parameters/image) |\n| [`viji.button(config)`](/p5/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/p5/parameters/button) |\n| [`viji.coordinate(default, config)`](/p5/parameters/coordinate) | `CoordinateParameter` | 2D coordinate pad: `{ x: number, y: number }`, both -1 to 1 | [Coordinate](/p5/parameters/coordinate) |\n\n> [!NOTE]\n> Image parameters have a `.p5` property for use with `p5.image()`. See [Drawing with P5: Image Parameters](/p5/drawing) for the pattern.\n\nSee [Parameters Overview](/p5/parameters) for the declaration pattern, [Grouping](/p5/parameters/grouping) and [Categories](/p5/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/p5/audio) | `boolean` | Whether an audio source is active | [Overview](/p5/audio) |\n| [`viji.audio.volume.current`](/p5/audio/volume#property-reference) | `number` | Current RMS volume 0-1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.peak`](/p5/audio/volume#property-reference) | `number` | Peak volume 0-1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.smoothed`](/p5/audio/volume#property-reference) | `number` | Smoothed volume 0-1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.bands.low`](/p5/audio/bands#instant-bands) | `number` | Low frequency band energy (20-120 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMid`](/p5/audio/bands#instant-bands) | `number` | Low-mid band energy (120-500 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.mid`](/p5/audio/bands#instant-bands) | `number` | Mid band energy (500-2 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMid`](/p5/audio/bands#instant-bands) | `number` | High-mid band energy (2-6 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.high`](/p5/audio/bands#instant-bands) | `number` | High band energy (6-16 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed low band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed low-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed high-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed high band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.beat.kick`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Kick beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snare`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Snare beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hat`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Hi-hat beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.any`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Combined beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed kick | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed snare | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed hi-hat | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed combined | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/p5/audio/beat#triggers) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/p5/audio/beat#triggers) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/p5/audio/beat#triggers) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/p5/audio/beat#triggers) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.events`](/p5/audio/beat#events) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.bpm`](/p5/audio/beat#tempo) | `number` | Tracked BPM | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.confidence`](/p5/audio/beat#tempo) | `number` | Beat-tracker confidence 0-1 | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.isLocked`](/p5/audio/beat#tempo) | `boolean` | Whether beat tracking is locked | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.spectral.brightness`](/p5/audio/spectral#property-reference) | `number` | Spectral brightness 0-1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/p5/audio/spectral#property-reference) | `number` | Spectral flatness 0-1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/p5/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0-255) | [Frequency Data](/p5/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/p5/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/p5/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/p5/video) | `boolean` | Whether a video source is active | [Overview](/p5/video) |\n| [`viji.video.currentFrame`](/p5/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameWidth`](/p5/video/basics) | `number` | Frame width in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameHeight`](/p5/video/basics) | `number` | Frame height in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameRate`](/p5/video/basics) | `number` | Video frame rate | [Video Basics](/p5/video/basics) |\n| [`viji.video.getFrameData()`](/p5/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.cv.faces`](/p5/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.cv.hands`](/p5/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.cv.pose`](/p5/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.cv.segmentation`](/p5/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/p5/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/p5/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/p5/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/p5/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/p5/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/p5/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/p5/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/p5/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/p5/video) | `() => CVFeature[]` | List of active CV features | [Overview](/p5/video) |\n| [`viji.video.cv.isProcessing()`](/p5/video) | `() => boolean` | Whether CV is currently processing | [Overview](/p5/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/p5/mouse#position) | `number` | Cursor X position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.y`](/p5/mouse#position) | `number` | Cursor Y position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.isInCanvas`](/p5/mouse#position) | `boolean` | Whether cursor is inside the canvas | [Mouse](/p5/mouse) |\n| [`viji.mouse.isPressed`](/p5/mouse#buttons) | `boolean` | Whether any button is pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.leftButton`](/p5/mouse#buttons) | `boolean` | Left button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.rightButton`](/p5/mouse#buttons) | `boolean` | Right button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.middleButton`](/p5/mouse#buttons) | `boolean` | Middle button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaX`](/p5/mouse#movement) | `number` | Horizontal movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaY`](/p5/mouse#movement) | `number` | Vertical movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelDelta`](/p5/mouse#scroll-wheel) | `number` | Scroll wheel delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelX`](/p5/mouse#scroll-wheel) | `number` | Horizontal scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelY`](/p5/mouse#scroll-wheel) | `number` | Vertical scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasPressed`](/p5/mouse#frame-events) | `boolean` | True for one frame when pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasReleased`](/p5/mouse#frame-events) | `boolean` | True for one frame when released | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasMoved`](/p5/mouse#frame-events) | `boolean` | True for one frame when moved | [Mouse](/p5/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/p5/keyboard#methods) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/p5/keyboard#methods) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/p5/keyboard#methods) | `(string) => boolean` | True for one frame when released | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.activeKeys`](/p5/keyboard#properties) | `Set<string>` | All currently held keys | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/p5/keyboard#properties) | `Set<string>` | Keys pressed this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/p5/keyboard#properties) | `Set<string>` | Keys released this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/p5/keyboard#properties) | `string` | Most recently pressed key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/p5/keyboard#properties) | `string` | Most recently released key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.shift`](/p5/keyboard#modifier-keys) | `boolean` | Shift key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.ctrl`](/p5/keyboard#modifier-keys) | `boolean` | Ctrl/Cmd key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.alt`](/p5/keyboard#modifier-keys) | `boolean` | Alt/Option key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.meta`](/p5/keyboard#modifier-keys) | `boolean` | Meta/Win key state | [Keyboard](/p5/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | All active touch points | [Touch](/p5/touch) |\n| [`viji.touches.count`](/p5/touch#touchapi-vijitouches) | `number` | Number of active touches | [Touch](/p5/touch) |\n| [`viji.touches.started`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that started this frame | [Touch](/p5/touch) |\n| [`viji.touches.moved`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/p5/touch) |\n| [`viji.touches.ended`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/p5/touch) |\n| [`viji.touches.primary`](/p5/touch#touchapi-vijitouches) | `TouchPoint \\| null` | The first active touch point | [Touch](/p5/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/p5/pointer#api-reference) | `number` | Primary pointer X position | [Pointer](/p5/pointer) |\n| [`viji.pointer.y`](/p5/pointer#api-reference) | `number` | Primary pointer Y position | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaX`](/p5/pointer#api-reference) | `number` | Horizontal movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaY`](/p5/pointer#api-reference) | `number` | Vertical movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.isDown`](/p5/pointer#api-reference) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasPressed`](/p5/pointer#api-reference) | `boolean` | True for one frame when pressed | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasReleased`](/p5/pointer#api-reference) | `boolean` | True for one frame when released | [Pointer](/p5/pointer) |\n| [`viji.pointer.isInCanvas`](/p5/pointer#api-reference) | `boolean` | Whether pointer is inside the canvas | [Pointer](/p5/pointer) |\n| [`viji.pointer.type`](/p5/pointer#api-reference) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/p5/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/p5/sensors#devicemotiondata-vijidevicemotion) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/p5/sensors) |\n| [`viji.device.orientation`](/p5/sensors#deviceorientationdata-vijideviceorientation) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/p5/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/p5/external-devices#devicestate-vijidevicesi) | `DeviceState[]` | Connected external devices | [Overview](/p5/external-devices) |\n| [`viji.devices[i].id`](/p5/external-devices#devicestate-vijidevicesi) | `string` | Unique device identifier | [Overview](/p5/external-devices) |\n| [`viji.devices[i].name`](/p5/external-devices#devicestate-vijidevicesi) | `string` | User-friendly device name | [Overview](/p5/external-devices) |\n| [`viji.devices[i].motion`](/p5/external-devices/sensors#devicemotiondata-devicemotion) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/p5/external-devices/sensors#deviceorientationdata-deviceorientation) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].video`](/p5/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/p5/external-devices/video) |\n| [`viji.devices[i].audio`](/p5/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/p5/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.videoStreams`](/p5/video) | `VideoAPI[]` | Additional video streams provided by the host | [Video](/p5/video) |\n| [`viji.audioStreams`](/p5/audio#beyond-the-main-stream) | `AudioStreamAPI[]` | Additional audio streams provided by the host | [Audio](/p5/audio) |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/p5/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking: see [`viji.audio`](/p5/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/p5/quickstart): getting started with the P5 renderer\n- [Scene Structure](/p5/scene-structure): setup/render pattern and instance mode\n- [Drawing with P5](/p5/drawing): Viji-specific drawing patterns\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers\n- [Native API Reference](/native/api-reference): the same API in the Native renderer\n- [Shader API Reference](/shader/api-reference): built-in uniforms for shaders\n- [P5.js Reference](https://p5js.org/reference/): official P5.js documentation"
4439
+ "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your P5 scene functions. The `viji` API is identical to the [Native renderer](/native/api-reference): the difference is that P5 scenes also receive a `p5` instance as the second argument.\n\nNew to Viji P5? Start with the [Quick Start](/p5/quickstart) instead.\n\nAll types named on this page are available globally in scene code: annotate variables with them directly, with no imports or aliases. TypeScript is optional; see [TypeScript Support](/native/quickstart#typescript-support).\n\n## Entry Points\n\n```javascript\n// @renderer p5\n\nfunction setup(viji, p5) {\n // Called once when the scene starts (optional)\n}\n\nfunction render(viji, p5) {\n // Called every frame\n}\n```\n\nThe `p5` parameter is a full [P5.js](https://p5js.org/reference/) instance (v1.9.4) in instance mode. All P5 drawing methods (`p5.rect()`, `p5.fill()`, `p5.ellipse()`, etc.) are accessed through it. See [Drawing with P5](/p5/drawing) for Viji-specific drawing patterns.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/p5/canvas-resolution#vijicanvas-in-p5-scenes) | `OffscreenCanvas` | The rendering canvas | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.width`](/p5/canvas-resolution#resolution-properties) | `number` | Canvas width in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.height`](/p5/canvas-resolution#resolution-properties) | `number` | Canvas height in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n\n> [!WARNING]\n> `viji.useContext()` is **not available** in P5 scenes. The canvas and its rendering context (2D or WEBGL, depending on the directive) are managed by P5 internally. Calling `useContext()` would conflict with P5's rendering pipeline.\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/p5/timing#vijitime-oscillations--cycles) | `number` | Seconds elapsed since the scene started | [Timing](/p5/timing) |\n| [`viji.deltaTime`](/p5/timing#vijideltatime-accumulation) | `number` | Seconds since the previous frame | [Timing](/p5/timing) |\n| [`viji.frameCount`](/p5/timing#vijiframecount-vs-p5framecount) | `number` | Monotonically increasing frame counter | [Timing](/p5/timing) |\n| [`viji.fps`](/p5/timing#vijifps-target-frame-rate) | `number` | Target FPS based on the host's frame rate mode | [Timing](/p5/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/p5/parameters/slider) | `SliderParameter` | `number` | [Slider](/p5/parameters/slider) |\n| [`viji.color(default, config)`](/p5/parameters/color) | `ColorParameter` | `string` (hex; `#rrggbbaa` with `alpha`) | [Color](/p5/parameters/color) |\n| [`viji.gradient(default, config)`](/p5/parameters/gradient) | `GradientParameter` | Sorted `{ color, position }[]` plus `at(t)` | [Gradient](/p5/parameters/gradient) |\n| [`viji.toggle(default, config)`](/p5/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/p5/parameters/toggle) |\n| [`viji.select(default, config)`](/p5/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/p5/parameters/select) |\n| [`viji.number(default, config)`](/p5/parameters/number) | `NumberParameter` | `number` | [Number](/p5/parameters/number) |\n| [`viji.text(default, config)`](/p5/parameters/text) | `TextParameter` | `string` | [Text](/p5/parameters/text) |\n| [`viji.image(null, config)`](/p5/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/p5/parameters/image) |\n| [`viji.button(config)`](/p5/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/p5/parameters/button) |\n| [`viji.coordinate(default, config)`](/p5/parameters/coordinate) | `CoordinateParameter` | 2D coordinate pad: `{ x: number, y: number }`, both -1 to 1 | [Coordinate](/p5/parameters/coordinate) |\n\n> [!NOTE]\n> Image parameters have a `.p5` property for use with `p5.image()`. See [Drawing with P5: Image Parameters](/p5/drawing) for the pattern.\n\nSee [Parameters Overview](/p5/parameters) for the declaration pattern, [Grouping](/p5/parameters/grouping) and [Categories](/p5/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/p5/audio) | `boolean` | Whether an audio source is active | [Overview](/p5/audio) |\n| [`viji.audio.volume.current`](/p5/audio/volume#property-reference) | `number` | Current RMS volume 0-1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.peak`](/p5/audio/volume#property-reference) | `number` | Peak volume 0-1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.smoothed`](/p5/audio/volume#property-reference) | `number` | Smoothed volume 0-1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.bands.low`](/p5/audio/bands#instant-bands) | `number` | Low frequency band energy (20-120 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMid`](/p5/audio/bands#instant-bands) | `number` | Low-mid band energy (120-500 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.mid`](/p5/audio/bands#instant-bands) | `number` | Mid band energy (500-2 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMid`](/p5/audio/bands#instant-bands) | `number` | High-mid band energy (2-6 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.high`](/p5/audio/bands#instant-bands) | `number` | High band energy (6-16 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed low band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed low-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed high-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/p5/audio/bands#smoothed-bands) | `number` | Smoothed high band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.beat.kick`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Kick beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snare`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Snare beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hat`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Hi-hat beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.any`](/p5/audio/beat#energy-curves-fast-decay) | `number` | Combined beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed kick | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed snare | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed hi-hat | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/p5/audio/beat#energy-curves-smoothed) | `number` | Smoothed combined | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/p5/audio/beat#triggers) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/p5/audio/beat#triggers) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/p5/audio/beat#triggers) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/p5/audio/beat#triggers) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.events`](/p5/audio/beat#events) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.bpm`](/p5/audio/beat#tempo) | `number` | Tracked BPM | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.confidence`](/p5/audio/beat#tempo) | `number` | Beat-tracker confidence 0-1 | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.isLocked`](/p5/audio/beat#tempo) | `boolean` | Whether beat tracking is locked | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.spectral.brightness`](/p5/audio/spectral#property-reference) | `number` | Spectral brightness 0-1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/p5/audio/spectral#property-reference) | `number` | Spectral flatness 0-1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/p5/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0-255) | [Frequency Data](/p5/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/p5/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/p5/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/p5/video) | `boolean` | Whether a video source is active | [Overview](/p5/video) |\n| [`viji.video.currentFrame`](/p5/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameWidth`](/p5/video/basics) | `number` | Frame width in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameHeight`](/p5/video/basics) | `number` | Frame height in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameRate`](/p5/video/basics) | `number` | Video frame rate | [Video Basics](/p5/video/basics) |\n| [`viji.video.getFrameData()`](/p5/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.cv.faces`](/p5/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.cv.hands`](/p5/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.cv.pose`](/p5/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.cv.segmentation`](/p5/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/p5/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/p5/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/p5/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/p5/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/p5/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/p5/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/p5/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/p5/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/p5/video) | `() => CVFeature[]` | List of active CV features | [Overview](/p5/video) |\n| [`viji.video.cv.isProcessing()`](/p5/video) | `() => boolean` | Whether CV is currently processing | [Overview](/p5/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/p5/mouse#position) | `number` | Cursor X position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.y`](/p5/mouse#position) | `number` | Cursor Y position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.isInCanvas`](/p5/mouse#position) | `boolean` | Whether cursor is inside the canvas | [Mouse](/p5/mouse) |\n| [`viji.mouse.isPressed`](/p5/mouse#buttons) | `boolean` | Whether any button is pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.leftButton`](/p5/mouse#buttons) | `boolean` | Left button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.rightButton`](/p5/mouse#buttons) | `boolean` | Right button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.middleButton`](/p5/mouse#buttons) | `boolean` | Middle button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaX`](/p5/mouse#movement) | `number` | Horizontal movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaY`](/p5/mouse#movement) | `number` | Vertical movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelDelta`](/p5/mouse#scroll-wheel) | `number` | Scroll wheel delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelX`](/p5/mouse#scroll-wheel) | `number` | Horizontal scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelY`](/p5/mouse#scroll-wheel) | `number` | Vertical scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasPressed`](/p5/mouse#frame-events) | `boolean` | True for one frame when pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasReleased`](/p5/mouse#frame-events) | `boolean` | True for one frame when released | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasMoved`](/p5/mouse#frame-events) | `boolean` | True for one frame when moved | [Mouse](/p5/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/p5/keyboard#methods) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/p5/keyboard#methods) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/p5/keyboard#methods) | `(string) => boolean` | True for one frame when released | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.activeKeys`](/p5/keyboard#properties) | `Set<string>` | All currently held keys | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/p5/keyboard#properties) | `Set<string>` | Keys pressed this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/p5/keyboard#properties) | `Set<string>` | Keys released this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/p5/keyboard#properties) | `string` | Most recently pressed key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/p5/keyboard#properties) | `string` | Most recently released key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.shift`](/p5/keyboard#modifier-keys) | `boolean` | Shift key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.ctrl`](/p5/keyboard#modifier-keys) | `boolean` | Ctrl/Cmd key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.alt`](/p5/keyboard#modifier-keys) | `boolean` | Alt/Option key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.meta`](/p5/keyboard#modifier-keys) | `boolean` | Meta/Win key state | [Keyboard](/p5/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | All active touch points | [Touch](/p5/touch) |\n| [`viji.touches.count`](/p5/touch#touchapi-vijitouches) | `number` | Number of active touches | [Touch](/p5/touch) |\n| [`viji.touches.started`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that started this frame | [Touch](/p5/touch) |\n| [`viji.touches.moved`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/p5/touch) |\n| [`viji.touches.ended`](/p5/touch#touchapi-vijitouches) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/p5/touch) |\n| [`viji.touches.primary`](/p5/touch#touchapi-vijitouches) | `TouchPoint \\| null` | The first active touch point | [Touch](/p5/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/p5/pointer#api-reference) | `number` | Primary pointer X position | [Pointer](/p5/pointer) |\n| [`viji.pointer.y`](/p5/pointer#api-reference) | `number` | Primary pointer Y position | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaX`](/p5/pointer#api-reference) | `number` | Horizontal movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaY`](/p5/pointer#api-reference) | `number` | Vertical movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.isDown`](/p5/pointer#api-reference) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasPressed`](/p5/pointer#api-reference) | `boolean` | True for one frame when pressed | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasReleased`](/p5/pointer#api-reference) | `boolean` | True for one frame when released | [Pointer](/p5/pointer) |\n| [`viji.pointer.isInCanvas`](/p5/pointer#api-reference) | `boolean` | Whether pointer is inside the canvas | [Pointer](/p5/pointer) |\n| [`viji.pointer.type`](/p5/pointer#api-reference) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/p5/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/p5/sensors#devicemotiondata-vijidevicemotion) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/p5/sensors) |\n| [`viji.device.orientation`](/p5/sensors#deviceorientationdata-vijideviceorientation) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/p5/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/p5/external-devices#devicestate-vijidevicesi) | `DeviceState[]` | Connected external devices | [Overview](/p5/external-devices) |\n| [`viji.devices[i].id`](/p5/external-devices#devicestate-vijidevicesi) | `string` | Unique device identifier | [Overview](/p5/external-devices) |\n| [`viji.devices[i].name`](/p5/external-devices#devicestate-vijidevicesi) | `string` | User-friendly device name | [Overview](/p5/external-devices) |\n| [`viji.devices[i].motion`](/p5/external-devices/sensors#devicemotiondata-devicemotion) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/p5/external-devices/sensors#deviceorientationdata-deviceorientation) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].video`](/p5/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/p5/external-devices/video) |\n| [`viji.devices[i].audio`](/p5/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/p5/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.videoStreams`](/p5/video) | `VideoAPI[]` | Additional video streams provided by the host | [Video](/p5/video) |\n| [`viji.audioStreams`](/p5/audio#beyond-the-main-stream) | `AudioStreamAPI[]` | Additional audio streams provided by the host | [Audio](/p5/audio) |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/p5/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking: see [`viji.audio`](/p5/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/p5/quickstart): getting started with the P5 renderer\n- [Scene Structure](/p5/scene-structure): setup/render pattern and instance mode\n- [Drawing with P5](/p5/drawing): Viji-specific drawing patterns\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers\n- [Native API Reference](/native/api-reference): the same API in the Native renderer\n- [Shader API Reference](/shader/api-reference): built-in uniforms for shaders\n- [P5.js Reference](https://p5js.org/reference/): official P5.js documentation"
4367
4440
  }
4368
4441
  ]
4369
4442
  },
@@ -4781,7 +4854,7 @@ export const docsApi = {
4781
4854
  "content": [
4782
4855
  {
4783
4856
  "type": "text",
4784
- "markdown": "# Parameters\n\nParameters give users real-time control over your P5 scene. Define them at the top level, and Viji renders corresponding UI controls in the host application. Read `.value` inside `render()` to get the current state.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB) |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures (use `.p5` with P5 drawing functions) |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// @renderer p5\n\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji, p5) {\n const r = parseInt(color.value.slice(1, 3), 16);\n const g = parseInt(color.value.slice(3, 5), 16);\n const b = parseInt(color.value.slice(5, 7), 16);\n p5.fill(r, g, b);\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Declaring them inside `setup()` would register the parameter too late: no UI control would appear. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default.\n\n## Image Parameters in P5\n\nWhen using [`viji.image()`](image/) with P5 drawing functions, use the `.p5` property instead of `.value`:\n\n```javascript\nconst photo = viji.image(null, { label: 'Photo' });\n\nfunction render(viji, p5) {\n if (photo.value) {\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nThe `.p5` property wraps the raw image data in a P5-compatible object. Use `.value` to check if an image is loaded, and `.p5` when passing to P5 drawing functions.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Image](image/): image parameters with the `.p5` property\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [Native Parameters](/native/parameters): same system in the native renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
4857
+ "markdown": "# Parameters\n\nParameters give users real-time control over your P5 scene. Define them at the top level, and Viji renders corresponding UI controls in the host application. Read `.value` inside `render()` to get the current state.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB), optional alpha channel |\n| [Gradient](gradient/) | [`viji.gradient(default, config)`](gradient/) | `{ color, position }[]` | Color ramp editor. Sorted stops plus `at(t)` sampling. |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures (use `.p5` with P5 drawing functions) |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// @renderer p5\n\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji, p5) {\n const r = parseInt(color.value.slice(1, 3), 16);\n const g = parseInt(color.value.slice(3, 5), 16);\n const b = parseInt(color.value.slice(5, 7), 16);\n p5.fill(r, g, b);\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Declaring them inside `setup()` would register the parameter too late: no UI control would appear. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default.\n\n## Image Parameters in P5\n\nWhen using [`viji.image()`](image/) with P5 drawing functions, use the `.p5` property instead of `.value`:\n\n```javascript\nconst photo = viji.image(null, { label: 'Photo' });\n\nfunction render(viji, p5) {\n if (photo.value) {\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nThe `.p5` property wraps the raw image data in a P5-compatible object. Use `.value` to check if an image is loaded, and `.p5` when passing to P5 drawing functions.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp with `at(t)` sampling\n- [Image](image/): image parameters with the `.p5` property\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [Native Parameters](/native/parameters): same system in the native renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
4785
4858
  }
4786
4859
  ]
4787
4860
  },
@@ -4873,6 +4946,11 @@ export const docsApi = {
4873
4946
  "level": 2,
4874
4947
  "text": "Working with P5 color modes"
4875
4948
  },
4949
+ {
4950
+ "id": "alpha",
4951
+ "level": 2,
4952
+ "text": "Alpha"
4953
+ },
4876
4954
  {
4877
4955
  "id": "related",
4878
4956
  "level": 2,
@@ -4882,7 +4960,7 @@ export const docsApi = {
4882
4960
  "content": [
4883
4961
  {
4884
4962
  "type": "text",
4885
- "markdown": "# viji.color()\n\n```\ncolor(defaultValue: ColorInput, config: ColorConfig): ColorParameter\n```\n\nCreates a color picker parameter. The host renders it as a color swatch that opens a full color picker when clicked. The returned object exposes the live value as canonical hex plus auto-derived `.rgb` and `.hsb` accessors that map cleanly onto P5's color modes.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `ColorInput` | Yes | | Initial color. Accepts hex (3- or 6-digit), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), `'rgb(...)'` or `'hsl(...)'` strings. |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ColorParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Canonical lowercase hex `#rrggbb`. Updates in real-time. |\n| `rgb` | `{ r: number, g: number, b: number }` | Integer RGB components in `0..255`. Frozen, recomputed when `value` changes. |\n| `hsb` | `{ h: number, s: number, b: number }` | HSB components: `h` in `0..360`, `s` and `b` in `0..100`. Frozen, recomputed when `value` changes. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n`defaultValue` accepts hex strings, RGB / HSB objects, or CSS `rgb()` / `hsl()` strings; pick whichever form is most natural for the color you want. The returned object always exposes the same three accessors (`.value`, `.rgb`, `.hsb`) regardless of which form you used.\n\n```javascript\nconst bg = viji.color('#1a1a2e', { label: 'Background' }); // 6-digit hex\nconst accent = viji.color({ r: 255, g: 102, b: 0 }, { label: 'Accent', group: 'colors' }); // RGB object, 0..255\nconst tint = viji.color({ h: 200, s: 80, b: 90 }, { label: 'Tint', group: 'colors' }); // HSB object, 0..360 / 0..100\n\nfunction render(viji, p5) {\n p5.noStroke();\n\n // .value: hex string, accepted directly by p5.background / p5.fill / p5.stroke\n p5.background(bg.value);\n p5.fill(accent.value);\n p5.ellipse(p5.width * 0.3, p5.height / 2, p5.width * 0.3);\n\n // .rgb: components in 0..255, lines up with default colorMode(RGB, 255), so pass them straight through (last arg is alpha)\n const { r, g, b } = tint.rgb;\n p5.fill(r, g, b, 180);\n p5.ellipse(p5.width * 0.7, p5.height / 2, p5.width * 0.3);\n}\n```\n\nThe `.value` is always a 6-digit lowercase hex string (`#rrggbb`). P5.js accepts hex strings directly in `p5.fill()`, `p5.stroke()`, and `p5.background()`. See [Working with P5 color modes](#working-with-p5-color-modes) below for using `.hsb` together with `colorMode(HSB, ...)`.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Defining them inside `setup()` would register the parameter too late: no UI control would appear. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default."
4963
+ "markdown": "# viji.color()\n\n```\ncolor(defaultValue: ColorInput, config: ColorConfig): ColorParameter\n```\n\nCreates a color picker parameter. The host renders it as a color swatch that opens a full color picker when clicked. The returned object exposes the live value as canonical hex plus auto-derived `.rgb` and `.hsb` accessors that map cleanly onto P5's color modes.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `ColorInput` | Yes | | Initial color. Accepts hex (3- or 6-digit), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), `'rgb(...)'` or `'hsl(...)'` strings. |\n| `config.label` | `string` | No | parameter name | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n| `config.alpha` | `boolean` | No | `false` | Enable transparency: the value becomes `#rrggbbaa`, the picker gains an alpha slider, and alpha-bearing inputs are accepted. See [Alpha](#alpha). |\n\n## Return Value\n\nReturns a `ColorParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Canonical lowercase hex: `#rrggbb`, or `#rrggbbaa` when `alpha: true`. Updates in real-time. |\n| `rgb` | `{ r: number, g: number, b: number }` | Integer RGB components in `0..255`. Frozen, recomputed when `value` changes. |\n| `hsb` | `{ h: number, s: number, b: number }` | HSB components: `h` in `0..360`, `s` and `b` in `0..100`. Frozen, recomputed when `value` changes. |\n| `alpha` | `number` | Alpha in `0..1`. Always present: `1` on an opaque parameter, the live alpha when `alpha: true`. Recomputed with `rgb` and `hsb`. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n`defaultValue` accepts hex strings, RGB / HSB objects, or CSS `rgb()` / `hsl()` strings; pick whichever form is most natural for the color you want. The returned object always exposes the same three accessors (`.value`, `.rgb`, `.hsb`) regardless of which form you used.\n\n```javascript\nconst bg = viji.color('#1a1a2e', { label: 'Background' }); // 6-digit hex\nconst accent = viji.color({ r: 255, g: 102, b: 0 }, { label: 'Accent', group: 'colors' }); // RGB object, 0..255\nconst tint = viji.color({ h: 200, s: 80, b: 90 }, { label: 'Tint', group: 'colors' }); // HSB object, 0..360 / 0..100\n\nfunction render(viji, p5) {\n p5.noStroke();\n\n // .value: hex string, accepted directly by p5.background / p5.fill / p5.stroke\n p5.background(bg.value);\n p5.fill(accent.value);\n p5.ellipse(p5.width * 0.3, p5.height / 2, p5.width * 0.3);\n\n // .rgb: components in 0..255, lines up with default colorMode(RGB, 255), so pass them straight through (last arg is alpha)\n const { r, g, b } = tint.rgb;\n p5.fill(r, g, b, 180);\n p5.ellipse(p5.width * 0.7, p5.height / 2, p5.width * 0.3);\n}\n```\n\nThe `.value` is always a 6-digit lowercase hex string (`#rrggbb`). P5.js accepts hex strings directly in `p5.fill()`, `p5.stroke()`, and `p5.background()`. See [Working with P5 color modes](#working-with-p5-color-modes) below for using `.hsb` together with `colorMode(HSB, ...)`.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Defining them inside `setup()` would register the parameter too late: no UI control would appear. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default."
4886
4964
  },
4887
4965
  {
4888
4966
  "type": "live-example",
@@ -4892,7 +4970,60 @@ export const docsApi = {
4892
4970
  },
4893
4971
  {
4894
4972
  "type": "text",
4895
- "markdown": "## Input forms for `defaultValue`\n\n```javascript\nviji.color('#ff6600', { label: 'A' }); // 6-digit hex\nviji.color('#f60', { label: 'B' }); // 3-digit hex (auto-expanded)\nviji.color({ r: 255, g: 102, b: 0 }, { label: 'C' }); // RGB object, 0..255\nviji.color({ h: 24, s: 100, b: 100 }, { label: 'D' }); // HSB object, 0..360 / 0..100\nviji.color('rgb(255, 102, 0)', { label: 'E' }); // CSS rgb()\nviji.color('hsl(24, 100%, 50%)', { label: 'F' }); // CSS hsl()\n```\n\nThe same forms are accepted by `core.setParameter('myColor', ...)` from the host.\n\n## Working with P5 color modes\n\nThe component ranges of `.rgb` and `.hsb` align with P5's defaults for `colorMode(RGB, 255)` and `colorMode(HSB, 360, 100, 100)`, so you can pass them straight through:\n\n```javascript\nconst c = viji.color('#ff6600', { label: 'Tint' });\n\nfunction render(viji, p5) {\n // RGB with custom alpha (defaults to colorMode(RGB, 255))\n const { r, g, b } = c.rgb;\n p5.fill(r, g, b, 128);\n p5.rect(0, 0, p5.width / 2, p5.height);\n\n // HSB with rotated hue: switch to HSB mode and feed components directly\n p5.colorMode(p5.HSB, 360, 100, 100);\n const { h, s, b: brightness } = c.hsb;\n p5.fill((h + 60) % 360, s, brightness);\n p5.rect(p5.width / 2, 0, p5.width / 2, p5.height);\n p5.colorMode(p5.RGB, 255); // restore default for the rest of the frame\n}\n```\n\n`c.value`, `c.rgb`, and `c.hsb` always reflect the current picker value; they update together whenever the user changes the color.\n\n## Related\n\n- [Slider](../slider/): numeric slider parameter\n- [Toggle](../toggle/): boolean on/off parameter\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility\n- [Native Color](/native/parameters/color): equivalent for the Native renderer\n- [Shader Color](/shader/parameters/color): equivalent for the Shader renderer"
4973
+ "markdown": "## Input forms for `defaultValue`\n\n```javascript\nviji.color('#ff6600', { label: 'A' }); // 6-digit hex\nviji.color('#f60', { label: 'B' }); // 3-digit hex (auto-expanded)\nviji.color({ r: 255, g: 102, b: 0 }, { label: 'C' }); // RGB object, 0..255\nviji.color({ h: 24, s: 100, b: 100 }, { label: 'D' }); // HSB object, 0..360 / 0..100\nviji.color('rgb(255, 102, 0)', { label: 'E' }); // CSS rgb()\nviji.color('hsl(24, 100%, 50%)', { label: 'F' }); // CSS hsl()\n```\n\nThe same forms are accepted by `core.setParameter('myColor', ...)` from the host.\n\n## Working with P5 color modes\n\nThe component ranges of `.rgb` and `.hsb` align with P5's defaults for `colorMode(RGB, 255)` and `colorMode(HSB, 360, 100, 100)`, so you can pass them straight through:\n\n```javascript\nconst c = viji.color('#ff6600', { label: 'Tint' });\n\nfunction render(viji, p5) {\n // RGB with custom alpha (defaults to colorMode(RGB, 255))\n const { r, g, b } = c.rgb;\n p5.fill(r, g, b, 128);\n p5.rect(0, 0, p5.width / 2, p5.height);\n\n // HSB with rotated hue: switch to HSB mode and feed components directly\n p5.colorMode(p5.HSB, 360, 100, 100);\n const { h, s, b: brightness } = c.hsb;\n p5.fill((h + 60) % 360, s, brightness);\n p5.rect(p5.width / 2, 0, p5.width / 2, p5.height);\n p5.colorMode(p5.RGB, 255); // restore default for the rest of the frame\n}\n```\n\n`c.value`, `c.rgb`, and `c.hsb` always reflect the current picker value; they update together whenever the user changes the color.\n\n## Alpha\n\nPass `alpha: true` to give the color a transparency channel. The value becomes a fixed 8-digit `#rrggbbaa` (opaque inputs are padded to `ff`), the picker gains an alpha slider, and the `alpha` accessor reports the live alpha in `0..1`.\n\n```javascript\nconst glow = viji.color('#ffffff80', { label: 'Glow', alpha: true });\n// glow.value -> '#ffffff80', glow.alpha -> 0.5, glow.rgb -> { r: 255, g: 255, b: 255 }\n```\n\nOnly a color declared with `alpha: true` accepts a default that carries transparency: `#rrggbbaa`, `#rgba`, `rgba()`, `hsla()`, `vec4()`, or an object with an `a` field. Without `alpha: true`, those forms are rejected when the scene loads, even at full alpha, and the error message says what to do: add `alpha: true`, or use `rgb()`, `hsb()`, or a 6-digit hex.\n\nWithout `alpha: true` nothing changes: `value` stays `#rrggbb`, `alpha` is always `1`, and existing scenes keep working.\n\n## Related\n\n- [Slider](../slider/): numeric slider parameter\n- [Toggle](../toggle/): boolean on/off parameter\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility\n- [Native Color](/native/parameters/color): equivalent for the Native renderer\n- [Gradient](../gradient/): a ramp of colors sampled with `at(t)`\n- [Shader Color](/shader/parameters/color): equivalent for the Shader renderer"
4974
+ }
4975
+ ]
4976
+ },
4977
+ "p5-param-gradient": {
4978
+ "id": "p5-param-gradient",
4979
+ "title": "Gradient Parameter",
4980
+ "description": "Create a color ramp editor whose stops color P5 fills and strokes per element.",
4981
+ "headings": [
4982
+ {
4983
+ "id": "parameters",
4984
+ "level": 2,
4985
+ "text": "Parameters"
4986
+ },
4987
+ {
4988
+ "id": "return-value",
4989
+ "level": 2,
4990
+ "text": "Return Value"
4991
+ },
4992
+ {
4993
+ "id": "usage",
4994
+ "level": 2,
4995
+ "text": "Usage"
4996
+ },
4997
+ {
4998
+ "id": "interpolation",
4999
+ "level": 2,
5000
+ "text": "Interpolation"
5001
+ },
5002
+ {
5003
+ "id": "alpha",
5004
+ "level": 2,
5005
+ "text": "Alpha"
5006
+ },
5007
+ {
5008
+ "id": "related",
5009
+ "level": 2,
5010
+ "text": "Related"
5011
+ }
5012
+ ],
5013
+ "content": [
5014
+ {
5015
+ "type": "text",
5016
+ "markdown": "# viji.gradient()\n\n```\ngradient(defaultValue: GradientInput, config: GradientConfig): GradientParameter\n```\n\nCreates a gradient parameter that the host application renders as a color ramp editor: stops can be added, moved, recolored and removed. The returned object exposes the live stop list as `value` and a sampling function `at(t)` that returns a hex string P5 accepts directly in `fill()` and `stroke()`, so every particle, bar or segment can take its color from one editable ramp.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `GradientInput` | Yes | | Initial stops: an array of `{ color, position }`. `color` accepts every form [`viji.color()`](../color/) accepts; `position` is `0..1`. Any order; normalized and sorted. |\n| `config.minStops` | `number` | No | `2` | Fewest stops the editor allows. Integer in `2..32`. |\n| `config.maxStops` | `number` | No | `8` | Most stops the editor allows. Integer in `2..32`. |\n| `config.label` | `string` | No | parameter name | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n| `config.alpha` | `boolean` | No | `false` | Per-stop transparency. When `true`, stop colors are `#rrggbbaa` and alpha-bearing inputs are accepted; when `false`, stops are `#rrggbb` and alpha-bearing inputs are rejected. |\n\n## Return Value\n\nReturns a `GradientParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `GradientValue` | Current stops, frozen and sorted ascending by `position`. Each stop is `{ color: string, position: number }` with `color` in canonical lowercase hex. Updates in real-time. |\n| `at(t)` | `(t: number) => string` | Color at `t` (clamped to `0..1`) in the same hex width as the stops. Pass it to `p5.fill()` or `p5.stroke()`. |\n| `label` | `string \\| undefined` | Display label |\n| `description` | `string` | Description text (empty string when not configured) |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\nDefine the gradient at the **top level** of your sketch, before `render()`. Call `at(t)` inside `render()` wherever an element needs its color; P5 parses the returned hex string, 6- or 8-digit.\n\n> [!NOTE]\n> Parameters must be defined at the top level, not inside `render()`. They are registered once during scene initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
5017
+ },
5018
+ {
5019
+ "type": "live-example",
5020
+ "title": "Gradient Bars",
5021
+ "sceneCode": "// @renderer p5\n\nconst ramp = viji.gradient([\n { color: '#ed225d', position: 0 },\n { color: '#ffd166', position: 0.5 },\n { color: '#667eea', position: 1 }\n], {\n label: 'Ramp',\n description: 'Each bar samples at(t) at its own position',\n group: 'appearance'\n});\n\nconst bars = viji.slider(24, {\n min: 4,\n max: 64,\n step: 1,\n label: 'Bars',\n group: 'appearance'\n});\n\nconst wave = viji.slider(1, {\n min: 0,\n max: 3,\n step: 0.1,\n label: 'Wave Speed',\n group: 'motion'\n});\n\nlet phase = 0;\n\nfunction render(viji, p5) {\n p5.background(26, 26, 46);\n phase += viji.deltaTime * wave.value;\n\n const count = bars.value;\n const gap = p5.width / count;\n const s = Math.min(p5.width, p5.height);\n\n p5.noStroke();\n for (let i = 0; i < count; i++) {\n const t = i / (count - 1);\n const height = p5.height * (0.25 + 0.35 * (0.5 + 0.5 * Math.sin(phase * 2 + t * 6)));\n p5.fill(ramp.at(t));\n p5.rect(i * gap + gap * 0.15, p5.height - height, gap * 0.7, height);\n }\n\n // Readout: the same at() value the middle bar uses\n p5.fill(255, 255, 255, 130);\n p5.textAlign(p5.CENTER);\n p5.textSize(Math.round(s * 0.025));\n p5.text('at(0.5) = ' + ramp.at(0.5) + ' stops: ' + ramp.value.length, p5.width / 2, s * 0.06);\n}\n",
5022
+ "sceneFile": "gradient-p5.scene.js"
5023
+ },
5024
+ {
5025
+ "type": "text",
5026
+ "markdown": "## Interpolation\n\n`at(t)` follows CSS `linear-gradient` semantics: between two stops the color is mixed in premultiplied sRGB, then rounded per channel to 8 bits. Two stops at the same position form a hard edge: everything past that position takes the later stop. Stops themselves are returned verbatim, so `at(0)` is the first stop and `at(1)` the last. When both stops of a segment are fully transparent, the color is the plain blend of their rgb values with alpha `0`.\n\nP5's `lerpColor()` mixes in the current color mode without premultiplying, so prefer `at(t)` when the result must match the ramp shown in the parameter panel.\n\n## Alpha\n\nWith `alpha: true` every stop carries its own transparency. Stop colors accept the alpha forms (`#rrggbbaa`, `#rgba`, `rgba()`, `hsla()`, `vec4()`, objects with `a`) and opaque inputs are padded to `ff`. `at(t)` then returns 8-digit hex, which `p5.fill()` reads as color plus alpha.\n\n```javascript\nconst trail = viji.gradient([\n { color: '#ed225d', position: 0 },\n { color: '#ed225d00', position: 1 }\n], { label: 'Trail', alpha: true });\n\nfunction render(viji, p5) {\n p5.fill(trail.at(0.5)); // '#ed225d80': half transparent\n}\n```\n\nWithout `alpha: true`, a stop color that carries transparency is rejected when the scene loads. See [Color](../color/#alpha) for the accepted forms.\n\n## Related\n\n- [Color](../color/): single color with the same input grammar\n- [Slider](../slider/): numeric range, useful for driving `t`\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility based on capabilities\n- [Native Gradient](/native/parameters/gradient): equivalent for the Native renderer\n- [Shader Gradient](/shader/parameters/gradient): equivalent for the Shader renderer"
4896
5027
  }
4897
5028
  ]
4898
5029
  },
@@ -7020,7 +7151,7 @@ export const docsApi = {
7020
7151
  },
7021
7152
  {
7022
7153
  "type": "text",
7023
- "markdown": "### What's Happening\n\n**Comment directives (parsed before compilation):**\n\n- `// @renderer shader` tells Viji to use the shader renderer.\n- `// @viji-slider:speed ...` declares a parameter. Viji generates a `uniform float speed;` automatically.\n- `// @viji-color:tint ...` declares a color parameter. Viji generates a `uniform vec3 tint;`.\n- `// @viji-accumulator:phase rate:speed` creates a CPU-side accumulator that adds `speed × deltaTime` every frame. The result is a `uniform float phase;` that increases smoothly, with no jumps when the slider changes.\n\n**`void main()` (runs for every pixel, every frame):**\n\n- `gl_FragCoord.xy / u_resolution` gives normalized UV coordinates (0-1).\n- `phase` is the accumulator. Use it instead of `u_time * speed` for smooth, slider-driven animation.\n- `speed`, `scale`, and `tint` are your parameter uniforms, updated live as the user adjusts controls.\n- `gl_FragColor` sets the output color for each pixel.\n\n> [!NOTE]\n> Parameter declarations use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\n\n## Scene Structure\n\nA shader scene is a GLSL fragment shader with comment directives:\n\n```glsl\n// @renderer shader\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv * brightness, 0.5, 1.0);\n}\n```\n\n- **No `precision` or `uniform` declarations needed.** Viji auto-injects `precision mediump float;` and all uniform declarations.\n- **No vertex shader.** Viji renders a fullscreen quad; your fragment shader defines the color of every pixel.\n- **Parameters become uniforms.** `// @viji-slider:name` becomes `uniform float name;` automatically.\n\n> [!NOTE]\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations, both built-in uniforms (`u_resolution`, `u_time`, etc.) and parameter uniforms from `@viji-*` directives. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or any uniforms, they will conflict. If you use `#version 300 es`, Viji will handle its placement automatically.\n\n## Parameter Types\n\nDeclare parameters with `// @viji-TYPE:uniformName key:value` syntax:\n\n| Type | Directive | Uniform Type | Default | Description |\n|------|-----------|--------------|---------|-------------|\n| `slider` | `@viji-slider` | `float` | `default:1.0 min:0.0 max:5.0` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\n| `number` | `@viji-number` | `float` | `default:10.0 min:1.0 max:100.0` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\n| `color` | `@viji-color` | `vec3` | `default:#ff6600` | `// @viji-color:tint label:\"Tint\" default:#ff6600` |\n| `toggle` | `@viji-toggle` | `bool` | `default:false` | `// @viji-toggle:invert label:\"Invert\" default:false` |\n| `select` | `@viji-select` | `int` | `default:0 options:[\"Wave\",\"Spiral\",\"Grid\"]` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"Wave\",\"Spiral\",\"Grid\"]` |\n| `image` | `@viji-image` | `sampler2D` | `label:\"Texture\"` | `// @viji-image:tex label:\"Texture\"` |\n| `button` | `@viji-button` | `bool` | `label:\"Trigger\"` | `// @viji-button:trigger label:\"Trigger\"` |\n| `coordinate` | `@viji-coordinate` | `vec2` | `default:[0,0]` | 2D position pad, -1 to 1 |\n| `accumulator` | `@viji-accumulator` | `float` | `rate:speed` | `// @viji-accumulator:phase rate:speed` |\n\nConfig keys: `label`, `default`, `min`, `max`, `step`, `description`, `group`, `category`.\n\n### Accumulators\n\nAccumulators solve the \"jumping animation\" problem. When you write `u_time * speed`, changing the `speed` slider causes a visible jump because the entire phase is recalculated instantly. Accumulators integrate the rate over time on the CPU side:\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n```\n\n- `phase` increases by `speed × deltaTime` each frame. Changing `speed` only affects future growth, so the animation never jumps backward.\n- `rate` can reference any declared parameter name, or be a numeric constant (e.g., `rate:1.5`).\n- Accumulators have no UI control. They are internal uniform values.\n- Optional `default` sets the starting value (defaults to 0).\n\n> [!WARNING]\n> Do not use the `u_` prefix for your parameter uniform names. That prefix is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `colorMix`, or `intensity` instead.\n\n## Built-in Uniforms\n\nThese are always available, no declaration needed:\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_pointer` | `vec2` | Unified input position, mouse or touch (pixels) |\n| `u_pointerDown` | `bool` | Unified input active (left-click or touch) |\n| `u_mouse` | `vec2` | Mouse position in pixels |\n| `u_mousePressed` | `bool` | Any mouse button is pressed |\n| `u_audioVolume` | `float` | Overall audio volume (0-1) |\n| `u_audioLow` | `float` | Low frequency energy (0-1) |\n| `u_audioMid` | `float` | Mid frequency energy (0-1) |\n| `u_audioHigh` | `float` | High frequency energy (0-1) |\n| `u_audioKick` | `float` | Kick beat detection (0-1) |\n| `u_video` | `sampler2D` | Current video frame |\n\nSee [API Reference](/shader/api-reference) for the complete list of 100+ built-in uniforms.\n\n## Essential Patterns\n\n**Normalized coordinates:**\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution; // 0..1\nvec2 centered = uv - 0.5; // -0.5..0.5\ncentered.x *= u_resolution.x / u_resolution.y; // aspect-corrected\n```\n\n**Distance fields:**\n\n```glsl\nfloat d = length(centered); // distance from center\nfloat circle = smoothstep(0.3, 0.29, d); // anti-aliased circle\n```\n\n> [!NOTE]\n> Always use `u_resolution` for positioning and sizing and `u_time` / `u_deltaTime` for animation. This keeps your shader resolution-agnostic and frame-rate-independent.\n\n## GLSL Version\n\nBy default, shaders use **GLSL ES 1.00** (WebGL 1). If you need WebGL 2 features, add `#version 300 es` as the first line:\n\n```glsl\n#version 300 es\n// @renderer shader\n\n// ES 3.00 requires explicit output declaration\nout vec4 fragColor;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n fragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\nThe main ES 3.00 differences: `gl_FragColor` becomes `out vec4`, and `texture2D()` becomes `texture()`. Use ES 1.00 for maximum compatibility.\n\n## Backbuffer (Previous Frame)\n\nViji gives you access to the previous frame as a texture. Reference `backbuffer` anywhere in your code and it's enabled automatically:\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec4 prev = texture2D(backbuffer, uv); // previous frame\n vec3 current = vec3(/* ... your effect ... */);\n gl_FragColor = vec4(mix(prev.rgb, current, 0.1), 1.0); // 90% trail\n}\n```\n\nThis enables feedback effects, trails, motion blur, and accumulation buffers. No setup needed. Viji detects the `backbuffer` reference and creates the ping-pong framebuffers automatically.\n\nSee [Backbuffer](/shader/backbuffer#how-to-enable) for detailed patterns and techniques.\n\n## Shadertoy Compatibility\n\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy#uniform-mapping) for a mapping of Shadertoy uniforms to Viji equivalents.\n\n## Next Steps\n\n- [Shader Basics](/shader/basics): uniforms, coordinate systems, techniques\n- [Parameters](/shader/parameters): all parameter types for shaders\n- [Audio Uniforms](/shader/audio): react to music in GLSL\n- [Backbuffer](/shader/backbuffer#how-to-enable): feedback effects using the previous frame\n- [API Reference](/shader/api-reference): complete list of built-in uniforms\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
7154
+ "markdown": "### What's Happening\n\n**Comment directives (parsed before compilation):**\n\n- `// @renderer shader` tells Viji to use the shader renderer.\n- `// @viji-slider:speed ...` declares a parameter. Viji generates a `uniform float speed;` automatically.\n- `// @viji-color:tint ...` declares a color parameter. Viji generates a `uniform vec3 tint;`.\n- `// @viji-accumulator:phase rate:speed` creates a CPU-side accumulator that adds `speed × deltaTime` every frame. The result is a `uniform float phase;` that increases smoothly, with no jumps when the slider changes.\n\n**`void main()` (runs for every pixel, every frame):**\n\n- `gl_FragCoord.xy / u_resolution` gives normalized UV coordinates (0-1).\n- `phase` is the accumulator. Use it instead of `u_time * speed` for smooth, slider-driven animation.\n- `speed`, `scale`, and `tint` are your parameter uniforms, updated live as the user adjusts controls.\n- `gl_FragColor` sets the output color for each pixel.\n\n> [!NOTE]\n> Parameter declarations use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\n\n## Scene Structure\n\nA shader scene is a GLSL fragment shader with comment directives:\n\n```glsl\n// @renderer shader\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv * brightness, 0.5, 1.0);\n}\n```\n\n- **No `precision` or `uniform` declarations needed.** Viji auto-injects `precision mediump float;` and all uniform declarations.\n- **No vertex shader.** Viji renders a fullscreen quad; your fragment shader defines the color of every pixel.\n- **Parameters become uniforms.** `// @viji-slider:name` becomes `uniform float name;` automatically.\n\n> [!NOTE]\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations, both built-in uniforms (`u_resolution`, `u_time`, etc.) and parameter uniforms from `@viji-*` directives. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or any uniforms, they will conflict. If you use `#version 300 es`, Viji will handle its placement automatically.\n\n## Parameter Types\n\nDeclare parameters with `// @viji-TYPE:uniformName key:value` syntax:\n\n| Type | Directive | Uniform Type | Default | Description |\n|------|-----------|--------------|---------|-------------|\n| `slider` | `@viji-slider` | `float` | `default:1.0 min:0.0 max:5.0` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\n| `number` | `@viji-number` | `float` | `default:10.0 min:1.0 max:100.0` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\n| `color` | `@viji-color` | `vec3`, or `vec4` with `alpha:true` | `default:#ff6600` | `// @viji-color:tint label:\"Tint\" default:#ff6600` |\n| `gradient` | `@viji-gradient` | arrays + `vec3 name(float t)` sampler (`vec4` with `alpha:true`) | `default:[#ff0000@0,#0000ff@1]` | `// @viji-gradient:ramp label:\"Ramp\" default:[#ff0000@0,#0000ff@1]` |\n| `toggle` | `@viji-toggle` | `bool` | `default:false` | `// @viji-toggle:invert label:\"Invert\" default:false` |\n| `select` | `@viji-select` | `int` | `default:0 options:[\"Wave\",\"Spiral\",\"Grid\"]` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"Wave\",\"Spiral\",\"Grid\"]` |\n| `image` | `@viji-image` | `sampler2D` | `label:\"Texture\"` | `// @viji-image:tex label:\"Texture\"` |\n| `button` | `@viji-button` | `bool` | `label:\"Trigger\"` | `// @viji-button:trigger label:\"Trigger\"` |\n| `coordinate` | `@viji-coordinate` | `vec2` | `default:[0,0]` | 2D position pad, -1 to 1 |\n| `accumulator` | `@viji-accumulator` | `float` | `rate:speed` | `// @viji-accumulator:phase rate:speed` |\n\nConfig keys: `label`, `default`, `min`, `max`, `step`, `description`, `group`, `category`.\n\n### Accumulators\n\nAccumulators solve the \"jumping animation\" problem. When you write `u_time * speed`, changing the `speed` slider causes a visible jump because the entire phase is recalculated instantly. Accumulators integrate the rate over time on the CPU side:\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n```\n\n- `phase` increases by `speed × deltaTime` each frame. Changing `speed` only affects future growth, so the animation never jumps backward.\n- `rate` can reference any declared parameter name, or be a numeric constant (e.g., `rate:1.5`).\n- Accumulators have no UI control. They are internal uniform values.\n- Optional `default` sets the starting value (defaults to 0).\n\n> [!WARNING]\n> Do not use the `u_` prefix for your parameter uniform names. That prefix is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `colorMix`, or `intensity` instead.\n\n## Built-in Uniforms\n\nThese are always available, no declaration needed:\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_pointer` | `vec2` | Unified input position, mouse or touch (pixels) |\n| `u_pointerDown` | `bool` | Unified input active (left-click or touch) |\n| `u_mouse` | `vec2` | Mouse position in pixels |\n| `u_mousePressed` | `bool` | Any mouse button is pressed |\n| `u_audioVolume` | `float` | Overall audio volume (0-1) |\n| `u_audioLow` | `float` | Low frequency energy (0-1) |\n| `u_audioMid` | `float` | Mid frequency energy (0-1) |\n| `u_audioHigh` | `float` | High frequency energy (0-1) |\n| `u_audioKick` | `float` | Kick beat detection (0-1) |\n| `u_video` | `sampler2D` | Current video frame |\n\nSee [API Reference](/shader/api-reference) for the complete list of 100+ built-in uniforms.\n\n## Essential Patterns\n\n**Normalized coordinates:**\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution; // 0..1\nvec2 centered = uv - 0.5; // -0.5..0.5\ncentered.x *= u_resolution.x / u_resolution.y; // aspect-corrected\n```\n\n**Distance fields:**\n\n```glsl\nfloat d = length(centered); // distance from center\nfloat circle = smoothstep(0.3, 0.29, d); // anti-aliased circle\n```\n\n> [!NOTE]\n> Always use `u_resolution` for positioning and sizing and `u_time` / `u_deltaTime` for animation. This keeps your shader resolution-agnostic and frame-rate-independent.\n\n## GLSL Version\n\nBy default, shaders use **GLSL ES 1.00** (WebGL 1). If you need WebGL 2 features, add `#version 300 es` as the first line:\n\n```glsl\n#version 300 es\n// @renderer shader\n\n// ES 3.00 requires explicit output declaration\nout vec4 fragColor;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n fragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\nThe main ES 3.00 differences: `gl_FragColor` becomes `out vec4`, and `texture2D()` becomes `texture()`. Use ES 1.00 for maximum compatibility.\n\n## Backbuffer (Previous Frame)\n\nViji gives you access to the previous frame as a texture. Reference `backbuffer` anywhere in your code and it's enabled automatically:\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec4 prev = texture2D(backbuffer, uv); // previous frame\n vec3 current = vec3(/* ... your effect ... */);\n gl_FragColor = vec4(mix(prev.rgb, current, 0.1), 1.0); // 90% trail\n}\n```\n\nThis enables feedback effects, trails, motion blur, and accumulation buffers. No setup needed. Viji detects the `backbuffer` reference and creates the ping-pong framebuffers automatically.\n\nSee [Backbuffer](/shader/backbuffer#how-to-enable) for detailed patterns and techniques.\n\n## Shadertoy Compatibility\n\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy#uniform-mapping) for a mapping of Shadertoy uniforms to Viji equivalents.\n\n## Next Steps\n\n- [Shader Basics](/shader/basics): uniforms, coordinate systems, techniques\n- [Parameters](/shader/parameters): all parameter types for shaders\n- [Audio Uniforms](/shader/audio): react to music in GLSL\n- [Backbuffer](/shader/backbuffer#how-to-enable): feedback effects using the previous frame\n- [API Reference](/shader/api-reference): complete list of built-in uniforms\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
7024
7155
  }
7025
7156
  ]
7026
7157
  },
@@ -7173,7 +7304,7 @@ export const docsApi = {
7173
7304
  "content": [
7174
7305
  {
7175
7306
  "type": "text",
7176
- "markdown": "# API Reference\n\nViji auto-injects 160+ uniforms into every shader scene. This page is the complete list: use it as a quick lookup. Each entry links to its dedicated documentation page for full details and examples.\n\nAll uniforms listed below are always declared in the shader preamble (except [`backbuffer`](/shader/backbuffer), which is conditional). When data is not available, uniforms hold default values (zeros, false, or empty textures). Your shader compiles once with all declarations present: you do not need to conditionally declare them.\n\nNew to Viji shaders? Start with [Shader Basics](/shader/basics) instead.\n\n## Core / Timing\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_time`](/shader/timing#u_time-absolute-time) | `float` | Seconds elapsed since the scene started | [Timing](/shader/timing) |\n| [`u_deltaTime`](/shader/timing#u_deltatime-frame-delta) | `float` | Seconds since the previous frame | [Timing](/shader/timing) |\n| [`u_frame`](/shader/timing#u_frame-frame-counter) | `int` | Frame index (monotonically increasing) | [Timing](/shader/timing) |\n| [`u_fps`](/shader/timing#u_fps-target-frame-rate) | `float` | Target FPS based on host's frame rate mode | [Timing](/shader/timing) |\n\n## Resolution\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_resolution`](/shader/resolution) | `vec2` | Canvas width and height in pixels | [Resolution](/shader/resolution) |\n\n## Mouse\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_mouse`](/shader/mouse#position) | `vec2` | Cursor position in pixels (WebGL Y-flipped) | [Mouse](/shader/mouse) |\n| [`u_mouseInCanvas`](/shader/mouse#position) | `bool` | Whether cursor is inside the canvas | [Mouse](/shader/mouse) |\n| [`u_mousePressed`](/shader/mouse#buttons) | `bool` | Whether any button is pressed | [Mouse](/shader/mouse) |\n| [`u_mouseLeft`](/shader/mouse#buttons) | `bool` | Left button state | [Mouse](/shader/mouse) |\n| [`u_mouseRight`](/shader/mouse#buttons) | `bool` | Right button state | [Mouse](/shader/mouse) |\n| [`u_mouseMiddle`](/shader/mouse#buttons) | `bool` | Middle button state | [Mouse](/shader/mouse) |\n| [`u_mouseDelta`](/shader/mouse#movement--wheel) | `vec2` | Pixel movement this frame (Y-flipped) | [Mouse](/shader/mouse) |\n| [`u_mouseWheel`](/shader/mouse#movement--wheel) | `float` | Scroll delta this frame | [Mouse](/shader/mouse) |\n| [`u_mouseWasPressed`](/shader/mouse#frame-events) | `bool` | True for one frame when pressed | [Mouse](/shader/mouse) |\n| [`u_mouseWasReleased`](/shader/mouse#frame-events) | `bool` | True for one frame when released | [Mouse](/shader/mouse) |\n\n## Keyboard\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_keySpace`](/shader/keyboard#action--modifiers) | `bool` | Space bar | [Keyboard](/shader/keyboard) |\n| [`u_keyShift`](/shader/keyboard#action--modifiers) | `bool` | Shift key | [Keyboard](/shader/keyboard) |\n| [`u_keyCtrl`](/shader/keyboard#action--modifiers) | `bool` | Ctrl/Cmd key | [Keyboard](/shader/keyboard) |\n| [`u_keyAlt`](/shader/keyboard#action--modifiers) | `bool` | Alt/Option key | [Keyboard](/shader/keyboard) |\n| [`u_keyW`](/shader/keyboard#movement-keys) | `bool` | W key | [Keyboard](/shader/keyboard) |\n| [`u_keyA`](/shader/keyboard#movement-keys) | `bool` | A key | [Keyboard](/shader/keyboard) |\n| [`u_keyS`](/shader/keyboard#movement-keys) | `bool` | S key | [Keyboard](/shader/keyboard) |\n| [`u_keyD`](/shader/keyboard#movement-keys) | `bool` | D key | [Keyboard](/shader/keyboard) |\n| [`u_keyUp`](/shader/keyboard#movement-keys) | `bool` | Up arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyDown`](/shader/keyboard#movement-keys) | `bool` | Down arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyLeft`](/shader/keyboard#movement-keys) | `bool` | Left arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyRight`](/shader/keyboard#movement-keys) | `bool` | Right arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyboard`](/shader/keyboard#keyboard-texture-u_keyboard) | `sampler2D` | 256×3 LUMINANCE texture (row 0: held, row 1: pressed, row 2: toggle) | [Keyboard](/shader/keyboard) |\n\n## Touch\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_touchCount`](/shader/touch#uniform-reference) | `int` | Number of active touches (0-5) | [Touch](/shader/touch) |\n| [`u_touch0`](/shader/touch#uniform-reference) | `vec2` | Touch point 0 position (pixels, Y-flipped) | [Touch](/shader/touch) |\n| [`u_touch1`](/shader/touch#uniform-reference) | `vec2` | Touch point 1 position | [Touch](/shader/touch) |\n| [`u_touch2`](/shader/touch#uniform-reference) | `vec2` | Touch point 2 position | [Touch](/shader/touch) |\n| [`u_touch3`](/shader/touch#uniform-reference) | `vec2` | Touch point 3 position | [Touch](/shader/touch) |\n| [`u_touch4`](/shader/touch#uniform-reference) | `vec2` | Touch point 4 position | [Touch](/shader/touch) |\n\n## Pointer (Unified)\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_pointer`](/shader/pointer#uniform-reference) | `vec2` | Primary pointer position (pixels, Y-flipped) | [Pointer](/shader/pointer) |\n| [`u_pointerDelta`](/shader/pointer#uniform-reference) | `vec2` | Movement delta (Y-flipped) | [Pointer](/shader/pointer) |\n| [`u_pointerDown`](/shader/pointer#uniform-reference) | `bool` | Whether pointer is active (click or touch) | [Pointer](/shader/pointer) |\n| [`u_pointerWasPressed`](/shader/pointer#uniform-reference) | `bool` | True for one frame when pressed | [Pointer](/shader/pointer) |\n| [`u_pointerWasReleased`](/shader/pointer#uniform-reference) | `bool` | True for one frame when released | [Pointer](/shader/pointer) |\n| [`u_pointerInCanvas`](/shader/pointer#uniform-reference) | `bool` | Whether pointer is inside the canvas | [Pointer](/shader/pointer) |\n\n## Audio: Scalars\n\n### Volume\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioVolume`](/shader/audio/volume#uniform-reference) | `float` | Current RMS volume 0-1 | [Volume](/shader/audio/volume) |\n| [`u_audioPeak`](/shader/audio/volume#uniform-reference) | `float` | Peak volume 0-1 | [Volume](/shader/audio/volume) |\n| [`u_audioVolumeSmoothed`](/shader/audio/volume#uniform-reference) | `float` | Smoothed volume (~200ms decay) | [Volume](/shader/audio/volume) |\n\n### Frequency Bands\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioLow`](/shader/audio/bands#instant-bands) | `float` | Low band energy (20-120 Hz) | [Bands](/shader/audio/bands) |\n| [`u_audioLowMid`](/shader/audio/bands#instant-bands) | `float` | Low-mid band energy (120-500 Hz) | [Bands](/shader/audio/bands) |\n| [`u_audioMid`](/shader/audio/bands#instant-bands) | `float` | Mid band energy (500-2 kHz) | [Bands](/shader/audio/bands) |\n| [`u_audioHighMid`](/shader/audio/bands#instant-bands) | `float` | High-mid band energy (2-6 kHz) | [Bands](/shader/audio/bands) |\n| [`u_audioHigh`](/shader/audio/bands#instant-bands) | `float` | High band energy (6-16 kHz) | [Bands](/shader/audio/bands) |\n| [`u_audioLowSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed low band | [Bands](/shader/audio/bands) |\n| [`u_audioLowMidSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed low-mid band | [Bands](/shader/audio/bands) |\n| [`u_audioMidSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed mid band | [Bands](/shader/audio/bands) |\n| [`u_audioHighMidSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed high-mid band | [Bands](/shader/audio/bands) |\n| [`u_audioHighSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed high band | [Bands](/shader/audio/bands) |\n\n### Beat Detection\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioKick`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Kick beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioSnare`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Snare beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioHat`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Hi-hat beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioAny`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Combined beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioKickSmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed kick | [Beat](/shader/audio/beat) |\n| [`u_audioSnareSmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed snare | [Beat](/shader/audio/beat) |\n| [`u_audioHatSmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed hi-hat | [Beat](/shader/audio/beat) |\n| [`u_audioAnySmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed combined | [Beat](/shader/audio/beat) |\n| [`u_audioKickTrigger`](/shader/audio/beat#triggers) | `bool` | Kick trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioSnareTrigger`](/shader/audio/beat#triggers) | `bool` | Snare trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioHatTrigger`](/shader/audio/beat#triggers) | `bool` | Hi-hat trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioAnyTrigger`](/shader/audio/beat#triggers) | `bool` | Any beat trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioBPM`](/shader/audio/beat#tempo) | `float` | Tracked BPM | [Beat](/shader/audio/beat) |\n| [`u_audioConfidence`](/shader/audio/beat#tempo) | `float` | Beat-tracker confidence 0-1 | [Beat](/shader/audio/beat) |\n| [`u_audioIsLocked`](/shader/audio/beat#tempo) | `bool` | Whether beat tracking is locked | [Beat](/shader/audio/beat) |\n\n### Spectral Analysis\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioBrightness`](/shader/audio/spectral#uniform-reference) | `float` | Spectral brightness 0-1 | [Spectral](/shader/audio/spectral) |\n| [`u_audioFlatness`](/shader/audio/spectral#uniform-reference) | `float` | Spectral flatness 0-1 | [Spectral](/shader/audio/spectral) |\n\n## Audio: Textures\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioFFT`](/shader/audio/fft) | `sampler2D` | FFT as 1D LUMINANCE strip (bin count × 1, values 0-255) | [FFT Texture](/shader/audio/fft) |\n| [`u_audioWaveform`](/shader/audio/waveform) | `sampler2D` | Time-domain waveform as 1D LUMINANCE strip (-1…1 mapped to 0-255) | [Waveform Texture](/shader/audio/waveform) |\n\n## Video\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_video`](/shader/video/basics#uniform-reference) | `sampler2D` | Main video frame texture | [Video Basics](/shader/video/basics) |\n| [`u_videoResolution`](/shader/video/basics#uniform-reference) | `vec2` | Video frame size in pixels (0,0 if disconnected) | [Video Basics](/shader/video/basics) |\n| [`u_videoFrameRate`](/shader/video/basics#uniform-reference) | `float` | Video frame rate | [Video Basics](/shader/video/basics) |\n\n## CV: Face Detection\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_faceCount`](/shader/video/face-detection#uniform-reference) | `int` | Number of detected faces | [Face Detection](/shader/video/face-detection) |\n| [`u_face0Bounds`](/shader/video/face-detection#uniform-reference) | `vec4` | Bounding box (x, y, w, h) normalized 0-1 | [Face Detection](/shader/video/face-detection) |\n| [`u_face0Center`](/shader/video/face-detection#uniform-reference) | `vec2` | Face center normalized 0-1 | [Face Detection](/shader/video/face-detection) |\n| [`u_face0HeadPose`](/shader/video/face-mesh) | `vec3` | Pitch, yaw, roll in degrees | [Face Mesh](/shader/video/face-mesh) |\n| [`u_face0Confidence`](/shader/video/face-detection#uniform-reference) | `float` | Detection confidence | [Face Detection](/shader/video/face-detection) |\n\n### Expressions\n\n| Uniform | Type | Details |\n|---------|------|---------|\n| [`u_face0Neutral`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Happy`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Sad`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Angry`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Surprised`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Disgusted`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Fearful`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n\n### Blendshapes (52 uniforms)\n\nAll blendshapes are `float` values 0-1, following the ARKit naming convention. See [Face Mesh](/shader/video/face-mesh) for the full list and usage.\n\n| Uniform | Uniform | Uniform |\n|---------|---------|---------|\n| `u_face0BrowDownLeft` | `u_face0BrowDownRight` | `u_face0BrowInnerUp` |\n| `u_face0BrowOuterUpLeft` | `u_face0BrowOuterUpRight` | `u_face0CheekPuff` |\n| `u_face0CheekSquintLeft` | `u_face0CheekSquintRight` | `u_face0EyeBlinkLeft` |\n| `u_face0EyeBlinkRight` | `u_face0EyeLookDownLeft` | `u_face0EyeLookDownRight` |\n| `u_face0EyeLookInLeft` | `u_face0EyeLookInRight` | `u_face0EyeLookOutLeft` |\n| `u_face0EyeLookOutRight` | `u_face0EyeLookUpLeft` | `u_face0EyeLookUpRight` |\n| `u_face0EyeSquintLeft` | `u_face0EyeSquintRight` | `u_face0EyeWideLeft` |\n| `u_face0EyeWideRight` | `u_face0JawForward` | `u_face0JawLeft` |\n| `u_face0JawOpen` | `u_face0JawRight` | `u_face0MouthClose` |\n| `u_face0MouthDimpleLeft` | `u_face0MouthDimpleRight` | `u_face0MouthFrownLeft` |\n| `u_face0MouthFrownRight` | `u_face0MouthFunnel` | `u_face0MouthLeft` |\n| `u_face0MouthLowerDownLeft` | `u_face0MouthLowerDownRight` | `u_face0MouthPressLeft` |\n| `u_face0MouthPressRight` | `u_face0MouthPucker` | `u_face0MouthRight` |\n| `u_face0MouthRollLower` | `u_face0MouthRollUpper` | `u_face0MouthShrugLower` |\n| `u_face0MouthShrugUpper` | `u_face0MouthSmileLeft` | `u_face0MouthSmileRight` |\n| `u_face0MouthStretchLeft` | `u_face0MouthStretchRight` | `u_face0MouthUpperUpLeft` |\n| `u_face0MouthUpperUpRight` | `u_face0NoseSneerLeft` | `u_face0NoseSneerRight` |\n| `u_face0TongueOut` | | |\n\n## CV: Hand Tracking\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_handCount`](/shader/video/hand-tracking#detection) | `int` | Number of detected hands (0-2) | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandPalm`](/shader/video/hand-tracking#per-hand-data) | `vec3` | Left hand palm position | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_rightHandPalm`](/shader/video/hand-tracking#per-hand-data) | `vec3` | Right hand palm position | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandConfidence`](/shader/video/hand-tracking#per-hand-data) | `float` | Left hand confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_rightHandConfidence`](/shader/video/hand-tracking#per-hand-data) | `float` | Right hand confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandBounds`](/shader/video/hand-tracking#per-hand-data) | `vec4` | Left hand bounding box (x, y, w, h) | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_rightHandBounds`](/shader/video/hand-tracking#per-hand-data) | `vec4` | Right hand bounding box (x, y, w, h) | [Hand Tracking](/shader/video/hand-tracking) |\n\n### Gesture Scores (per hand)\n\nAll gesture uniforms are `float` values 0-1. Replace `left` with `right` for the other hand.\n\n| Uniform | Description | Details |\n|---------|-------------|---------|\n| [`u_leftHandFist`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Fist gesture confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandOpenPalm`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Open palm confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandPeace`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Peace/V-sign confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandThumbsUp`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Thumbs up confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandThumbsDown`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Thumbs down confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandPointing`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Pointing confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandILoveYou`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | I Love You sign confidence | [Hand Tracking](/shader/video/hand-tracking) |\n\n## CV: Pose Detection\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_poseDetected`](/shader/video/pose-detection#detection-state) | `bool` | Whether a body pose is detected | [Pose Detection](/shader/video/pose-detection) |\n| [`u_poseConfidence`](/shader/video/pose-detection#detection-state) | `float` | Pose detection confidence | [Pose Detection](/shader/video/pose-detection) |\n| [`u_nosePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Nose landmark position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftShoulderPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left shoulder position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightShoulderPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right shoulder position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftElbowPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left elbow position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightElbowPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right elbow position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftWristPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left wrist position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightWristPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right wrist position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftHipPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left hip position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightHipPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right hip position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftKneePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left knee position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightKneePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right knee position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftAnklePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left ankle position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightAnklePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right ankle position | [Pose Detection](/shader/video/pose-detection) |\n\n## CV: Body Segmentation\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_segmentationMask`](/shader/video/body-segmentation#uniform-reference) | `sampler2D` | Body mask (LUMINANCE: 0 = background, 1 = person) | [Segmentation](/shader/video/body-segmentation) |\n| [`u_segmentationRes`](/shader/video/body-segmentation#uniform-reference) | `vec2` | Mask dimensions in pixels | [Segmentation](/shader/video/body-segmentation) |\n\n## Device Sensors\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_deviceAcceleration`](/shader/sensors#uniform-reference) | `vec3` | Acceleration without gravity (m/s²) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceAccelerationGravity`](/shader/sensors#uniform-reference) | `vec3` | Acceleration with gravity (m/s²) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceRotationRate`](/shader/sensors#uniform-reference) | `vec3` | Gyroscope: alpha, beta, gamma (deg/s) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceOrientation`](/shader/sensors#uniform-reference) | `vec3` | Orientation: alpha, beta, gamma (degrees) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceOrientationAbsolute`](/shader/sensors#uniform-reference) | `bool` | Whether orientation is magnetometer-based | [Sensor Uniforms](/shader/sensors) |\n\n## External Devices: Video\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_deviceCount`](/shader/external-devices) | `int` | Number of devices with active cameras (0-8) | [Overview](/shader/external-devices) |\n| [`u_device0`](/shader/external-devices/video) - `u_device7` | `sampler2D` | Device camera frame texture | [Video Textures](/shader/external-devices/video) |\n| [`u_device0Resolution`](/shader/external-devices/video) - `u_device7Resolution` | `vec2` | Device camera frame size | [Video Textures](/shader/external-devices/video) |\n| [`u_device0Connected`](/shader/external-devices/video) - `u_device7Connected` | `bool` | Whether device camera is active | [Video Textures](/shader/external-devices/video) |\n\n> [!NOTE]\n> Audio stream uniforms from devices are accessed via `u_audioStream{i}*` uniforms (same pool as additional audio streams, different index range managed by the host).\n\n## External Devices: Sensors\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_externalDeviceCount`](/shader/external-devices) | `int` | Number of connected external devices (0-8) | [Overview](/shader/external-devices) |\n| [`u_device0Acceleration`](/shader/external-devices/sensors) - `u_device7Acceleration` | `vec3` | Per-device acceleration without gravity | [Sensor Uniforms](/shader/external-devices/sensors) |\n| [`u_device0AccelerationGravity`](/shader/external-devices/sensors) - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration with gravity | [Sensor Uniforms](/shader/external-devices/sensors) |\n| [`u_device0RotationRate`](/shader/external-devices/sensors) - `u_device7RotationRate` | `vec3` | Per-device rotation rate | [Sensor Uniforms](/shader/external-devices/sensors) |\n| [`u_device0Orientation`](/shader/external-devices/sensors) - `u_device7Orientation` | `vec3` | Per-device orientation angles | [Sensor Uniforms](/shader/external-devices/sensors) |\n\n> [!NOTE]\n> `u_device{i}` (sampler2D) is the **camera texture** for device slot `i`. `u_device{i}Acceleration` and similar are the **IMU sensors** for the same device: different data, same index.\n\n## Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream frame textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream frame sizes in pixels |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Whether stream has an active frame |\n\nStreams are additional video sources injected by the host application: they are used internally by Viji's compositor for mixing multiple scenes together. When no streams are provided, `u_videoStreamCount` is `0` and the textures sample as black. Each stream works the same way as [`u_video`](/shader/video/basics).\n\n## Audio Streams\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether stream is actively providing audio |\n| `u_audioStream0Volume` - `u_audioStream7Volume` | `float` | Stream volume (0-1) |\n| `u_audioStream0Low` - `u_audioStream7Low` | `float` | Low frequency band energy (0-1) |\n| `u_audioStream0LowMid` - `u_audioStream7LowMid` | `float` | Low-mid band energy (0-1) |\n| `u_audioStream0Mid` - `u_audioStream7Mid` | `float` | Mid band energy (0-1) |\n| `u_audioStream0HighMid` - `u_audioStream7HighMid` | `float` | High-mid band energy (0-1) |\n| `u_audioStream0High` - `u_audioStream7High` | `float` | High band energy (0-1) |\n| `u_audioStream0Brightness` - `u_audioStream7Brightness` | `float` | Spectral brightness (0-1) |\n| `u_audioStream0Flatness` - `u_audioStream7Flatness` | `float` | Spectral flatness (0-1) |\n\nAdditional audio streams provide lightweight scalar uniforms only: **no** FFT or waveform textures. Beat detection, BPM, and onset uniforms are only available on the main audio stream (`u_audioVolume`, `u_audioBPM`, etc.).\n\nWhen no audio streams are provided, `u_audioStreamCount` is `0` and all per-stream uniforms default to `0.0` / `false`.\n\n## Backbuffer\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`backbuffer`](/shader/backbuffer) | `sampler2D` | Previous frame texture (feedback effects) | [Backbuffer & Feedback](/shader/backbuffer) |\n\n> [!WARNING]\n> `backbuffer` is **conditional**: it is only injected if the word `backbuffer` appears anywhere in your shader source (including comments). It has no `u_` prefix. See [Backbuffer & Feedback](/shader/backbuffer) for details.\n\n## Parameter Directives\n\nDeclare parameters using `// @viji-TYPE:uniformName key:value ...` comments. Each directive generates a uniform and a UI control in the host.\n\n| Directive | Uniform Type | UI Control | Details |\n|-----------|-------------|------------|---------|\n| [`@viji-slider`](/shader/parameters/slider) | `float` | Numeric slider | [Slider](/shader/parameters/slider) |\n| [`@viji-number`](/shader/parameters/number) | `float` | Numeric input | [Number](/shader/parameters/number) |\n| [`@viji-color`](/shader/parameters/color) | `vec3` | Color picker (hex → RGB 0-1) | [Color](/shader/parameters/color) |\n| [`@viji-toggle`](/shader/parameters/toggle) | `bool` | On/off switch | [Toggle](/shader/parameters/toggle) |\n| [`@viji-select`](/shader/parameters/select) | `int` | Dropdown (0-based option index) | [Select](/shader/parameters/select) |\n| [`@viji-image`](/shader/parameters/image) | `sampler2D` | Image upload | [Image](/shader/parameters/image) |\n| [`@viji-button`](/shader/parameters/button) | `bool` | Momentary button (true for one frame) | [Button](/shader/parameters/button) |\n| [`@viji-coordinate`](/shader/parameters/coordinate) | `vec2` | 2D coordinate pad: both components -1 to 1 | [Coordinate](/shader/parameters/coordinate) |\n| [`@viji-accumulator`](/shader/parameters/accumulator) | `float` | CPU-side: `+= rate × deltaTime` | [Accumulator](/shader/parameters/accumulator) |\n| [`@viji-cv:<feature>`](/shader/video) | `bool` (toggleable form only) | Activates a CV pipeline. Bare form is always-on, toggleable form synthesizes a host parameter and a `bool <feature>` uniform | [Video & CV](/shader/video) |\n\nSee [Parameters Overview](/shader/parameters) for syntax, [Grouping](/shader/parameters/grouping) and [Categories](/shader/parameters/categories) for organization.\n\n## Related\n\n- [Shader Basics](/shader/basics): auto-injection, GLSL versions, `@renderer shader`\n- [Shader Quick Start](/shader/quickstart): getting started with shader scenes\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers\n- [Native API Reference](/native/api-reference): JavaScript API for the Native renderer\n- [P5 API Reference](/p5/api-reference): JavaScript API for the P5 renderer"
7307
+ "markdown": "# API Reference\n\nViji auto-injects 160+ uniforms into every shader scene. This page is the complete list: use it as a quick lookup. Each entry links to its dedicated documentation page for full details and examples.\n\nAll uniforms listed below are always declared in the shader preamble (except [`backbuffer`](/shader/backbuffer), which is conditional). When data is not available, uniforms hold default values (zeros, false, or empty textures). Your shader compiles once with all declarations present: you do not need to conditionally declare them.\n\nNew to Viji shaders? Start with [Shader Basics](/shader/basics) instead.\n\n## Core / Timing\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_time`](/shader/timing#u_time-absolute-time) | `float` | Seconds elapsed since the scene started | [Timing](/shader/timing) |\n| [`u_deltaTime`](/shader/timing#u_deltatime-frame-delta) | `float` | Seconds since the previous frame | [Timing](/shader/timing) |\n| [`u_frame`](/shader/timing#u_frame-frame-counter) | `int` | Frame index (monotonically increasing) | [Timing](/shader/timing) |\n| [`u_fps`](/shader/timing#u_fps-target-frame-rate) | `float` | Target FPS based on host's frame rate mode | [Timing](/shader/timing) |\n\n## Resolution\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_resolution`](/shader/resolution) | `vec2` | Canvas width and height in pixels | [Resolution](/shader/resolution) |\n\n## Mouse\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_mouse`](/shader/mouse#position) | `vec2` | Cursor position in pixels (WebGL Y-flipped) | [Mouse](/shader/mouse) |\n| [`u_mouseInCanvas`](/shader/mouse#position) | `bool` | Whether cursor is inside the canvas | [Mouse](/shader/mouse) |\n| [`u_mousePressed`](/shader/mouse#buttons) | `bool` | Whether any button is pressed | [Mouse](/shader/mouse) |\n| [`u_mouseLeft`](/shader/mouse#buttons) | `bool` | Left button state | [Mouse](/shader/mouse) |\n| [`u_mouseRight`](/shader/mouse#buttons) | `bool` | Right button state | [Mouse](/shader/mouse) |\n| [`u_mouseMiddle`](/shader/mouse#buttons) | `bool` | Middle button state | [Mouse](/shader/mouse) |\n| [`u_mouseDelta`](/shader/mouse#movement--wheel) | `vec2` | Pixel movement this frame (Y-flipped) | [Mouse](/shader/mouse) |\n| [`u_mouseWheel`](/shader/mouse#movement--wheel) | `float` | Scroll delta this frame | [Mouse](/shader/mouse) |\n| [`u_mouseWasPressed`](/shader/mouse#frame-events) | `bool` | True for one frame when pressed | [Mouse](/shader/mouse) |\n| [`u_mouseWasReleased`](/shader/mouse#frame-events) | `bool` | True for one frame when released | [Mouse](/shader/mouse) |\n\n## Keyboard\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_keySpace`](/shader/keyboard#action--modifiers) | `bool` | Space bar | [Keyboard](/shader/keyboard) |\n| [`u_keyShift`](/shader/keyboard#action--modifiers) | `bool` | Shift key | [Keyboard](/shader/keyboard) |\n| [`u_keyCtrl`](/shader/keyboard#action--modifiers) | `bool` | Ctrl/Cmd key | [Keyboard](/shader/keyboard) |\n| [`u_keyAlt`](/shader/keyboard#action--modifiers) | `bool` | Alt/Option key | [Keyboard](/shader/keyboard) |\n| [`u_keyW`](/shader/keyboard#movement-keys) | `bool` | W key | [Keyboard](/shader/keyboard) |\n| [`u_keyA`](/shader/keyboard#movement-keys) | `bool` | A key | [Keyboard](/shader/keyboard) |\n| [`u_keyS`](/shader/keyboard#movement-keys) | `bool` | S key | [Keyboard](/shader/keyboard) |\n| [`u_keyD`](/shader/keyboard#movement-keys) | `bool` | D key | [Keyboard](/shader/keyboard) |\n| [`u_keyUp`](/shader/keyboard#movement-keys) | `bool` | Up arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyDown`](/shader/keyboard#movement-keys) | `bool` | Down arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyLeft`](/shader/keyboard#movement-keys) | `bool` | Left arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyRight`](/shader/keyboard#movement-keys) | `bool` | Right arrow | [Keyboard](/shader/keyboard) |\n| [`u_keyboard`](/shader/keyboard#keyboard-texture-u_keyboard) | `sampler2D` | 256×3 LUMINANCE texture (row 0: held, row 1: pressed, row 2: toggle) | [Keyboard](/shader/keyboard) |\n\n## Touch\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_touchCount`](/shader/touch#uniform-reference) | `int` | Number of active touches (0-5) | [Touch](/shader/touch) |\n| [`u_touch0`](/shader/touch#uniform-reference) | `vec2` | Touch point 0 position (pixels, Y-flipped) | [Touch](/shader/touch) |\n| [`u_touch1`](/shader/touch#uniform-reference) | `vec2` | Touch point 1 position | [Touch](/shader/touch) |\n| [`u_touch2`](/shader/touch#uniform-reference) | `vec2` | Touch point 2 position | [Touch](/shader/touch) |\n| [`u_touch3`](/shader/touch#uniform-reference) | `vec2` | Touch point 3 position | [Touch](/shader/touch) |\n| [`u_touch4`](/shader/touch#uniform-reference) | `vec2` | Touch point 4 position | [Touch](/shader/touch) |\n\n## Pointer (Unified)\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_pointer`](/shader/pointer#uniform-reference) | `vec2` | Primary pointer position (pixels, Y-flipped) | [Pointer](/shader/pointer) |\n| [`u_pointerDelta`](/shader/pointer#uniform-reference) | `vec2` | Movement delta (Y-flipped) | [Pointer](/shader/pointer) |\n| [`u_pointerDown`](/shader/pointer#uniform-reference) | `bool` | Whether pointer is active (click or touch) | [Pointer](/shader/pointer) |\n| [`u_pointerWasPressed`](/shader/pointer#uniform-reference) | `bool` | True for one frame when pressed | [Pointer](/shader/pointer) |\n| [`u_pointerWasReleased`](/shader/pointer#uniform-reference) | `bool` | True for one frame when released | [Pointer](/shader/pointer) |\n| [`u_pointerInCanvas`](/shader/pointer#uniform-reference) | `bool` | Whether pointer is inside the canvas | [Pointer](/shader/pointer) |\n\n## Audio: Scalars\n\n### Volume\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioVolume`](/shader/audio/volume#uniform-reference) | `float` | Current RMS volume 0-1 | [Volume](/shader/audio/volume) |\n| [`u_audioPeak`](/shader/audio/volume#uniform-reference) | `float` | Peak volume 0-1 | [Volume](/shader/audio/volume) |\n| [`u_audioVolumeSmoothed`](/shader/audio/volume#uniform-reference) | `float` | Smoothed volume (~200ms decay) | [Volume](/shader/audio/volume) |\n\n### Frequency Bands\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioLow`](/shader/audio/bands#instant-bands) | `float` | Low band energy (20-120 Hz) | [Bands](/shader/audio/bands) |\n| [`u_audioLowMid`](/shader/audio/bands#instant-bands) | `float` | Low-mid band energy (120-500 Hz) | [Bands](/shader/audio/bands) |\n| [`u_audioMid`](/shader/audio/bands#instant-bands) | `float` | Mid band energy (500-2 kHz) | [Bands](/shader/audio/bands) |\n| [`u_audioHighMid`](/shader/audio/bands#instant-bands) | `float` | High-mid band energy (2-6 kHz) | [Bands](/shader/audio/bands) |\n| [`u_audioHigh`](/shader/audio/bands#instant-bands) | `float` | High band energy (6-16 kHz) | [Bands](/shader/audio/bands) |\n| [`u_audioLowSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed low band | [Bands](/shader/audio/bands) |\n| [`u_audioLowMidSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed low-mid band | [Bands](/shader/audio/bands) |\n| [`u_audioMidSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed mid band | [Bands](/shader/audio/bands) |\n| [`u_audioHighMidSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed high-mid band | [Bands](/shader/audio/bands) |\n| [`u_audioHighSmoothed`](/shader/audio/bands#smoothed-bands) | `float` | Smoothed high band | [Bands](/shader/audio/bands) |\n\n### Beat Detection\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioKick`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Kick beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioSnare`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Snare beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioHat`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Hi-hat beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioAny`](/shader/audio/beat#energy-curves-fast-decay) | `float` | Combined beat energy | [Beat](/shader/audio/beat) |\n| [`u_audioKickSmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed kick | [Beat](/shader/audio/beat) |\n| [`u_audioSnareSmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed snare | [Beat](/shader/audio/beat) |\n| [`u_audioHatSmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed hi-hat | [Beat](/shader/audio/beat) |\n| [`u_audioAnySmoothed`](/shader/audio/beat#energy-curves-smoothed) | `float` | Smoothed combined | [Beat](/shader/audio/beat) |\n| [`u_audioKickTrigger`](/shader/audio/beat#triggers) | `bool` | Kick trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioSnareTrigger`](/shader/audio/beat#triggers) | `bool` | Snare trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioHatTrigger`](/shader/audio/beat#triggers) | `bool` | Hi-hat trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioAnyTrigger`](/shader/audio/beat#triggers) | `bool` | Any beat trigger (true for one frame) | [Beat](/shader/audio/beat) |\n| [`u_audioBPM`](/shader/audio/beat#tempo) | `float` | Tracked BPM | [Beat](/shader/audio/beat) |\n| [`u_audioConfidence`](/shader/audio/beat#tempo) | `float` | Beat-tracker confidence 0-1 | [Beat](/shader/audio/beat) |\n| [`u_audioIsLocked`](/shader/audio/beat#tempo) | `bool` | Whether beat tracking is locked | [Beat](/shader/audio/beat) |\n\n### Spectral Analysis\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioBrightness`](/shader/audio/spectral#uniform-reference) | `float` | Spectral brightness 0-1 | [Spectral](/shader/audio/spectral) |\n| [`u_audioFlatness`](/shader/audio/spectral#uniform-reference) | `float` | Spectral flatness 0-1 | [Spectral](/shader/audio/spectral) |\n\n## Audio: Textures\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_audioFFT`](/shader/audio/fft) | `sampler2D` | FFT as 1D LUMINANCE strip (bin count × 1, values 0-255) | [FFT Texture](/shader/audio/fft) |\n| [`u_audioWaveform`](/shader/audio/waveform) | `sampler2D` | Time-domain waveform as 1D LUMINANCE strip (-1…1 mapped to 0-255) | [Waveform Texture](/shader/audio/waveform) |\n\n## Video\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_video`](/shader/video/basics#uniform-reference) | `sampler2D` | Main video frame texture | [Video Basics](/shader/video/basics) |\n| [`u_videoResolution`](/shader/video/basics#uniform-reference) | `vec2` | Video frame size in pixels (0,0 if disconnected) | [Video Basics](/shader/video/basics) |\n| [`u_videoFrameRate`](/shader/video/basics#uniform-reference) | `float` | Video frame rate | [Video Basics](/shader/video/basics) |\n\n## CV: Face Detection\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_faceCount`](/shader/video/face-detection#uniform-reference) | `int` | Number of detected faces | [Face Detection](/shader/video/face-detection) |\n| [`u_face0Bounds`](/shader/video/face-detection#uniform-reference) | `vec4` | Bounding box (x, y, w, h) normalized 0-1 | [Face Detection](/shader/video/face-detection) |\n| [`u_face0Center`](/shader/video/face-detection#uniform-reference) | `vec2` | Face center normalized 0-1 | [Face Detection](/shader/video/face-detection) |\n| [`u_face0HeadPose`](/shader/video/face-mesh) | `vec3` | Pitch, yaw, roll in degrees | [Face Mesh](/shader/video/face-mesh) |\n| [`u_face0Confidence`](/shader/video/face-detection#uniform-reference) | `float` | Detection confidence | [Face Detection](/shader/video/face-detection) |\n\n### Expressions\n\n| Uniform | Type | Details |\n|---------|------|---------|\n| [`u_face0Neutral`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Happy`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Sad`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Angry`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Surprised`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Disgusted`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n| [`u_face0Fearful`](/shader/video/emotion-detection) | `float` | [Emotion Detection](/shader/video/emotion-detection) |\n\n### Blendshapes (52 uniforms)\n\nAll blendshapes are `float` values 0-1, following the ARKit naming convention. See [Face Mesh](/shader/video/face-mesh) for the full list and usage.\n\n| Uniform | Uniform | Uniform |\n|---------|---------|---------|\n| `u_face0BrowDownLeft` | `u_face0BrowDownRight` | `u_face0BrowInnerUp` |\n| `u_face0BrowOuterUpLeft` | `u_face0BrowOuterUpRight` | `u_face0CheekPuff` |\n| `u_face0CheekSquintLeft` | `u_face0CheekSquintRight` | `u_face0EyeBlinkLeft` |\n| `u_face0EyeBlinkRight` | `u_face0EyeLookDownLeft` | `u_face0EyeLookDownRight` |\n| `u_face0EyeLookInLeft` | `u_face0EyeLookInRight` | `u_face0EyeLookOutLeft` |\n| `u_face0EyeLookOutRight` | `u_face0EyeLookUpLeft` | `u_face0EyeLookUpRight` |\n| `u_face0EyeSquintLeft` | `u_face0EyeSquintRight` | `u_face0EyeWideLeft` |\n| `u_face0EyeWideRight` | `u_face0JawForward` | `u_face0JawLeft` |\n| `u_face0JawOpen` | `u_face0JawRight` | `u_face0MouthClose` |\n| `u_face0MouthDimpleLeft` | `u_face0MouthDimpleRight` | `u_face0MouthFrownLeft` |\n| `u_face0MouthFrownRight` | `u_face0MouthFunnel` | `u_face0MouthLeft` |\n| `u_face0MouthLowerDownLeft` | `u_face0MouthLowerDownRight` | `u_face0MouthPressLeft` |\n| `u_face0MouthPressRight` | `u_face0MouthPucker` | `u_face0MouthRight` |\n| `u_face0MouthRollLower` | `u_face0MouthRollUpper` | `u_face0MouthShrugLower` |\n| `u_face0MouthShrugUpper` | `u_face0MouthSmileLeft` | `u_face0MouthSmileRight` |\n| `u_face0MouthStretchLeft` | `u_face0MouthStretchRight` | `u_face0MouthUpperUpLeft` |\n| `u_face0MouthUpperUpRight` | `u_face0NoseSneerLeft` | `u_face0NoseSneerRight` |\n| `u_face0TongueOut` | | |\n\n## CV: Hand Tracking\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_handCount`](/shader/video/hand-tracking#detection) | `int` | Number of detected hands (0-2) | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandPalm`](/shader/video/hand-tracking#per-hand-data) | `vec3` | Left hand palm position | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_rightHandPalm`](/shader/video/hand-tracking#per-hand-data) | `vec3` | Right hand palm position | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandConfidence`](/shader/video/hand-tracking#per-hand-data) | `float` | Left hand confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_rightHandConfidence`](/shader/video/hand-tracking#per-hand-data) | `float` | Right hand confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandBounds`](/shader/video/hand-tracking#per-hand-data) | `vec4` | Left hand bounding box (x, y, w, h) | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_rightHandBounds`](/shader/video/hand-tracking#per-hand-data) | `vec4` | Right hand bounding box (x, y, w, h) | [Hand Tracking](/shader/video/hand-tracking) |\n\n### Gesture Scores (per hand)\n\nAll gesture uniforms are `float` values 0-1. Replace `left` with `right` for the other hand.\n\n| Uniform | Description | Details |\n|---------|-------------|---------|\n| [`u_leftHandFist`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Fist gesture confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandOpenPalm`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Open palm confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandPeace`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Peace/V-sign confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandThumbsUp`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Thumbs up confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandThumbsDown`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Thumbs down confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandPointing`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | Pointing confidence | [Hand Tracking](/shader/video/hand-tracking) |\n| [`u_leftHandILoveYou`](/shader/video/hand-tracking#gesture-uniforms-per-hand) | I Love You sign confidence | [Hand Tracking](/shader/video/hand-tracking) |\n\n## CV: Pose Detection\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_poseDetected`](/shader/video/pose-detection#detection-state) | `bool` | Whether a body pose is detected | [Pose Detection](/shader/video/pose-detection) |\n| [`u_poseConfidence`](/shader/video/pose-detection#detection-state) | `float` | Pose detection confidence | [Pose Detection](/shader/video/pose-detection) |\n| [`u_nosePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Nose landmark position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftShoulderPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left shoulder position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightShoulderPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right shoulder position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftElbowPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left elbow position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightElbowPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right elbow position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftWristPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left wrist position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightWristPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right wrist position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftHipPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left hip position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightHipPosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right hip position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftKneePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left knee position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightKneePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right knee position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_leftAnklePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Left ankle position | [Pose Detection](/shader/video/pose-detection) |\n| [`u_rightAnklePosition`](/shader/video/pose-detection#key-landmark-positions) | `vec2` | Right ankle position | [Pose Detection](/shader/video/pose-detection) |\n\n## CV: Body Segmentation\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_segmentationMask`](/shader/video/body-segmentation#uniform-reference) | `sampler2D` | Body mask (LUMINANCE: 0 = background, 1 = person) | [Segmentation](/shader/video/body-segmentation) |\n| [`u_segmentationRes`](/shader/video/body-segmentation#uniform-reference) | `vec2` | Mask dimensions in pixels | [Segmentation](/shader/video/body-segmentation) |\n\n## Device Sensors\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_deviceAcceleration`](/shader/sensors#uniform-reference) | `vec3` | Acceleration without gravity (m/s²) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceAccelerationGravity`](/shader/sensors#uniform-reference) | `vec3` | Acceleration with gravity (m/s²) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceRotationRate`](/shader/sensors#uniform-reference) | `vec3` | Gyroscope: alpha, beta, gamma (deg/s) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceOrientation`](/shader/sensors#uniform-reference) | `vec3` | Orientation: alpha, beta, gamma (degrees) | [Sensor Uniforms](/shader/sensors) |\n| [`u_deviceOrientationAbsolute`](/shader/sensors#uniform-reference) | `bool` | Whether orientation is magnetometer-based | [Sensor Uniforms](/shader/sensors) |\n\n## External Devices: Video\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_deviceCount`](/shader/external-devices) | `int` | Number of devices with active cameras (0-8) | [Overview](/shader/external-devices) |\n| [`u_device0`](/shader/external-devices/video) - `u_device7` | `sampler2D` | Device camera frame texture | [Video Textures](/shader/external-devices/video) |\n| [`u_device0Resolution`](/shader/external-devices/video) - `u_device7Resolution` | `vec2` | Device camera frame size | [Video Textures](/shader/external-devices/video) |\n| [`u_device0Connected`](/shader/external-devices/video) - `u_device7Connected` | `bool` | Whether device camera is active | [Video Textures](/shader/external-devices/video) |\n\n> [!NOTE]\n> Audio stream uniforms from devices are accessed via `u_audioStream{i}*` uniforms (same pool as additional audio streams, different index range managed by the host).\n\n## External Devices: Sensors\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_externalDeviceCount`](/shader/external-devices) | `int` | Number of connected external devices (0-8) | [Overview](/shader/external-devices) |\n| [`u_device0Acceleration`](/shader/external-devices/sensors) - `u_device7Acceleration` | `vec3` | Per-device acceleration without gravity | [Sensor Uniforms](/shader/external-devices/sensors) |\n| [`u_device0AccelerationGravity`](/shader/external-devices/sensors) - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration with gravity | [Sensor Uniforms](/shader/external-devices/sensors) |\n| [`u_device0RotationRate`](/shader/external-devices/sensors) - `u_device7RotationRate` | `vec3` | Per-device rotation rate | [Sensor Uniforms](/shader/external-devices/sensors) |\n| [`u_device0Orientation`](/shader/external-devices/sensors) - `u_device7Orientation` | `vec3` | Per-device orientation angles | [Sensor Uniforms](/shader/external-devices/sensors) |\n\n> [!NOTE]\n> `u_device{i}` (sampler2D) is the **camera texture** for device slot `i`. `u_device{i}Acceleration` and similar are the **IMU sensors** for the same device: different data, same index.\n\n## Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream frame textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream frame sizes in pixels |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Whether stream has an active frame |\n\nStreams are additional video sources injected by the host application: they are used internally by Viji's compositor for mixing multiple scenes together. When no streams are provided, `u_videoStreamCount` is `0` and the textures sample as black. Each stream works the same way as [`u_video`](/shader/video/basics).\n\n## Audio Streams\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether stream is actively providing audio |\n| `u_audioStream0Volume` - `u_audioStream7Volume` | `float` | Stream volume (0-1) |\n| `u_audioStream0Low` - `u_audioStream7Low` | `float` | Low frequency band energy (0-1) |\n| `u_audioStream0LowMid` - `u_audioStream7LowMid` | `float` | Low-mid band energy (0-1) |\n| `u_audioStream0Mid` - `u_audioStream7Mid` | `float` | Mid band energy (0-1) |\n| `u_audioStream0HighMid` - `u_audioStream7HighMid` | `float` | High-mid band energy (0-1) |\n| `u_audioStream0High` - `u_audioStream7High` | `float` | High band energy (0-1) |\n| `u_audioStream0Brightness` - `u_audioStream7Brightness` | `float` | Spectral brightness (0-1) |\n| `u_audioStream0Flatness` - `u_audioStream7Flatness` | `float` | Spectral flatness (0-1) |\n\nAdditional audio streams provide lightweight scalar uniforms only: **no** FFT or waveform textures. Beat detection, BPM, and onset uniforms are only available on the main audio stream (`u_audioVolume`, `u_audioBPM`, etc.).\n\nWhen no audio streams are provided, `u_audioStreamCount` is `0` and all per-stream uniforms default to `0.0` / `false`.\n\n## Backbuffer\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`backbuffer`](/shader/backbuffer) | `sampler2D` | Previous frame texture (feedback effects) | [Backbuffer & Feedback](/shader/backbuffer) |\n\n> [!WARNING]\n> `backbuffer` is **conditional**: it is only injected if the word `backbuffer` appears anywhere in your shader source (including comments). It has no `u_` prefix. See [Backbuffer & Feedback](/shader/backbuffer) for details.\n\n## Parameter Directives\n\nDeclare parameters using `// @viji-TYPE:uniformName key:value ...` comments. Each directive generates a uniform and a UI control in the host.\n\n| Directive | Uniform Type | UI Control | Details |\n|-----------|-------------|------------|---------|\n| [`@viji-slider`](/shader/parameters/slider) | `float` | Numeric slider | [Slider](/shader/parameters/slider) |\n| [`@viji-number`](/shader/parameters/number) | `float` | Numeric input | [Number](/shader/parameters/number) |\n| [`@viji-color`](/shader/parameters/color) | `vec3`, or `vec4` with `alpha:true` | Color picker (hex → RGB 0-1, plus alpha) | [Color](/shader/parameters/color) |\n| [`@viji-gradient`](/shader/parameters/gradient) | arrays + `vec3 name(float t)` (or `vec4`) | Color ramp sampled by an injected function | [Gradient](/shader/parameters/gradient) |\n| [`@viji-toggle`](/shader/parameters/toggle) | `bool` | On/off switch | [Toggle](/shader/parameters/toggle) |\n| [`@viji-select`](/shader/parameters/select) | `int` | Dropdown (0-based option index) | [Select](/shader/parameters/select) |\n| [`@viji-image`](/shader/parameters/image) | `sampler2D` | Image upload | [Image](/shader/parameters/image) |\n| [`@viji-button`](/shader/parameters/button) | `bool` | Momentary button (true for one frame) | [Button](/shader/parameters/button) |\n| [`@viji-coordinate`](/shader/parameters/coordinate) | `vec2` | 2D coordinate pad: both components -1 to 1 | [Coordinate](/shader/parameters/coordinate) |\n| [`@viji-accumulator`](/shader/parameters/accumulator) | `float` | CPU-side: `+= rate × deltaTime` | [Accumulator](/shader/parameters/accumulator) |\n| [`@viji-cv:<feature>`](/shader/video) | `bool` (toggleable form only) | Activates a CV pipeline. Bare form is always-on, toggleable form synthesizes a host parameter and a `bool <feature>` uniform | [Video & CV](/shader/video) |\n\nSee [Parameters Overview](/shader/parameters) for syntax, [Grouping](/shader/parameters/grouping) and [Categories](/shader/parameters/categories) for organization.\n\n## Related\n\n- [Shader Basics](/shader/basics): auto-injection, GLSL versions, `@renderer shader`\n- [Shader Quick Start](/shader/quickstart): getting started with shader scenes\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers\n- [Native API Reference](/native/api-reference): JavaScript API for the Native renderer\n- [P5 API Reference](/p5/api-reference): JavaScript API for the P5 renderer"
7177
7308
  }
7178
7309
  ]
7179
7310
  },
@@ -7510,7 +7641,7 @@ export const docsApi = {
7510
7641
  "content": [
7511
7642
  {
7512
7643
  "type": "text",
7513
- "markdown": "# Shader Parameters\n\nParameters are the primary way to give users control over your scene. You declare them at the top of your shader, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Values update in real-time as users interact with the controls.\n\nIn the shader renderer, parameters are declared using `// @viji-*` comment directives. Each directive creates a UI control and a corresponding GLSL uniform: no manual `uniform` declarations needed. Read the uniform directly in your shader code to get the current value.\n\n## Parameter Types\n\n| Directive | Uniform Type | Value | Use For |\n|---|---|---|---|\n| [`@viji-slider`](slider/) | `float` | Continuous range | Speed, intensity, size |\n| [`@viji-number`](number/) | `float` | Precise numeric | Count, threshold |\n| [`@viji-color`](color/) | `vec3` | RGB (0-1) | Tint, palette |\n| [`@viji-toggle`](toggle/) | `bool` | On/off | Invert, enable effect |\n| [`@viji-select`](select/) | `int` | Index (0-based) | Mode, pattern selection |\n| [`@viji-image`](image/) | `sampler2D` | Texture | Overlay, displacement map |\n| [`@viji-button`](button/) | `bool` | Momentary trigger | Reset, spawn, one-shot action |\n| [`@viji-coordinate`](coordinate/) | `vec2` | 2D point (-1 to 1) | XY pad, origin, target position |\n| [`@viji-accumulator`](accumulator/) | `float` | CPU-side integration | Smooth animation phase |\n\n## Basic Pattern\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// @viji-toggle:invert label:\"Invert\" default:false\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec3 col = tint * uv.x * speed;\n if (invert) col = 1.0 - col;\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nEach `@viji-*` directive auto-generates a `uniform` declaration with the name after the colon. `speed` becomes `uniform float speed;`, `tint` becomes `uniform vec3 tint;`, etc.\n\n> [!WARNING]\n> Do not use the `u_` prefix for parameter names: it is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `tint`, `brightness`.\n\n> [!NOTE]\n> Parameter directives use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\n\n## Directive Syntax\n\n```\n// @viji-TYPE:uniformName key:value key:\"string value\" key:[array]\n```\n\n- **TYPE**: one of: [`slider`](slider/), [`number`](number/), [`color`](color/), [`toggle`](toggle/), [`select`](select/), [`image`](image/), [`button`](button/), [`coordinate`](coordinate/), [`accumulator`](accumulator/)\n- **uniformName**: the GLSL uniform name (no `u_` prefix)\n- **key:value** pairs: configure the parameter\n\n## Common Config Keys\n\n| Key | Type | Description |\n|---|---|---|\n| `label` | `string` | *(optional, recommended)* Display name in the UI. Falls back to the uniform name. |\n| `default` | varies | **(required for most types)** Initial value |\n| `min` | `number` | Minimum value ([slider](slider/)/[number](number/)) |\n| `max` | `number` | Maximum value ([slider](slider/)/[number](number/)) |\n| `step` | `number` | Step increment ([slider](slider/)/[number](number/)) |\n| `description` | `string` | Help text |\n| `group` | `string` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `string` | Visibility category: see [Categories](categories/) |\n| `options` | `array` | Options list ([select](select/) only) |\n\n## Uniform Type Mapping\n\n| Parameter Type | GLSL Uniform | Notes |\n|---|---|---|\n| [`slider`](slider/) | `uniform float` | |\n| [`number`](number/) | `uniform float` | Same as slider |\n| [`color`](color/) | `uniform vec3` | RGB components, each 0-1 |\n| [`toggle`](toggle/) | `uniform bool` | |\n| [`select`](select/) | `uniform int` | 0-based index of selected option |\n| [`image`](image/) | `uniform sampler2D` | |\n| [`button`](button/) | `uniform bool` | `true` for 1 frame after click, then `false` |\n| [`coordinate`](coordinate/) | `uniform vec2` | `{ x: number, y: number }` mapped to `vec2(x, y)` |\n| [`accumulator`](accumulator/) | `uniform float` | CPU-side `value += rate × deltaTime` each frame |\n\n> [!WARNING]\n> All `@viji-*` directives must appear as top-level comments before `void main()`. They are parsed once during shader compilation. Viji's shader auto-injection places the generated `uniform` declarations before your code.\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)**. Use the `group:` key to collect related parameters under a shared heading (e.g., `group:animation`). Parameters with the same `group` value appear together in the UI.\n- **[Categories](categories/)**. Use the `category:` key to tag parameters as `general`, `audio`, `video`, or `interaction` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Accumulator](accumulator/): smooth animation without `u_time * speed` jumps\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [Native Parameters](/native/parameters): equivalent JavaScript API\n- [P5 Parameters](/p5/parameters): same system in the P5 renderer\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
7644
+ "markdown": "# Shader Parameters\n\nParameters are the primary way to give users control over your scene. You declare them at the top of your shader, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Values update in real-time as users interact with the controls.\n\nIn the shader renderer, parameters are declared using `// @viji-*` comment directives. Each directive creates a UI control and a corresponding GLSL uniform: no manual `uniform` declarations needed. Read the uniform directly in your shader code to get the current value.\n\n## Parameter Types\n\n| Directive | Uniform Type | Value | Use For |\n|---|---|---|---|\n| [`@viji-slider`](slider/) | `float` | Continuous range | Speed, intensity, size |\n| [`@viji-number`](number/) | `float` | Precise numeric | Count, threshold |\n| [`@viji-color`](color/) | `vec3`, or `vec4` with `alpha:true` | RGB (0-1), plus alpha | Tint, palette |\n| [`@viji-gradient`](gradient/) | uniform arrays + `vec3 name(float t)` (or `vec4`) | Color ramp | Palettes, heat maps, glows |\n| [`@viji-toggle`](toggle/) | `bool` | On/off | Invert, enable effect |\n| [`@viji-select`](select/) | `int` | Index (0-based) | Mode, pattern selection |\n| [`@viji-image`](image/) | `sampler2D` | Texture | Overlay, displacement map |\n| [`@viji-button`](button/) | `bool` | Momentary trigger | Reset, spawn, one-shot action |\n| [`@viji-coordinate`](coordinate/) | `vec2` | 2D point (-1 to 1) | XY pad, origin, target position |\n| [`@viji-accumulator`](accumulator/) | `float` | CPU-side integration | Smooth animation phase |\n\n## Basic Pattern\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// @viji-toggle:invert label:\"Invert\" default:false\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec3 col = tint * uv.x * speed;\n if (invert) col = 1.0 - col;\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nEach `@viji-*` directive auto-generates a `uniform` declaration with the name after the colon. `speed` becomes `uniform float speed;`, `tint` becomes `uniform vec3 tint;`, etc.\n\n> [!WARNING]\n> Do not use the `u_` prefix for parameter names: it is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `tint`, `brightness`.\n\n> [!NOTE]\n> Parameter directives use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\n\n## Directive Syntax\n\n```\n// @viji-TYPE:uniformName key:value key:\"string value\" key:[array]\n```\n\n- **TYPE**: one of: [`slider`](slider/), [`number`](number/), [`color`](color/), [`gradient`](gradient/), [`toggle`](toggle/), [`select`](select/), [`image`](image/), [`button`](button/), [`coordinate`](coordinate/), [`accumulator`](accumulator/)\n- **uniformName**: the GLSL uniform name (no `u_` prefix)\n- **key:value** pairs: configure the parameter\n\n## Common Config Keys\n\n| Key | Type | Description |\n|---|---|---|\n| `label` | `string` | *(optional, recommended)* Display name in the UI. Falls back to the uniform name. |\n| `default` | varies | **(required for most types)** Initial value |\n| `min` | `number` | Minimum value ([slider](slider/)/[number](number/)) |\n| `max` | `number` | Maximum value ([slider](slider/)/[number](number/)) |\n| `step` | `number` | Step increment ([slider](slider/)/[number](number/)) |\n| `description` | `string` | Help text |\n| `group` | `string` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `string` | Visibility category: see [Categories](categories/) |\n| `options` | `array` | Options list ([select](select/) only) |\n\n## Uniform Type Mapping\n\n| Parameter Type | GLSL Uniform | Notes |\n|---|---|---|\n| [`slider`](slider/) | `uniform float` | |\n| [`number`](number/) | `uniform float` | Same as slider |\n| [`color`](color/) | `uniform vec3`, or `uniform vec4` with `alpha:true` | RGB components, each 0-1; `.a` when alpha is on |\n| [`gradient`](gradient/) | `uniform vec3 name_colors[N]`, `float name_positions[N]`, `int name_count` + injected `name(t)` | Sorted stops uploaded as arrays; sample with the function |\n| [`toggle`](toggle/) | `uniform bool` | |\n| [`select`](select/) | `uniform int` | 0-based index of selected option |\n| [`image`](image/) | `uniform sampler2D` | |\n| [`button`](button/) | `uniform bool` | `true` for 1 frame after click, then `false` |\n| [`coordinate`](coordinate/) | `uniform vec2` | `{ x: number, y: number }` mapped to `vec2(x, y)` |\n| [`accumulator`](accumulator/) | `uniform float` | CPU-side `value += rate × deltaTime` each frame |\n\n> [!WARNING]\n> All `@viji-*` directives must appear as top-level comments before `void main()`. They are parsed once during shader compilation. Viji's shader auto-injection places the generated `uniform` declarations before your code.\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)**. Use the `group:` key to collect related parameters under a shared heading (e.g., `group:animation`). Parameters with the same `group` value appear together in the UI.\n- **[Categories](categories/)**. Use the `category:` key to tag parameters as `general`, `audio`, `video`, or `interaction` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp sampled by an injected function\n- [Accumulator](accumulator/): smooth animation without `u_time * speed` jumps\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [Native Parameters](/native/parameters): equivalent JavaScript API\n- [P5 Parameters](/p5/parameters): same system in the P5 renderer\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
7514
7645
  }
7515
7646
  ]
7516
7647
  },
@@ -7575,7 +7706,7 @@ export const docsApi = {
7575
7706
  "shader-param-color": {
7576
7707
  "id": "shader-param-color",
7577
7708
  "title": "Color Parameter",
7578
- "description": "Declare a color picker that maps to a vec3 uniform in GLSL shaders.",
7709
+ "description": "Declare a color picker that maps to a vec3 uniform, or a vec4 with alpha, in GLSL shaders.",
7579
7710
  "headings": [
7580
7711
  {
7581
7712
  "id": "directive-syntax",
@@ -7597,6 +7728,11 @@ export const docsApi = {
7597
7728
  "level": 2,
7598
7729
  "text": "Usage"
7599
7730
  },
7731
+ {
7732
+ "id": "alpha",
7733
+ "level": 2,
7734
+ "text": "Alpha"
7735
+ },
7600
7736
  {
7601
7737
  "id": "rules",
7602
7738
  "level": 2,
@@ -7611,7 +7747,7 @@ export const docsApi = {
7611
7747
  "content": [
7612
7748
  {
7613
7749
  "type": "text",
7614
- "markdown": "# @viji-color\n\n```glsl\n// @viji-color:myColor label:\"Color\" default:#ff6600\nuniform vec3 myColor;\n```\n\nDeclares a color picker parameter. The host renders a color swatch that opens a full picker when clicked. The chosen color is normalized to canonical hex internally and uploaded to the GPU as a `vec3` uniform with components in `0.0-1.0`.\n\n## Directive Syntax\n\n```\n// @viji-color:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | | Display name in the parameter UI |\n| `default` | Yes | | Initial color in any of the [supported forms](#default-value-forms) — **no quotes** |\n| `description` | No | | Tooltip text (use quotes: `description:\"Help text\"`) |\n| `group` | No | `general` | Group name (use quotes: `group:\"colors\"`) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe color is injected as a `vec3`:\n\n| Component | Value | Range |\n|-----------|-------|-------|\n| `.r` | Red channel | 0.0 - 1.0 |\n| `.g` | Green channel | 0.0 - 1.0 |\n| `.b` | Blue channel | 0.0 - 1.0 |\n\nFor hex `#ff8040`: `.r = 1.0`, `.g ≈ 0.502`, `.b ≈ 0.251`.\n\n## Default value forms\n\nThe `default:` key accepts any of the following forms. They are all converted\nto canonical lowercase hex internally; the GPU uniform is always a `vec3` in `0..1`.\n\n| Form | Example | Component ranges |\n|------|---------|------------------|\n| Hex (6-digit) | `default:#ff6600` | — |\n| Hex (3-digit) | `default:#f60` | Auto-expanded |\n| GLSL `vec3()` | `default:vec3(1.0, 0.4, 0.0)` | `0..1` (matches the uniform's runtime range) |\n| CSS-style `rgb()` | `default:rgb(255, 102, 0)` | `0..255` |\n| CSS-style `hsl()` | `default:hsl(24, 100%, 50%)` | `h`: `0..360`, `s`/`l`: `0..100%` |\n| P5-style `hsb()` | `default:hsb(24, 100, 100)` | `h`: `0..360`, `s`/`b`: `0..100` |\n\nFunction-call forms must not be wrapped in quotes (e.g., `default:rgb(255, 102, 0)`,\nnot `default:\"rgb(255, 102, 0)\"`). Spaces inside the parentheses are fine.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-color:bgColor label:\"Background\" default:#0f0f1a\n// @viji-color:accent label:\"Accent\" default:vec3(1.0, 0.27, 0.53)\n// @viji-color:warm label:\"Warm\" default:rgb(255, 200, 100)\n// @viji-color:cool label:\"Cool\" default:hsb(200, 80, 90)\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float d = distance(uv, vec2(0.5));\n vec3 col = mix(accent, bgColor, smoothstep(0.1, 0.4, d));\n // bgColor, accent, warm, cool are all vec3 in 0..1 — usable directly in any\n // GLSL color expression (mix, *, +, etc.)\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations: both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
7750
+ "markdown": "# @viji-color\n\n```glsl\n// @viji-color:myColor label:\"Color\" default:#ff6600\nuniform vec3 myColor;\n```\n\nDeclares a color picker parameter. The host renders a color swatch that opens a full picker when clicked. The chosen color is normalized to canonical hex internally and uploaded to the GPU as a `vec3` uniform with components in `0.0-1.0`.\n\n## Directive Syntax\n\n```\n// @viji-color:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `default` | Yes | | Initial color in any of the [supported forms](#default-value-forms) — **no quotes** |\n| `label` | No | uniform name | Display name in the parameter UI |\n| `description` | No | | Tooltip text (use quotes: `description:\"Help text\"`) |\n| `group` | No | `general` | Group name (use quotes: `group:\"colors\"`) |\n| `category` | No | `general` | Visibility category |\n| `alpha` | No | `false` | `alpha:true` adds a transparency channel: the uniform becomes a `vec4`, the value `#rrggbbaa`, and alpha-bearing defaults are accepted. See [Alpha](#alpha). |\n\n## Uniform Type\n\nThe color is injected as a `vec3`:\n\n| Component | Value | Range |\n|-----------|-------|-------|\n| `.r` | Red channel | 0.0 - 1.0 |\n| `.g` | Green channel | 0.0 - 1.0 |\n| `.b` | Blue channel | 0.0 - 1.0 |\n\nFor hex `#ff8040`: `.r = 1.0`, `.g ≈ 0.502`, `.b ≈ 0.251`.\n\nWith `alpha:true` the uniform is a `vec4` and `.a` carries the alpha in `0.0 - 1.0` (`1.0` for an opaque pick).\n\n## Default value forms\n\nThe `default:` key accepts any of the following forms. They are all converted\nto canonical lowercase hex internally; the GPU uniform is always a `vec3` in `0..1`.\n\n| Form | Example | Component ranges |\n|------|---------|------------------|\n| Hex (6-digit) | `default:#ff6600` | — |\n| Hex (3-digit) | `default:#f60` | Auto-expanded |\n| GLSL `vec3()` | `default:vec3(1.0, 0.4, 0.0)` | `0..1` (matches the uniform's runtime range) |\n| CSS-style `rgb()` | `default:rgb(255, 102, 0)` | `0..255` |\n| CSS-style `hsl()` | `default:hsl(24, 100%, 50%)` | `h`: `0..360`, `s`/`l`: `0..100%` |\n| P5-style `hsb()` | `default:hsb(24, 100, 100)` | `h`: `0..360`, `s`/`b`: `0..100` |\n| Hex (8-digit), needs `alpha:true` | `default:#ff660080 alpha:true` | Alpha byte `00..ff` |\n| Hex (4-digit), needs `alpha:true` | `default:#f608 alpha:true` | Auto-expanded |\n| CSS-style `rgba()`, needs `alpha:true` | `default:rgba(255, 102, 0, 0.5) alpha:true` | `a`: `0..1` |\n| CSS-style `hsla()`, needs `alpha:true` | `default:hsla(24, 100%, 50%, 0.5) alpha:true` | `a`: `0..1` |\n| GLSL `vec4()`, needs `alpha:true` | `default:vec4(1.0, 0.4, 0.0, 0.5) alpha:true` | `0..1` |\n\nFunction-call forms must not be wrapped in quotes (e.g., `default:rgb(255, 102, 0)`,\nnot `default:\"rgb(255, 102, 0)\"`). Spaces inside the parentheses are fine.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-color:bgColor label:\"Background\" default:#0f0f1a\n// @viji-color:accent label:\"Accent\" default:vec3(1.0, 0.27, 0.53)\n// @viji-color:warm label:\"Warm\" default:rgb(255, 200, 100)\n// @viji-color:cool label:\"Cool\" default:hsb(200, 80, 90)\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float d = distance(uv, vec2(0.5));\n vec3 col = mix(accent, bgColor, smoothstep(0.1, 0.4, d));\n // bgColor, accent, warm, cool are all vec3 in 0..1 — usable directly in any\n // GLSL color expression (mix, *, +, etc.)\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations: both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
7615
7751
  },
7616
7752
  {
7617
7753
  "type": "live-example",
@@ -7621,7 +7757,65 @@ export const docsApi = {
7621
7757
  },
7622
7758
  {
7623
7759
  "type": "text",
7624
- "markdown": "## Rules\n\n- Hex values do not use quotes: `default:#ff6600` (not `default:\"#ff6600\"`)\n- Function-call values do not use quotes: `default:vec3(1.0, 0.4, 0.0)`\n- String values do use quotes: `label:\"My Color\"`, `description:\"Pick a color\"`\n- The `label` key is required for all parameter directives\n- The `default` key is required for color parameters\n\n## Related\n\n- [Shader Basics](/shader/basics): shader file structure and directives\n- [Toggle](../toggle/): boolean `uniform bool`\n- [Select](../select/): dropdown `uniform int`\n- [Native Color](/native/parameters/color): equivalent for the Native renderer\n- [P5 Color](/p5/parameters/color): equivalent for the P5 renderer"
7760
+ "markdown": "## Alpha\n\n`alpha:true` switches the directive to a transparency-carrying color: the uniform is a `vec4`, the value is a fixed 8-digit `#rrggbbaa` (opaque defaults are padded to `ff`), and the picker gains an alpha slider.\n\n```glsl\n// @viji-color:tint label:\"Tint\" default:#ff880080 alpha:true\n// uniform vec4 tint; -> tint.rgb, tint.a\n```\n\nOnly a directive with `alpha:true` accepts a default that carries transparency: 8- or 4-digit hex, `rgba()`, `hsla()`, or `vec4()`. Without `alpha:true`, such a default is an error reported with the directive's line, even at full alpha, and the message says what to do: add `alpha:true`, or use a 6-digit hex, `rgb()`, `hsl()`, `hsb()`, or `vec3()`.\n\n## Rules\n\n- Hex values do not use quotes: `default:#ff6600` (not `default:\"#ff6600\"`)\n- Function-call values do not use quotes: `default:vec3(1.0, 0.4, 0.0)`\n- String values do use quotes: `label:\"My Color\"`, `description:\"Pick a color\"`\n- The `label` key is optional; the uniform name is the display fallback\n- The `default` key is required for color parameters\n- An alpha-bearing default requires `alpha:true`\n\n## Related\n\n- [Shader Basics](/shader/basics): shader file structure and directives\n- [Toggle](../toggle/): boolean `uniform bool`\n- [Select](../select/): dropdown `uniform int`\n- [Gradient](../gradient/): a ramp of colors sampled by an injected function\n- [Native Color](/native/parameters/color): equivalent for the Native renderer\n- [P5 Color](/p5/parameters/color): equivalent for the P5 renderer"
7761
+ }
7762
+ ]
7763
+ },
7764
+ "shader-param-gradient": {
7765
+ "id": "shader-param-gradient",
7766
+ "title": "Gradient Parameter",
7767
+ "description": "Declare a color ramp editor that maps to uniform arrays plus an injected GLSL sampling function.",
7768
+ "headings": [
7769
+ {
7770
+ "id": "directive-syntax",
7771
+ "level": 2,
7772
+ "text": "Directive Syntax"
7773
+ },
7774
+ {
7775
+ "id": "generated-glsl",
7776
+ "level": 2,
7777
+ "text": "Generated GLSL"
7778
+ },
7779
+ {
7780
+ "id": "usage",
7781
+ "level": 2,
7782
+ "text": "Usage"
7783
+ },
7784
+ {
7785
+ "id": "interpolation",
7786
+ "level": 2,
7787
+ "text": "Interpolation"
7788
+ },
7789
+ {
7790
+ "id": "budget",
7791
+ "level": 2,
7792
+ "text": "Budget"
7793
+ },
7794
+ {
7795
+ "id": "rules",
7796
+ "level": 2,
7797
+ "text": "Rules"
7798
+ },
7799
+ {
7800
+ "id": "related",
7801
+ "level": 2,
7802
+ "text": "Related"
7803
+ }
7804
+ ],
7805
+ "content": [
7806
+ {
7807
+ "type": "text",
7808
+ "markdown": "# @viji-gradient\n\n```glsl\n// @viji-gradient:ramp label:\"Ramp\" default:[#ff0000@0,#0000ff@1]\n```\n\nDeclares a gradient parameter that the host application renders as a color ramp editor: stops can be added, moved, recolored and removed. Viji uploads the stops as uniform arrays and injects a function named after the directive, so the shader samples the ramp with one call: `ramp(t)`.\n\n## Directive Syntax\n\n```\n// @viji-gradient:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `default` | Yes | | Initial stops as a bracketed list of `#hex@position` items, e.g. `default:[#ff0000@0,#00ff00@0.5,#0000ff@1]`. Hex only (3, 4, 6 or 8 digits), no spaces inside an item, positions in `0..1`, any order. |\n| `label` | No | uniform name | Display name in the parameter UI |\n| `minStops` | No | `2` | Fewest stops the editor allows. Integer in `2..32`. |\n| `maxStops` | No | `8` | Most stops the editor allows. Integer in `2..32`. Sizes the uniform arrays. |\n| `description` | No | | Tooltip text (use quotes: `description:\"Help text\"`) |\n| `group` | No | `general` | Group name (use quotes: `group:\"colors\"`) |\n| `category` | No | `general` | Visibility category |\n| `alpha` | No | `false` | Per-stop transparency. `alpha:true` makes every stop `#rrggbbaa` and the sampler a `vec4`. |\n\n## Generated GLSL\n\nOne gradient directive named `ramp` with `maxStops:8` generates three uniforms and one function:\n\n```glsl\nuniform vec3 ramp_colors[8];\nuniform float ramp_positions[8];\nuniform int ramp_count;\nvec3 ramp(float t);\n```\n\nWith `alpha:true` the array and the function are `vec4`. The function clamps `t` to `0..1`, walks the live stops, and returns the interpolated color. Only the function is meant for shader code; the three uniforms are the upload target and their names are reserved (no other directive may use `ramp_colors`, `ramp_positions` or `ramp_count`).\n\n| `alpha` | Sampler signature | Stop upload |\n|---------|-------------------|-------------|\n| off (default) | `vec3 name(float t)` | `vec3` array, rgb in `0..1` |\n| `alpha:true` | `vec4 name(float t)` | `vec4` array, rgba in `0..1` |\n\n> [!WARNING]\n> Do not use the `u_` prefix for your parameter uniform names: it is reserved for built-in Viji uniforms. Use descriptive names like `ramp`, `palette`, `heat` instead. A gradient name must also differ from every GLSL built-in function (`mix`, `smoothstep`, `texture2D`, ...) and from any function your own shader defines, because the directive name becomes the sampler function.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-gradient:ramp label:\"Ramp\" default:[#ff4488@0,#ffd166@0.5,#4488ff@1]\n// @viji-gradient:glow label:\"Glow\" default:[#ffffff00@0,#ffffffff@0.5,#ffffff00@1] alpha:true\n// @viji-slider:speed label:\"Speed\" default:0.5 min:0.0 max:3.0 step:0.1\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n // Opaque ramp scrolling across the width\n vec3 band = ramp(fract(uv.x + phase * 0.25));\n\n // Alpha ramp as a radial glow, composited over the band\n float d = distance(uv, vec2(0.5));\n vec4 halo = glow(1.0 - smoothstep(0.0, 0.5, d));\n vec3 col = mix(band, halo.rgb, halo.a);\n\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations and the sampler function: both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
7809
+ },
7810
+ {
7811
+ "type": "live-example",
7812
+ "title": "Gradient Ramp",
7813
+ "sceneCode": "// @renderer shader\n// @viji-gradient:ramp label:\"Ramp\" default:[#ff4488@0,#ffd166@0.5,#4488ff@1]\n// @viji-gradient:glow label:\"Glow\" default:[#ffffff00@0,#ffffffff@0.5,#ffffff00@1] alpha:true\n// @viji-slider:speed label:\"Speed\" default:0.5 min:0.0 max:3.0 step:0.1\n// @viji-slider:bands label:\"Bands\" default:2.0 min:1.0 max:8.0 step:1.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n // Opaque ramp repeated `bands` times and scrolling with the accumulator\n vec3 band = ramp(fract(uv.x * bands + phase * 0.25));\n\n // Alpha ramp as a radial glow, aspect-corrected, composited over the band\n vec2 centered = (uv - 0.5) * vec2(u_resolution.x / u_resolution.y, 1.0);\n vec4 halo = glow(1.0 - smoothstep(0.0, 0.5, length(centered)));\n vec3 col = mix(band, halo.rgb, halo.a);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
7814
+ "sceneFile": "gradient-shader.scene.glsl"
7815
+ },
7816
+ {
7817
+ "type": "text",
7818
+ "markdown": "## Interpolation\n\nThe sampler follows CSS `linear-gradient` semantics: colors are mixed premultiplied between stops, two stops at the same position form a hard edge, a stop is returned verbatim at its own position, and when both stops of a segment are fully transparent the color is the plain rgb blend with alpha `0`. Interpolation happens per fragment in float, so stop positions are not quantized and hard edges are exact. The ramp editor uses the same rule, so the rendered ramp matches the swatch in the parameter panel to within `1/255` per channel.\n\n## Budget\n\nEach gradient costs `2 * maxStops + 1` fragment uniform vectors (a `vec3` array element occupies a full `vec4` slot, so the cost is the same in both modes). Eight stops cost 17 vectors; the 32-stop cap costs 65. Viji checks the total against the GPU's fragment uniform budget at load time and reports a clear error instead of a link failure. Gradients use no texture units.\n\n## Rules\n\n- The `default` key is required and must be a bracketed list of at least `minStops` items\n- Each item is `#hex@position`: a hex color and a position in `0..1`, no spaces inside the item\n- An 8- or 4-digit hex stop requires `alpha:true`; without it the directive is an error\n- `minStops` and `maxStops` are integers in `2..32` with `minStops` not above `maxStops`\n- The directive name becomes the sampler function and reserves `<name>_colors`, `<name>_positions` and `<name>_count`\n- String values use quotes: `label:\"Ramp\"`\n\n## Related\n\n- [Shader Basics](/shader/basics): shader file structure and directives\n- [Color](../color/): single color as `uniform vec3` or `vec4`\n- [Accumulator](../accumulator/): parameter-driven animation speed for scrolling a ramp\n- [Native Gradient](/native/parameters/gradient): equivalent for the Native renderer\n- [P5 Gradient](/p5/parameters/gradient): equivalent for the P5 renderer"
7625
7819
  }
7626
7820
  ]
7627
7821
  },
@@ -9878,15 +10072,15 @@ export const docsApi = {
9878
10072
  },
9879
10073
  "ai": {
9880
10074
  "systemPrompts": {
9881
- "base": "# Viji API: Base Reference\n\nThis document is the renderer-agnostic foundation for generating Viji scenes. It is loaded into every AI turn alongside the renderer-specific reference for the renderer in use.\n\n## Architecture\n\nViji scenes run inside a **Web Worker** on an **OffscreenCanvas**. There is no DOM.\n\n- Top-level code runs once when the scene loads (parameter declarations, state, imports).\n- A `render` function (or, for shaders, `void main()`) runs every frame.\n- `fetch()` and `await import()` are available. `window`, `document`, `Image()`, and `localStorage` are not.\n- The global `viji` object exposes everything: canvas, timing, audio, video, computer vision, input, sensors, and parameters. Each renderer also has its own surface (P5's `p5.*` API; the shader's auto-injected uniforms).\n\n## Renderers\n\nThree renderers. Pick by the visual goal, not by perceived difficulty.\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| Language | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js 1.9.4 | GLSL fragment shader |\n| Best for | Full control, Three.js, generative art, pixel-perfect Canvas 2D | Creative coding with shapes, colors, transforms; ports of existing P5 sketches | GPU effects, patterns, raymarching, SDF, post-processing |\n| 3D | Yes (WebGL, Three.js via `await import()`) | Yes (`// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n| External libraries | Yes (any ESM via `await import()`) | P5.js built-in | None (GPU only) |\n\n### Decision criteria\n\n- **Per-pixel GPU effects** (raymarching, fractals, distance fields, fullscreen post-processing, patterns that read every pixel): use **Shader**.\n- **3D with Three.js, custom WebGL, pixel-perfect Canvas 2D, or any external ESM library**: use **Native**.\n- **Classic P5 shapes and transforms** (`ellipse`, `line`, `bezier`, color modes, `push`/`pop`), or a port of an existing P5 sketch: use **P5**.\n- **When ambiguous, default to Native**. Audio reactivity, camera/CV, parameters, and input work in every renderer. Pick on visual style, not data sources.\n\n## Scene shape per renderer\n\nEach example below is the minimum viable entry point. Full surface lives in the matching renderer reference.\n\n**Native**:\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx\n}\n```\n\n**P5**:\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.* prefixed calls\n}\n```\n\n**Shader (GLSL)**:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## Cross-cutting rules (apply to every renderer)\n\n1. **Never** touch the DOM. No `window`, `document`, `Image()`, `localStorage`. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the **top level**, never inside `render()` or `main()`. Parameters declared inside a render call are recreated every frame and break the host UI.\n3. **Always** use `viji.width` / `viji.height` (or `u_resolution` in shaders) for canvas dimensions. Never hardcode pixel sizes.\n4. **Always** use `viji.deltaTime` (or `u_deltaTime` / `@viji-accumulator` in shaders) for animation. Never count frames or assume a fixed frame rate.\n5. **Never** multiply `viji.time` (or `u_time`) by a parameter to drive animation speed. It causes visible jumps when the slider changes. Use a `deltaTime` accumulator at the top level instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter. Each independent speed gets its own accumulator.\n6. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n7. **Always** check `viji.audio.isConnected` (or `u_audioVolume > 0.0`) before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` (or `u_videoConnected`) before reading video. When an input is not connected, degrade gracefully — keep rendering the scene's normal idle/ambient visual. Do **not** draw on-canvas text telling the user to connect or turn on a camera, microphone, or other input; the host provides those controls. (The connection-demo examples draw \"Waiting for…\" text only to illustrate this check itself — don't copy that into real scenes unless the user explicitly asks for an on-canvas prompt.)\n8. **CV features are independent**: each `enableX()` populates only its own subset of fields. Face detection populates `face.bounds` / `center` / `confidence`; face mesh populates `face.landmarks` / `headPose`; emotion detection populates `face.expressions` / `blendshapes`. Fields whose source model is not enabled read as `null` (or `[]` for `landmarks`) — always null-check before reading. CV is expensive and is **not** host-gated, so it is the one feature class that legitimately belongs behind a parameter toggle — but gate by intent: if a CV feature is the scene's defining purpose (the scene is meaningless without it), enable it always-on from module scope; if CV is optional/supplementary, or the scene uses several heavy features, gate each behind a `viji.toggle` (shader: the toggleable `@viji-cv:` form) so the user opts into the cost — and make sure the base visual stands on its own when a toggle is off (no \"enable in parameters\" banner). Each active CV feature uses its own WebGL context; 4+ on low-end hardware can hit context limits, so enable only what you need.\n9. **Always** set `category` on parameters that depend on an external input: `category: 'audio'` for audio-driven controls, `category: 'video'` for video / CV-driven controls, `category: 'interaction'` for mouse / keyboard / touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input. **Never** add on/off parameter toggles to enable/disable built-in inputs or device features — audio, video, mouse, keyboard, touch, motion/orientation sensors, and external devices are all gated by the host's own controls. Read them in code behind `isConnected` / `isInCanvas` / null checks. A parameter that shapes how an input drives the visual (e.g. a sensitivity slider) is fine; an on/off enable toggle for the input itself is not. If the user explicitly asks for one, advise that the host already provides it before complying. (CV features are the sole exception — see the CV rule above.)\n10. **In P5**: prefix every P5 function and constant with `p5.` (instance mode). Never call `createCanvas()`; Viji creates the canvas.\n11. **In shaders**: never redeclare `precision`, never redeclare built-in uniforms (`u_time`, `u_resolution`, etc.), never redeclare parameter uniforms. Never use the `u_` prefix for your own parameter names; it is reserved for Viji's built-ins.\n\n## Parameter declaration shape (cross-renderer)\n\nAssign each parameter to its own top-level `const` with a unique camelCase\nname (`const speed = viji.slider(1, { label: 'Speed' })`) and read it with\n`.value`. `label` is optional but recommended (falls back to the variable\nname). Native and P5 use JavaScript calls at the top level:\n\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // .value: number\nviji.color(default, { label }) // .value: '#rrggbb', .rgb, .hsb\nviji.toggle(default, { label }) // .value: boolean\nviji.select(default, { options, label }) // .value: string | number\nviji.number(default, { min, max, step, label }) // .value: number\nviji.text(default, { label, maxLength }) // .value: string\nviji.image(null, { label }) // .value: ImageBitmap | null\nviji.button({ label }) // .value: boolean (true for 1 frame)\nviji.coordinate({ x: 0, y: 0 }, { step, label, group, category }) // .value: { x, y }, range -1..1\n```\n\n**`slider` / `number` `step` default**: when omitted, `step` auto-derives from `min`/`max` as the largest power of 10 giving ~100 positions across the range (e.g., range `0..1` → `0.01`; range `0.1..5` → `0.01`; range `0..100` → `1`). Specify explicitly only when you need a different grid: `step: 1` for integer counts (`{ min: 3, max: 30, step: 1 }`), `step: 0.001` for fine precision.\n\nShaders use GLSL comment directives that compile to typed uniforms:\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 -> uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 -> uniform vec3 name;\n// @viji-toggle:name label:\"Toggle\" default:false -> uniform bool name;\n// @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\"] -> uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 -> uniform float name;\n// @viji-image:name label:\"Texture\" -> uniform sampler2D name;\n// @viji-button:name label:\"Reset\" -> uniform bool name;\n// @viji-coordinate:name label:\"Pos\" default:[0.0,0.0] -> uniform vec2 name; // range -1..1\n// @viji-accumulator:name rate:speed -> uniform float name;\n```\n\nDirectives use `//` line comments only. Block comments (`/* */`) are not parsed.\n\n## Where to look next\n\nFor the full API surface of the renderer in use, consult the matching reference: Native, P5, or Shader. Each renderer reference includes the complete data shapes for audio, video, CV, input, sensors, external devices, and renderer-specific patterns (e.g. `viji.useContext` for Native, `videoFit` for P5, the complete uniform table for Shader).\n",
9882
- "native": "# Viji API: Native Renderer Reference\r\n\r\nNative scenes run JavaScript on an `OffscreenCanvas` in a Web Worker, with full access to Canvas 2D, WebGL, WebGL2, and WebGPU contexts. External libraries load via `await import()`. Top-level code runs once; a `render(viji)` function runs every frame.\r\n\r\n## Architecture\r\n\r\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters.\r\n- Call `viji.useContext('2d' | 'webgl' | 'webgl2' | 'webgpu')` once at the top of `render()` (or top-level) to obtain a rendering context. After the first call, the matching shortcut is populated: `viji.ctx` / `viji.gl` / `viji.gpu`.\r\n- **Pick one context type for the entire scene.** Calling `useContext` with a different type after the first call returns `null`.\r\n- **WebGPU**: request the adapter and device at top level. `useContext('webgpu')` returns `null` where unsupported: guard and fall back.\r\n- There is **no `setup()` function** in Native. All initialization goes at the top level (which supports `await` for dynamic imports).\r\n- The DOM is unavailable. `fetch()` and `await import()` are available.\r\n\r\n## Rules\r\n\r\n1. **Never** access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` are available.\r\n2. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()`. `label` is optional but recommended (falls back to the variable name).\r\n3. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }` with `h` in 0..360, `s` / `b` in 0..100). Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS `'rgb(...)'` / `'hsl(...)'` strings.\r\n4. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\r\n5. **Always** use `viji.deltaTime` for animation. Use `viji.time` only for constant-speed oscillations (`Math.sin(viji.time * 2.0)`). For anything driven by a parameter, use a `deltaTime` accumulator.\r\n6. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator at the top level. This also applies to nested multiplications: each independent speed gets its own accumulator.\r\n\r\n ```javascript\r\n // WRONG\r\n const t = viji.time * speed.value;\r\n const rot = phase * rotSpeed.value;\r\n\r\n // RIGHT\r\n let phase = 0, rotPhase = 0;\r\n phase += speed.value * viji.deltaTime;\r\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\r\n ```\r\n7. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\r\n8. **Always** call `viji.useContext()` to get a context. Pick one type and use it for the entire scene.\r\n9. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\r\n10. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle` so the user opts into the cost.\r\n11. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\r\n12. **Always** type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so the error can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\r\n\r\n ```javascript\r\n function render(viji: VijiAPI) { ... }\r\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\r\n ```\r\n13. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `native-api-reference` docs page instead of guessing.\r\n\r\n## API reference\r\n\r\n### Canvas and context\r\n\r\n| Member | Type | Description |\r\n|---|---|---|\r\n| `viji.canvas` | `OffscreenCanvas` | The canvas |\r\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Acquire 2D context |\r\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Acquire WebGL 1 |\r\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Acquire WebGL 2 |\r\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Acquire WebGPU |\r\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut after `useContext('2d')` |\r\n| `viji.gl` | `WebGLRenderingContext \\| WebGL2RenderingContext` | Shortcut after WebGL context acquired |\r\n| `viji.gpu` | `GPUCanvasContext` | Shortcut after `useContext('webgpu')` |\r\n| `viji.width`, `viji.height` | `number` | Current canvas dimensions in pixels |\r\n\r\n### Timing\r\n\r\n| Member | Type | Description |\r\n|---|---|---|\r\n| `viji.time` | `number` | Seconds since scene start |\r\n| `viji.deltaTime` | `number` | Seconds since last frame |\r\n| `viji.frameCount` | `number` | Total frames rendered |\r\n| `viji.fps` | `number` | Target frame rate (host-controlled) |\r\n\r\n### Parameters\r\n\r\nDeclare at top level. All accept `{ label, description?, group?, category? }`. Category values: `'audio'`, `'video'`, `'interaction'`, `'general'` (default).\r\n\r\n```javascript\r\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\r\nviji.color(default, { label, group?, category? }) // .value: '#rrggbb', .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}\r\nviji.toggle(default, { label, group?, category? }) // .value: boolean\r\nviji.select(default, { options, label, group?, category? }) // .value: string | number\r\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\r\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\r\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\r\nviji.button({ label, description?, group?, category? }) // .value: boolean (true for 1 frame)\r\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\r\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\r\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\r\n```\r\n\r\n### Audio: `viji.audio`\r\n\r\nCheck `isConnected` first.\r\n\r\n| Member | Type | Notes |\r\n|---|---|---|\r\n| `isConnected` | `boolean` | Audio source active |\r\n| `volume.current`, `volume.peak`, `volume.smoothed` | `number` 0..1 | `smoothed` = 200ms decay envelope of `current` |\r\n| `bands.low`, `lowMid`, `mid`, `highMid`, `high` | `number` 0..1 | Instant band energies (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\r\n| `bands.lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed` | `number` 0..1 | 150ms decay envelope siblings (not nested) |\r\n| `beat.kick`, `snare`, `hat`, `any` | `number` 0..1 | Beat energy curves with a 300ms decay; peak on each detected beat then fall off |\r\n| `beat.kickSmoothed`, `snareSmoothed`, `hatSmoothed`, `anySmoothed` | `number` 0..1 | Smoother 500ms decay envelopes; use for ambient pulses |\r\n| `beat.triggers.kick`, `snare`, `hat`, `any` | `boolean` | True for exactly one frame on the matching beat, then auto-resets. OR-accumulated between frames so no beat is lost |\r\n| `beat.events` | `Array<{type, time, strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in milliseconds; cleared each frame |\r\n| `beat.bpm`, `beat.confidence`, `beat.isLocked` | `number`, `number`, `boolean` | `bpm` is `0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120` as a fallback before lock-on. `confidence` in 0..1. `isLocked` is `true` on stable tempo lock |\r\n| `spectral.brightness`, `spectral.flatness` | `number` 0..1 | Spectral centroid; spectral flatness |\r\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins 0..255 |\r\n| `getWaveform()` | `Float32Array` | Time-domain waveform -1..1 |\r\n\r\nExternal-device audio (`viji.devices[i].audio`) and host-supplied audio streams (`viji.audioStreams[i]`) use the `AudioStreamAPI` shape: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` plus each `*Smoothed` sibling, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. No `beat`, BPM, triggers, or events on streams: those exist only on the main `viji.audio`.\r\n\r\n### Video: `viji.video`\r\n\r\nCheck `isConnected && currentFrame` before drawing.\r\n\r\n| Member | Type | Notes |\r\n|---|---|---|\r\n| `isConnected` | `boolean` | Video source active |\r\n| `currentFrame` | `OffscreenCanvas \\| ImageBitmap \\| null` | Most recent frame |\r\n| `frameWidth`, `frameHeight` | `number` | Source frame size |\r\n| `frameRate` | `number` | Source frame rate |\r\n| `getFrameData()` | `ImageData \\| null` | Pixel data of `currentFrame` |\r\n| `cv` | `VideoCVAPI` | Computer-vision surface: see below |\r\n\r\nCV data outputs (`analysedFrame`, `getAnalysedFrameData()`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs (`enableFaceDetection`, etc.) all live on `viji.video.cv`, not on `viji.video` directly.\r\n\r\n**Aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches video and misaligns CV bounds. Use this helper at module scope:\r\n\r\n```javascript\r\nfunction videoFit(viji, mode = 'cover') {\r\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\r\n const w = viji.width, h = viji.height;\r\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\r\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\r\n const dw = vw * scale, dh = vh * scale;\r\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\r\n}\r\n\r\nconst v = videoFit(viji); // 'cover' or 'contain'\r\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\r\n\r\n// CV coords are normalized 0..1 to the source frame. Map through v to align with the fitted video:\r\nviji.video.cv.faces.forEach(face => {\r\n const bx = v.x + face.bounds.x * v.width;\r\n const by = v.y + face.bounds.y * v.height;\r\n const bw = face.bounds.width * v.width;\r\n const bh = face.bounds.height * v.height;\r\n ctx.strokeRect(bx, by, bw, bh);\r\n});\r\n```\r\n\r\nDefault to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. `viji.video.currentFrame` is the right default for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions (segmentation compositing, face-mesh texturing, landmark-anchored color sampling). The common fallback pattern is `viji.video.cv.analysedFrame ?? viji.video.currentFrame` during the brief window before the first CV inference lands.\r\n\r\n### Computer Vision: `viji.video.cv`\r\n\r\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature consumes a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\r\n\r\n```javascript\r\n// Verbs return Promise<void>. await is optional — without it, the model loads\r\n// asynchronously while render runs and data fields populate when ready.\r\n// Safe to call from module scope (always-on CV) or per-frame inside render()\r\n// gated by a viji.toggle(...) (opt-in CV). Idempotent + reference-counted.\r\nawait viji.video.cv.enableFaceDetection(true | false); // populates face.bounds, face.center, face.confidence\r\nawait viji.video.cv.enableFaceMesh(true | false); // populates face.landmarks (468 pts) + face.headPose\r\nawait viji.video.cv.enableEmotionDetection(true | false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\r\nawait viji.video.cv.enableHandTracking(true | false); // populates viji.video.cv.hands[]\r\nawait viji.video.cv.enablePoseDetection(true | false); // populates viji.video.cv.pose\r\nawait viji.video.cv.enableBodySegmentation(true | false); // populates viji.video.cv.segmentation\r\nviji.video.cv.getActiveFeatures(); // CVFeature[]\r\nviji.video.cv.isProcessing(); // boolean\r\n```\r\n\r\nData outputs on `viji.video.cv`:\r\n- `analysedFrame: OffscreenCanvas | null`: the exact frame paired with the current CV results. `null` until the first inference lands after a feature is enabled. Use the pattern `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during the startup window.\r\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`, cached and re-extracted only on new CV results.\r\n\r\n**`viji.video.cv.faces: FaceData[]`**: `id` always present. Other fields are populated only by their source model and read `null` (or empty array for `landmarks`) otherwise — always null-check before reading.\r\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\r\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\r\n- `confidence: number | null` (0..1) — populated by face detection\r\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\r\n- `headPose: { pitch, yaw, roll } | null` (degrees) — populated by face mesh\r\n- `expressions: { neutral, happy, sad, angry, surprised, disgusted, fearful } | null` (0..1 each) — populated by emotion detection\r\n- `blendshapes | null` — populated by emotion detection; 52 ARKit coefficients (`browDownLeft`/`Right`, `browInnerUp`, `browOuterUpLeft`/`Right`, `cheekPuff`, `cheekSquintLeft`/`Right`, `eyeBlinkLeft`/`Right`, `eyeLookDown`/`In`/`Out`/`UpLeft`/`Right`, `eyeSquintLeft`/`Right`, `eyeWideLeft`/`Right`, `jawForward`/`Left`/`Open`/`Right`, `mouthClose`/`DimpleLeft`/`Right`/`FrownLeft`/`Right`/`Funnel`/`Left`/`LowerDownLeft`/`Right`/`PressLeft`/`Right`/`Pucker`/`Right`/`RollLower`/`Upper`/`ShrugLower`/`Upper`/`SmileLeft`/`Right`/`StretchLeft`/`Right`/`UpperUpLeft`/`Right`, `noseSneerLeft`/`Right`, `tongueOut`: all 0..1)\r\n\r\nIf you need bounds while only running face mesh, either also enable face detection or compute the bounding box from `face.landmarks` min/max yourself.\r\n\r\n**`viji.video.cv.hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks: {x,y,z}[]` (21 points), `palm: {x,y,z}`, `gestures: { fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou }` (0..1 confidence each).\r\n\r\n**`viji.video.cv.pose: PoseData | null`**: `confidence`, `landmarks: {x,y,z,visibility}[]` (33 points), plus body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\r\n\r\n**`viji.video.cv.segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`. Note: the shader `u_segmentationMask` sampler is sampled as a normalized `float` (0.0 = background, 1.0 = person); the JS `mask` byte values are 0/1, not 0/255.\r\n\r\n### Input\r\n\r\n**Pointer** (unified mouse and touch): `viji.pointer`\r\n| Member | Type | Notes |\r\n|---|---|---|\r\n| `x`, `y` | `number` | Position in pixels |\r\n| `deltaX`, `deltaY` | `number` | Per-frame movement |\r\n| `isDown` | `boolean` | Pressed or touching |\r\n| `wasPressed`, `wasReleased` | `boolean` | One-frame edge triggers |\r\n| `isInCanvas` | `boolean` | Inside canvas bounds |\r\n| `type` | `'mouse' \\| 'touch' \\| 'none'` | Active input type |\r\n\r\n**Mouse**: `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\r\n\r\n**Keyboard**: `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys: Set<string>`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\r\n\r\n**Touch**: `viji.touches`: `count`, `points: TouchPoint[]`, `started`, `moved`, `ended` (each a `TouchPoint[]`), `primary: TouchPoint | null`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity: {x,y}`, `isNew`, `isActive`, `isEnding`.\r\n\r\n### Sensors and external devices\r\n\r\n**`viji.device`**: host device motion and orientation:\r\n- `motion: DeviceMotionData | null`: `acceleration: {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate: {alpha,beta,gamma}` (deg/s), `interval` (ms).\r\n- `orientation: DeviceOrientationData | null`: `alpha` (0-360° compass), `beta` (-180..180° front-back tilt), `gamma` (-90..90° left-right tilt), `absolute`.\r\n\r\n**`viji.devices`**: `DeviceState[]`, externally connected devices. Each: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, same shape as `viji.video` but without CV), `audio` (`AudioStreamAPI | null`, lightweight; no beat or BPM).\r\n\r\n### Host-supplied streams\r\n\r\n- `viji.videoStreams: VideoAPI[]`: extra video sources from the host compositor. Same shape as `viji.video`, including a per-stream `cv` surface with the same enable verbs and result fields; `cv.analysedFrame` is populated on the main stream only. May be empty.\r\n- `viji.audioStreams: AudioStreamAPI[]`: extra audio sources from the host. Lightweight shape (no beat or BPM). May be empty.\r\n\r\n### External libraries\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\r\nrenderer.setSize(viji.width, viji.height, false); // always pass false to skip CSS styling\r\n```\r\n\r\nPin library versions in the import URL. Always pass `viji.canvas` to the renderer. Always pass `false` as the third argument to Three.js `setSize`. Handle resize by comparing `viji.width` / `viji.height` against previous values inside `render()`. Transformers.js needs `env.useBrowserCache = false` (Cache Storage is unavailable in the scene worker; model downloads are cached by the engine anyway). Load ONNX Runtime Web 1.27+ from jsdelivr (`https://cdn.jsdelivr.net/npm/onnxruntime-web@<version>/dist/ort.webgpu.min.mjs`, and set `ort.env.wasm.wasmPaths` to that `dist/` folder); esm.sh builds of it are broken. Run model inference off the render path: start it at an interval when not already running, keep the latest result, and draw that.\r\n\r\n## Template\r\n\r\n```javascript\r\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\r\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n angle += speed.value * viji.deltaTime;\r\n\r\n ctx.fillStyle = bgColor.value;\r\n ctx.fillRect(0, 0, viji.width, viji.height);\r\n\r\n const cx = viji.width / 2;\r\n const cy = viji.height / 2;\r\n const radius = Math.min(viji.width, viji.height) * 0.3;\r\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\r\n const n = Math.floor(count.value);\r\n\r\n for (let i = 0; i < n; i++) {\r\n const a = angle + (i / n) * Math.PI * 2;\r\n const x = cx + Math.cos(a) * radius;\r\n const y = cy + Math.sin(a) * radius;\r\n const hue = (i / n) * 360;\r\n ctx.beginPath();\r\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\r\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\r\n ctx.fill();\r\n }\r\n}\r\n```\r\n",
9883
- "p5": "# Viji API: P5 Renderer Reference\n\nP5 scenes use P5.js 1.9.4 inside a Web Worker on an `OffscreenCanvas`. P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix. The first line of the scene must be `// @renderer p5` (2D) or `// @renderer p5 webgl` (3D / WEBGL).\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters: identical to Native.\n- The canvas and its rendering context are managed by P5. **Do not call `createCanvas()`.** Viji creates the canvas in the mode declared by the `// @renderer` directive.\n- `viji.useContext()` is **not available** in P5 mode (the context belongs to P5).\n- Top-level code runs once. `function setup(viji, p5)` runs once for configuration. `function render(viji, p5)` runs every frame.\n- The `p5` instance passed to `setup` and `render` is the P5 instance. Use `p5.background`, `p5.fill`, `p5.circle`, `p5.PI`, etc. Unprefixed names will not resolve.\n\n## Rules\n\n1. **Always** add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line.\n2. **Always** use `render(viji, p5)` (not `draw()`) and `setup(viji, p5)` (not `setup()`).\n3. **Always** prefix every P5 function and constant with `p5.`:\n - `background(0)` -> `p5.background(0)`\n - `fill(255)` -> `p5.fill(255)`\n - `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`\n - `createVector(1, 0)` -> `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` -> `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`\n4. **Never** call `createCanvas()`. The canvas is created and managed by Viji.\n5. **Never** use `preload()`. Use `viji.image(null, { label })` for images, or `fetch()` inside `setup()`.\n6. **Never** use P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edges.\n7. **Never** use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. **Never** use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** use `createCapture()` or `createVideo()`. Use `viji.video.*`.\n10. **Never** use `p5.dom` or `p5.sound`. Use Viji parameters and `viji.audio.*`.\n11. **Never** access `window`, `document`, `Image()`, `localStorage`. `fetch()` is available.\n12. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()` or `setup()`. `label` is optional but recommended (falls back to the variable name).\n13. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (matches `colorMode(RGB, 255)`) and `.hsb` (matches `colorMode(HSB, 360, 100, 100)`). Color defaults accept hex, `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS color strings.\n14. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n15. **Always** use `viji.deltaTime` for animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator. This also applies to nested multiplications; each independent speed gets its own accumulator.\n17. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. **Image parameters with P5:** use `.p5` (a `P5Image`) instead of `.value` when passing to `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. **Video drawing.** In 2D, use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In WEBGL, use `p5.image(viji.video.currentFrame, ...)` only (`p5.drawingContext` is WebGL, not Canvas 2D). **Always preserve aspect ratio** via the `videoFit` helper below.\n20. `p5.createGraphics(w, h)` is **2D only** (`createGraphics(w, h, p5.WEBGL)` is not supported). It creates an OffscreenCanvas internally.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is not available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode is declared by the `// @renderer` directive.** Use `// @renderer p5` for 2D, `// @renderer p5 webgl` for WEBGL. Never call `createCanvas()` or `createCanvas(..., p5.WEBGL)`.\n24. In WEBGL scenes, `p5.drawingContext` is a WebGL context. Use P5 3D drawing primitives or `p5.image()` / textures for images and video.\n25. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes; WEBGL pixel readback follows standard P5.js rules.\n26. **Always** check `viji.audio.isConnected` before reading audio data.\n27. **Always** check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n28. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle`.\n29. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n30. **Always** type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n31. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `p5-api-reference` docs page instead of guessing.\n\n## Video aspect helper\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nif (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji, 'cover');\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n}\n\n// CV bounds and landmarks are normalized 0..1 to the source frame.\n// Map them through v to align with the drawn video.\n```\n\n## P5 to Viji mapping\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `mousePressed()` / `mouseReleased()` callbacks | Check `viji.pointer.wasPressed` / `wasReleased` inside `render()`. |\n| `keyPressed()` / `keyReleased()` callbacks | Check `viji.keyboard.wasPressed(key)` / `wasReleased(key)` inside `render()`. |\n\n## API reference\n\nAll `viji.*` members are identical to Native (same object, same types).\n\n### Canvas and timing\n\n| Member | Type | Notes |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | Managed by P5 |\n| `viji.width`, `viji.height` | `number` | Canvas dimensions |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate |\n\n### Parameters\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category? }) // .value, .rgb, .hsb\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null, .p5: P5Image\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first. Members:\n\n`isConnected` (boolean); `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (0..1; ranges 20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (0..1, 300ms decay curves; peak on each detected beat); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for exactly one frame, OR-accumulated between frames); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; `time` ms; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins, 0..255); `getWaveform(): Float32Array` (2048 samples, -1..1).\n\nExternal-device audio (`viji.devices[i].audio`) and host streams (`viji.audioStreams[i]`) follow the `AudioStreamAPI` shape: `isConnected`, `volume`, `bands` (instant + smoothed siblings), `spectral`, `getFrequencyData`, `getWaveform`. No beat, BPM, triggers, or events on streams.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing. Members:\n\n`isConnected` (boolean); `currentFrame` (`OffscreenCanvas | ImageBitmap | null`); `frameWidth`, `frameHeight`, `frameRate`; `getFrameData(): ImageData | null`; `cv: VideoCVAPI` (CV outputs and verbs live here, not on `viji.video` directly).\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature uses a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true | false); // viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the frame paired with the current CV results. `null` until the first inference lands. Use `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during startup.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`faces: FaceData[]`**: `id` always present. Other fields populated only by their source model; otherwise `null` (`landmarks` is `[]` when empty). Always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks` (21 points), `palm`, `gestures` (`fist`, `openPalm`, `peace`, `thumbsUp`, `thumbsDown`, `pointing`, `iLoveYou` 0..1).\n\n**`pose: PoseData | null`**: `confidence`, `landmarks` (33 points), body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input\n\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`).\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n`viji.device.motion`: `acceleration {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate {alpha,beta,gamma}` (deg/s), `interval` (ms).\n`viji.device.orientation`: `alpha` (0-360°), `beta` (-180..180°), `gamma` (-90..90°), `absolute`.\n\n`viji.devices: DeviceState[]`: each entry: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, no CV), `audio` (`AudioStreamAPI | null`, lightweight).\n\n### Host streams\n\n`viji.videoStreams: VideoAPI[]`: extra video sources. Same shape as `viji.video`, including a per-stream `cv` surface (same verbs and result fields; `cv.analysedFrame` is main-stream only). May be empty.\n`viji.audioStreams: AudioStreamAPI[]`: extra audio sources. May be empty. Lightweight shape (no beat / BPM).\n\n## Template\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n",
9884
- "shader": "# Viji API: Shader Renderer Reference\n\nShader scenes are GLSL fragment shaders running on a fullscreen quad. Write helper functions and `void main()`. Precision and all uniforms (built-in plus those generated from `@viji-*` directives) are auto-injected by Viji. The first line must be `// @renderer shader` (or `#version 300 es` followed by `// @renderer shader` for GLSL ES 3.00).\n\n## Architecture\n\n- The shader is **GLSL ES 1.00** by default. Opt into ES 3.00 by making `#version 300 es` the literal first line.\n- Viji auto-injects `precision mediump float;` and every uniform declaration. Never declare them yourself.\n- ES 3.00 requires `out vec4 fragColor;` before `main` and `fragColor = ...` instead of `gl_FragColor`. ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader references `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n- The `backbuffer` sampler is auto-enabled when referenced. It carries the previous frame.\n\n## Rules\n\n1. **Always** add `// @renderer shader` as the first line (or as line 2 after `#version 300 es`).\n2. **Never** declare `precision mediump float;` or `precision highp float;`. Viji auto-injects precision.\n3. **Never** redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, audio / video / CV uniforms, etc.). They are auto-injected.\n4. **Never** redeclare parameter uniforms. They are auto-generated from `@viji-*` directives.\n5. **Never** use the `u_` prefix for your own parameter names. It is reserved for Viji's built-ins. Use descriptive names (`speed`, `colorMix`, `intensity`).\n6. `@viji-*` parameter directives work with `//` comments only. Block comments (`/* */`) are not parsed.\n7. **Always** use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation. Multiplying `u_time` by a parameter causes visible jumps when sliders change.\n\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase);\n ```\n\n This also applies to **nested** multiplications. Each independent speed needs its own accumulator:\n\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame): reference it directly in code. Viji auto-detects and enables it. Sample with `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n9. Remove any `#ifdef GL_ES` / `precision` blocks; Viji handles them.\n10. **Always** set `category` on input-dependent directives: `category:audio`, `category:video`, `category:interaction`. Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV activations are the exception (the `@viji-cv:` directives below).\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Without the matching `@viji-cv:` activation, CV uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero. Choose the form by intent: use the bare form (`// @viji-cv:faceDetection`) for always-on detection when CV is the scene's defining purpose, or the toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) — which synthesizes both a host-side toggle parameter and a `bool <featureToken>` shader uniform mirroring its state — when CV is optional or the scene uses several heavy features. The six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## Parameter directives\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 step:0.1\n// -> uniform float name;\n\n// @viji-color:name label:\"Color\" default:#ff6600\n// -> uniform vec3 name; (RGB 0..1)\n// default: accepts #rrggbb, #rgb, vec3(r,g,b) (0..1), rgb(r,g,b) (0..255),\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:name label:\"Toggle\" default:false\n// -> uniform bool name;\n\n// @viji-select:name label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// -> uniform int name; (0-based)\n\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// -> uniform float name;\n\n// @viji-image:name label:\"Texture\"\n// -> uniform sampler2D name;\n\n// @viji-button:name label:\"Reset\"\n// -> uniform bool name; (true for one frame on press)\n\n// @viji-coordinate:name label:\"Origin\" default:[0.0,0.0]\n// -> uniform vec2 name; (each component -1..1)\n\n// @viji-accumulator:name rate:speed\n// -> uniform float name; (CPU-side: += rate * deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## Uniform reference\n\nAll uniforms below are always available. Do not declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (host-controlled) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords, bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | Mouse inside canvas |\n| `u_mousePressed` | `bool` | Any button pressed |\n| `u_mouseLeft`, `u_mouseRight`, `u_mouseMiddle` | `bool` | Specific buttons |\n| `u_mouseDelta` | `vec2` | Per-frame movement |\n| `u_mouseWheel` | `float` | Scroll wheel delta |\n| `u_mouseWasPressed`, `u_mouseWasReleased` | `bool` | One-frame edges |\n\n### Pointer (unified mouse / touch)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | Primary input active |\n| `u_pointerWasPressed`, `u_pointerWasReleased` | `bool` | One-frame edges |\n| `u_pointerInCanvas` | `bool` | Inside canvas |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_keySpace`, `u_keyShift`, `u_keyCtrl`, `u_keyAlt` | `bool` | Modifier and space |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrows |\n| `u_keyboard` | `sampler2D` | Full keyboard state (256x3 LUMINANCE). Row 0 = held, row 1 = pressed-this-frame, row 2 = toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_touchCount` | `int` | Active touches (0-5) |\n| `u_touch0`-`u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Audio (scalars)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioVolume`, `u_audioPeak` | `float` | RMS / peak 0..1 |\n| `u_audioVolumeSmoothed` | `float` | 200ms decay envelope of `u_audioVolume` |\n| `u_audioLow`, `u_audioLowMid`, `u_audioMid`, `u_audioHighMid`, `u_audioHigh` | `float` | Band energies 0..1 (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | 150ms decay envelopes of the bands |\n| `u_audioKick`, `u_audioSnare`, `u_audioHat`, `u_audioAny` | `float` | Beat energy curves with 300ms decay; peak on each detected beat then fall off |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes |\n| `u_audioKickTrigger`, `u_audioSnareTrigger`, `u_audioHatTrigger`, `u_audioAnyTrigger` | `bool` | True for exactly one frame on the matching beat, then auto-resets; OR-accumulated between frames |\n| `u_audioBPM`, `u_audioConfidence`, `u_audioIsLocked` | `float`, `float`, `bool` | `u_audioBPM` is `0.0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120.0` as a fallback before lock-on. `u_audioConfidence` 0..1. `u_audioIsLocked` true on stable tempo lock |\n| `u_audioBrightness`, `u_audioFlatness` | `float` | Spectral features 0..1 |\n\n### Audio (textures, main source only)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioFFT` | `sampler2D` | FFT spectrum (1024 bins, 0..255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (-1..1) |\n\nAdditional audio streams and device audio expose scalar uniforms only (no FFT / waveform textures): see \"Streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_video` | `sampler2D` | Most recent video frame |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with current CV uniforms |\n| `u_videoAnalysedAvailable` | `bool` | True after first CV result lands |\n| `u_videoResolution` | `vec2` | Source frame size in pixels |\n| `u_videoFrameRate` | `float` | Source frame rate |\n| `u_videoConnected` | `bool` | Video source active |\n\n**Aspect handling.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with canvas UV stretches the video and misaligns CV uniforms. Use these helpers at the top of every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, video edges cropped)\n// 0 = contain (fits video, canvas letterboxed)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n```\n\nDefault to mode `1` (cover) for live cameras. Use `0` (contain) for CV-overlay shaders where features near edges must stay visible. CV uniforms (`u_face0Center`, etc.) share the same bottom-up convention as `videoUV`; compare them directly without flipping.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when sampling pixels at CV-derived positions (segmentation compositing onto the body, sampling skin under face landmarks, face-mesh texturing). For shaders that consume CV uniforms without sampling at CV positions, stay on `u_video` (`u_videoAnalysed` advances only on inference completion and will stutter the displayed video). When `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` for the brief startup window:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: face detection\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_faceCount` | `int` | Detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | `(x, y, width, height)` normalized 0..1 |\n| `u_face0Center` | `vec2` | Center normalized 0..1 |\n| `u_face0HeadPose` | `vec3` | `(pitch, yaw, roll)` in degrees |\n| `u_face0Confidence` | `float` | 0..1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores 0..1 |\n\n**52 blendshape uniforms** (all `float`, 0..1, ARKit names prefixed with `u_face0`):\n`BrowDownLeft`, `BrowDownRight`, `BrowInnerUp`, `BrowOuterUpLeft`, `BrowOuterUpRight`, `CheekPuff`, `CheekSquintLeft`, `CheekSquintRight`, `EyeBlinkLeft`, `EyeBlinkRight`, `EyeLookDownLeft`, `EyeLookDownRight`, `EyeLookInLeft`, `EyeLookInRight`, `EyeLookOutLeft`, `EyeLookOutRight`, `EyeLookUpLeft`, `EyeLookUpRight`, `EyeSquintLeft`, `EyeSquintRight`, `EyeWideLeft`, `EyeWideRight`, `JawForward`, `JawLeft`, `JawOpen`, `JawRight`, `MouthClose`, `MouthDimpleLeft`, `MouthDimpleRight`, `MouthFrownLeft`, `MouthFrownRight`, `MouthFunnel`, `MouthLeft`, `MouthLowerDownLeft`, `MouthLowerDownRight`, `MouthPressLeft`, `MouthPressRight`, `MouthPucker`, `MouthRight`, `MouthRollLower`, `MouthRollUpper`, `MouthShrugLower`, `MouthShrugUpper`, `MouthSmileLeft`, `MouthSmileRight`, `MouthStretchLeft`, `MouthStretchRight`, `MouthUpperUpLeft`, `MouthUpperUpRight`, `NoseSneerLeft`, `NoseSneerRight`, `TongueOut`.\n\n### CV: hands\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_handCount` | `int` | Detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position `(x, y, z)` |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | 0..1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounds normalized 0..1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: pose\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_poseDetected` | `bool` | Pose detected |\n| `u_poseConfidence` | `float` | 0..1 |\n| `u_nosePosition` | `vec2` | Nose landmark, normalized 0..1 |\n| `u_leftShoulderPosition` … `u_rightAnklePosition` | `vec2` | Joint landmarks (shoulders, elbows, wrists, hips, knees, ankles) |\n\n### CV: body segmentation\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_segmentationMask` | `sampler2D` | 0 = background, 1 = person |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device sensors\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration including gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | `(alpha, beta, gamma)` in degrees |\n| `u_deviceOrientationAbsolute` | `bool` | Using magnetometer |\n\n### External devices\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_deviceCount` | `int` | Device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | External devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Per-device resolution |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Per-device connection |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration (no gravity) |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration (with gravity) |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n### Streams (compositor)\n\nVideo streams (host-provided):\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_videoStreamCount` | `int` | Active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Per-stream resolution |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Per-stream connection |\n\nAudio streams (additional sources, including device audio):\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioStreamCount` | `int` | Active streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Per-slot active |\n| `u_audioStream{i}Volume` | `float` | Volume 0..1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0..1 |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0..1 |\n\n`i` ranges 0..7. **Scalars only**: no `u_audioFFT` / `u_audioWaveform` per stream. Beat / BPM / trigger uniforms are main audio only.\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---|---|---|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, `LINEAR` filtering, `CLAMP_TO_EDGE` wrapping. First frame samples as black. Clears on canvas resize.\n\n## Template\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n"
10075
+ "base": "# Viji API: Base Reference\n\nThis document is the renderer-agnostic foundation for generating Viji scenes. It is loaded into every AI turn alongside the renderer-specific reference for the renderer in use.\n\n## Architecture\n\nViji scenes run inside a **Web Worker** on an **OffscreenCanvas**. There is no DOM.\n\n- Top-level code runs once when the scene loads (parameter declarations, state, imports).\n- A `render` function (or, for shaders, `void main()`) runs every frame.\n- `fetch()` and `await import()` are available. `window`, `document`, `Image()`, and `localStorage` are not.\n- The global `viji` object exposes everything: canvas, timing, audio, video, computer vision, input, sensors, and parameters. Each renderer also has its own surface (P5's `p5.*` API; the shader's auto-injected uniforms).\n\n## Renderers\n\nThree renderers. Pick by the visual goal, not by perceived difficulty.\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| Language | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js 1.9.4 | GLSL fragment shader |\n| Best for | Full control, Three.js, generative art, pixel-perfect Canvas 2D | Creative coding with shapes, colors, transforms; ports of existing P5 sketches | GPU effects, patterns, raymarching, SDF, post-processing |\n| 3D | Yes (WebGL, Three.js via `await import()`) | Yes (`// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n| External libraries | Yes (any ESM via `await import()`) | P5.js built-in | None (GPU only) |\n\n### Decision criteria\n\n- **Per-pixel GPU effects** (raymarching, fractals, distance fields, fullscreen post-processing, patterns that read every pixel): use **Shader**.\n- **3D with Three.js, custom WebGL, pixel-perfect Canvas 2D, or any external ESM library**: use **Native**.\n- **Classic P5 shapes and transforms** (`ellipse`, `line`, `bezier`, color modes, `push`/`pop`), or a port of an existing P5 sketch: use **P5**.\n- **When ambiguous, default to Native**. Audio reactivity, camera/CV, parameters, and input work in every renderer. Pick on visual style, not data sources.\n\n## Scene shape per renderer\n\nEach example below is the minimum viable entry point. Full surface lives in the matching renderer reference.\n\n**Native**:\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx\n}\n```\n\n**P5**:\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.* prefixed calls\n}\n```\n\n**Shader (GLSL)**:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## Cross-cutting rules (apply to every renderer)\n\n1. **Never** touch the DOM. No `window`, `document`, `Image()`, `localStorage`. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the **top level**, never inside `render()` or `main()`. Parameters declared inside a render call are recreated every frame and break the host UI.\n3. **Always** use `viji.width` / `viji.height` (or `u_resolution` in shaders) for canvas dimensions. Never hardcode pixel sizes.\n4. **Always** use `viji.deltaTime` (or `u_deltaTime` / `@viji-accumulator` in shaders) for animation. Never count frames or assume a fixed frame rate.\n5. **Never** multiply `viji.time` (or `u_time`) by a parameter to drive animation speed. It causes visible jumps when the slider changes. Use a `deltaTime` accumulator at the top level instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter. Each independent speed gets its own accumulator.\n6. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n7. **Always** check `viji.audio.isConnected` (or `u_audioVolume > 0.0`) before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` (or `u_videoConnected`) before reading video. When an input is not connected, degrade gracefully — keep rendering the scene's normal idle/ambient visual. Do **not** draw on-canvas text telling the user to connect or turn on a camera, microphone, or other input; the host provides those controls. (The connection-demo examples draw \"Waiting for…\" text only to illustrate this check itself — don't copy that into real scenes unless the user explicitly asks for an on-canvas prompt.)\n8. **CV features are independent**: each `enableX()` populates only its own subset of fields. Face detection populates `face.bounds` / `center` / `confidence`; face mesh populates `face.landmarks` / `headPose`; emotion detection populates `face.expressions` / `blendshapes`. Fields whose source model is not enabled read as `null` (or `[]` for `landmarks`) — always null-check before reading. CV is expensive and is **not** host-gated, so it is the one feature class that legitimately belongs behind a parameter toggle — but gate by intent: if a CV feature is the scene's defining purpose (the scene is meaningless without it), enable it always-on from module scope; if CV is optional/supplementary, or the scene uses several heavy features, gate each behind a `viji.toggle` (shader: the toggleable `@viji-cv:` form) so the user opts into the cost — and make sure the base visual stands on its own when a toggle is off (no \"enable in parameters\" banner). Each active CV feature uses its own WebGL context; 4+ on low-end hardware can hit context limits, so enable only what you need.\n9. **Always** set `category` on parameters that depend on an external input: `category: 'audio'` for audio-driven controls, `category: 'video'` for video / CV-driven controls, `category: 'interaction'` for mouse / keyboard / touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input. **Never** add on/off parameter toggles to enable/disable built-in inputs or device features — audio, video, mouse, keyboard, touch, motion/orientation sensors, and external devices are all gated by the host's own controls. Read them in code behind `isConnected` / `isInCanvas` / null checks. A parameter that shapes how an input drives the visual (e.g. a sensitivity slider) is fine; an on/off enable toggle for the input itself is not. If the user explicitly asks for one, advise that the host already provides it before complying. (CV features are the sole exception — see the CV rule above.)\n10. **In P5**: prefix every P5 function and constant with `p5.` (instance mode). Never call `createCanvas()`; Viji creates the canvas.\n11. **In shaders**: never redeclare `precision`, never redeclare built-in uniforms (`u_time`, `u_resolution`, etc.), never redeclare parameter uniforms. Never use the `u_` prefix for your own parameter names; it is reserved for Viji's built-ins.\n\n## Parameter declaration shape (cross-renderer)\n\nAssign each parameter to its own top-level `const` with a unique camelCase\nname (`const speed = viji.slider(1, { label: 'Speed' })`) and read it with\n`.value`. `label` is optional but recommended (falls back to the variable\nname). Native and P5 use JavaScript calls at the top level:\n\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // .value: number\nviji.color(default, { label, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb, .hsb, .alpha\nviji.gradient([{ color, position }], { label, minStops, maxStops, alpha }) // .value: sorted stops, .at(t): hex at t; default required (2..8 stops)\nviji.toggle(default, { label }) // .value: boolean\nviji.select(default, { options, label }) // .value: string | number\nviji.number(default, { min, max, step, label }) // .value: number\nviji.text(default, { label, maxLength }) // .value: string\nviji.image(null, { label }) // .value: ImageBitmap | null\nviji.button({ label }) // .value: boolean (true for 1 frame)\nviji.coordinate({ x: 0, y: 0 }, { step, label, group, category }) // .value: { x, y }, range -1..1\n```\n\n**`slider` / `number` `step` default**: when omitted, `step` auto-derives from `min`/`max` as the largest power of 10 giving ~100 positions across the range (e.g., range `0..1` → `0.01`; range `0.1..5` → `0.01`; range `0..100` → `1`). Specify explicitly only when you need a different grid: `step: 1` for integer counts (`{ min: 3, max: 30, step: 1 }`), `step: 0.001` for fine precision.\n\nShaders use GLSL comment directives that compile to typed uniforms:\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 -> uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 -> uniform vec3 name; (alpha:true -> vec4; 8-digit default needs alpha:true)\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] -> vec3 name(float t) sampler + arrays (alpha:true -> vec4; stops are #hex@position)\n// @viji-toggle:name label:\"Toggle\" default:false -> uniform bool name;\n// @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\"] -> uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 -> uniform float name;\n// @viji-image:name label:\"Texture\" -> uniform sampler2D name;\n// @viji-button:name label:\"Reset\" -> uniform bool name;\n// @viji-coordinate:name label:\"Pos\" default:[0.0,0.0] -> uniform vec2 name; // range -1..1\n// @viji-accumulator:name rate:speed -> uniform float name;\n```\n\nDirectives use `//` line comments only. Block comments (`/* */`) are not parsed.\n\n## Where to look next\n\nFor the full API surface of the renderer in use, consult the matching reference: Native, P5, or Shader. Each renderer reference includes the complete data shapes for audio, video, CV, input, sensors, external devices, and renderer-specific patterns (e.g. `viji.useContext` for Native, `videoFit` for P5, the complete uniform table for Shader).\n",
10076
+ "native": "# Viji API: Native Renderer Reference\n\nNative scenes run JavaScript on an `OffscreenCanvas` in a Web Worker, with full access to Canvas 2D, WebGL, WebGL2, and WebGPU contexts. External libraries load via `await import()`. Top-level code runs once; a `render(viji)` function runs every frame.\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters.\n- Call `viji.useContext('2d' | 'webgl' | 'webgl2' | 'webgpu')` once at the top of `render()` (or top-level) to obtain a rendering context. After the first call, the matching shortcut is populated: `viji.ctx` / `viji.gl` / `viji.gpu`.\n- **Pick one context type for the entire scene.** Calling `useContext` with a different type after the first call returns `null`.\n- **WebGPU**: request the adapter and device at top level. `useContext('webgpu')` returns `null` where unsupported: guard and fall back.\n- There is **no `setup()` function** in Native. All initialization goes at the top level (which supports `await` for dynamic imports).\n- The DOM is unavailable. `fetch()` and `await import()` are available.\n\n## Rules\n\n1. **Never** access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()`. `label` is optional but recommended (falls back to the variable name).\n3. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }` with `h` in 0..360, `s` / `b` in 0..100). Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n5. **Always** use `viji.deltaTime` for animation. Use `viji.time` only for constant-speed oscillations (`Math.sin(viji.time * 2.0)`). For anything driven by a parameter, use a `deltaTime` accumulator.\n6. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator at the top level. This also applies to nested multiplications: each independent speed gets its own accumulator.\n\n ```javascript\n // WRONG\n const t = viji.time * speed.value;\n const rot = phase * rotSpeed.value;\n\n // RIGHT\n let phase = 0, rotPhase = 0;\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n7. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. **Always** call `viji.useContext()` to get a context. Pick one type and use it for the entire scene.\n9. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n10. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle` so the user opts into the cost.\n11. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n12. **Always** type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so the error can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\n\n ```javascript\n function render(viji: VijiAPI) { ... }\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\n ```\n13. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `native-api-reference` docs page instead of guessing.\n\n## API reference\n\n### Canvas and context\n\n| Member | Type | Description |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | The canvas |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Acquire 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Acquire WebGL 1 |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Acquire WebGL 2 |\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Acquire WebGPU |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut after `useContext('2d')` |\n| `viji.gl` | `WebGLRenderingContext \\| WebGL2RenderingContext` | Shortcut after WebGL context acquired |\n| `viji.gpu` | `GPUCanvasContext` | Shortcut after `useContext('webgpu')` |\n| `viji.width`, `viji.height` | `number` | Current canvas dimensions in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|---|---|---|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (host-controlled) |\n\n### Parameters\n\nDeclare at top level. All accept `{ label, description?, group?, category? }`. Category values: `'audio'`, `'video'`, `'interaction'`, `'general'` (default).\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.button({ label, description?, group?, category? }) // .value: boolean (true for 1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first.\n\n| Member | Type | Notes |\n|---|---|---|\n| `isConnected` | `boolean` | Audio source active |\n| `volume.current`, `volume.peak`, `volume.smoothed` | `number` 0..1 | `smoothed` = 200ms decay envelope of `current` |\n| `bands.low`, `lowMid`, `mid`, `highMid`, `high` | `number` 0..1 | Instant band energies (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\n| `bands.lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed` | `number` 0..1 | 150ms decay envelope siblings (not nested) |\n| `beat.kick`, `snare`, `hat`, `any` | `number` 0..1 | Beat energy curves with a 300ms decay; peak on each detected beat then fall off |\n| `beat.kickSmoothed`, `snareSmoothed`, `hatSmoothed`, `anySmoothed` | `number` 0..1 | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick`, `snare`, `hat`, `any` | `boolean` | True for exactly one frame on the matching beat, then auto-resets. OR-accumulated between frames so no beat is lost |\n| `beat.events` | `Array<{type, time, strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in milliseconds; cleared each frame |\n| `beat.bpm`, `beat.confidence`, `beat.isLocked` | `number`, `number`, `boolean` | `bpm` is `0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120` as a fallback before lock-on. `confidence` in 0..1. `isLocked` is `true` on stable tempo lock |\n| `spectral.brightness`, `spectral.flatness` | `number` 0..1 | Spectral centroid; spectral flatness |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins 0..255 |\n| `getWaveform()` | `Float32Array` | Time-domain waveform -1..1 |\n\nExternal-device audio (`viji.devices[i].audio`) and host-supplied audio streams (`viji.audioStreams[i]`) use the `AudioStreamAPI` shape: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` plus each `*Smoothed` sibling, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. No `beat`, BPM, triggers, or events on streams: those exist only on the main `viji.audio`.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing.\n\n| Member | Type | Notes |\n|---|---|---|\n| `isConnected` | `boolean` | Video source active |\n| `currentFrame` | `OffscreenCanvas \\| ImageBitmap \\| null` | Most recent frame |\n| `frameWidth`, `frameHeight` | `number` | Source frame size |\n| `frameRate` | `number` | Source frame rate |\n| `getFrameData()` | `ImageData \\| null` | Pixel data of `currentFrame` |\n| `cv` | `VideoCVAPI` | Computer-vision surface: see below |\n\nCV data outputs (`analysedFrame`, `getAnalysedFrameData()`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs (`enableFaceDetection`, etc.) all live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches video and misaligns CV bounds. Use this helper at module scope:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0..1 to the source frame. Map through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. `viji.video.currentFrame` is the right default for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions (segmentation compositing, face-mesh texturing, landmark-anchored color sampling). The common fallback pattern is `viji.video.cv.analysedFrame ?? viji.video.currentFrame` during the brief window before the first CV inference lands.\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature consumes a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional — without it, the model loads\n// asynchronously while render runs and data fields populate when ready.\n// Safe to call from module scope (always-on CV) or per-frame inside render()\n// gated by a viji.toggle(...) (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\nawait viji.video.cv.enableHandTracking(true | false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the exact frame paired with the current CV results. `null` until the first inference lands after a feature is enabled. Use the pattern `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during the startup window.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`, cached and re-extracted only on new CV results.\n\n**`viji.video.cv.faces: FaceData[]`**: `id` always present. Other fields are populated only by their source model and read `null` (or empty array for `landmarks`) otherwise — always null-check before reading.\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\n- `confidence: number | null` (0..1) — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\n- `headPose: { pitch, yaw, roll } | null` (degrees) — populated by face mesh\n- `expressions: { neutral, happy, sad, angry, surprised, disgusted, fearful } | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` — populated by emotion detection; 52 ARKit coefficients (`browDownLeft`/`Right`, `browInnerUp`, `browOuterUpLeft`/`Right`, `cheekPuff`, `cheekSquintLeft`/`Right`, `eyeBlinkLeft`/`Right`, `eyeLookDown`/`In`/`Out`/`UpLeft`/`Right`, `eyeSquintLeft`/`Right`, `eyeWideLeft`/`Right`, `jawForward`/`Left`/`Open`/`Right`, `mouthClose`/`DimpleLeft`/`Right`/`FrownLeft`/`Right`/`Funnel`/`Left`/`LowerDownLeft`/`Right`/`PressLeft`/`Right`/`Pucker`/`Right`/`RollLower`/`Upper`/`ShrugLower`/`Upper`/`SmileLeft`/`Right`/`StretchLeft`/`Right`/`UpperUpLeft`/`Right`, `noseSneerLeft`/`Right`, `tongueOut`: all 0..1)\n\nIf you need bounds while only running face mesh, either also enable face detection or compute the bounding box from `face.landmarks` min/max yourself.\n\n**`viji.video.cv.hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks: {x,y,z}[]` (21 points), `palm: {x,y,z}`, `gestures: { fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou }` (0..1 confidence each).\n\n**`viji.video.cv.pose: PoseData | null`**: `confidence`, `landmarks: {x,y,z,visibility}[]` (33 points), plus body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`. Note: the shader `u_segmentationMask` sampler is sampled as a normalized `float` (0.0 = background, 1.0 = person); the JS `mask` byte values are 0/1, not 0/255.\n\n### Input\n\n**Pointer** (unified mouse and touch): `viji.pointer`\n| Member | Type | Notes |\n|---|---|---|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Per-frame movement |\n| `isDown` | `boolean` | Pressed or touching |\n| `wasPressed`, `wasReleased` | `boolean` | One-frame edge triggers |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `type` | `'mouse' \\| 'touch' \\| 'none'` | Active input type |\n\n**Mouse**: `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n\n**Keyboard**: `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys: Set<string>`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n\n**Touch**: `viji.touches`: `count`, `points: TouchPoint[]`, `started`, `moved`, `ended` (each a `TouchPoint[]`), `primary: TouchPoint | null`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity: {x,y}`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n**`viji.device`**: host device motion and orientation:\n- `motion: DeviceMotionData | null`: `acceleration: {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate: {alpha,beta,gamma}` (deg/s), `interval` (ms).\n- `orientation: DeviceOrientationData | null`: `alpha` (0-360° compass), `beta` (-180..180° front-back tilt), `gamma` (-90..90° left-right tilt), `absolute`.\n\n**`viji.devices`**: `DeviceState[]`, externally connected devices. Each: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, same shape as `viji.video` but without CV), `audio` (`AudioStreamAPI | null`, lightweight; no beat or BPM).\n\n### Host-supplied streams\n\n- `viji.videoStreams: VideoAPI[]`: extra video sources from the host compositor. Same shape as `viji.video`, including a per-stream `cv` surface with the same enable verbs and result fields; `cv.analysedFrame` is populated on the main stream only. May be empty.\n- `viji.audioStreams: AudioStreamAPI[]`: extra audio sources from the host. Lightweight shape (no beat or BPM). May be empty.\n\n### External libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // always pass false to skip CSS styling\n```\n\nPin library versions in the import URL. Always pass `viji.canvas` to the renderer. Always pass `false` as the third argument to Three.js `setSize`. Handle resize by comparing `viji.width` / `viji.height` against previous values inside `render()`. Transformers.js needs `env.useBrowserCache = false` (Cache Storage is unavailable in the scene worker; model downloads are cached by the engine anyway). Load ONNX Runtime Web 1.27+ from jsdelivr (`https://cdn.jsdelivr.net/npm/onnxruntime-web@<version>/dist/ort.webgpu.min.mjs`, and set `ort.env.wasm.wasmPaths` to that `dist/` folder); esm.sh builds of it are broken. Run model inference off the render path: start it at an interval when not already running, keep the latest result, and draw that.\n\n## Template\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n",
10077
+ "p5": "# Viji API: P5 Renderer Reference\n\nP5 scenes use P5.js 1.9.4 inside a Web Worker on an `OffscreenCanvas`. P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix. The first line of the scene must be `// @renderer p5` (2D) or `// @renderer p5 webgl` (3D / WEBGL).\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters: identical to Native.\n- The canvas and its rendering context are managed by P5. **Do not call `createCanvas()`.** Viji creates the canvas in the mode declared by the `// @renderer` directive.\n- `viji.useContext()` is **not available** in P5 mode (the context belongs to P5).\n- Top-level code runs once. `function setup(viji, p5)` runs once for configuration. `function render(viji, p5)` runs every frame.\n- The `p5` instance passed to `setup` and `render` is the P5 instance. Use `p5.background`, `p5.fill`, `p5.circle`, `p5.PI`, etc. Unprefixed names will not resolve.\n\n## Rules\n\n1. **Always** add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line.\n2. **Always** use `render(viji, p5)` (not `draw()`) and `setup(viji, p5)` (not `setup()`).\n3. **Always** prefix every P5 function and constant with `p5.`:\n - `background(0)` -> `p5.background(0)`\n - `fill(255)` -> `p5.fill(255)`\n - `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`\n - `createVector(1, 0)` -> `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` -> `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`\n4. **Never** call `createCanvas()`. The canvas is created and managed by Viji.\n5. **Never** use `preload()`. Use `viji.image(null, { label })` for images, or `fetch()` inside `setup()`.\n6. **Never** use P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edges.\n7. **Never** use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. **Never** use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** use `createCapture()` or `createVideo()`. Use `viji.video.*`.\n10. **Never** use `p5.dom` or `p5.sound`. Use Viji parameters and `viji.audio.*`.\n11. **Never** access `window`, `document`, `Image()`, `localStorage`. `fetch()` is available.\n12. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()` or `setup()`. `label` is optional but recommended (falls back to the variable name).\n13. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (matches `colorMode(RGB, 255)`) and `.hsb` (matches `colorMode(HSB, 360, 100, 100)`). Color defaults accept hex, `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS color strings.\n14. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n15. **Always** use `viji.deltaTime` for animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator. This also applies to nested multiplications; each independent speed gets its own accumulator.\n17. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. **Image parameters with P5:** use `.p5` (a `P5Image`) instead of `.value` when passing to `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. **Video drawing.** In 2D, use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In WEBGL, use `p5.image(viji.video.currentFrame, ...)` only (`p5.drawingContext` is WebGL, not Canvas 2D). **Always preserve aspect ratio** via the `videoFit` helper below.\n20. `p5.createGraphics(w, h)` is **2D only** (`createGraphics(w, h, p5.WEBGL)` is not supported). It creates an OffscreenCanvas internally.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is not available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode is declared by the `// @renderer` directive.** Use `// @renderer p5` for 2D, `// @renderer p5 webgl` for WEBGL. Never call `createCanvas()` or `createCanvas(..., p5.WEBGL)`.\n24. In WEBGL scenes, `p5.drawingContext` is a WebGL context. Use P5 3D drawing primitives or `p5.image()` / textures for images and video.\n25. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes; WEBGL pixel readback follows standard P5.js rules.\n26. **Always** check `viji.audio.isConnected` before reading audio data.\n27. **Always** check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n28. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle`.\n29. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n30. **Always** type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n31. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `p5-api-reference` docs page instead of guessing.\n\n## Video aspect helper\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nif (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji, 'cover');\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n}\n\n// CV bounds and landmarks are normalized 0..1 to the source frame.\n// Map them through v to align with the drawn video.\n```\n\n## P5 to Viji mapping\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `mousePressed()` / `mouseReleased()` callbacks | Check `viji.pointer.wasPressed` / `wasReleased` inside `render()`. |\n| `keyPressed()` / `keyReleased()` callbacks | Check `viji.keyboard.wasPressed(key)` / `wasReleased(key)` inside `render()`. |\n\n## API reference\n\nAll `viji.*` members are identical to Native (same object, same types).\n\n### Canvas and timing\n\n| Member | Type | Notes |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | Managed by P5 |\n| `viji.width`, `viji.height` | `number` | Canvas dimensions |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate |\n\n### Parameters\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null, .p5: P5Image\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first. Members:\n\n`isConnected` (boolean); `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (0..1; ranges 20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (0..1, 300ms decay curves; peak on each detected beat); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for exactly one frame, OR-accumulated between frames); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; `time` ms; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins, 0..255); `getWaveform(): Float32Array` (2048 samples, -1..1).\n\nExternal-device audio (`viji.devices[i].audio`) and host streams (`viji.audioStreams[i]`) follow the `AudioStreamAPI` shape: `isConnected`, `volume`, `bands` (instant + smoothed siblings), `spectral`, `getFrequencyData`, `getWaveform`. No beat, BPM, triggers, or events on streams.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing. Members:\n\n`isConnected` (boolean); `currentFrame` (`OffscreenCanvas | ImageBitmap | null`); `frameWidth`, `frameHeight`, `frameRate`; `getFrameData(): ImageData | null`; `cv: VideoCVAPI` (CV outputs and verbs live here, not on `viji.video` directly).\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature uses a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true | false); // viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the frame paired with the current CV results. `null` until the first inference lands. Use `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during startup.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`faces: FaceData[]`**: `id` always present. Other fields populated only by their source model; otherwise `null` (`landmarks` is `[]` when empty). Always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks` (21 points), `palm`, `gestures` (`fist`, `openPalm`, `peace`, `thumbsUp`, `thumbsDown`, `pointing`, `iLoveYou` 0..1).\n\n**`pose: PoseData | null`**: `confidence`, `landmarks` (33 points), body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input\n\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`).\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n`viji.device.motion`: `acceleration {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate {alpha,beta,gamma}` (deg/s), `interval` (ms).\n`viji.device.orientation`: `alpha` (0-360°), `beta` (-180..180°), `gamma` (-90..90°), `absolute`.\n\n`viji.devices: DeviceState[]`: each entry: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, no CV), `audio` (`AudioStreamAPI | null`, lightweight).\n\n### Host streams\n\n`viji.videoStreams: VideoAPI[]`: extra video sources. Same shape as `viji.video`, including a per-stream `cv` surface (same verbs and result fields; `cv.analysedFrame` is main-stream only). May be empty.\n`viji.audioStreams: AudioStreamAPI[]`: extra audio sources. May be empty. Lightweight shape (no beat / BPM).\n\n## Template\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n",
10078
+ "shader": "# Viji API: Shader Renderer Reference\n\nShader scenes are GLSL fragment shaders running on a fullscreen quad. Write helper functions and `void main()`. Precision and all uniforms (built-in plus those generated from `@viji-*` directives) are auto-injected by Viji. The first line must be `// @renderer shader` (or `#version 300 es` followed by `// @renderer shader` for GLSL ES 3.00).\n\n## Architecture\n\n- The shader is **GLSL ES 1.00** by default. Opt into ES 3.00 by making `#version 300 es` the literal first line.\n- Viji auto-injects `precision mediump float;` and every uniform declaration. Never declare them yourself.\n- ES 3.00 requires `out vec4 fragColor;` before `main` and `fragColor = ...` instead of `gl_FragColor`. ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader references `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n- The `backbuffer` sampler is auto-enabled when referenced. It carries the previous frame.\n\n## Rules\n\n1. **Always** add `// @renderer shader` as the first line (or as line 2 after `#version 300 es`).\n2. **Never** declare `precision mediump float;` or `precision highp float;`. Viji auto-injects precision.\n3. **Never** redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, audio / video / CV uniforms, etc.). They are auto-injected.\n4. **Never** redeclare parameter uniforms. They are auto-generated from `@viji-*` directives.\n5. **Never** use the `u_` prefix for your own parameter names. It is reserved for Viji's built-ins. Use descriptive names (`speed`, `colorMix`, `intensity`).\n6. `@viji-*` parameter directives work with `//` comments only. Block comments (`/* */`) are not parsed.\n7. **Always** use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation. Multiplying `u_time` by a parameter causes visible jumps when sliders change.\n\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase);\n ```\n\n This also applies to **nested** multiplications. Each independent speed needs its own accumulator:\n\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame): reference it directly in code. Viji auto-detects and enables it. Sample with `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n9. Remove any `#ifdef GL_ES` / `precision` blocks; Viji handles them.\n10. **Always** set `category` on input-dependent directives: `category:audio`, `category:video`, `category:interaction`. Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV activations are the exception (the `@viji-cv:` directives below).\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Without the matching `@viji-cv:` activation, CV uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero. Choose the form by intent: use the bare form (`// @viji-cv:faceDetection`) for always-on detection when CV is the scene's defining purpose, or the toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) — which synthesizes both a host-side toggle parameter and a `bool <featureToken>` shader uniform mirroring its state — when CV is optional or the scene uses several heavy features. The six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## Parameter directives\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 step:0.1\n// -> uniform float name;\n\n// @viji-color:name label:\"Color\" default:#ff6600\n// (alpha:true -> uniform vec4 name; an 8-digit default without alpha:true is an error)\n\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] minStops:2 maxStops:8\n// -> vec3 name(float t) sampler (vec4 with alpha:true) over uniform arrays name_colors[N], name_positions[N], name_count\n// stops are #hex@position (hex only), default required; call name(t) in main(), never declare the uniforms\n// -> uniform vec3 name; (RGB 0..1)\n// default: accepts #rrggbb, #rgb, vec3(r,g,b) (0..1), rgb(r,g,b) (0..255),\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:name label:\"Toggle\" default:false\n// -> uniform bool name;\n\n// @viji-select:name label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// -> uniform int name; (0-based)\n\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// -> uniform float name;\n\n// @viji-image:name label:\"Texture\"\n// -> uniform sampler2D name;\n\n// @viji-button:name label:\"Reset\"\n// -> uniform bool name; (true for one frame on press)\n\n// @viji-coordinate:name label:\"Origin\" default:[0.0,0.0]\n// -> uniform vec2 name; (each component -1..1)\n\n// @viji-accumulator:name rate:speed\n// -> uniform float name; (CPU-side: += rate * deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## Uniform reference\n\nAll uniforms below are always available. Do not declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (host-controlled) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords, bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | Mouse inside canvas |\n| `u_mousePressed` | `bool` | Any button pressed |\n| `u_mouseLeft`, `u_mouseRight`, `u_mouseMiddle` | `bool` | Specific buttons |\n| `u_mouseDelta` | `vec2` | Per-frame movement |\n| `u_mouseWheel` | `float` | Scroll wheel delta |\n| `u_mouseWasPressed`, `u_mouseWasReleased` | `bool` | One-frame edges |\n\n### Pointer (unified mouse / touch)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | Primary input active |\n| `u_pointerWasPressed`, `u_pointerWasReleased` | `bool` | One-frame edges |\n| `u_pointerInCanvas` | `bool` | Inside canvas |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_keySpace`, `u_keyShift`, `u_keyCtrl`, `u_keyAlt` | `bool` | Modifier and space |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrows |\n| `u_keyboard` | `sampler2D` | Full keyboard state (256x3 LUMINANCE). Row 0 = held, row 1 = pressed-this-frame, row 2 = toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_touchCount` | `int` | Active touches (0-5) |\n| `u_touch0`-`u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Audio (scalars)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioVolume`, `u_audioPeak` | `float` | RMS / peak 0..1 |\n| `u_audioVolumeSmoothed` | `float` | 200ms decay envelope of `u_audioVolume` |\n| `u_audioLow`, `u_audioLowMid`, `u_audioMid`, `u_audioHighMid`, `u_audioHigh` | `float` | Band energies 0..1 (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | 150ms decay envelopes of the bands |\n| `u_audioKick`, `u_audioSnare`, `u_audioHat`, `u_audioAny` | `float` | Beat energy curves with 300ms decay; peak on each detected beat then fall off |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes |\n| `u_audioKickTrigger`, `u_audioSnareTrigger`, `u_audioHatTrigger`, `u_audioAnyTrigger` | `bool` | True for exactly one frame on the matching beat, then auto-resets; OR-accumulated between frames |\n| `u_audioBPM`, `u_audioConfidence`, `u_audioIsLocked` | `float`, `float`, `bool` | `u_audioBPM` is `0.0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120.0` as a fallback before lock-on. `u_audioConfidence` 0..1. `u_audioIsLocked` true on stable tempo lock |\n| `u_audioBrightness`, `u_audioFlatness` | `float` | Spectral features 0..1 |\n\n### Audio (textures, main source only)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioFFT` | `sampler2D` | FFT spectrum (1024 bins, 0..255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (-1..1) |\n\nAdditional audio streams and device audio expose scalar uniforms only (no FFT / waveform textures): see \"Streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_video` | `sampler2D` | Most recent video frame |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with current CV uniforms |\n| `u_videoAnalysedAvailable` | `bool` | True after first CV result lands |\n| `u_videoResolution` | `vec2` | Source frame size in pixels |\n| `u_videoFrameRate` | `float` | Source frame rate |\n| `u_videoConnected` | `bool` | Video source active |\n\n**Aspect handling.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with canvas UV stretches the video and misaligns CV uniforms. Use these helpers at the top of every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, video edges cropped)\n// 0 = contain (fits video, canvas letterboxed)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n```\n\nDefault to mode `1` (cover) for live cameras. Use `0` (contain) for CV-overlay shaders where features near edges must stay visible. CV uniforms (`u_face0Center`, etc.) share the same bottom-up convention as `videoUV`; compare them directly without flipping.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when sampling pixels at CV-derived positions (segmentation compositing onto the body, sampling skin under face landmarks, face-mesh texturing). For shaders that consume CV uniforms without sampling at CV positions, stay on `u_video` (`u_videoAnalysed` advances only on inference completion and will stutter the displayed video). When `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` for the brief startup window:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: face detection\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_faceCount` | `int` | Detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | `(x, y, width, height)` normalized 0..1 |\n| `u_face0Center` | `vec2` | Center normalized 0..1 |\n| `u_face0HeadPose` | `vec3` | `(pitch, yaw, roll)` in degrees |\n| `u_face0Confidence` | `float` | 0..1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores 0..1 |\n\n**52 blendshape uniforms** (all `float`, 0..1, ARKit names prefixed with `u_face0`):\n`BrowDownLeft`, `BrowDownRight`, `BrowInnerUp`, `BrowOuterUpLeft`, `BrowOuterUpRight`, `CheekPuff`, `CheekSquintLeft`, `CheekSquintRight`, `EyeBlinkLeft`, `EyeBlinkRight`, `EyeLookDownLeft`, `EyeLookDownRight`, `EyeLookInLeft`, `EyeLookInRight`, `EyeLookOutLeft`, `EyeLookOutRight`, `EyeLookUpLeft`, `EyeLookUpRight`, `EyeSquintLeft`, `EyeSquintRight`, `EyeWideLeft`, `EyeWideRight`, `JawForward`, `JawLeft`, `JawOpen`, `JawRight`, `MouthClose`, `MouthDimpleLeft`, `MouthDimpleRight`, `MouthFrownLeft`, `MouthFrownRight`, `MouthFunnel`, `MouthLeft`, `MouthLowerDownLeft`, `MouthLowerDownRight`, `MouthPressLeft`, `MouthPressRight`, `MouthPucker`, `MouthRight`, `MouthRollLower`, `MouthRollUpper`, `MouthShrugLower`, `MouthShrugUpper`, `MouthSmileLeft`, `MouthSmileRight`, `MouthStretchLeft`, `MouthStretchRight`, `MouthUpperUpLeft`, `MouthUpperUpRight`, `NoseSneerLeft`, `NoseSneerRight`, `TongueOut`.\n\n### CV: hands\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_handCount` | `int` | Detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position `(x, y, z)` |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | 0..1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounds normalized 0..1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: pose\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_poseDetected` | `bool` | Pose detected |\n| `u_poseConfidence` | `float` | 0..1 |\n| `u_nosePosition` | `vec2` | Nose landmark, normalized 0..1 |\n| `u_leftShoulderPosition` … `u_rightAnklePosition` | `vec2` | Joint landmarks (shoulders, elbows, wrists, hips, knees, ankles) |\n\n### CV: body segmentation\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_segmentationMask` | `sampler2D` | 0 = background, 1 = person |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device sensors\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration including gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | `(alpha, beta, gamma)` in degrees |\n| `u_deviceOrientationAbsolute` | `bool` | Using magnetometer |\n\n### External devices\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_deviceCount` | `int` | Device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | External devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Per-device resolution |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Per-device connection |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration (no gravity) |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration (with gravity) |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n### Streams (compositor)\n\nVideo streams (host-provided):\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_videoStreamCount` | `int` | Active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Per-stream resolution |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Per-stream connection |\n\nAudio streams (additional sources, including device audio):\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioStreamCount` | `int` | Active streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Per-slot active |\n| `u_audioStream{i}Volume` | `float` | Volume 0..1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0..1 |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0..1 |\n\n`i` ranges 0..7. **Scalars only**: no `u_audioFFT` / `u_audioWaveform` per stream. Beat / BPM / trigger uniforms are main audio only.\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---|---|---|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, `LINEAR` filtering, `CLAMP_TO_EDGE` wrapping. First frame samples as black. Clears on canvas resize.\n\n## Template\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n"
9885
10079
  },
9886
10080
  "conversionGuides": {
9887
- "p5": "# Converting P5.js Sketches to Viji P5\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing P5.js sketch into a Viji scene. It is self-contained: it includes both the source-to-target mapping and the Viji P5 target reference needed to produce correct converted output, without requiring the Viji P5 system prompt to also be loaded.\n\n## Source platform: standard P5.js sketches\n\nStandard P5.js sketches run in a browser tab. They typically:\n- Use `setup()` to call `createCanvas(w, h)` and configure the sketch (`colorMode`, `frameRate`, etc.).\n- Use `draw()` as the per-frame loop (`background`, `fill`, shape primitives, etc.).\n- Use global `mouseX`, `mouseY`, `mouseIsPressed`, `keyIsPressed`, `frameCount`, `width`, `height`.\n- Define event callbacks at the top level: `mousePressed`, `keyPressed`, `touchStarted`, etc.\n- Use `preload()` to fetch assets via `loadImage`, `loadFont`, `loadJSON`.\n- Possibly use `createCapture(VIDEO)`, `p5.AudioIn()`, `p5.sound`, `p5.dom` add-ons.\n\nP5.js v1.9.4 is what Viji pins. When in doubt about a P5 function, the p5.js v1.x reference is authoritative.\n\n## Target platform: Viji P5\n\nViji P5 scenes run inside a Web Worker on an `OffscreenCanvas` and use P5.js v1.9.4 in **instance mode**. The DOM is unavailable. Every P5 function and constant requires the `p5.` prefix. The canvas is created and managed by Viji; never call `createCanvas()`.\n\n## Conversion rules\n\n1. **Always** set the first line based on the source canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or any 3D primitives. Never keep `createCanvas()`.\n2. Rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it does not exist, do not add one.\n4. **Always** prefix every P5 function and constant with `p5.`. No exceptions: `background` -> `p5.background`, `fill` -> `p5.fill`, `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`, `createVector(...)` -> `p5.createVector(...)`, `map(...)` -> `p5.map(...)`, `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`.\n5. **Never** keep `createCanvas()`. The canvas is created by Viji. WEBGL is selected only via `// @renderer p5 webgl`, never via `createCanvas(..., p5.WEBGL)`.\n6. **Never** keep `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` inside an async `setup()` for other data.\n7. **Never** keep P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edge detection.\n8. **Never** keep `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, or `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** keep `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n10. **Never** keep `createCapture()` or `createVideo()`. Use `viji.video.*`.\n11. **Never** keep `p5.dom` or `p5.sound` add-ons. Replace UI with Viji parameters and audio with `viji.audio.*`.\n12. **Never** keep `window`, `document`, `Image()`, or `localStorage`. `fetch()` is available.\n13. Lift constants and tunable values to top-level parameters using `viji.slider`, `viji.color`, `viji.toggle`, etc. Read via `.value` inside `render()`.\n14. Replace `frameCount`-based timing with a `viji.deltaTime` accumulator. Never multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator.\n15. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n16. **Never** enable CV features by default; use toggle parameters.\n17. **Always** set `category` on input-dependent parameters: `'audio'`, `'video'`, `'interaction'`.\n18. **Always** type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Direct mapping table\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. Use `// @renderer p5`. |\n| `createCanvas(w, h, WEBGL)` | Remove. Use `// @renderer p5 webgl` as the first line. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `loadFont(url)` | Not available. Use CSS generic font names with `p5.textFont('monospace' \\| 'serif' \\| 'sans-serif')`. |\n| `loadJSON(url)` | `await fetch(url).then(r => r.json())` inside async `setup()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `createCapture(VIDEO)` | `viji.video.currentFrame` (plus `viji.video.isConnected` check). |\n| `new p5.AudioIn(); mic.getLevel()` | `viji.audio.volume.current` (after `viji.audio.isConnected` check). |\n| `new p5.FFT(); fft.analyze()` | `viji.audio.getFrequencyData()` (`Uint8Array` of 1024 bins). |\n| `mousePressed()` callback | `if (viji.pointer.wasPressed) { ... }` inside `render()`. |\n| `keyPressed()` callback | `if (viji.keyboard.wasPressed(key)) { ... }` inside `render()`. |\n| `touchStarted()` callback | `viji.touches.started.forEach(t => ...)` inside `render()`. |\n\n## Target API reference (Viji P5)\n\nThe `viji` object is identical to Native. Access it inside `setup(viji, p5)` and `render(viji, p5)`. `viji.useContext()` is **not available** in P5; the context is managed by P5.\n\n**Canvas and timing**: `viji.canvas` (`OffscreenCanvas`), `viji.width`, `viji.height`, `viji.time`, `viji.deltaTime`, `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category? }) // .value: '#rrggbb', .rgb, .hsb\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap|null, .p5: P5Image\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\nFor images displayed with P5, pass `param.p5` (a `P5Image`) to `p5.image()`, not `param.value`.\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves; peak on detected beats); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1).\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. Members: `currentFrame` (`OffscreenCanvas | ImageBitmap | null`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv` (VideoCVAPI). CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect handling** (use for every video / CV conversion):\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\nconst v = videoFit(viji, 'cover');\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0..1 to the source frame; map through v to align.\n```\nDefault `viji.video.currentFrame` for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\n**CV: `viji.video.cv`**: enable explicitly (never by default):\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (frame paired with current CV results), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures(): CVFeature[]`, `isProcessing(): boolean`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes}`; `blendshapes` are 52 ARKit coefficients 0..1), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures: {fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou}}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type`.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`: each `TouchPoint` has `id`, `x`, `y`, `pressure`, `radius`, `velocity`, etc.\n\n**Sensors / streams**: `viji.device.{motion, orientation}`, `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]` (lightweight `AudioStreamAPI`: no beat / BPM).\n\n## P5 gotchas in the worker\n\n- **Fonts**: `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is unavailable.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is not supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes, it is a WebGL context; do not call Canvas-2D APIs on it.\n- **`viji.useContext()`** is not available in P5; the context belongs to P5.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## Worked example\n\nSource P5 sketch:\n```javascript\nlet angle = 0;\nfunction setup() {\n createCanvas(800, 600);\n colorMode(HSB, 360, 100, 100);\n}\nfunction draw() {\n angle += 0.02;\n background(0, 0, 10);\n const x = width / 2 + cos(angle) * width * 0.3;\n const y = height / 2 + sin(angle) * height * 0.3;\n noStroke();\n fill((frameCount * 0.5) % 360, 80, 100);\n circle(x, y, width * 0.05);\n}\nfunction mousePressed() {\n angle = 0;\n}\n```\n\nConverted Viji P5 scene:\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst reactivity = viji.slider(0, { min: 0, max: 1, label: 'Audio Reactivity', category: 'audio' });\n\nlet angle = 0;\nlet huePhase = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) angle = 0;\n\n const audioBoost = viji.audio.isConnected ? viji.audio.volume.smoothed * reactivity.value : 0;\n angle += (speed.value + audioBoost * 4) * viji.deltaTime;\n huePhase += 30 * viji.deltaTime;\n\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(huePhase % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nKey changes: removed `createCanvas`, added `// @renderer p5`; renamed `draw` to `render(viji, p5)`; gave `setup` the `(viji, p5)` signature; added `p5.` prefix everywhere; replaced `width`/`height` with `viji.width`/`viji.height`; replaced the implicit `0.02` frame-rate-coupled animation with a `viji.deltaTime` accumulator driven by a `speed` slider; replaced `(frameCount * 0.5) % 360` hue with a separate `huePhase` accumulator (so changing speed never jumps the color); replaced the `mousePressed()` callback with a `viji.pointer.wasPressed` check inside `render`; added an opt-in audio-reactive `reactivity` slider with `category: 'audio'`.\n",
9888
- "shadertoy": "# Converting Shadertoy Shaders to Viji Shader Scenes\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing Shadertoy shader into a Viji shader scene. It is self-contained: it includes both the source-to-target mapping and the Viji shader target reference needed to produce correct converted output, without requiring the Viji shader system prompt to also be loaded.\n\n## Source platform: Shadertoy\n\nShadertoy shaders are GLSL fragment shaders authored against a fixed API:\n- Entry point: `void mainImage(out vec4 fragColor, in vec2 fragCoord)`.\n- Built-in uniforms: `iResolution` (`vec3`: `.xy` size, `.z` aspect), `iTime`, `iTimeDelta`, `iFrame`, `iMouse` (`vec4`: `.xy` current position, `.zw` click origin), `iDate` (year, month, day, seconds), `iChannel0`-`iChannel3` (textures whose meaning depends on the channel binding: Image, FFT, Sound, Music, Microphone, Keyboard, Cubemap).\n- Side metadata: `iChannelResolution[i]`, `iChannelTime[i]`, `iSampleRate`.\n- Multi-buffer pipelines (Buffer A, B, C, D), Cubemap buffer, Sound output buffer, VR mode (`mainVR`). None of these have Viji equivalents.\n\n## Target platform: Viji shader\n\nGLSL fragment shader on a fullscreen quad. Standard `void main()` entry. Precision and all uniforms (built-ins and `@viji-*` directive uniforms) are auto-injected by Viji. **Never declare them.** Artists declare parameters via `// @viji-*` line-comment directives, which compile to typed uniforms.\n\nGLSL ES 1.00 by default. Opt into ES 3.00 by making `#version 300 es` the literal first line.\n\n## Conversion rules\n\n1. **Always** add `// @renderer shader` as the very first line (or as line 2 after `#version 300 es`).\n2. **Never** declare `precision mediump float;` or `precision highp float;`. Viji auto-injects precision.\n3. **Never** redeclare built-in uniforms. They are auto-injected.\n4. **Never** redeclare parameter uniforms. They are auto-generated from `@viji-*` directives.\n5. Convert the `mainImage` signature to standard `void main()`:\n - Replace `fragCoord` with `gl_FragCoord.xy`.\n - Replace `fragColor` (the `out` parameter) with `gl_FragColor` (ES 1.00) or the declared `out vec4 fragColor` (ES 3.00).\n - Remove the `mainImage` wrapper entirely.\n\n6. Replace Shadertoy uniforms with Viji equivalents:\n\n | Shadertoy | Viji | Notes |\n |---|---|---|\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. |\n | `iResolution.z` | `u_resolution.x / u_resolution.y` | Aspect ratio. |\n | `iResolution.x` / `.y` | `u_resolution.x` / `.y` | Direct match. |\n | `iTime` | `u_time` | Elapsed seconds. |\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\n | `iFrame` | `u_frame` (`int`) | Frame counter. |\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\n | `iMouse.z` | `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click origin. |\n | `iMouse.w` | `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click origin. |\n | `iChannelN` (static Image) | `@viji-image:channelN label:\"…\"` | See rule 7. |\n | `iChannelN` (FFT) | `u_audioFFT` | Auto-injected `sampler2D`, 1024 bins 0..255. |\n | `iChannelN` (Sound / Music / Microphone) | `u_audioFFT` and/or `u_audioWaveform` | See two-row note in rule 7. |\n | `iChannelN` (Keyboard) | `u_keyboard` | See rule 8. |\n | `iChannelN` (Cubemap) | Not supported | Only 2D textures available. |\n | `iChannelResolution[i]` | Not available | Track manually if needed. |\n | `iChannelTime[i]` | Not available | Per-channel time is not tracked. |\n | `iDate` | Not available | Use `u_time`. |\n | `iSampleRate` | Not available | Not applicable. |\n\n7. **Static-image `iChannel` slots** become `@viji-image` parameters:\n ```glsl\n // @viji-image:channel0 label:\"Texture 1\"\n ```\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (ES 1.00) or `texture(channel0, uv)` (ES 3.00).\n\n **Audio iChannel slots** (Sound / Music / Microphone) sample the auto-injected audio textures directly:\n ```glsl\n float spectrum = texture2D(u_audioFFT, vec2(uv.x, 0.0)).r;\n float waveform = texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r;\n ```\n\n **Two-row audio channel**: Shadertoy's Sound / Music / Microphone iChannels encode FFT in row 0 (`y` near 0.0) and waveform in row 1 (`y` near 0.5+) of a single texture. When converting:\n - `texture(iChannelN, vec2(uv.x, 0.0)).r` (row 0) -> `texture2D(u_audioFFT, vec2(uv.x, 0.0)).r`.\n - `texture(iChannelN, vec2(uv.x, 0.5)).r` or `vec2(uv.x, 0.75)` (row 1) -> `texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r`.\n - When unsure: prefer `u_audioFFT` for spectrum-style visuals, `u_audioWaveform` for oscilloscope-style visuals.\n\n8. **Keyboard `iChannel` slots** become `u_keyboard`:\n ```glsl\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\n ```\n `u_keyboard` is a built-in `sampler2D` (256x3). Row 0 = held, row 1 = pressed-this-frame, row 2 = toggle. Do not declare it.\n\n9. `iResolution` used as `vec3`: replace with `vec3(u_resolution, u_resolution.x / u_resolution.y)`, or refactor to use the `vec2` form directly.\n\n10. **Parameter-driven animation must use `@viji-accumulator`, never `u_time * speed`.** Multiplying `u_time` by a parameter causes visible jumps when the slider changes.\n\n ```glsl\n // WRONG (source pattern)\n float t = iTime * speed;\n\n // RIGHT (converted)\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float t = phase;\n ```\n\n Same rule for nested multiplications. Each independent speed gets its own accumulator.\n\n11. Adding artist-controllable parameters uses `@viji-*` directives:\n ```glsl\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\n // @viji-color:name label:\"Label\" default:#ff6600\n // @viji-toggle:name label:\"Label\" default:true\n // @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\",\"C\"]\n // @viji-image:name label:\"Texture\"\n // @viji-button:name label:\"Reset\"\n // @viji-coordinate:name label:\"Origin\" default:[0.0,0.0]\n // @viji-accumulator:name rate:speed\n ```\n Never use the `u_` prefix for parameter names; it is reserved for built-in uniforms. Directives work with `//` line comments only.\n\n12. **`#version 300 es`** stays as the literal first line if present. Then `// @renderer shader` on line 2. Replace `gl_FragColor = ...` with a declared `out vec4 fragColor;` (before `main`) and `fragColor = ...`. Replace `texture2D()` with `texture()`.\n\n13. Remove any `#ifdef GL_ES` / `precision` blocks. Viji handles them.\n\n14. **Always** set `category` on input-dependent directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse / touch controls.\n\n15. **Single Buffer feedback** becomes Viji's `backbuffer`:\n ```glsl\n vec4 prev = texture2D(backbuffer, uv);\n ```\n `backbuffer` is auto-detected and enabled when referenced. RGBA 8-bit, `LINEAR`, `CLAMP_TO_EDGE`, no `u_` prefix. First frame samples as black.\n\n## Unsupported features (warn the artist)\n\n- **Multi-buffer pipelines** (Buffer A -> B -> C -> D): only single `backbuffer` is available.\n- **Cubemap buffer** (`samplerCube`): not supported.\n- **3D textures** (`sampler3D`): not supported.\n- **`iChannelTime[i]`**, **`iChannelResolution[i]`**, **`iSampleRate`**, **`iDate`**: not available.\n- **Sound output buffer**: not supported.\n- **`mainVR()`**: not supported.\n- **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\n\n## Target uniform reference (Viji shader)\n\nAll uniforms below are auto-injected. Never declare them.\n\n**Core**: `u_resolution` (`vec2`), `u_time` (`float`), `u_deltaTime` (`float`), `u_frame` (`int`), `u_fps` (`float`).\n\n**Mouse**: `u_mouse` (`vec2`, pixels, bottom-left origin), `u_mouseInCanvas`, `u_mousePressed`, `u_mouseLeft`, `u_mouseRight`, `u_mouseMiddle` (`bool`), `u_mouseDelta` (`vec2`), `u_mouseWheel` (`float`), `u_mouseWasPressed`, `u_mouseWasReleased` (`bool`).\n\n**Pointer (unified mouse / touch)**: `u_pointer` (`vec2`), `u_pointerDelta` (`vec2`), `u_pointerDown`, `u_pointerWasPressed`, `u_pointerWasReleased`, `u_pointerInCanvas` (`bool`).\n\n**Keyboard**: `u_keySpace`, `u_keyShift`, `u_keyCtrl`, `u_keyAlt`, `u_keyW`/`A`/`S`/`D`, `u_keyUp`/`Down`/`Left`/`Right` (`bool`). `u_keyboard` (`sampler2D`, 256x3): row 0 held, row 1 pressed-this-frame, row 2 toggle.\n\n**Touch**: `u_touchCount` (`int`), `u_touch0`-`u_touch4` (`vec2`).\n\n**Audio scalars**: `u_audioVolume`, `u_audioPeak`, `u_audioVolumeSmoothed` (200ms decay), `u_audioLow`/`LowMid`/`Mid`/`HighMid`/`High` (band energies 20-120 / 120-400 / 400-1600 / 1600-6000 / 6000-16000 Hz), `u_audioLowSmoothed`-`u_audioHighSmoothed` (150ms decay), `u_audioKick`/`Snare`/`Hat`/`Any` (300ms decay curves), `u_audioKickSmoothed`-`u_audioAnySmoothed` (500ms decay), `u_audioKickTrigger`/`SnareTrigger`/`HatTrigger`/`AnyTrigger` (`bool`, true for one frame, OR-accumulated), `u_audioBPM` (`0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on), `u_audioConfidence`, `u_audioIsLocked` (`bool`), `u_audioBrightness`, `u_audioFlatness`.\n\n**Audio textures (main source only)**: `u_audioFFT` (`sampler2D`, 1024 FFT bins 0..255), `u_audioWaveform` (`sampler2D`, time-domain -1..1).\n\n**Video**: `u_video` (`sampler2D`), `u_videoAnalysed` (`sampler2D`, paired with CV uniforms), `u_videoAnalysedAvailable` (`bool`), `u_videoResolution` (`vec2`), `u_videoFrameRate` (`float`), `u_videoConnected` (`bool`). Use the `vijiVideoUV` helper below for aspect handling.\n\n**CV face**: `u_faceCount` (`int`), `u_face0Bounds` (`vec4`, normalized 0..1), `u_face0Center` (`vec2`, normalized), `u_face0HeadPose` (`vec3` degrees), `u_face0Confidence` (`float`), `u_face0Neutral`-`u_face0Fearful` (7 expressions 0..1), 52 blendshape uniforms prefixed `u_face0` (e.g. `u_face0JawOpen`, `u_face0EyeBlinkLeft`, `u_face0MouthSmileRight`). Activated via `// @viji-cv:faceDetection` / `// @viji-cv:faceMesh` / `// @viji-cv:emotionDetection`.\n\n**CV hands**: `u_handCount` (`int`), `u_leftHandPalm`/`u_rightHandPalm` (`vec3`), `u_leftHandConfidence`/`u_rightHandConfidence`, `u_leftHandBounds`/`u_rightHandBounds` (`vec4`), 7 gesture uniforms per hand (`Fist`, `OpenPalm`, `Peace`, `ThumbsUp`, `ThumbsDown`, `Pointing`, `ILoveYou`). Activated via `// @viji-cv:handTracking`.\n\n**CV pose**: `u_poseDetected` (`bool`), `u_poseConfidence`, joint landmarks (`u_nosePosition`, `u_leftShoulderPosition`-`u_rightAnklePosition`, all `vec2` normalized 0..1). Activated via `// @viji-cv:poseDetection`.\n\n**CV segmentation**: `u_segmentationMask` (`sampler2D`, 0 = background, 1 = person), `u_segmentationRes` (`vec2`). Activated via `// @viji-cv:bodySegmentation`.\n\n**Device sensors**: `u_deviceAcceleration` (`vec3`, m/s² without gravity), `u_deviceAccelerationGravity`, `u_deviceRotationRate` (deg/s), `u_deviceOrientation` (`vec3`, degrees), `u_deviceOrientationAbsolute` (`bool`).\n\n**External devices**: `u_deviceCount` (`int`, 0..8), `u_device0`-`u_device7` (`sampler2D` camera textures), `u_device0Resolution`-`u_device7Resolution` (`vec2`), `u_device0Connected`-`u_device7Connected` (`bool`), plus per-device acceleration / rotation / orientation `vec3` uniforms.\n\n**Compositor streams**: `u_videoStreamCount`, `u_videoStream0`-`u_videoStream7` (`sampler2D`), with `*Resolution` and `*Connected` per slot. `u_audioStreamCount`, `u_audioStream0Connected`-`u_audioStream7Connected` (`bool`), `u_audioStream{i}Volume`, `u_audioStream{i}Low`-`u_audioStream{i}High`, `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` (`float` 0..1). Scalars only on streams: no per-stream FFT / waveform. Beat / BPM / trigger uniforms are main audio only.\n\n**Backbuffer**: `backbuffer` (`sampler2D`, no `u_` prefix). Auto-enabled when referenced.\n\n## Video aspect helper\n\nCamera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with canvas UV stretches the video and misaligns CV uniforms. Use these at the top of any video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, video edges cropped)\n// 0 = contain (fits video, canvas letterboxed)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n```\n\n## Worked example\n\nSource Shadertoy shader:\n```glsl\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = fragCoord / iResolution.xy;\n float t = iTime * 0.5;\n float wave = sin(uv.x * 10.0 + t) * 0.5 + 0.5;\n vec3 col = vec3(uv, wave);\n fragColor = vec4(col, 1.0);\n}\n```\n\nConverted Viji shader:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:0.5 min:0.1 max:3.0\n// @viji-color:tint label:\"Tint\" default:#ffffff\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n vec3 col = vec3(uv, wave) * tint;\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nKey changes: removed the `mainImage(out vec4 fragColor, in vec2 fragCoord)` wrapper and used `void main()` with `gl_FragCoord.xy`; replaced `iResolution.xy` with `u_resolution`; replaced `iTime * 0.5` with a `@viji-accumulator` keyed to a `speed` slider (so changing speed never jumps the animation); added a `@viji-color` for the tint so the artist can adjust it; preserved the visual intent (UV gradient + horizontal wave).\n",
9889
- "threejs": "# Converting Three.js Applications to Viji Native\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert a standalone Three.js application into a Viji scene. The target renderer is Viji **Native** (Three.js loads as an ESM dynamic import). This guide is self-contained: it includes both the source-to-target mapping and the Viji Native target reference needed to produce correct converted output, without requiring the Viji Native system prompt to also be loaded.\n\n## Source platform: standalone Three.js\n\nStandalone Three.js apps typically:\n- Create their own `<canvas>` element or accept one from the DOM.\n- Construct a `THREE.Scene`, `THREE.PerspectiveCamera`, and `THREE.WebGLRenderer`.\n- Use `requestAnimationFrame()` as the per-frame loop.\n- Use `THREE.Clock` / `clock.getDelta()` for timing.\n- Attach `window.addEventListener('resize', ...)` and use `window.innerWidth` / `window.innerHeight`.\n- Listen to DOM mouse / keyboard events, or use `OrbitControls`.\n- Load assets via `THREE.TextureLoader`, `GLTFLoader`, etc.\n\n## Target platform: Viji Native + Three.js\n\nViji Native scenes run on an `OffscreenCanvas` inside a Web Worker. The DOM is unavailable. Three.js loads as an ESM dynamic import. The canvas is created by Viji and passed to the Three.js renderer. Viji controls the render loop; the artist's `render(viji)` function runs each frame.\n\n## Conversion rules\n\n1. **Always** import Three.js dynamically at the top level with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Never use `<script>` tags, `require()`, or static `import` statements.\n\n2. **Always** use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n **Always** pass `false` as the third argument to `setSize()`. The Worker has no DOM, so any attempt to update CSS styles (Three.js's default) will throw.\n\n3. **Never** use `requestAnimationFrame()`. Viji controls the render loop. Put all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. **Always** handle resize by checking `viji.width` / `viji.height` inside `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n // ...\n renderer.render(scene, camera);\n }\n ```\n Remove any `window.addEventListener('resize', ...)`. Resize is handled here.\n\n5. **Never** access `window`, `document`, `Image()`, or `localStorage`. `fetch()` and `await import()` are available.\n\n6. **Never** use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. **Lift hardcoded values to Viji parameters** at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const meshColor = viji.color('#049ef4', { label: 'Color' });\n ```\n Read via `.value` inside `render()`. Three.js colors accept the hex string: `material.color.set(meshColor.value)`. For more control, color parameters also expose `.rgb` (0..255) and `.hsb`.\n\n8. **Always** use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n Remove `THREE.Clock` and `clock.getDelta()`. Never use `Date.now()` or `performance.now()` directly.\n\n **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator at the top level.\n\n9. **Replace mouse / keyboard event listeners with Viji input APIs**:\n - `event.clientX` -> `viji.pointer.x` (works for both mouse and touch), or `viji.mouse.x`.\n - `event.clientY` -> `viji.pointer.y`, or `viji.mouse.y`.\n - Mouse buttons -> `viji.mouse.leftButton`, `viji.mouse.rightButton`, `viji.mouse.middleButton`.\n - Key presses -> `viji.keyboard.isPressed('keyName')`, `viji.keyboard.wasPressed('keyName')` for one-frame edges.\n\n10. **`OrbitControls` and other DOM-event-based controls do not work** in the Worker. For camera interaction, read `viji.pointer` and `viji.mouse.wheelDelta` directly and update the camera manually. (You can typically build a working orbit with: pointer delta when down -> yaw / pitch; wheel delta -> radius.)\n\n11. **Three.js addons** import from the examples directory with the same pinned version:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n Always pin the same version for addons as for the main library.\n\n12. **File textures** become `viji.image` parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n let texture = null;\n\n function render(viji) {\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n // ...\n }\n ```\n\n13. **Video textures** use `viji.video`:\n ```javascript\n let videoTexture = null;\n\n function render(viji) {\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n // ...\n }\n ```\n\n14. **Never** allocate new objects (vectors, colors, materials, geometries) inside `render()`. Pre-create at the top level and mutate in place. Three.js reuses `Vector3` and `Color` mutably; this is the idiomatic pattern.\n\n15. **Always** set `category` on parameters that depend on an external input: `category: 'audio'`, `category: 'video'`, `category: 'interaction'`.\n\n16. **Remove** any CSS, HTML, or DOM manipulation code. Viji scenes produce canvas output only.\n\n17. If the source uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js): flag this to the artist. These cannot be converted directly; they would need to be rewritten as plain Three.js first.\n\n18. **Always** type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Target API reference (Viji Native + Three.js patterns)\n\n**Canvas and context**: `viji.canvas` (`OffscreenCanvas`: pass to `new THREE.WebGLRenderer({ canvas: viji.canvas })`), `viji.width`, `viji.height`. `viji.useContext()` is available for non-Three.js Native scenes, but with Three.js you let it manage the GL context.\n\n**Timing**: `viji.time` (seconds since scene start), `viji.deltaTime` (seconds since last frame), `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category? }) // .value, .rgb, .hsb\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) + each `*Smoothed` sibling (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves) + each `*Smoothed` (500ms); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked`; `spectral.{brightness, flatness}`; `getFrequencyData(): Uint8Array` (1024 bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1). Apply audio reactivity to Three.js objects via `material.uniforms` (for `ShaderMaterial`), `material.emissiveIntensity`, mesh `scale`, `rotation`, position, etc.\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. `currentFrame` (`OffscreenCanvas | ImageBitmap`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv: VideoCVAPI`. CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs all live on `viji.video.cv`, not on `viji.video` directly. Wrap `viji.video.currentFrame` in `THREE.CanvasTexture(viji.video.currentFrame)` and set `texture.needsUpdate = true` each frame the frame changes.\n\n**Computer Vision: `viji.video.cv`**: enable explicitly (never by default). All CV state lives on `viji.video.cv`:\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (paired with the current CV results; `null` until first inference), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures()`, `isProcessing()`. Common fallback pattern when texturing from a CV-paired frame: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes (52 ARKit coefficients)}`), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`). Unified across mouse and touch: preferred for OrbitControls replacements.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`.\n\n**Sensors / streams**: `viji.device.{motion, orientation}` (for tilt-driven cameras), `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]`.\n\n## Three.js setup pattern\n\nThe canonical Viji-Native + Three.js scaffold:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst meshColor = viji.color('#049ef4', { label: 'Color' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\nrenderer.setPixelRatio(1);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(50, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 0, 5);\nscene.add(new THREE.AmbientLight(0xffffff, 0.4));\nconst dir = new THREE.DirectionalLight(0xffffff, 0.9);\ndir.position.set(2, 3, 4);\nscene.add(dir);\n\nconst geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);\nconst material = new THREE.MeshStandardMaterial({ color: meshColor.value, roughness: 0.5 });\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n material.color.set(meshColor.value);\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime * 0.7;\n\n renderer.render(scene, camera);\n}\n```\n\n## Worked example\n\nSource Three.js app:\n```javascript\nimport * as THREE from 'three';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(window.innerWidth, window.innerHeight);\ndocument.body.appendChild(renderer.domElement);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);\ncamera.position.z = 5;\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nconst clock = new THREE.Clock();\nfunction animate() {\n requestAnimationFrame(animate);\n const dt = clock.getDelta();\n cube.rotation.x += dt;\n cube.rotation.y += dt;\n controls.update();\n renderer.render(scene, camera);\n}\nanimate();\n\nwindow.addEventListener('resize', () => {\n renderer.setSize(window.innerWidth, window.innerHeight);\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n});\n```\n\nConverted Viji Native scene:\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Rotation Speed' });\nconst orbitSensitivity = viji.slider(0.005, { min: 0.001, max: 0.02, step: 0.001, label: 'Orbit Sensitivity', category: 'interaction' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, viji.width / viji.height, 0.1, 1000);\n\nlet cameraYaw = 0;\nlet cameraPitch = 0;\nlet cameraRadius = 5;\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n // Manual orbit replacement (OrbitControls cannot run in the worker).\n if (viji.pointer.isDown) {\n cameraYaw -= viji.pointer.deltaX * orbitSensitivity.value;\n cameraPitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01,\n cameraPitch - viji.pointer.deltaY * orbitSensitivity.value));\n }\n cameraRadius = Math.max(1.5, Math.min(20, cameraRadius - viji.mouse.wheelDelta * 0.002));\n camera.position.set(\n cameraRadius * Math.cos(cameraPitch) * Math.sin(cameraYaw),\n cameraRadius * Math.sin(cameraPitch),\n cameraRadius * Math.cos(cameraPitch) * Math.cos(cameraYaw)\n );\n camera.lookAt(0, 0, 0);\n\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime;\n\n renderer.render(scene, camera);\n}\n```\n\nKey changes: replaced static `import` with dynamic `await import('https://esm.sh/three@0.160.0')` pinned to a version; passed `viji.canvas` to the renderer and added the mandatory `false` third argument to `setSize`; removed `document.body.appendChild`; removed `requestAnimationFrame` (Viji owns the loop) and moved per-frame logic into `render(viji)`; replaced `THREE.Clock` / `clock.getDelta()` with `viji.deltaTime`; replaced the `OrbitControls` instance with a manual orbit driven by `viji.pointer.deltaX/Y` and `viji.mouse.wheelDelta` (with an `orbitSensitivity` slider tagged `category: 'interaction'`); moved resize logic from a `window.addEventListener('resize', ...)` callback into a `prevWidth`/`prevHeight` check inside `render`; lifted the implicit rotation speed to a `speed` slider so the artist can adjust it.\n"
10081
+ "p5": "# Converting P5.js Sketches to Viji P5\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing P5.js sketch into a Viji scene. It is self-contained: it includes both the source-to-target mapping and the Viji P5 target reference needed to produce correct converted output, without requiring the Viji P5 system prompt to also be loaded.\n\n## Source platform: standard P5.js sketches\n\nStandard P5.js sketches run in a browser tab. They typically:\n- Use `setup()` to call `createCanvas(w, h)` and configure the sketch (`colorMode`, `frameRate`, etc.).\n- Use `draw()` as the per-frame loop (`background`, `fill`, shape primitives, etc.).\n- Use global `mouseX`, `mouseY`, `mouseIsPressed`, `keyIsPressed`, `frameCount`, `width`, `height`.\n- Define event callbacks at the top level: `mousePressed`, `keyPressed`, `touchStarted`, etc.\n- Use `preload()` to fetch assets via `loadImage`, `loadFont`, `loadJSON`.\n- Possibly use `createCapture(VIDEO)`, `p5.AudioIn()`, `p5.sound`, `p5.dom` add-ons.\n\nP5.js v1.9.4 is what Viji pins. When in doubt about a P5 function, the p5.js v1.x reference is authoritative.\n\n## Target platform: Viji P5\n\nViji P5 scenes run inside a Web Worker on an `OffscreenCanvas` and use P5.js v1.9.4 in **instance mode**. The DOM is unavailable. Every P5 function and constant requires the `p5.` prefix. The canvas is created and managed by Viji; never call `createCanvas()`.\n\n## Conversion rules\n\n1. **Always** set the first line based on the source canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or any 3D primitives. Never keep `createCanvas()`.\n2. Rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it does not exist, do not add one.\n4. **Always** prefix every P5 function and constant with `p5.`. No exceptions: `background` -> `p5.background`, `fill` -> `p5.fill`, `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`, `createVector(...)` -> `p5.createVector(...)`, `map(...)` -> `p5.map(...)`, `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`.\n5. **Never** keep `createCanvas()`. The canvas is created by Viji. WEBGL is selected only via `// @renderer p5 webgl`, never via `createCanvas(..., p5.WEBGL)`.\n6. **Never** keep `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` inside an async `setup()` for other data.\n7. **Never** keep P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edge detection.\n8. **Never** keep `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, or `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** keep `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n10. **Never** keep `createCapture()` or `createVideo()`. Use `viji.video.*`.\n11. **Never** keep `p5.dom` or `p5.sound` add-ons. Replace UI with Viji parameters and audio with `viji.audio.*`.\n12. **Never** keep `window`, `document`, `Image()`, or `localStorage`. `fetch()` is available.\n13. Lift constants and tunable values to top-level parameters using `viji.slider`, `viji.color`, `viji.toggle`, etc. Read via `.value` inside `render()`.\n14. Replace `frameCount`-based timing with a `viji.deltaTime` accumulator. Never multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator.\n15. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n16. **Never** enable CV features by default; use toggle parameters.\n17. **Always** set `category` on input-dependent parameters: `'audio'`, `'video'`, `'interaction'`.\n18. **Always** type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Direct mapping table\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. Use `// @renderer p5`. |\n| `createCanvas(w, h, WEBGL)` | Remove. Use `// @renderer p5 webgl` as the first line. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `loadFont(url)` | Not available. Use CSS generic font names with `p5.textFont('monospace' \\| 'serif' \\| 'sans-serif')`. |\n| `loadJSON(url)` | `await fetch(url).then(r => r.json())` inside async `setup()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `createCapture(VIDEO)` | `viji.video.currentFrame` (plus `viji.video.isConnected` check). |\n| `new p5.AudioIn(); mic.getLevel()` | `viji.audio.volume.current` (after `viji.audio.isConnected` check). |\n| `new p5.FFT(); fft.analyze()` | `viji.audio.getFrequencyData()` (`Uint8Array` of 1024 bins). |\n| `mousePressed()` callback | `if (viji.pointer.wasPressed) { ... }` inside `render()`. |\n| `keyPressed()` callback | `if (viji.keyboard.wasPressed(key)) { ... }` inside `render()`. |\n| `touchStarted()` callback | `viji.touches.started.forEach(t => ...)` inside `render()`. |\n\n## Target API reference (Viji P5)\n\nThe `viji` object is identical to Native. Access it inside `setup(viji, p5)` and `render(viji, p5)`. `viji.useContext()` is **not available** in P5; the context is managed by P5.\n\n**Canvas and timing**: `viji.canvas` (`OffscreenCanvas`), `viji.width`, `viji.height`, `viji.time`, `viji.deltaTime`, `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap|null, .p5: P5Image\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\nFor images displayed with P5, pass `param.p5` (a `P5Image`) to `p5.image()`, not `param.value`.\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves; peak on detected beats); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1).\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. Members: `currentFrame` (`OffscreenCanvas | ImageBitmap | null`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv` (VideoCVAPI). CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect handling** (use for every video / CV conversion):\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\nconst v = videoFit(viji, 'cover');\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0..1 to the source frame; map through v to align.\n```\nDefault `viji.video.currentFrame` for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\n**CV: `viji.video.cv`**: enable explicitly (never by default):\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (frame paired with current CV results), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures(): CVFeature[]`, `isProcessing(): boolean`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes}`; `blendshapes` are 52 ARKit coefficients 0..1), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures: {fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou}}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type`.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`: each `TouchPoint` has `id`, `x`, `y`, `pressure`, `radius`, `velocity`, etc.\n\n**Sensors / streams**: `viji.device.{motion, orientation}`, `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]` (lightweight `AudioStreamAPI`: no beat / BPM).\n\n## P5 gotchas in the worker\n\n- **Fonts**: `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is unavailable.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is not supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes, it is a WebGL context; do not call Canvas-2D APIs on it.\n- **`viji.useContext()`** is not available in P5; the context belongs to P5.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## Worked example\n\nSource P5 sketch:\n```javascript\nlet angle = 0;\nfunction setup() {\n createCanvas(800, 600);\n colorMode(HSB, 360, 100, 100);\n}\nfunction draw() {\n angle += 0.02;\n background(0, 0, 10);\n const x = width / 2 + cos(angle) * width * 0.3;\n const y = height / 2 + sin(angle) * height * 0.3;\n noStroke();\n fill((frameCount * 0.5) % 360, 80, 100);\n circle(x, y, width * 0.05);\n}\nfunction mousePressed() {\n angle = 0;\n}\n```\n\nConverted Viji P5 scene:\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst reactivity = viji.slider(0, { min: 0, max: 1, label: 'Audio Reactivity', category: 'audio' });\n\nlet angle = 0;\nlet huePhase = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) angle = 0;\n\n const audioBoost = viji.audio.isConnected ? viji.audio.volume.smoothed * reactivity.value : 0;\n angle += (speed.value + audioBoost * 4) * viji.deltaTime;\n huePhase += 30 * viji.deltaTime;\n\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(huePhase % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nKey changes: removed `createCanvas`, added `// @renderer p5`; renamed `draw` to `render(viji, p5)`; gave `setup` the `(viji, p5)` signature; added `p5.` prefix everywhere; replaced `width`/`height` with `viji.width`/`viji.height`; replaced the implicit `0.02` frame-rate-coupled animation with a `viji.deltaTime` accumulator driven by a `speed` slider; replaced `(frameCount * 0.5) % 360` hue with a separate `huePhase` accumulator (so changing speed never jumps the color); replaced the `mousePressed()` callback with a `viji.pointer.wasPressed` check inside `render`; added an opt-in audio-reactive `reactivity` slider with `category: 'audio'`.\n",
10082
+ "shadertoy": "# Converting Shadertoy Shaders to Viji Shader Scenes\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing Shadertoy shader into a Viji shader scene. It is self-contained: it includes both the source-to-target mapping and the Viji shader target reference needed to produce correct converted output, without requiring the Viji shader system prompt to also be loaded.\n\n## Source platform: Shadertoy\n\nShadertoy shaders are GLSL fragment shaders authored against a fixed API:\n- Entry point: `void mainImage(out vec4 fragColor, in vec2 fragCoord)`.\n- Built-in uniforms: `iResolution` (`vec3`: `.xy` size, `.z` aspect), `iTime`, `iTimeDelta`, `iFrame`, `iMouse` (`vec4`: `.xy` current position, `.zw` click origin), `iDate` (year, month, day, seconds), `iChannel0`-`iChannel3` (textures whose meaning depends on the channel binding: Image, FFT, Sound, Music, Microphone, Keyboard, Cubemap).\n- Side metadata: `iChannelResolution[i]`, `iChannelTime[i]`, `iSampleRate`.\n- Multi-buffer pipelines (Buffer A, B, C, D), Cubemap buffer, Sound output buffer, VR mode (`mainVR`). None of these have Viji equivalents.\n\n## Target platform: Viji shader\n\nGLSL fragment shader on a fullscreen quad. Standard `void main()` entry. Precision and all uniforms (built-ins and `@viji-*` directive uniforms) are auto-injected by Viji. **Never declare them.** Artists declare parameters via `// @viji-*` line-comment directives, which compile to typed uniforms.\n\nGLSL ES 1.00 by default. Opt into ES 3.00 by making `#version 300 es` the literal first line.\n\n## Conversion rules\n\n1. **Always** add `// @renderer shader` as the very first line (or as line 2 after `#version 300 es`).\n2. **Never** declare `precision mediump float;` or `precision highp float;`. Viji auto-injects precision.\n3. **Never** redeclare built-in uniforms. They are auto-injected.\n4. **Never** redeclare parameter uniforms. They are auto-generated from `@viji-*` directives.\n5. Convert the `mainImage` signature to standard `void main()`:\n - Replace `fragCoord` with `gl_FragCoord.xy`.\n - Replace `fragColor` (the `out` parameter) with `gl_FragColor` (ES 1.00) or the declared `out vec4 fragColor` (ES 3.00).\n - Remove the `mainImage` wrapper entirely.\n\n6. Replace Shadertoy uniforms with Viji equivalents:\n\n | Shadertoy | Viji | Notes |\n |---|---|---|\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. |\n | `iResolution.z` | `u_resolution.x / u_resolution.y` | Aspect ratio. |\n | `iResolution.x` / `.y` | `u_resolution.x` / `.y` | Direct match. |\n | `iTime` | `u_time` | Elapsed seconds. |\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\n | `iFrame` | `u_frame` (`int`) | Frame counter. |\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\n | `iMouse.z` | `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click origin. |\n | `iMouse.w` | `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click origin. |\n | `iChannelN` (static Image) | `@viji-image:channelN label:\"…\"` | See rule 7. |\n | `iChannelN` (FFT) | `u_audioFFT` | Auto-injected `sampler2D`, 1024 bins 0..255. |\n | `iChannelN` (Sound / Music / Microphone) | `u_audioFFT` and/or `u_audioWaveform` | See two-row note in rule 7. |\n | `iChannelN` (Keyboard) | `u_keyboard` | See rule 8. |\n | `iChannelN` (Cubemap) | Not supported | Only 2D textures available. |\n | `iChannelResolution[i]` | Not available | Track manually if needed. |\n | `iChannelTime[i]` | Not available | Per-channel time is not tracked. |\n | `iDate` | Not available | Use `u_time`. |\n | `iSampleRate` | Not available | Not applicable. |\n\n7. **Static-image `iChannel` slots** become `@viji-image` parameters:\n ```glsl\n // @viji-image:channel0 label:\"Texture 1\"\n ```\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (ES 1.00) or `texture(channel0, uv)` (ES 3.00).\n\n **Audio iChannel slots** (Sound / Music / Microphone) sample the auto-injected audio textures directly:\n ```glsl\n float spectrum = texture2D(u_audioFFT, vec2(uv.x, 0.0)).r;\n float waveform = texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r;\n ```\n\n **Two-row audio channel**: Shadertoy's Sound / Music / Microphone iChannels encode FFT in row 0 (`y` near 0.0) and waveform in row 1 (`y` near 0.5+) of a single texture. When converting:\n - `texture(iChannelN, vec2(uv.x, 0.0)).r` (row 0) -> `texture2D(u_audioFFT, vec2(uv.x, 0.0)).r`.\n - `texture(iChannelN, vec2(uv.x, 0.5)).r` or `vec2(uv.x, 0.75)` (row 1) -> `texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r`.\n - When unsure: prefer `u_audioFFT` for spectrum-style visuals, `u_audioWaveform` for oscilloscope-style visuals.\n\n8. **Keyboard `iChannel` slots** become `u_keyboard`:\n ```glsl\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\n ```\n `u_keyboard` is a built-in `sampler2D` (256x3). Row 0 = held, row 1 = pressed-this-frame, row 2 = toggle. Do not declare it.\n\n9. `iResolution` used as `vec3`: replace with `vec3(u_resolution, u_resolution.x / u_resolution.y)`, or refactor to use the `vec2` form directly.\n\n10. **Parameter-driven animation must use `@viji-accumulator`, never `u_time * speed`.** Multiplying `u_time` by a parameter causes visible jumps when the slider changes.\n\n ```glsl\n // WRONG (source pattern)\n float t = iTime * speed;\n\n // RIGHT (converted)\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float t = phase;\n ```\n\n Same rule for nested multiplications. Each independent speed gets its own accumulator.\n\n11. Adding artist-controllable parameters uses `@viji-*` directives:\n ```glsl\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\n // @viji-color:name label:\"Label\" default:#ff6600 (alpha:true -> vec4; then #rrggbbaa / rgba() defaults are allowed)\n // @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] (sample with name(t); alpha:true -> vec4)\n // @viji-toggle:name label:\"Label\" default:true\n // @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\",\"C\"]\n // @viji-image:name label:\"Texture\"\n // @viji-button:name label:\"Reset\"\n // @viji-coordinate:name label:\"Origin\" default:[0.0,0.0]\n // @viji-accumulator:name rate:speed\n ```\n Never use the `u_` prefix for parameter names; it is reserved for built-in uniforms. Directives work with `//` line comments only.\n\n12. **`#version 300 es`** stays as the literal first line if present. Then `// @renderer shader` on line 2. Replace `gl_FragColor = ...` with a declared `out vec4 fragColor;` (before `main`) and `fragColor = ...`. Replace `texture2D()` with `texture()`.\n\n13. Remove any `#ifdef GL_ES` / `precision` blocks. Viji handles them.\n\n14. **Always** set `category` on input-dependent directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse / touch controls.\n\n15. **Single Buffer feedback** becomes Viji's `backbuffer`:\n ```glsl\n vec4 prev = texture2D(backbuffer, uv);\n ```\n `backbuffer` is auto-detected and enabled when referenced. RGBA 8-bit, `LINEAR`, `CLAMP_TO_EDGE`, no `u_` prefix. First frame samples as black.\n\n## Unsupported features (warn the artist)\n\n- **Multi-buffer pipelines** (Buffer A -> B -> C -> D): only single `backbuffer` is available.\n- **Cubemap buffer** (`samplerCube`): not supported.\n- **3D textures** (`sampler3D`): not supported.\n- **`iChannelTime[i]`**, **`iChannelResolution[i]`**, **`iSampleRate`**, **`iDate`**: not available.\n- **Sound output buffer**: not supported.\n- **`mainVR()`**: not supported.\n- **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\n\n## Target uniform reference (Viji shader)\n\nAll uniforms below are auto-injected. Never declare them.\n\n**Core**: `u_resolution` (`vec2`), `u_time` (`float`), `u_deltaTime` (`float`), `u_frame` (`int`), `u_fps` (`float`).\n\n**Mouse**: `u_mouse` (`vec2`, pixels, bottom-left origin), `u_mouseInCanvas`, `u_mousePressed`, `u_mouseLeft`, `u_mouseRight`, `u_mouseMiddle` (`bool`), `u_mouseDelta` (`vec2`), `u_mouseWheel` (`float`), `u_mouseWasPressed`, `u_mouseWasReleased` (`bool`).\n\n**Pointer (unified mouse / touch)**: `u_pointer` (`vec2`), `u_pointerDelta` (`vec2`), `u_pointerDown`, `u_pointerWasPressed`, `u_pointerWasReleased`, `u_pointerInCanvas` (`bool`).\n\n**Keyboard**: `u_keySpace`, `u_keyShift`, `u_keyCtrl`, `u_keyAlt`, `u_keyW`/`A`/`S`/`D`, `u_keyUp`/`Down`/`Left`/`Right` (`bool`). `u_keyboard` (`sampler2D`, 256x3): row 0 held, row 1 pressed-this-frame, row 2 toggle.\n\n**Touch**: `u_touchCount` (`int`), `u_touch0`-`u_touch4` (`vec2`).\n\n**Audio scalars**: `u_audioVolume`, `u_audioPeak`, `u_audioVolumeSmoothed` (200ms decay), `u_audioLow`/`LowMid`/`Mid`/`HighMid`/`High` (band energies 20-120 / 120-400 / 400-1600 / 1600-6000 / 6000-16000 Hz), `u_audioLowSmoothed`-`u_audioHighSmoothed` (150ms decay), `u_audioKick`/`Snare`/`Hat`/`Any` (300ms decay curves), `u_audioKickSmoothed`-`u_audioAnySmoothed` (500ms decay), `u_audioKickTrigger`/`SnareTrigger`/`HatTrigger`/`AnyTrigger` (`bool`, true for one frame, OR-accumulated), `u_audioBPM` (`0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on), `u_audioConfidence`, `u_audioIsLocked` (`bool`), `u_audioBrightness`, `u_audioFlatness`.\n\n**Audio textures (main source only)**: `u_audioFFT` (`sampler2D`, 1024 FFT bins 0..255), `u_audioWaveform` (`sampler2D`, time-domain -1..1).\n\n**Video**: `u_video` (`sampler2D`), `u_videoAnalysed` (`sampler2D`, paired with CV uniforms), `u_videoAnalysedAvailable` (`bool`), `u_videoResolution` (`vec2`), `u_videoFrameRate` (`float`), `u_videoConnected` (`bool`). Use the `vijiVideoUV` helper below for aspect handling.\n\n**CV face**: `u_faceCount` (`int`), `u_face0Bounds` (`vec4`, normalized 0..1), `u_face0Center` (`vec2`, normalized), `u_face0HeadPose` (`vec3` degrees), `u_face0Confidence` (`float`), `u_face0Neutral`-`u_face0Fearful` (7 expressions 0..1), 52 blendshape uniforms prefixed `u_face0` (e.g. `u_face0JawOpen`, `u_face0EyeBlinkLeft`, `u_face0MouthSmileRight`). Activated via `// @viji-cv:faceDetection` / `// @viji-cv:faceMesh` / `// @viji-cv:emotionDetection`.\n\n**CV hands**: `u_handCount` (`int`), `u_leftHandPalm`/`u_rightHandPalm` (`vec3`), `u_leftHandConfidence`/`u_rightHandConfidence`, `u_leftHandBounds`/`u_rightHandBounds` (`vec4`), 7 gesture uniforms per hand (`Fist`, `OpenPalm`, `Peace`, `ThumbsUp`, `ThumbsDown`, `Pointing`, `ILoveYou`). Activated via `// @viji-cv:handTracking`.\n\n**CV pose**: `u_poseDetected` (`bool`), `u_poseConfidence`, joint landmarks (`u_nosePosition`, `u_leftShoulderPosition`-`u_rightAnklePosition`, all `vec2` normalized 0..1). Activated via `// @viji-cv:poseDetection`.\n\n**CV segmentation**: `u_segmentationMask` (`sampler2D`, 0 = background, 1 = person), `u_segmentationRes` (`vec2`). Activated via `// @viji-cv:bodySegmentation`.\n\n**Device sensors**: `u_deviceAcceleration` (`vec3`, m/s² without gravity), `u_deviceAccelerationGravity`, `u_deviceRotationRate` (deg/s), `u_deviceOrientation` (`vec3`, degrees), `u_deviceOrientationAbsolute` (`bool`).\n\n**External devices**: `u_deviceCount` (`int`, 0..8), `u_device0`-`u_device7` (`sampler2D` camera textures), `u_device0Resolution`-`u_device7Resolution` (`vec2`), `u_device0Connected`-`u_device7Connected` (`bool`), plus per-device acceleration / rotation / orientation `vec3` uniforms.\n\n**Compositor streams**: `u_videoStreamCount`, `u_videoStream0`-`u_videoStream7` (`sampler2D`), with `*Resolution` and `*Connected` per slot. `u_audioStreamCount`, `u_audioStream0Connected`-`u_audioStream7Connected` (`bool`), `u_audioStream{i}Volume`, `u_audioStream{i}Low`-`u_audioStream{i}High`, `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` (`float` 0..1). Scalars only on streams: no per-stream FFT / waveform. Beat / BPM / trigger uniforms are main audio only.\n\n**Backbuffer**: `backbuffer` (`sampler2D`, no `u_` prefix). Auto-enabled when referenced.\n\n## Video aspect helper\n\nCamera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with canvas UV stretches the video and misaligns CV uniforms. Use these at the top of any video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, video edges cropped)\n// 0 = contain (fits video, canvas letterboxed)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n```\n\n## Worked example\n\nSource Shadertoy shader:\n```glsl\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = fragCoord / iResolution.xy;\n float t = iTime * 0.5;\n float wave = sin(uv.x * 10.0 + t) * 0.5 + 0.5;\n vec3 col = vec3(uv, wave);\n fragColor = vec4(col, 1.0);\n}\n```\n\nConverted Viji shader:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:0.5 min:0.1 max:3.0\n// @viji-color:tint label:\"Tint\" default:#ffffff\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n vec3 col = vec3(uv, wave) * tint;\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nKey changes: removed the `mainImage(out vec4 fragColor, in vec2 fragCoord)` wrapper and used `void main()` with `gl_FragCoord.xy`; replaced `iResolution.xy` with `u_resolution`; replaced `iTime * 0.5` with a `@viji-accumulator` keyed to a `speed` slider (so changing speed never jumps the animation); added a `@viji-color` for the tint so the artist can adjust it; preserved the visual intent (UV gradient + horizontal wave).\n",
10083
+ "threejs": "# Converting Three.js Applications to Viji Native\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert a standalone Three.js application into a Viji scene. The target renderer is Viji **Native** (Three.js loads as an ESM dynamic import). This guide is self-contained: it includes both the source-to-target mapping and the Viji Native target reference needed to produce correct converted output, without requiring the Viji Native system prompt to also be loaded.\n\n## Source platform: standalone Three.js\n\nStandalone Three.js apps typically:\n- Create their own `<canvas>` element or accept one from the DOM.\n- Construct a `THREE.Scene`, `THREE.PerspectiveCamera`, and `THREE.WebGLRenderer`.\n- Use `requestAnimationFrame()` as the per-frame loop.\n- Use `THREE.Clock` / `clock.getDelta()` for timing.\n- Attach `window.addEventListener('resize', ...)` and use `window.innerWidth` / `window.innerHeight`.\n- Listen to DOM mouse / keyboard events, or use `OrbitControls`.\n- Load assets via `THREE.TextureLoader`, `GLTFLoader`, etc.\n\n## Target platform: Viji Native + Three.js\n\nViji Native scenes run on an `OffscreenCanvas` inside a Web Worker. The DOM is unavailable. Three.js loads as an ESM dynamic import. The canvas is created by Viji and passed to the Three.js renderer. Viji controls the render loop; the artist's `render(viji)` function runs each frame.\n\n## Conversion rules\n\n1. **Always** import Three.js dynamically at the top level with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Never use `<script>` tags, `require()`, or static `import` statements.\n\n2. **Always** use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n **Always** pass `false` as the third argument to `setSize()`. The Worker has no DOM, so any attempt to update CSS styles (Three.js's default) will throw.\n\n3. **Never** use `requestAnimationFrame()`. Viji controls the render loop. Put all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. **Always** handle resize by checking `viji.width` / `viji.height` inside `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n // ...\n renderer.render(scene, camera);\n }\n ```\n Remove any `window.addEventListener('resize', ...)`. Resize is handled here.\n\n5. **Never** access `window`, `document`, `Image()`, or `localStorage`. `fetch()` and `await import()` are available.\n\n6. **Never** use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. **Lift hardcoded values to Viji parameters** at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const meshColor = viji.color('#049ef4', { label: 'Color' });\n ```\n Read via `.value` inside `render()`. Three.js colors accept the hex string: `material.color.set(meshColor.value)`. For more control, color parameters also expose `.rgb` (0..255) and `.hsb`.\n\n8. **Always** use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n Remove `THREE.Clock` and `clock.getDelta()`. Never use `Date.now()` or `performance.now()` directly.\n\n **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator at the top level.\n\n9. **Replace mouse / keyboard event listeners with Viji input APIs**:\n - `event.clientX` -> `viji.pointer.x` (works for both mouse and touch), or `viji.mouse.x`.\n - `event.clientY` -> `viji.pointer.y`, or `viji.mouse.y`.\n - Mouse buttons -> `viji.mouse.leftButton`, `viji.mouse.rightButton`, `viji.mouse.middleButton`.\n - Key presses -> `viji.keyboard.isPressed('keyName')`, `viji.keyboard.wasPressed('keyName')` for one-frame edges.\n\n10. **`OrbitControls` and other DOM-event-based controls do not work** in the Worker. For camera interaction, read `viji.pointer` and `viji.mouse.wheelDelta` directly and update the camera manually. (You can typically build a working orbit with: pointer delta when down -> yaw / pitch; wheel delta -> radius.)\n\n11. **Three.js addons** import from the examples directory with the same pinned version:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n Always pin the same version for addons as for the main library.\n\n12. **File textures** become `viji.image` parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n let texture = null;\n\n function render(viji) {\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n // ...\n }\n ```\n\n13. **Video textures** use `viji.video`:\n ```javascript\n let videoTexture = null;\n\n function render(viji) {\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n // ...\n }\n ```\n\n14. **Never** allocate new objects (vectors, colors, materials, geometries) inside `render()`. Pre-create at the top level and mutate in place. Three.js reuses `Vector3` and `Color` mutably; this is the idiomatic pattern.\n\n15. **Always** set `category` on parameters that depend on an external input: `category: 'audio'`, `category: 'video'`, `category: 'interaction'`.\n\n16. **Remove** any CSS, HTML, or DOM manipulation code. Viji scenes produce canvas output only.\n\n17. If the source uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js): flag this to the artist. These cannot be converted directly; they would need to be rewritten as plain Three.js first.\n\n18. **Always** type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Target API reference (Viji Native + Three.js patterns)\n\n**Canvas and context**: `viji.canvas` (`OffscreenCanvas`: pass to `new THREE.WebGLRenderer({ canvas: viji.canvas })`), `viji.width`, `viji.height`. `viji.useContext()` is available for non-Three.js Native scenes, but with Three.js you let it manage the GL context.\n\n**Timing**: `viji.time` (seconds since scene start), `viji.deltaTime` (seconds since last frame), `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) + each `*Smoothed` sibling (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves) + each `*Smoothed` (500ms); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked`; `spectral.{brightness, flatness}`; `getFrequencyData(): Uint8Array` (1024 bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1). Apply audio reactivity to Three.js objects via `material.uniforms` (for `ShaderMaterial`), `material.emissiveIntensity`, mesh `scale`, `rotation`, position, etc.\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. `currentFrame` (`OffscreenCanvas | ImageBitmap`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv: VideoCVAPI`. CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs all live on `viji.video.cv`, not on `viji.video` directly. Wrap `viji.video.currentFrame` in `THREE.CanvasTexture(viji.video.currentFrame)` and set `texture.needsUpdate = true` each frame the frame changes.\n\n**Computer Vision: `viji.video.cv`**: enable explicitly (never by default). All CV state lives on `viji.video.cv`:\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (paired with the current CV results; `null` until first inference), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures()`, `isProcessing()`. Common fallback pattern when texturing from a CV-paired frame: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes (52 ARKit coefficients)}`), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`). Unified across mouse and touch: preferred for OrbitControls replacements.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`.\n\n**Sensors / streams**: `viji.device.{motion, orientation}` (for tilt-driven cameras), `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]`.\n\n## Three.js setup pattern\n\nThe canonical Viji-Native + Three.js scaffold:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst meshColor = viji.color('#049ef4', { label: 'Color' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\nrenderer.setPixelRatio(1);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(50, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 0, 5);\nscene.add(new THREE.AmbientLight(0xffffff, 0.4));\nconst dir = new THREE.DirectionalLight(0xffffff, 0.9);\ndir.position.set(2, 3, 4);\nscene.add(dir);\n\nconst geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);\nconst material = new THREE.MeshStandardMaterial({ color: meshColor.value, roughness: 0.5 });\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n material.color.set(meshColor.value);\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime * 0.7;\n\n renderer.render(scene, camera);\n}\n```\n\n## Worked example\n\nSource Three.js app:\n```javascript\nimport * as THREE from 'three';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(window.innerWidth, window.innerHeight);\ndocument.body.appendChild(renderer.domElement);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);\ncamera.position.z = 5;\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nconst clock = new THREE.Clock();\nfunction animate() {\n requestAnimationFrame(animate);\n const dt = clock.getDelta();\n cube.rotation.x += dt;\n cube.rotation.y += dt;\n controls.update();\n renderer.render(scene, camera);\n}\nanimate();\n\nwindow.addEventListener('resize', () => {\n renderer.setSize(window.innerWidth, window.innerHeight);\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n});\n```\n\nConverted Viji Native scene:\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Rotation Speed' });\nconst orbitSensitivity = viji.slider(0.005, { min: 0.001, max: 0.02, step: 0.001, label: 'Orbit Sensitivity', category: 'interaction' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, viji.width / viji.height, 0.1, 1000);\n\nlet cameraYaw = 0;\nlet cameraPitch = 0;\nlet cameraRadius = 5;\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n // Manual orbit replacement (OrbitControls cannot run in the worker).\n if (viji.pointer.isDown) {\n cameraYaw -= viji.pointer.deltaX * orbitSensitivity.value;\n cameraPitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01,\n cameraPitch - viji.pointer.deltaY * orbitSensitivity.value));\n }\n cameraRadius = Math.max(1.5, Math.min(20, cameraRadius - viji.mouse.wheelDelta * 0.002));\n camera.position.set(\n cameraRadius * Math.cos(cameraPitch) * Math.sin(cameraYaw),\n cameraRadius * Math.sin(cameraPitch),\n cameraRadius * Math.cos(cameraPitch) * Math.cos(cameraYaw)\n );\n camera.lookAt(0, 0, 0);\n\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime;\n\n renderer.render(scene, camera);\n}\n```\n\nKey changes: replaced static `import` with dynamic `await import('https://esm.sh/three@0.160.0')` pinned to a version; passed `viji.canvas` to the renderer and added the mandatory `false` third argument to `setSize`; removed `document.body.appendChild`; removed `requestAnimationFrame` (Viji owns the loop) and moved per-frame logic into `render(viji)`; replaced `THREE.Clock` / `clock.getDelta()` with `viji.deltaTime`; replaced the `OrbitControls` instance with a manual orbit driven by `viji.pointer.deltaX/Y` and `viji.mouse.wheelDelta` (with an `orbitSensitivity` slider tagged `category: 'interaction'`); moved resize logic from a `window.addEventListener('resize', ...)` callback into a `prevWidth`/`prevHeight` check inside `render`; lifted the implicit rotation speed to a `speed` slider so the artist can adjust it.\n"
9890
10084
  }
9891
10085
  }
9892
10086
  };