@viji-dev/core 0.18.0 → 0.20.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.18.0",
4
- "generatedAt": "2026-09-26T16:47:08.751Z",
3
+ "coreVersion": "0.20.0",
4
+ "generatedAt": "2026-10-08T18:58:39.849Z",
5
5
  "navigation": [
6
6
  {
7
7
  "id": "getting-started",
@@ -905,7 +905,7 @@ export const docsApi = {
905
905
  },
906
906
  "renderers-overview": {
907
907
  "id": "renderers-overview",
908
- "title": "renderers-overview",
908
+ "title": "Renderers Overview",
909
909
  "description": "How to choose and activate each of Viji's three rendering modes, Native, P5.js, and Shader, and what makes each one unique.",
910
910
  "headings": [
911
911
  {
@@ -942,7 +942,7 @@ export const docsApi = {
942
942
  "content": [
943
943
  {
944
944
  "type": "text",
945
- "markdown": "# Renderers Overview\r\n\r\nViji supports three rendering modes. Each produces visuals on the same canvas and shares the same Artist API for parameters, audio, video, interaction, and sensors. The difference is the language and paradigm you use to draw.\r\n\r\n## Choosing a Renderer\r\n\r\nYou pick a renderer with a **comment directive** at the top of your scene code:\r\n\r\n```javascript\r\n// @renderer p5 → P5.js renderer (2D default canvas)\r\n// @renderer p5 webgl → P5.js with WEBGL main canvas (3D / shaders)\r\n// @renderer shader → Shader renderer\r\n// (no directive) → Native renderer (default)\r\n```\r\n\r\nIf no `@renderer` directive is present, the scene runs in **Native** mode. You can also write `// @renderer native` for clarity, but it's not required.\r\n\r\n> [!IMPORTANT]\r\n> P5 and shader scenes must declare their renderer type as the first comment. For example:\r\n> ```\r\n> // @renderer p5\r\n> ```\r\n> For P5's **WEBGL** main canvas, use `// @renderer p5 webgl` as the first line instead of `// @renderer p5`.\r\n> Shader scenes:\r\n> ```\r\n> // @renderer shader\r\n> ```\r\n> Without a matching directive, the scene defaults to the native renderer.\r\n\r\n---\r\n\r\n## Native Renderer\r\n\r\nThe native renderer gives you direct access to the canvas. Write standard JavaScript (or TypeScript) and pick any rendering approach you like, whether Canvas 2D, WebGL, or an external library like Three.js.\r\n\r\n**Entry point:** `render(viji)`"
945
+ "markdown": "# Renderers Overview\n\nViji supports three rendering modes. Each produces visuals on the same canvas and shares the same Artist API for parameters, audio, video, interaction, and sensors. The difference is the language and paradigm you use to draw.\n\n## Choosing a Renderer\n\nYou pick a renderer with a **comment directive** at the top of your scene code:\n\n```javascript\n// @renderer p5 → P5.js renderer (2D default canvas)\n// @renderer p5 webgl → P5.js with WEBGL main canvas (3D / shaders)\n// @renderer shader → Shader renderer\n// (no directive) → Native renderer (default)\n```\n\nIf no `@renderer` directive is present, the scene runs in **Native** mode. You can also write `// @renderer native` for clarity, but it's not required.\n\n> [!IMPORTANT]\n> P5 and shader scenes must declare their renderer type as the first comment. For example:\n> ```\n> // @renderer p5\n> ```\n> For P5's **WEBGL** main canvas, use `// @renderer p5 webgl` as the first line instead of `// @renderer p5`.\n> Shader scenes:\n> ```\n> // @renderer shader\n> ```\n> Without a matching directive, the scene defaults to the native renderer.\n\n---\n\n## Native Renderer\n\nThe native renderer gives you direct access to the canvas. Write standard JavaScript (or TypeScript) and pick any rendering approach you like, whether Canvas 2D, WebGL, or an external library like Three.js.\n\n**Entry point:** `render(viji)`"
946
946
  },
947
947
  {
948
948
  "type": "live-example",
@@ -952,7 +952,7 @@ export const docsApi = {
952
952
  },
953
953
  {
954
954
  "type": "text",
955
- "markdown": "**Key characteristics:**\r\n\r\n- **No setup function.** All initialization happens at the top level of your scene code. Top-level `await` is supported, which enables dynamic imports.\r\n- **Full canvas control.** Call [`viji.useContext('2d')`](/native/canvas-context#choosing-a-context) for Canvas 2D, [`viji.useContext('webgl')`](/native/canvas-context#choosing-a-context) for WebGL 1, [`viji.useContext('webgl2')`](/native/canvas-context#choosing-a-context) for WebGL 2, or [`viji.useContext('webgpu')`](/native/canvas-context#webgpu-context) for WebGPU. Choose one and use it for the entire scene. A canvas only supports one context type, so requesting a different type returns `null`.\r\n- **External libraries** can be loaded via dynamic import from a CDN. Here's a full Three.js scene running inside the native renderer:"
955
+ "markdown": "**Key characteristics:**\n\n- **No setup function.** All initialization happens at the top level of your scene code. Top-level `await` is supported, which enables dynamic imports.\n- **Full canvas control.** Call [`viji.useContext('2d')`](/native/canvas-context#choosing-a-context) for Canvas 2D, [`viji.useContext('webgl')`](/native/canvas-context#choosing-a-context) for WebGL 1, [`viji.useContext('webgl2')`](/native/canvas-context#choosing-a-context) for WebGL 2, or [`viji.useContext('webgpu')`](/native/canvas-context#webgpu-context) for WebGPU. Choose one and use it for the entire scene. A canvas only supports one context type, so requesting a different type returns `null`.\n- **External libraries** can be loaded via dynamic import from a CDN. Here's a full Three.js scene running inside the native renderer:"
956
956
  },
957
957
  {
958
958
  "type": "live-example",
@@ -962,7 +962,7 @@ export const docsApi = {
962
962
  },
963
963
  {
964
964
  "type": "text",
965
- "markdown": "See [External Libraries](/native/external-libraries#threejs) for detailed patterns with Three.js and other libraries.\r\n\r\n---\r\n\r\n## P5.js Renderer\r\n\r\nThe P5.js renderer gives you the familiar Processing/P5.js creative coding API. Viji loads P5.js automatically when you use `// @renderer p5`, so there's no installation or setup to worry about. Add **`webgl`** after `p5` on that same line (`// @renderer p5 webgl`) to switch the main canvas to P5's WEBGL renderer for 3D and shader-based drawing.\r\n\r\n**Entry points:** `render(viji, p5)` (required), `setup(viji, p5)` (optional)"
965
+ "markdown": "See [External Libraries](/native/external-libraries#threejs) for detailed patterns with Three.js and other libraries.\n\n---\n\n## P5.js Renderer\n\nThe P5.js renderer gives you the familiar Processing/P5.js creative coding API. Viji loads P5.js automatically when you use `// @renderer p5`, so there's no installation or setup to worry about. Add **`webgl`** after `p5` on that same line (`// @renderer p5 webgl`) to switch the main canvas to P5's WEBGL renderer for 3D and shader-based drawing.\n\n**Entry points:** `render(viji, p5)` (required), `setup(viji, p5)` (optional)"
966
966
  },
967
967
  {
968
968
  "type": "live-example",
@@ -978,17 +978,17 @@ export const docsApi = {
978
978
  },
979
979
  {
980
980
  "type": "text",
981
- "markdown": "> [!WARNING]\r\n> Viji uses P5 in **instance mode**. All P5 functions require the `p5.` prefix:\r\n> ```javascript\r\n> // Correct\r\n> p5.background(0);\r\n> p5.circle(p5.width / 2, p5.height / 2, 100);\r\n>\r\n> // Wrong: will throw ReferenceError\r\n> background(0);\r\n> circle(width / 2, height / 2, 100);\r\n> ```\r\n\r\n**Key characteristics:**\r\n\r\n- **`setup()` is optional.** Use it for one-time configuration like `p5.colorMode()`. If you don't need it, omit it entirely.\r\n- **`render()` replaces `draw()`.** P5's built-in draw loop is disabled; Viji calls your `render()` function each frame.\r\n- **No `createCanvas()`.** The canvas is created and managed by Viji. The mode (2D or WEBGL) comes from the directive itself, either `// @renderer p5` or `// @renderer p5 webgl`. Never call `createCanvas(..., p5.WEBGL)` yourself.\r\n- **Viji APIs for input.** Use [`viji.pointer`](/p5/pointer#why-use-pointer) for cross-device interactions, or [`viji.mouse`](/p5/mouse#position), [`viji.keyboard`](/p5/keyboard#api-reference), and [`viji.touches`](/p5/touch#touchapi-vijitouches) for device-specific access, instead of P5's `mouseX`, `keyIsPressed`, etc.\r\n- **No `preload()`.** Load assets using Viji's [`viji.image()`](/native/parameters/image) parameter, or use `fetch()` in `setup()`.\r\n\r\nIf you have existing P5.js sketches, see [Converting P5 Sketches](/p5/converting-sketches) for a step-by-step migration guide.\r\n\r\n---\r\n\r\n## Shader Renderer\r\n\r\nThe shader renderer lets you write GLSL fragment shaders that run directly on the GPU. Viji injects all uniform declarations for you, so you write only your helper functions and `void main()`.\r\n\r\n**Entry point:** `void main()` (GLSL)"
981
+ "markdown": "> [!WARNING]\n> Viji uses P5 in **instance mode**. All P5 functions require the `p5.` prefix:\n> ```javascript\n> // Correct\n> p5.background(0);\n> p5.circle(p5.width / 2, p5.height / 2, 100);\n>\n> // Wrong: will throw ReferenceError\n> background(0);\n> circle(width / 2, height / 2, 100);\n> ```\n\n**Key characteristics:**\n\n- **`setup()` is optional.** Use it for one-time configuration like `p5.colorMode()`. If you don't need it, omit it entirely.\n- **`render()` replaces `draw()`.** P5's built-in draw loop is disabled; Viji calls your `render()` function each frame.\n- **No `createCanvas()`.** The canvas is created and managed by Viji. The mode (2D or WEBGL) comes from the directive itself, either `// @renderer p5` or `// @renderer p5 webgl`. Never call `createCanvas(..., p5.WEBGL)` yourself.\n- **Viji APIs for input.** Use [`viji.pointer`](/p5/pointer#why-use-pointer) for cross-device interactions, or [`viji.mouse`](/p5/mouse#position), [`viji.keyboard`](/p5/keyboard#api-reference), and [`viji.touches`](/p5/touch#touchapi-vijitouches) for device-specific access, instead of P5's `mouseX`, `keyIsPressed`, etc.\n- **No `preload()`.** Load assets using Viji's [`viji.image()`](/native/parameters/image) parameter, or use `fetch()` in `setup()`.\n\nIf you have existing P5.js sketches, see [Converting P5 Sketches](/p5/converting-sketches) for a step-by-step migration guide.\n\n---\n\n## Shader Renderer\n\nThe shader renderer lets you write GLSL fragment shaders that run directly on the GPU. Viji injects all uniform declarations for you, so you write only your helper functions and `void main()`.\n\n**Entry point:** `void main()` (GLSL)"
982
982
  },
983
983
  {
984
984
  "type": "live-example",
985
985
  "title": "Shader Renderer",
986
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-color:baseColor label:\"Base Color\" default:#ff6600\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\r\n vec3 color = baseColor * wave;\r\n\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
986
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-color:baseColor label:\"Base 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 vec3 color = baseColor * wave;\n\n gl_FragColor = vec4(color, 1.0);\n}\n",
987
987
  "sceneFile": "renderers-shader.scene.glsl"
988
988
  },
989
989
  {
990
990
  "type": "text",
991
- "markdown": "> [!NOTE]\r\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.\r\n\r\n**Key characteristics:**\r\n\r\n- **Fragment shader only.** Viji renders a fullscreen quad; your shader defines the color of every pixel.\r\n- **GLSL ES 1.00 by default.** If you add `#version 300 es` as the first line, Viji switches to WebGL 2. Note that ES 3.00 requires `out vec4` for output instead of `gl_FragColor`, and `texture()` instead of `texture2D()`. ES 1.00 is recommended for maximum compatibility.\r\n- **Built-in uniforms** like `u_time`, `u_resolution`, `u_mouse`, `u_audioVolume`, `u_video`, and many more are always available. No declaration needed.\r\n- **Parameters via comments.** Declare parameters with `// @viji-TYPE:uniformName key:value` syntax. They become uniforms automatically.\r\n- **Accumulators for smooth animation.** Use `// @viji-accumulator:phase rate:speed` instead of `u_time * speed`. The value grows smoothly without jumping when the rate parameter changes.\r\n- **No `u_` prefix for your parameters.** The `u_` prefix is reserved for Viji's built-in uniforms. Name your parameters descriptively, for example `speed`, `colorMix`, or `intensity`.\r\n\r\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy) for a compatibility layer that lets you paste code with minimal changes.\r\n\r\n---\r\n\r\n## Comparison\r\n\r\n| | Native | P5.js | Shader |\r\n|---|--------|-------|--------|\r\n| **Language** | JavaScript / TypeScript | JavaScript with P5 API | GLSL ES 1.00 (or 3.00 with `#version 300 es`) |\r\n| **Directive** | None (default) | `// @renderer p5` or `// @renderer p5 webgl` | `// @renderer shader` |\r\n| **Entry point** | `render(viji)` | `render(viji, p5)` | `void main()` |\r\n| **Setup** | Top-level code + `await` | Optional `setup(viji, p5)` | N/A |\r\n| **Canvas access** | [`viji.useContext('2d'/'webgl'/'webgl2')`](/native/canvas-context) | P5 drawing functions | Automatic fullscreen quad |\r\n| **External libraries** | Yes (`await import(...)`) | P5.js only | No |\r\n| **Best for** | Full control, WebGL, Three.js | Familiar P5 workflows | GPU effects, raymarching |\r\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), etc. | [`viji.slider()`](/native/parameters/slider), etc. | `// @viji-slider:name ...` |\r\n\r\n## Next Steps\r\n\r\n- [Native Quick Start](/native/quickstart): build your first native scene\r\n- [P5 Quick Start](/p5/quickstart): build your first P5.js scene\r\n- [Shader Quick Start](/shader/quickstart): build your first shader\r\n- [Best Practices](../best-practices/): essential patterns all artists should follow"
991
+ "markdown": "> [!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.\n\n**Key characteristics:**\n\n- **Fragment shader only.** Viji renders a fullscreen quad; your shader defines the color of every pixel.\n- **GLSL ES 1.00 by default.** If you add `#version 300 es` as the first line, Viji switches to WebGL 2. Note that ES 3.00 requires `out vec4` for output instead of `gl_FragColor`, and `texture()` instead of `texture2D()`. ES 1.00 is recommended for maximum compatibility.\n- **Built-in uniforms** like `u_time`, `u_resolution`, `u_mouse`, `u_audioVolume`, `u_video`, and many more are always available. No declaration needed.\n- **Parameters via comments.** Declare parameters with `// @viji-TYPE:uniformName key:value` syntax. They become uniforms automatically.\n- **Accumulators for smooth animation.** Use `// @viji-accumulator:phase rate:speed` instead of `u_time * speed`. The value grows smoothly without jumping when the rate parameter changes.\n- **No `u_` prefix for your parameters.** The `u_` prefix is reserved for Viji's built-in uniforms. Name your parameters descriptively, for example `speed`, `colorMix`, or `intensity`.\n\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy) for a compatibility layer that lets you paste code with minimal changes.\n\n---\n\n## Comparison\n\n| | Native | P5.js | Shader |\n|---|--------|-------|--------|\n| **Language** | JavaScript / TypeScript | JavaScript with P5 API | GLSL ES 1.00 (or 3.00 with `#version 300 es`) |\n| **Directive** | None (default) | `// @renderer p5` or `// @renderer p5 webgl` | `// @renderer shader` |\n| **Entry point** | `render(viji)` | `render(viji, p5)` | `void main()` |\n| **Setup** | Top-level code + `await` | Optional `setup(viji, p5)` | N/A |\n| **Canvas access** | [`viji.useContext('2d'/'webgl'/'webgl2')`](/native/canvas-context) | P5 drawing functions | Automatic fullscreen quad |\n| **External libraries** | Yes (`await import(...)`) | P5.js only | No |\n| **Best for** | Full control, WebGL, Three.js | Familiar P5 workflows | GPU effects, raymarching |\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), etc. | [`viji.slider()`](/native/parameters/slider), etc. | `// @viji-slider:name ...` |\n\n## Next Steps\n\n- [Native Quick Start](/native/quickstart): build your first native scene\n- [P5 Quick Start](/p5/quickstart): build your first P5.js scene\n- [Shader Quick Start](/shader/quickstart): build your first shader\n- [Best Practices](../best-practices/): essential patterns all artists should follow"
992
992
  }
993
993
  ]
994
994
  },
@@ -1317,7 +1317,7 @@ export const docsApi = {
1317
1317
  "content": [
1318
1318
  {
1319
1319
  "type": "text",
1320
- "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"
1320
+ "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:\"A\" 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"
1321
1321
  }
1322
1322
  ]
1323
1323
  },
@@ -1401,7 +1401,7 @@ export const docsApi = {
1401
1401
  "content": [
1402
1402
  {
1403
1403
  "type": "text",
1404
- "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"
1404
+ "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:\"Solid\" options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index of the selected option)\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"
1405
1405
  }
1406
1406
  ]
1407
1407
  },
@@ -1535,7 +1535,7 @@ export const docsApi = {
1535
1535
  "content": [
1536
1536
  {
1537
1537
  "type": "text",
1538
- "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"
1538
+ "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:\"A\" 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:\"A\" 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"
1539
1539
  }
1540
1540
  ]
1541
1541
  },
@@ -1631,7 +1631,7 @@ export const docsApi = {
1631
1631
  {
1632
1632
  "type": "live-example",
1633
1633
  "title": "TypeScript Scene",
1634
- "sceneCode": "const speed: SliderParameter = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst size: SliderParameter = viji.slider(0.05, { min: 0.02, max: 0.15, step: 0.01, label: 'Size' });\r\nconst color: ColorParameter = viji.color('#ff6600', { label: 'Color' });\r\n\r\nlet angle: number = 0;\r\n\r\nfunction render(viji: VijiAPI) {\r\n const ctx = viji.useContext('2d');\r\n const w: number = viji.width;\r\n const h: number = viji.height;\r\n\r\n angle += speed.value * viji.deltaTime;\r\n\r\n ctx.fillStyle = '#0a0a2e';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n const cx: number = w / 2 + Math.cos(angle) * w * 0.3;\r\n const cy: number = h / 2 + Math.sin(angle) * h * 0.3;\r\n const r: number = Math.min(w, h) * size.value;\r\n\r\n ctx.beginPath();\r\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\r\n ctx.fillStyle = color.value;\r\n ctx.fill();\r\n}\r\n",
1634
+ "sceneCode": "const speed: SliderParameter = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst size: SliderParameter = viji.slider(0.05, { min: 0.02, max: 0.15, step: 0.01, label: 'Size' });\nconst color: ColorParameter = viji.color('#ff6600', { label: 'Color' });\n\nlet angle: number = 0;\n\nfunction render(viji: VijiAPI) {\n const ctx = viji.useContext('2d');\n const w: number = viji.width;\n const h: number = viji.height;\n\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = '#0a0a2e';\n ctx.fillRect(0, 0, w, h);\n\n const cx: number = w / 2 + Math.cos(angle) * w * 0.3;\n const cy: number = h / 2 + Math.sin(angle) * h * 0.3;\n const r: number = Math.min(w, h) * size.value;\n\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n ctx.fillStyle = color.value;\n ctx.fill();\n}\n",
1635
1635
  "sceneFile": "quickstart-ts.scene.ts"
1636
1636
  },
1637
1637
  {
@@ -1725,7 +1725,7 @@ export const docsApi = {
1725
1725
  },
1726
1726
  "native-canvas": {
1727
1727
  "id": "native-canvas",
1728
- "title": "native-canvas",
1728
+ "title": "Canvas & Context",
1729
1729
  "description": "Access the canvas, choose a rendering context, and work with resolution-agnostic dimensions.",
1730
1730
  "headings": [
1731
1731
  {
@@ -1812,7 +1812,7 @@ export const docsApi = {
1812
1812
  "content": [
1813
1813
  {
1814
1814
  "type": "text",
1815
- "markdown": "# Canvas & Context\r\n\r\nEvery native scene gets a canvas and a set of properties for resolution-agnostic drawing. This page covers `viji.canvas`, `viji.useContext()`, `viji.width`, `viji.height`, `viji.ctx`, and `viji.gl`.\r\n\r\n## The Canvas\r\n\r\n`viji.canvas` is an [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas): not a DOM `<canvas>` element. It lives inside a Web Worker and has no associated DOM node. You never create or resize it yourself; the host application manages its lifecycle and dimensions.\r\n\r\n| Property | Type | Description |\r\n|----------|------|-------------|\r\n| `viji.canvas` | `OffscreenCanvas` | The raw canvas. Rarely needed: use `viji.useContext()` instead. |\r\n| `viji.width` | `number` | Current canvas width in pixels |\r\n| `viji.height` | `number` | Current canvas height in pixels |\r\n\r\n`viji.width` and `viji.height` always reflect the actual pixel dimensions of the canvas and update automatically when the host resizes it. Use them for all positioning and sizing.\r\n\r\n## Choosing a Context\r\n\r\nCall `viji.useContext()` to get a rendering context:\r\n\r\n```javascript\r\nconst ctx = viji.useContext('2d'); // OffscreenCanvasRenderingContext2D\r\nconst gl = viji.useContext('webgl'); // WebGLRenderingContext\r\nconst gl2 = viji.useContext('webgl2'); // WebGL2RenderingContext\r\nconst gctx = viji.useContext('webgpu'); // GPUCanvasContext | null\r\n```\r\n\r\n| Argument | Return Type | Also Sets |\r\n|----------|------------|-----------|\r\n| `'2d'` | `OffscreenCanvasRenderingContext2D` | `viji.ctx` |\r\n| `'webgl'` | `WebGLRenderingContext` | `viji.gl` |\r\n| `'webgl2'` | `WebGL2RenderingContext` | `viji.gl` |\r\n| `'webgpu'` | `GPUCanvasContext \\| null` | `viji.gpu` |\r\n\r\nThe context is created once and cached: calling `useContext()` again with the same argument returns the same instance. Call it at the top of `render()` or once at the top level and store the result.\r\n\r\n> [!WARNING]\r\n> A canvas only supports one context type. If you call `useContext('2d')` and later call `useContext('webgl')` (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\r\n\r\n## 2D Context\r\n\r\nThe most common choice. You get a standard [`CanvasRenderingContext2D`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) with full access to paths, fills, strokes, transforms, gradients, and compositing."
1815
+ "markdown": "# Canvas & Context\n\nEvery native scene gets a canvas and a set of properties for resolution-agnostic drawing. This page covers `viji.canvas`, `viji.useContext()`, `viji.width`, `viji.height`, `viji.ctx`, and `viji.gl`.\n\n## The Canvas\n\n`viji.canvas` is an [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas): not a DOM `<canvas>` element. It lives inside a Web Worker and has no associated DOM node. You never create or resize it yourself; the host application manages its lifecycle and dimensions.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The raw canvas. Rarely needed: use `viji.useContext()` instead. |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n`viji.width` and `viji.height` always reflect the actual pixel dimensions of the canvas and update automatically when the host resizes it. Use them for all positioning and sizing.\n\n## Choosing a Context\n\nCall `viji.useContext()` to get a rendering context:\n\n```javascript\nconst ctx = viji.useContext('2d'); // OffscreenCanvasRenderingContext2D\nconst gl = viji.useContext('webgl'); // WebGLRenderingContext\nconst gl2 = viji.useContext('webgl2'); // WebGL2RenderingContext\nconst gctx = viji.useContext('webgpu'); // GPUCanvasContext | null\n```\n\n| Argument | Return Type | Also Sets |\n|----------|------------|-----------|\n| `'2d'` | `OffscreenCanvasRenderingContext2D` | `viji.ctx` |\n| `'webgl'` | `WebGLRenderingContext` | `viji.gl` |\n| `'webgl2'` | `WebGL2RenderingContext` | `viji.gl` |\n| `'webgpu'` | `GPUCanvasContext \\| null` | `viji.gpu` |\n\nThe context is created once and cached: calling `useContext()` again with the same argument returns the same instance. Call it at the top of `render()` or once at the top level and store the result.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call `useContext('2d')` and later call `useContext('webgl')` (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n## 2D Context\n\nThe most common choice. You get a standard [`CanvasRenderingContext2D`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) with full access to paths, fills, strokes, transforms, gradients, and compositing."
1816
1816
  },
1817
1817
  {
1818
1818
  "type": "live-example",
@@ -1822,7 +1822,7 @@ export const docsApi = {
1822
1822
  },
1823
1823
  {
1824
1824
  "type": "text",
1825
- "markdown": "### Shorthand: `viji.ctx`\r\n\r\nAfter calling `viji.useContext('2d')`, the same context is also available as `viji.ctx`:\r\n\r\n```javascript\r\nfunction render(viji) {\r\n viji.useContext('2d');\r\n viji.ctx.fillStyle = '#000';\r\n viji.ctx.fillRect(0, 0, viji.width, viji.height);\r\n}\r\n```\r\n\r\nBoth `viji.useContext('2d')` and `viji.ctx` reference the same object.\r\n\r\n## WebGL Context\r\n\r\nFor GPU-accelerated rendering, request a WebGL context. The returned object is a standard [`WebGLRenderingContext`](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) or [`WebGL2RenderingContext`](https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext): you manage shaders, buffers, and draw calls yourself."
1825
+ "markdown": "### Shorthand: `viji.ctx`\n\nAfter calling `viji.useContext('2d')`, the same context is also available as `viji.ctx`:\n\n```javascript\nfunction render(viji) {\n viji.useContext('2d');\n viji.ctx.fillStyle = '#000';\n viji.ctx.fillRect(0, 0, viji.width, viji.height);\n}\n```\n\nBoth `viji.useContext('2d')` and `viji.ctx` reference the same object.\n\n## WebGL Context\n\nFor GPU-accelerated rendering, request a WebGL context. The returned object is a standard [`WebGLRenderingContext`](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) or [`WebGL2RenderingContext`](https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext): you manage shaders, buffers, and draw calls yourself."
1826
1826
  },
1827
1827
  {
1828
1828
  "type": "live-example",
@@ -1832,7 +1832,7 @@ export const docsApi = {
1832
1832
  },
1833
1833
  {
1834
1834
  "type": "text",
1835
- "markdown": "### Shorthand: `viji.gl`\r\n\r\nAfter calling `viji.useContext('webgl')` or `viji.useContext('webgl2')`, the context is also available as `viji.gl`. Its runtime type matches whichever variant you requested.\r\n\r\n> [!TIP]\r\n> If you want full GPU control but don't want to manage WebGL boilerplate, consider the [Shader Renderer](/shader/quickstart): it handles the fullscreen quad, uniform injection, and compilation for you.\r\n\r\n## WebGPU Context\r\n\r\nThe modern GPU API, for compute shaders and custom render pipelines. `useContext('webgpu')` returns a raw [`GPUCanvasContext`](https://developer.mozilla.org/en-US/docs/Web/API/GPUCanvasContext). Unlike the other context types, it can also return `null` at runtime on a browser without WebGPU support, so always guard and provide a fallback. Requesting the adapter and device, and configuring the context, stay in your scene code (top-level `await` works):\r\n\r\n```javascript\r\nconst gctx = viji.useContext('webgpu');\r\nif (!navigator.gpu || !gctx) {\r\n // fall back: render a message with a 2D context instead\r\n} else {\r\n const adapter = await navigator.gpu.requestAdapter();\r\n const device = await adapter.requestDevice();\r\n const format = navigator.gpu.getPreferredCanvasFormat();\r\n gctx.configure({ device, format, alphaMode: 'opaque' });\r\n}\r\n```"
1835
+ "markdown": "### Shorthand: `viji.gl`\n\nAfter calling `viji.useContext('webgl')` or `viji.useContext('webgl2')`, the context is also available as `viji.gl`. Its runtime type matches whichever variant you requested.\n\n> [!TIP]\n> If you want full GPU control but don't want to manage WebGL boilerplate, consider the [Shader Renderer](/shader/quickstart): it handles the fullscreen quad, uniform injection, and compilation for you.\n\n## WebGPU Context\n\nThe modern GPU API, for compute shaders and custom render pipelines. `useContext('webgpu')` returns a raw [`GPUCanvasContext`](https://developer.mozilla.org/en-US/docs/Web/API/GPUCanvasContext). Unlike the other context types, it can also return `null` at runtime on a browser without WebGPU support, so always guard and provide a fallback. Requesting the adapter and device, and configuring the context, stay in your scene code (top-level `await` works):\n\n```javascript\nconst gctx = viji.useContext('webgpu');\nif (!navigator.gpu || !gctx) {\n // fall back: render a message with a 2D context instead\n} else {\n const adapter = await navigator.gpu.requestAdapter();\n const device = await adapter.requestDevice();\n const format = navigator.gpu.getPreferredCanvasFormat();\n gctx.configure({ device, format, alphaMode: 'opaque' });\n}\n```"
1836
1836
  },
1837
1837
  {
1838
1838
  "type": "live-example",
@@ -1842,7 +1842,7 @@ export const docsApi = {
1842
1842
  },
1843
1843
  {
1844
1844
  "type": "text",
1845
- "markdown": "### Shorthand: `viji.gpu`\r\n\r\nAfter a successful `viji.useContext('webgpu')` (or a direct `viji.canvas.getContext('webgpu')`), the same context is also available as `viji.gpu`.\r\n\r\nGet a fresh swapchain texture with `getCurrentTexture()` every frame inside `render()`; never cache it across frames. A failed WebGPU request doesn't bind the canvas, so the fallback branch can still acquire `'2d'`. A GPU device can be lost (driver reset, tab backgrounding on some platforms): listen on `device.lost`, then re-request the adapter and device and `configure()` again. The engine tolerates reconfiguration at any time. For running ONNX models on the WebGPU execution provider, see [External Libraries](/native/external-libraries).\r\n\r\n## Transparency\r\n\r\nA scene that leaves alpha below 1 renders with a transparent background: whatever the host shows behind the canvas stays visible through unpainted and semi-transparent pixels. Skip the opaque background fill and the pixels stay clear. What appears behind a transparent scene is the host's choice, not the scene's.\r\n\r\nPer context:\r\n\r\n- **2D**: use alpha as usual (`'rgba(...)'` fills and clears). The browser handles the pixel format.\r\n- **WebGL / WebGL2**: the drawing buffer is premultiplied. Output premultiplied colors or use the premultiplied blend mode (`gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`). Three.js manages this for you.\r\n- **WebGPU**: configure with `alphaMode: 'premultiplied'` instead of `'opaque'`.\r\n\r\n## Resolution-Agnostic Drawing\r\n\r\nThe canvas can be any size, from a small preview to fullscreen 4K, and the host can resize it at any time. Your scene must adapt automatically.\r\n\r\n> [!NOTE]\r\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\r\n\r\n### Dimension Properties\r\n\r\n| Property | Type | Description |\r\n|----------|------|-------------|\r\n| `viji.width` | `number` | Current canvas width in pixels |\r\n| `viji.height` | `number` | Current canvas height in pixels |\r\n\r\nBoth values update automatically when the host resizes the canvas. Read them every frame inside `render()`: don't cache them at the top level.\r\n\r\n### Centering and Proportional Sizing\r\n\r\nExpress all positions and sizes relative to the canvas dimensions:\r\n\r\n```javascript\r\n// Good: scales with canvas\r\nconst cx = viji.width / 2;\r\nconst cy = viji.height / 2;\r\nconst r = Math.min(viji.width, viji.height) * 0.3;\r\n\r\n// Bad: breaks at different resolutions\r\nconst cx = 400;\r\nconst cy = 300;\r\nconst r = 150;\r\n```\r\n\r\nUse `Math.min(viji.width, viji.height)` when you need uniform scaling: it ensures elements stay proportional regardless of aspect ratio. Use `viji.width` or `viji.height` individually when you want an element to stretch with one axis (e.g., a full-width bar).\r\n\r\n### Proportional Margins and Spacing\r\n\r\n```javascript\r\nconst margin = Math.min(viji.width, viji.height) * 0.05;\r\nconst usableW = viji.width - margin * 2;\r\nconst usableH = viji.height - margin * 2;\r\n```\r\n\r\n### Comparison Across Renderers\r\n\r\n| Concept | Native | P5 | Shader |\r\n|---------|--------|-----|--------|\r\n| Canvas width | `viji.width` | `viji.width` / `p5.width` | `u_resolution.x` |\r\n| Canvas height | `viji.height` | `viji.height` / `p5.height` | `u_resolution.y` |\r\n| Aspect ratio | `viji.width / viji.height` | `viji.width / viji.height` | `u_resolution.x / u_resolution.y` |\r\n| Uniform scale | `Math.min(viji.width, viji.height)` | `Math.min(viji.width, viji.height)` | `min(u_resolution.x, u_resolution.y)` |\r\n\r\n## Environment Constraints\r\n\r\n> [!WARNING]\r\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\r\n\r\n## Next Steps\r\n\r\n- [Timing](/native/timing): [`viji.time`](/native/timing), [`viji.deltaTime`](/native/timing), frame counting\r\n- [Parameters](/native/parameters): sliders, colors, toggles\r\n- [P5 Canvas & Resolution](/p5/canvas-resolution): how P5 handles canvas and sizing\r\n- [Shader Resolution](/shader/resolution): `u_resolution` and coordinate normalization\r\n- [API Reference](/native/api-reference): full list of everything available"
1845
+ "markdown": "### Shorthand: `viji.gpu`\n\nAfter a successful `viji.useContext('webgpu')` (or a direct `viji.canvas.getContext('webgpu')`), the same context is also available as `viji.gpu`.\n\nGet a fresh swapchain texture with `getCurrentTexture()` every frame inside `render()`; never cache it across frames. A failed WebGPU request doesn't bind the canvas, so the fallback branch can still acquire `'2d'`. A GPU device can be lost (driver reset, tab backgrounding on some platforms): listen on `device.lost`, then re-request the adapter and device and `configure()` again. The engine tolerates reconfiguration at any time. For running ONNX models on the WebGPU execution provider, see [External Libraries](/native/external-libraries).\n\n## Transparency\n\nA scene that leaves alpha below 1 renders with a transparent background: whatever the host shows behind the canvas stays visible through unpainted and semi-transparent pixels. Skip the opaque background fill and the pixels stay clear. What appears behind a transparent scene is the host's choice, not the scene's.\n\nPer context:\n\n- **2D**: use alpha as usual (`'rgba(...)'` fills and clears). The browser handles the pixel format.\n- **WebGL / WebGL2**: the drawing buffer is premultiplied. Output premultiplied colors or use the premultiplied blend mode (`gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`). Three.js manages this for you.\n- **WebGPU**: configure with `alphaMode: 'premultiplied'` instead of `'opaque'`.\n\n## Resolution-Agnostic Drawing\n\nThe canvas can be any size, from a small preview to fullscreen 4K, and the host can resize it at any time. Your scene must adapt automatically.\n\n> [!NOTE]\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n### Dimension Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\nBoth values update automatically when the host resizes the canvas. Read them every frame inside `render()`: don't cache them at the top level.\n\n### Centering and Proportional Sizing\n\nExpress all positions and sizes relative to the canvas dimensions:\n\n```javascript\n// Good: scales with canvas\nconst cx = viji.width / 2;\nconst cy = viji.height / 2;\nconst r = Math.min(viji.width, viji.height) * 0.3;\n\n// Bad: breaks at different resolutions\nconst cx = 400;\nconst cy = 300;\nconst r = 150;\n```\n\nUse `Math.min(viji.width, viji.height)` when you need uniform scaling: it ensures elements stay proportional regardless of aspect ratio. Use `viji.width` or `viji.height` individually when you want an element to stretch with one axis (e.g., a full-width bar).\n\n### Proportional Margins and Spacing\n\n```javascript\nconst margin = Math.min(viji.width, viji.height) * 0.05;\nconst usableW = viji.width - margin * 2;\nconst usableH = viji.height - margin * 2;\n```\n\n### Comparison Across Renderers\n\n| Concept | Native | P5 | Shader |\n|---------|--------|-----|--------|\n| Canvas width | `viji.width` | `viji.width` / `p5.width` | `u_resolution.x` |\n| Canvas height | `viji.height` | `viji.height` / `p5.height` | `u_resolution.y` |\n| Aspect ratio | `viji.width / viji.height` | `viji.width / viji.height` | `u_resolution.x / u_resolution.y` |\n| Uniform scale | `Math.min(viji.width, viji.height)` | `Math.min(viji.width, viji.height)` | `min(u_resolution.x, u_resolution.y)` |\n\n## Environment Constraints\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n## Next Steps\n\n- [Timing](/native/timing): [`viji.time`](/native/timing), [`viji.deltaTime`](/native/timing), frame counting\n- [Parameters](/native/parameters): sliders, colors, toggles\n- [P5 Canvas & Resolution](/p5/canvas-resolution): how P5 handles canvas and sizing\n- [Shader Resolution](/shader/resolution): `u_resolution` and coordinate normalization\n- [API Reference](/native/api-reference): full list of everything available"
1846
1846
  }
1847
1847
  ]
1848
1848
  },
@@ -1926,7 +1926,7 @@ export const docsApi = {
1926
1926
  },
1927
1927
  "native-external-libs": {
1928
1928
  "id": "native-external-libs",
1929
- "title": "native-external-libs",
1929
+ "title": "External Libraries",
1930
1930
  "description": "How to use Three.js, math libraries, and other ES modules in native Viji scenes.",
1931
1931
  "headings": [
1932
1932
  {
@@ -1983,7 +1983,7 @@ export const docsApi = {
1983
1983
  "content": [
1984
1984
  {
1985
1985
  "type": "text",
1986
- "markdown": "# External Libraries\r\n\r\nThe native renderer supports **top-level `await`**, which means you can dynamically import any ES module from a CDN directly in your scene code. No bundler, no install step: just `await import()`.\r\n\r\n> [!TIP]\r\n> Have an existing Three.js project? See [Convert: Three.js](/ai-prompts/convert-threejs) for a ready-to-paste AI prompt that adapts your code to Viji automatically.\r\n\r\n## Importing a Library\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\n```\r\n\r\nThis works because Viji scene code runs in a module-like context where top-level `await` is available. The import happens once during scene initialization, before `render()` is ever called.\r\n\r\n> [!TIP]\r\n> [esm.sh](https://esm.sh) is a recommended CDN for ES modules. It serves any npm package as a standard ES module and handles dependency resolution automatically.\r\n\r\n## Three.js\r\n\r\nThree.js is the most common external library used with Viji. It works naturally in native mode because it accepts an external canvas, has no automatic render loop, and uses standard WebGL."
1986
+ "markdown": "# External Libraries\n\nThe native renderer supports **top-level `await`**, which means you can dynamically import any ES module from a CDN directly in your scene code. No bundler, no install step: just `await import()`.\n\n> [!TIP]\n> Have an existing Three.js project? See [Convert: Three.js](/ai-prompts/convert-threejs) for a ready-to-paste AI prompt that adapts your code to Viji automatically.\n\n## Importing a Library\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n```\n\nThis works because Viji scene code runs in a module-like context where top-level `await` is available. The import happens once during scene initialization, before `render()` is ever called.\n\n> [!TIP]\n> [esm.sh](https://esm.sh) is a recommended CDN for ES modules. It serves any npm package as a standard ES module and handles dependency resolution automatically.\n\n## Three.js\n\nThree.js is the most common external library used with Viji. It works naturally in native mode because it accepts an external canvas, has no automatic render loop, and uses standard WebGL."
1987
1987
  },
1988
1988
  {
1989
1989
  "type": "live-example",
@@ -1993,7 +1993,7 @@ export const docsApi = {
1993
1993
  },
1994
1994
  {
1995
1995
  "type": "text",
1996
- "markdown": "### Key Integration Points\r\n\r\n**Canvas:** Pass `viji.canvas` to the Three.js renderer:\r\n\r\n```javascript\r\nconst renderer = new THREE.WebGLRenderer({\r\n canvas: viji.canvas,\r\n antialias: true\r\n});\r\n```\r\n\r\n**Resize:** Three.js doesn't auto-resize. Check for dimension changes in `render()`:\r\n\r\n```javascript\r\nlet prevWidth = viji.width;\r\nlet prevHeight = viji.height;\r\n\r\nfunction render(viji) {\r\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\r\n renderer.setSize(viji.width, viji.height, false);\r\n camera.aspect = viji.width / viji.height;\r\n camera.updateProjectionMatrix();\r\n prevWidth = viji.width;\r\n prevHeight = viji.height;\r\n }\r\n\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n> [!IMPORTANT]\r\n> Pass `false` as the third argument to `renderer.setSize()`: this prevents Three.js from setting CSS styles on the canvas, which would fail in the worker environment.\r\n\r\n**No `requestAnimationFrame`:** Viji controls the render loop. Write your animation logic inside `render(viji)` and call `renderer.render(scene, camera)` at the end.\r\n\r\n**Parameters:** All Viji parameter types work as expected:\r\n\r\n```javascript\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#049ef4', { label: 'Color' });\r\n\r\nfunction render(viji) {\r\n cube.rotation.y += speed.value * viji.deltaTime;\r\n material.color.set(color.value);\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n### Three.js Addons\r\n\r\nImport addons from the Three.js examples directory:\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\nconst { OrbitControls } = await import('https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js');\r\nconst { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\r\nconst { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\r\n```\r\n\r\n> [!NOTE]\r\n> `OrbitControls` requires DOM events and will not work as expected in the worker environment. For camera interaction, use [`viji.pointer`](/native/pointer) to drive the camera manually: it handles both mouse and touch input.\r\n\r\n### Video and Image Textures\r\n\r\nThree.js can consume Viji's video frames and image parameters directly:\r\n\r\n```javascript\r\nconst photo = viji.image(null, { label: 'Texture' });\r\nlet texture = null;\r\n\r\nfunction render(viji) {\r\n if (photo.value && !texture) {\r\n texture = new THREE.CanvasTexture(photo.value);\r\n material.map = texture;\r\n material.needsUpdate = true;\r\n }\r\n\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n if (!videoTexture) {\r\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\r\n videoMaterial.map = videoTexture;\r\n }\r\n videoTexture.needsUpdate = true;\r\n }\r\n\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n## Other Libraries\r\n\r\nAny library published as an ES module works the same way. Examples:\r\n\r\n**Math / utility libraries:**\r\n\r\n```javascript\r\nconst { vec3, mat4 } = await import('https://esm.sh/gl-matrix@3.4.3');\r\nconst SimplexNoise = (await import('https://esm.sh/simplex-noise@4.0.1')).createNoise3D;\r\n```\r\n\r\n**2D rendering libraries:**\r\n\r\n```javascript\r\nconst PIXI = await import('https://esm.sh/pixi.js@7.3.2');\r\n```\r\n\r\n> [!NOTE]\r\n> Some libraries expect a DOM environment and may not work in the worker context. Libraries that accept an external canvas or work with pure data (math, noise, data structures) are the best candidates.\r\n\r\n## WebGL Context\r\n\r\nWhen using a library that creates its own WebGL context (like Three.js or Pixi.js), keep in mind:\r\n\r\n- The library consumes the canvas's WebGL context. You cannot also call `viji.useContext('2d')` or `viji.useContext('webgl')` for the same canvas: pick one rendering approach.\r\n- CV features (face detection, hand tracking, etc.) use additional WebGL contexts internally. Browsers have a limit of 8-16 contexts. Combining a WebGL library with multiple CV features may cause context loss.\r\n\r\nSee [Canvas & Context](/native/canvas-context) for details on `useContext()`.\r\n\r\n## Machine Learning Libraries\r\n\r\nTensorFlow.js and ONNX Runtime Web both work inside scenes. Import the library at the top level, run inference outside `render()` (inference takes longer than a frame), and draw the most recent result each frame. Model files downloaded with `fetch()` are cached by the engine on the host, so a model downloads once and later scene loads read it from disk. Only large static files are cached: small responses such as live API data stay fresh on every call. Note that `import()` module loads are not cached this way; libraries are small compared to models, so this rarely matters.\r\n\r\n**ONNX Runtime Web** (version 1.27 and newer): import the module build from jsdelivr and tell the runtime where to download its WebAssembly files. Both values are URLs on the same CDN:\r\n\r\n```javascript\r\nconst ort = await import('https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/ort.webgpu.min.mjs');\r\nort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/';\r\n\r\nconst modelBytes = new Uint8Array(await (await fetch(modelUrl)).arrayBuffer());\r\nconst session = await ort.InferenceSession.create(modelBytes, {\r\n executionProviders: ['webgpu']\r\n});\r\n```\r\n\r\n> [!NOTE]\r\n> Import ONNX Runtime Web from jsdelivr, not esm.sh. The esm.sh build injects Node.js compatibility code that breaks the runtime during startup.\r\n\r\n**TensorFlow.js**: `importScripts('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js')` loads the classic bundle and exposes the global `tf`. The WebGL backend initializes automatically on first use.\r\n\r\n**Transformers.js**: import the plain-file build and turn off its Cache Storage use. The scene worker cannot access Cache Storage; plain fetch downloads work and are cached by the engine on the host:\r\n\r\n```javascript\r\nconst tf = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.1/dist/transformers.min.js');\r\ntf.env.useBrowserCache = false;\r\n```\r\n\r\n## Tips\r\n\r\n- **Pin library versions** in the import URL (e.g., `three@0.160.0`). Unpinned versions may break when the library updates.\r\n- **Import once at the top level.** Never put `await import()` inside `render()`: it would re-fetch the library every frame.\r\n- **Check worker compatibility** before choosing a library. Libraries that require `document`, `window`, or `Image()` will not work. `fetch()` is available.\r\n- **Use `viji.deltaTime`** for animation instead of the library's own timer, to stay frame-rate-independent and synchronized with other Viji features.\r\n- **Spawning a helper worker?** Build it from a `data:` URL: `new Worker('data:application/javascript,' + encodeURIComponent(workerSource))`. In Chrome, only `data:` workers can use WebGPU and other capabilities that need a secure context. Workers built from `blob:` URLs still run everywhere, they just cannot use those capabilities in Chrome.\r\n\r\n## Related\r\n\r\n- [Canvas & Context](/native/canvas-context): `useContext('2d')`, `useContext('webgl')`, `useContext('webgl2')`\r\n- [Native Quick Start](/native/quickstart): building native scenes from scratch\r\n- [Parameters](/native/parameters): sliders, colors, toggles\r\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
1996
+ "markdown": "### Key Integration Points\n\n**Canvas:** Pass `viji.canvas` to the Three.js renderer:\n\n```javascript\nconst renderer = new THREE.WebGLRenderer({\n canvas: viji.canvas,\n antialias: true\n});\n```\n\n**Resize:** Three.js doesn't auto-resize. Check for dimension changes in `render()`:\n\n```javascript\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 renderer.render(scene, camera);\n}\n```\n\n> [!IMPORTANT]\n> Pass `false` as the third argument to `renderer.setSize()`: this prevents Three.js from setting CSS styles on the canvas, which would fail in the worker environment.\n\n**No `requestAnimationFrame`:** Viji controls the render loop. Write your animation logic inside `render(viji)` and call `renderer.render(scene, camera)` at the end.\n\n**Parameters:** All Viji parameter types work as expected:\n\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nfunction render(viji) {\n cube.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n renderer.render(scene, camera);\n}\n```\n\n### Three.js Addons\n\nImport addons from the Three.js examples directory:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst { OrbitControls } = await import('https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js');\nconst { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\nconst { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n```\n\n> [!NOTE]\n> `OrbitControls` requires DOM events and will not work as expected in the worker environment. For camera interaction, use [`viji.pointer`](/native/pointer) to drive the camera manually: it handles both mouse and touch input.\n\n### Video and Image Textures\n\nThree.js can consume Viji's video frames and image parameters directly:\n\n```javascript\nconst photo = viji.image(null, { label: 'Texture' });\nlet texture = null;\n\nfunction 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 if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n videoMaterial.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n\n renderer.render(scene, camera);\n}\n```\n\n## Other Libraries\n\nAny library published as an ES module works the same way. Examples:\n\n**Math / utility libraries:**\n\n```javascript\nconst { vec3, mat4 } = await import('https://esm.sh/gl-matrix@3.4.3');\nconst SimplexNoise = (await import('https://esm.sh/simplex-noise@4.0.1')).createNoise3D;\n```\n\n**2D rendering libraries:**\n\n```javascript\nconst PIXI = await import('https://esm.sh/pixi.js@7.3.2');\n```\n\n> [!NOTE]\n> Some libraries expect a DOM environment and may not work in the worker context. Libraries that accept an external canvas or work with pure data (math, noise, data structures) are the best candidates.\n\n## WebGL Context\n\nWhen using a library that creates its own WebGL context (like Three.js or Pixi.js), keep in mind:\n\n- The library consumes the canvas's WebGL context. You cannot also call `viji.useContext('2d')` or `viji.useContext('webgl')` for the same canvas: pick one rendering approach.\n- CV features (face detection, hand tracking, etc.) use additional WebGL contexts internally. Browsers have a limit of 8-16 contexts. Combining a WebGL library with multiple CV features may cause context loss.\n\nSee [Canvas & Context](/native/canvas-context) for details on `useContext()`.\n\n## Machine Learning Libraries\n\nTensorFlow.js and ONNX Runtime Web both work inside scenes. Import the library at the top level, run inference outside `render()` (inference takes longer than a frame), and draw the most recent result each frame. Model files downloaded with `fetch()` are cached by the engine on the host, so a model downloads once and later scene loads read it from disk. Only large static files are cached: small responses such as live API data stay fresh on every call. Note that `import()` module loads are not cached this way; libraries are small compared to models, so this rarely matters.\n\n**ONNX Runtime Web** (version 1.27 and newer): import the module build from jsdelivr and tell the runtime where to download its WebAssembly files. Both values are URLs on the same CDN:\n\n```javascript\nconst ort = await import('https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/ort.webgpu.min.mjs');\nort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/';\n\nconst modelBytes = new Uint8Array(await (await fetch(modelUrl)).arrayBuffer());\nconst session = await ort.InferenceSession.create(modelBytes, {\n executionProviders: ['webgpu']\n});\n```\n\n> [!NOTE]\n> Import ONNX Runtime Web from jsdelivr, not esm.sh. The esm.sh build injects Node.js compatibility code that breaks the runtime during startup.\n\n**TensorFlow.js**: `importScripts('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js')` loads the classic bundle and exposes the global `tf`. The WebGL backend initializes automatically on first use.\n\n**Transformers.js**: import the plain-file build and turn off its Cache Storage use. The scene worker cannot access Cache Storage; plain fetch downloads work and are cached by the engine on the host:\n\n```javascript\nconst tf = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.1/dist/transformers.min.js');\ntf.env.useBrowserCache = false;\n```\n\n## Tips\n\n- **Pin library versions** in the import URL (e.g., `three@0.160.0`). Unpinned versions may break when the library updates.\n- **Import once at the top level.** Never put `await import()` inside `render()`: it would re-fetch the library every frame.\n- **Check worker compatibility** before choosing a library. Libraries that require `document`, `window`, or `Image()` will not work. `fetch()` is available.\n- **Use `viji.deltaTime`** for animation instead of the library's own timer, to stay frame-rate-independent and synchronized with other Viji features.\n- **Spawning a helper worker?** Build it from a `data:` URL: `new Worker('data:application/javascript,' + encodeURIComponent(workerSource))`. In Chrome, only `data:` workers can use WebGPU and other capabilities that need a secure context. Workers built from `blob:` URLs still run everywhere, they just cannot use those capabilities in Chrome.\n\n## Related\n\n- [Canvas & Context](/native/canvas-context): `useContext('2d')`, `useContext('webgl')`, `useContext('webgl2')`\n- [Native Quick Start](/native/quickstart): building native scenes from scratch\n- [Parameters](/native/parameters): sliders, colors, toggles\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
1997
1997
  }
1998
1998
  ]
1999
1999
  },
@@ -2698,7 +2698,7 @@ export const docsApi = {
2698
2698
  },
2699
2699
  "native-audio-overview": {
2700
2700
  "id": "native-audio-overview",
2701
- "title": "native-audio-overview",
2701
+ "title": "Audio",
2702
2702
  "description": "Real-time audio analysis API: volume, frequency bands, beat detection, spectral features, and raw FFT/waveform data.",
2703
2703
  "headings": [
2704
2704
  {
@@ -2725,12 +2725,12 @@ export const docsApi = {
2725
2725
  "content": [
2726
2726
  {
2727
2727
  "type": "text",
2728
- "markdown": "# Audio\r\n\r\nViji provides real-time audio analysis when the host application connects an audio stream. All analysis runs on the host side and results are delivered to the scene through `viji.audio`.\r\n\r\n## API Overview\r\n\r\n| Sub-object | Description | Page |\r\n|------------|-------------|------|\r\n| [`isConnected`](connection/) | Whether an audio stream is active | [Connection & Lifecycle](connection/) |\r\n| [`volume`](volume/) | RMS level, peak amplitude, smoothed volume | [Volume](volume/) |\r\n| [`bands`](bands/) | Five frequency bands (instant and smoothed) | [Frequency Bands](bands/) |\r\n| [`beat`](beat/) | Beat energy curves, triggers, events, BPM | [Beat Detection](beat/) |\r\n| [`spectral`](spectral/) | Brightness and flatness features | [Spectral Analysis](spectral/) |\r\n| [`getFrequencyData()`](frequency-data/) | Raw FFT spectrum as `Uint8Array` | [Frequency Data](frequency-data/) |\r\n| [`getWaveform()`](waveform/) | Raw time-domain samples as `Float32Array` | [Waveform](waveform/) |\r\n\r\n## Basic Usage\r\n\r\n```javascript\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n const w = viji.width;\r\n const h = viji.height;\r\n\r\n ctx.fillStyle = '#111';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n if (!viji.audio.isConnected) {\r\n ctx.fillStyle = '#555';\r\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\r\n ctx.textAlign = 'center';\r\n ctx.fillText('Waiting for audio...', w / 2, h / 2);\r\n return;\r\n }\r\n\r\n const vol = viji.audio.volume.smoothed;\r\n const r = Math.min(w, h) * 0.1 + vol * Math.min(w, h) * 0.3;\r\n\r\n ctx.beginPath();\r\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\r\n ctx.fillStyle = `hsl(${200 + vol * 160}, 80%, 60%)`;\r\n ctx.fill();\r\n}\r\n```\r\n\r\n> [!NOTE]\r\n> Always check [`viji.audio.isConnected`](connection/) before reading audio values. When no audio stream is connected, all numeric values are `0` (`bpm` included). The `120` value sometimes seen in BPM contexts is an internal tracker fallback after audio connects but before tempo lock-on; before any audio connects, `bpm` reads `0`."
2728
+ "markdown": "# Audio\n\nViji provides real-time audio analysis when the host application connects an audio stream. All analysis runs on the host side and results are delivered to the scene through `viji.audio`.\n\n## API Overview\n\n| Sub-object | Description | Page |\n|------------|-------------|------|\n| [`isConnected`](connection/) | Whether an audio stream is active | [Connection & Lifecycle](connection/) |\n| [`volume`](volume/) | RMS level, peak amplitude, smoothed volume | [Volume](volume/) |\n| [`bands`](bands/) | Five frequency bands (instant and smoothed) | [Frequency Bands](bands/) |\n| [`beat`](beat/) | Beat energy curves, triggers, events, BPM | [Beat Detection](beat/) |\n| [`spectral`](spectral/) | Brightness and flatness features | [Spectral Analysis](spectral/) |\n| [`getFrequencyData()`](frequency-data/) | Raw FFT spectrum as `Uint8Array` | [Frequency Data](frequency-data/) |\n| [`getWaveform()`](waveform/) | Raw time-domain samples as `Float32Array` | [Waveform](waveform/) |\n\n## Basic Usage\n\n```javascript\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (!viji.audio.isConnected) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for audio...', w / 2, h / 2);\n return;\n }\n\n const vol = viji.audio.volume.smoothed;\n const r = Math.min(w, h) * 0.1 + vol * Math.min(w, h) * 0.3;\n\n ctx.beginPath();\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${200 + vol * 160}, 80%, 60%)`;\n ctx.fill();\n}\n```\n\n> [!NOTE]\n> Always check [`viji.audio.isConnected`](connection/) before reading audio values. When no audio stream is connected, all numeric values are `0` (`bpm` included). The `120` value sometimes seen in BPM contexts is an internal tracker fallback after audio connects but before tempo lock-on; before any audio connects, `bpm` reads `0`."
2729
2729
  },
2730
2730
  {
2731
2731
  "type": "live-example",
2732
2732
  "title": "Audio-Reactive Circle",
2733
- "sceneCode": "function render(viji) {\r\n const ctx = viji.useContext('2d');\r\n const w = viji.width;\r\n const h = viji.height;\r\n\r\n ctx.fillStyle = '#111';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n if (!viji.audio.isConnected) {\r\n ctx.fillStyle = '#555';\r\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\r\n ctx.textAlign = 'center';\r\n ctx.fillText('Waiting for audio...', w / 2, h / 2);\r\n return;\r\n }\r\n\r\n const vol = viji.audio.volume.smoothed;\r\n const r = Math.min(w, h) * 0.1 + vol * Math.min(w, h) * 0.3;\r\n\r\n ctx.beginPath();\r\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\r\n ctx.fillStyle = `hsl(${200 + vol * 160}, 80%, 60%)`;\r\n ctx.fill();\r\n}\r\n",
2733
+ "sceneCode": "function render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (!viji.audio.isConnected) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for audio...', w / 2, h / 2);\n return;\n }\n\n const vol = viji.audio.volume.smoothed;\n const r = Math.min(w, h) * 0.1 + vol * Math.min(w, h) * 0.3;\n\n ctx.beginPath();\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${200 + vol * 160}, 80%, 60%)`;\n ctx.fill();\n}\n",
2734
2734
  "sceneFile": "audio-overview.scene.js",
2735
2735
  "capabilities": {
2736
2736
  "audio": true
@@ -2738,13 +2738,13 @@ export const docsApi = {
2738
2738
  },
2739
2739
  {
2740
2740
  "type": "text",
2741
- "markdown": "## Beyond the Main Stream\r\n\r\nWhen an external device (phone, tablet, second machine) connects with an audio source, it surfaces as `device.audio` on the matching entry in `viji.devices[]`. Each `device.audio` is an `AudioStreamAPI`: a lightweight subset of the full `AudioAPI` on this page. It includes `volume` (current/peak/smoothed), `bands` (low/lowMid/mid/highMid/high plus each `*Smoothed` sibling), `spectral` (brightness/flatness), `getFrequencyData()`, and `getWaveform()`, but **not** `beat` energies, triggers, events, or BPM. Those remain on `viji.audio` only.\r\n\r\nSee [Device Audio](/native/external-devices/audio) for the full `device.audio` reference.\r\n\r\nThe host may also expose additional standalone audio sources via `viji.audioStreams[]` (same `AudioStreamAPI` shape). This array is mostly used internally by Viji's compositor for mixing scenes; standalone scenes rarely read it directly. See the [Native API Reference](/native/api-reference) for details.\r\n\r\n## Related\r\n\r\n- [Connection & Lifecycle](connection/)\r\n- [Volume](volume/)\r\n- [Frequency Bands](bands/)\r\n- [Beat Detection](beat/)\r\n- [Spectral Analysis](spectral/)\r\n- [Frequency Data](frequency-data/)\r\n- [Waveform](waveform/)\r\n- [P5 Audio](/p5/audio)\r\n- [Shader Audio Uniforms](/shader/audio)"
2741
+ "markdown": "## Beyond the Main Stream\n\nWhen an external device (phone, tablet, second machine) connects with an audio source, it surfaces as `device.audio` on the matching entry in `viji.devices[]`. Each `device.audio` is an `AudioStreamAPI`: a lightweight subset of the full `AudioAPI` on this page. It includes `volume` (current/peak/smoothed), `bands` (low/lowMid/mid/highMid/high plus each `*Smoothed` sibling), `spectral` (brightness/flatness), `getFrequencyData()`, and `getWaveform()`, but **not** `beat` energies, triggers, events, or BPM. Those remain on `viji.audio` only.\n\nSee [Device Audio](/native/external-devices/audio) for the full `device.audio` reference.\n\nThe host may also expose additional standalone audio sources via `viji.audioStreams[]` (same `AudioStreamAPI` shape). This array is mostly used internally by Viji's compositor for mixing scenes; standalone scenes rarely read it directly. See the [Native API Reference](/native/api-reference) for details.\n\n## Related\n\n- [Connection & Lifecycle](connection/)\n- [Volume](volume/)\n- [Frequency Bands](bands/)\n- [Beat Detection](beat/)\n- [Spectral Analysis](spectral/)\n- [Frequency Data](frequency-data/)\n- [Waveform](waveform/)\n- [P5 Audio](/p5/audio)\n- [Shader Audio Uniforms](/shader/audio)"
2742
2742
  }
2743
2743
  ]
2744
2744
  },
2745
2745
  "native-audio-connection": {
2746
2746
  "id": "native-audio-connection",
2747
- "title": "native-audio-connection",
2747
+ "title": "Connection & Lifecycle",
2748
2748
  "description": "Audio connection state, guard patterns, and default values when no audio stream is active.",
2749
2749
  "headings": [
2750
2750
  {
@@ -2776,12 +2776,12 @@ export const docsApi = {
2776
2776
  "content": [
2777
2777
  {
2778
2778
  "type": "text",
2779
- "markdown": "# Connection & Lifecycle\r\n\r\nThe `viji.audio.isConnected` property indicates whether the host application has provided an active audio stream. All other audio properties depend on this: when disconnected, they hold default values.\r\n\r\n## Property Reference\r\n\r\n| Property | Type | Description |\r\n|----------|------|-------------|\r\n| `viji.audio.isConnected` | `boolean` | `true` when an audio stream is active and analysis results are flowing |\r\n\r\n## Guard Pattern\r\n\r\nAlways check `isConnected` before using audio data. This prevents your scene from reacting to default values as if they were real audio input.\r\n\r\n```javascript\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n const w = viji.width;\r\n const h = viji.height;\r\n\r\n ctx.fillStyle = '#111';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n if (!viji.audio.isConnected) {\r\n ctx.fillStyle = '#444';\r\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\r\n ctx.textAlign = 'center';\r\n ctx.fillText('No audio connected', w / 2, h / 2);\r\n return;\r\n }\r\n\r\n // Safe to use audio data here\r\n const vol = viji.audio.volume.current;\r\n ctx.fillStyle = `rgba(100, 200, 255, ${vol})`;\r\n ctx.fillRect(0, 0, w * vol, h);\r\n}\r\n```\r\n\r\n## Connection Lifecycle\r\n\r\n1. **Disconnected (default)**: `isConnected` is `false`. All audio values are at their defaults.\r\n2. **Connected**: The host provides a `MediaStream`. `isConnected` becomes `true` and audio analysis values begin updating every frame.\r\n3. **Disconnected again**: The stream is removed. `isConnected` returns to `false` and all values reset to defaults.\r\n\r\n## Default Values\r\n\r\nWhen `isConnected` is `false`, all audio properties hold these values:\r\n\r\n| Property | Default |\r\n|----------|---------|\r\n| `volume.current`, `volume.peak`, `volume.smoothed` | `0` |\r\n| All `bands.*` (instant and smoothed) | `0` |\r\n| `beat.kick`, `.snare`, `.hat`, `.any` (and smoothed) | `0` |\r\n| `beat.triggers.kick`, `.snare`, `.hat`, `.any` | `false` |\r\n| `beat.events` | `[]` (empty array) |\r\n| `beat.bpm` | `0` |\r\n| `beat.confidence` | `0` |\r\n| `beat.isLocked` | `false` |\r\n| `spectral.brightness`, `spectral.flatness` | `0` |\r\n| `getFrequencyData()` | Empty `Uint8Array` (length 0) |\r\n| `getWaveform()` | Empty `Float32Array` (length 0) |\r\n\r\n> [!NOTE]\r\n> `beat.bpm` is `0` when no audio is connected (the \"no signal\" sentinel). Once audio connects, it tracks the detected tempo clamped to 60-240, with `120` as a fallback before the tracker locks on. If your scene needs a fallback default for BPM-based calculations before audio connects, write `viji.audio.beat.bpm || 120` explicitly."
2779
+ "markdown": "# Connection & Lifecycle\n\nThe `viji.audio.isConnected` property indicates whether the host application has provided an active audio stream. All other audio properties depend on this: when disconnected, they hold default values.\n\n## Property Reference\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.audio.isConnected` | `boolean` | `true` when an audio stream is active and analysis results are flowing |\n\n## Guard Pattern\n\nAlways check `isConnected` before using audio data. This prevents your scene from reacting to default values as if they were real audio input.\n\n```javascript\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (!viji.audio.isConnected) {\n ctx.fillStyle = '#444';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('No audio connected', w / 2, h / 2);\n return;\n }\n\n // Safe to use audio data here\n const vol = viji.audio.volume.current;\n ctx.fillStyle = `rgba(100, 200, 255, ${vol})`;\n ctx.fillRect(0, 0, w * vol, h);\n}\n```\n\n## Connection Lifecycle\n\n1. **Disconnected (default)**: `isConnected` is `false`. All audio values are at their defaults.\n2. **Connected**: The host provides a `MediaStream`. `isConnected` becomes `true` and audio analysis values begin updating every frame.\n3. **Disconnected again**: The stream is removed. `isConnected` returns to `false` and all values reset to defaults.\n\n## Default Values\n\nWhen `isConnected` is `false`, all audio properties hold these values:\n\n| Property | Default |\n|----------|---------|\n| `volume.current`, `volume.peak`, `volume.smoothed` | `0` |\n| All `bands.*` (instant and smoothed) | `0` |\n| `beat.kick`, `.snare`, `.hat`, `.any` (and smoothed) | `0` |\n| `beat.triggers.kick`, `.snare`, `.hat`, `.any` | `false` |\n| `beat.events` | `[]` (empty array) |\n| `beat.bpm` | `0` |\n| `beat.confidence` | `0` |\n| `beat.isLocked` | `false` |\n| `spectral.brightness`, `spectral.flatness` | `0` |\n| `getFrequencyData()` | Empty `Uint8Array` (length 0) |\n| `getWaveform()` | Empty `Float32Array` (length 0) |\n\n> [!NOTE]\n> `beat.bpm` is `0` when no audio is connected (the \"no signal\" sentinel). Once audio connects, it tracks the detected tempo clamped to 60-240, with `120` as a fallback before the tracker locks on. If your scene needs a fallback default for BPM-based calculations before audio connects, write `viji.audio.beat.bpm || 120` explicitly."
2780
2780
  },
2781
2781
  {
2782
2782
  "type": "live-example",
2783
2783
  "title": "Connection State",
2784
- "sceneCode": "function render(viji) {\r\n const ctx = viji.useContext('2d');\r\n const w = viji.width;\r\n const h = viji.height;\r\n\r\n ctx.fillStyle = '#111';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n const fontSize = Math.min(w, h) * 0.035;\r\n ctx.font = `${fontSize}px sans-serif`;\r\n ctx.textAlign = 'center';\r\n\r\n if (!viji.audio.isConnected) {\r\n const pulse = 0.4 + Math.sin(viji.time * 2) * 0.15;\r\n ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;\r\n ctx.fillText('Waiting for audio stream...', w / 2, h / 2 - fontSize);\r\n ctx.fillStyle = '#444';\r\n ctx.fillText('Connect a microphone or audio source', w / 2, h / 2 + fontSize);\r\n return;\r\n }\r\n\r\n const vol = viji.audio.volume.smoothed;\r\n const barW = w * 0.6;\r\n const barH = Math.min(w, h) * 0.06;\r\n const barX = (w - barW) / 2;\r\n const barY = h / 2 - barH / 2;\r\n\r\n ctx.fillStyle = '#222';\r\n ctx.fillRect(barX, barY, barW, barH);\r\n ctx.fillStyle = '#4CAF50';\r\n ctx.fillRect(barX, barY, barW * vol, barH);\r\n\r\n ctx.fillStyle = '#aaa';\r\n ctx.fillText('Audio connected: volume: ' + vol.toFixed(2), w / 2, barY - fontSize);\r\n}\r\n",
2784
+ "sceneCode": "function render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n const fontSize = Math.min(w, h) * 0.035;\n ctx.font = `${fontSize}px sans-serif`;\n ctx.textAlign = 'center';\n\n if (!viji.audio.isConnected) {\n const pulse = 0.4 + Math.sin(viji.time * 2) * 0.15;\n ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;\n ctx.fillText('Waiting for audio stream...', w / 2, h / 2 - fontSize);\n ctx.fillStyle = '#444';\n ctx.fillText('Connect a microphone or audio source', w / 2, h / 2 + fontSize);\n return;\n }\n\n const vol = viji.audio.volume.smoothed;\n const barW = w * 0.6;\n const barH = Math.min(w, h) * 0.06;\n const barX = (w - barW) / 2;\n const barY = h / 2 - barH / 2;\n\n ctx.fillStyle = '#222';\n ctx.fillRect(barX, barY, barW, barH);\n ctx.fillStyle = '#4CAF50';\n ctx.fillRect(barX, barY, barW * vol, barH);\n\n ctx.fillStyle = '#aaa';\n ctx.fillText('Audio connected: volume: ' + vol.toFixed(2), w / 2, barY - fontSize);\n}\n",
2785
2785
  "sceneFile": "connection-demo.scene.js",
2786
2786
  "capabilities": {
2787
2787
  "audio": true
@@ -2789,7 +2789,7 @@ export const docsApi = {
2789
2789
  },
2790
2790
  {
2791
2791
  "type": "text",
2792
- "markdown": "## Related\r\n\r\n- [Audio Overview](../)\r\n- [Volume](../volume/)\r\n- [Beat Detection](../beat/)\r\n- [P5 Connection & Lifecycle](/p5/audio/connection)"
2792
+ "markdown": "## Related\n\n- [Audio Overview](../)\n- [Volume](../volume/)\n- [Beat Detection](../beat/)\n- [P5 Connection & Lifecycle](/p5/audio/connection)"
2793
2793
  }
2794
2794
  ]
2795
2795
  },
@@ -4283,7 +4283,7 @@ export const docsApi = {
4283
4283
  },
4284
4284
  "native-ext-audio": {
4285
4285
  "id": "native-ext-audio",
4286
- "title": "native-ext-audio",
4286
+ "title": "Device Audio",
4287
4287
  "description": "Lightweight audio analysis from externally connected devices: volume, bands, spectral features, and raw FFT/waveform via AudioStreamAPI.",
4288
4288
  "headings": [
4289
4289
  {
@@ -4300,7 +4300,7 @@ export const docsApi = {
4300
4300
  "content": [
4301
4301
  {
4302
4302
  "type": "text",
4303
- "markdown": "# Device Audio\r\n\r\nEach entry in `viji.devices` may expose **`device.audio`**: an [`AudioStreamAPI`](../../audio/) (or `null` when the host has not attached an audio source for that device).\r\n\r\n## Behavior\r\n\r\n- Check **`device.audio?.isConnected`** before reading values.\r\n- **Lightweight subset only:** `isConnected`, `volume` (`current` / `peak` / `smoothed`), `bands` (`low` / `lowMid` / `mid` / `highMid` / `high` plus each `*Smoothed` sibling), `spectral` (`brightness` / `flatness`), `getFrequencyData()`, `getWaveform()`.\r\n- **Not available:** beat energy, triggers, BPM, or beat events (those exist only on the main [`viji.audio`](../../audio/) stream).\r\n\r\n## Related\r\n\r\n- [External Devices: Overview](../)\r\n- [Native Audio](../../audio/)\r\n- [Shader: Audio stream uniforms](/shader/api-reference#audio-streams)"
4303
+ "markdown": "# Device Audio\n\nEach entry in `viji.devices` may expose **`device.audio`**: an [`AudioStreamAPI`](../../audio/) (or `null` when the host has not attached an audio source for that device).\n\n## Behavior\n\n- Check **`device.audio?.isConnected`** before reading values.\n- **Lightweight subset only:** `isConnected`, `volume` (`current` / `peak` / `smoothed`), `bands` (`low` / `lowMid` / `mid` / `highMid` / `high` plus each `*Smoothed` sibling), `spectral` (`brightness` / `flatness`), `getFrequencyData()`, `getWaveform()`.\n- **Not available:** beat energy, triggers, BPM, or beat events (those exist only on the main [`viji.audio`](../../audio/) stream).\n\n## Related\n\n- [External Devices: Overview](../)\n- [Native Audio](../../audio/)\n- [Shader: Audio stream uniforms](/shader/api-reference#audio-streams)"
4304
4304
  }
4305
4305
  ]
4306
4306
  },
@@ -7071,7 +7071,7 @@ export const docsApi = {
7071
7071
  },
7072
7072
  "p5-ext-audio": {
7073
7073
  "id": "p5-ext-audio",
7074
- "title": "p5-ext-audio",
7074
+ "title": "Device Audio",
7075
7075
  "description": "Lightweight audio analysis from externally connected devices in P5 scenes: volume, bands, spectral features, and raw FFT/waveform via AudioStreamAPI.",
7076
7076
  "headings": [
7077
7077
  {
@@ -7088,7 +7088,7 @@ export const docsApi = {
7088
7088
  "content": [
7089
7089
  {
7090
7090
  "type": "text",
7091
- "markdown": "# Device Audio\r\n\r\nEach entry in `viji.devices` may expose **`device.audio`**: an [`AudioStreamAPI`](../../audio/) (or `null` when the host has not attached an audio source for that device).\r\n\r\n## Behavior\r\n\r\n- Check **`device.audio?.isConnected`** before reading values.\r\n- **Lightweight subset only:** `isConnected`, `volume` (`current` / `peak` / `smoothed`), `bands` (`low` / `lowMid` / `mid` / `highMid` / `high` plus each `*Smoothed` sibling), `spectral` (`brightness` / `flatness`), `getFrequencyData()`, `getWaveform()`.\r\n- **Not available:** beat energy, triggers, BPM, or beat events (those exist only on the main [`viji.audio`](../../audio/) stream).\r\n\r\n## Related\r\n\r\n- [External Devices: Overview](../)\r\n- [P5 Audio](../../audio/)\r\n- [Shader: Audio stream uniforms](/shader/api-reference#audio-streams)"
7091
+ "markdown": "# Device Audio\n\nEach entry in `viji.devices` may expose **`device.audio`**: an [`AudioStreamAPI`](../../audio/) (or `null` when the host has not attached an audio source for that device).\n\n## Behavior\n\n- Check **`device.audio?.isConnected`** before reading values.\n- **Lightweight subset only:** `isConnected`, `volume` (`current` / `peak` / `smoothed`), `bands` (`low` / `lowMid` / `mid` / `highMid` / `high` plus each `*Smoothed` sibling), `spectral` (`brightness` / `flatness`), `getFrequencyData()`, `getWaveform()`.\n- **Not available:** beat energy, triggers, BPM, or beat events (those exist only on the main [`viji.audio`](../../audio/) stream).\n\n## Related\n\n- [External Devices: Overview](../)\n- [P5 Audio](../../audio/)\n- [Shader: Audio stream uniforms](/shader/api-reference#audio-streams)"
7092
7092
  }
7093
7093
  ]
7094
7094
  },
@@ -7161,12 +7161,12 @@ export const docsApi = {
7161
7161
  {
7162
7162
  "type": "live-example",
7163
7163
  "title": "Shader: Wave Pattern",
7164
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-slider:scale label:\"Scale\" default:8.0 min:1.0 max:20.0 step:0.5\r\n// @viji-color:tint label:\"Tint\" default:#00ffcc\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n float pattern = sin(uv.x * scale + phase)\r\n * cos(uv.y * scale - phase * 0.7)\r\n * 0.5 + 0.5;\r\n\r\n vec3 color = tint * pattern;\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
7164
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-slider:scale label:\"Scale\" default:8.0 min:1.0 max:20.0 step:0.5\n// @viji-color:tint label:\"Tint\" default:#00ffcc\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float pattern = sin(uv.x * scale + phase)\n * cos(uv.y * scale - phase * 0.7)\n * 0.5 + 0.5;\n\n vec3 color = tint * pattern;\n gl_FragColor = vec4(color, 1.0);\n}\n",
7165
7165
  "sceneFile": "quickstart-shader.scene.glsl"
7166
7166
  },
7167
7167
  {
7168
7168
  "type": "text",
7169
- "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"
7169
+ "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:\"Wave\" options:[\"Wave\",\"Spiral\",\"Grid\"]` | `// @viji-select:mode label:\"Mode\" default:\"Wave\" 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"
7170
7170
  }
7171
7171
  ]
7172
7172
  },
@@ -7319,7 +7319,7 @@ export const docsApi = {
7319
7319
  "content": [
7320
7320
  {
7321
7321
  "type": "text",
7322
- "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"
7322
+ "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; the uniform is the 0-based index of the selected option | [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"
7323
7323
  }
7324
7324
  ]
7325
7325
  },
@@ -7397,7 +7397,7 @@ export const docsApi = {
7397
7397
  {
7398
7398
  "type": "live-example",
7399
7399
  "title": "Minimal Shader",
7400
- "sceneCode": "// @renderer shader\r\n// @viji-color:color1 label:\"Color A\" default:#ff4488\r\n// @viji-color:color2 label:\"Color B\" default:#4488ff\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec3 color = mix(color1, color2, uv.x);\r\n float wave = sin(uv.y * 20.0 + u_time * 3.0) * 0.05;\r\n color += wave;\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
7400
+ "sceneCode": "// @renderer shader\n// @viji-color:color1 label:\"Color A\" default:#ff4488\n// @viji-color:color2 label:\"Color B\" default:#4488ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec3 color = mix(color1, color2, uv.x);\n float wave = sin(uv.y * 20.0 + u_time * 3.0) * 0.05;\n color += wave;\n gl_FragColor = vec4(color, 1.0);\n}\n",
7401
7401
  "sceneFile": "basics-minimal.scene.glsl"
7402
7402
  },
7403
7403
  {
@@ -7407,7 +7407,7 @@ export const docsApi = {
7407
7407
  {
7408
7408
  "type": "live-example",
7409
7409
  "title": "ES 3.00: Output Variable",
7410
- "sceneCode": "#version 300 es\r\n// @renderer shader\r\n// @viji-slider:zoom label:\"Zoom\" default:4.0 min:1.0 max:12.0 step:0.5\r\n// @viji-color:gridColor label:\"Grid Color\" default:#44ffaa\r\n\r\nout vec4 fragColor;\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 grid = fract(uv * zoom);\r\n float line = step(0.95, max(grid.x, grid.y));\r\n vec3 bg = vec3(0.04, 0.04, 0.1);\r\n vec3 color = mix(bg, gridColor, line);\r\n fragColor = vec4(color, 1.0);\r\n}\r\n",
7410
+ "sceneCode": "#version 300 es\n// @renderer shader\n// @viji-slider:zoom label:\"Zoom\" default:4.0 min:1.0 max:12.0 step:0.5\n// @viji-color:gridColor label:\"Grid Color\" default:#44ffaa\n\nout vec4 fragColor;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 grid = fract(uv * zoom);\n float line = step(0.95, max(grid.x, grid.y));\n vec3 bg = vec3(0.04, 0.04, 0.1);\n vec3 color = mix(bg, gridColor, line);\n fragColor = vec4(color, 1.0);\n}\n",
7411
7411
  "sceneFile": "basics-es300.scene.glsl"
7412
7412
  },
7413
7413
  {
@@ -7490,7 +7490,7 @@ export const docsApi = {
7490
7490
  {
7491
7491
  "type": "live-example",
7492
7492
  "title": "Normalized UV: Color Rings",
7493
- "sceneCode": "// @renderer shader\r\n// @viji-slider:rings label:\"Ring Count\" default:8.0 min:2.0 max:20.0 step:1.0\r\n// @viji-color:color1 label:\"Inner Color\" default:#ff3366\r\n// @viji-color:color2 label:\"Outer Color\" default:#3366ff\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 centered = uv - 0.5;\r\n float dist = length(centered) * 2.0;\r\n float ring = sin(dist * rings * 3.14159 - u_time * 2.0) * 0.5 + 0.5;\r\n vec3 color = mix(color1, color2, ring);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
7493
+ "sceneCode": "// @renderer shader\n// @viji-slider:rings label:\"Ring Count\" default:8.0 min:2.0 max:20.0 step:1.0\n// @viji-color:color1 label:\"Inner Color\" default:#ff3366\n// @viji-color:color2 label:\"Outer Color\" default:#3366ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 centered = uv - 0.5;\n float dist = length(centered) * 2.0;\n float ring = sin(dist * rings * 3.14159 - u_time * 2.0) * 0.5 + 0.5;\n vec3 color = mix(color1, color2, ring);\n gl_FragColor = vec4(color, 1.0);\n}\n",
7494
7494
  "sceneFile": "resolution-normalize.scene.glsl"
7495
7495
  },
7496
7496
  {
@@ -7500,7 +7500,7 @@ export const docsApi = {
7500
7500
  {
7501
7501
  "type": "live-example",
7502
7502
  "title": "Aspect-Correct: Circle & Grid",
7503
- "sceneCode": "// @renderer shader\r\n// @viji-slider:gridScale label:\"Grid Scale\" default:6.0 min:2.0 max:16.0 step:1.0\r\n// @viji-slider:radius label:\"Circle Radius\" default:0.3 min:0.05 max:0.45 step:0.01\r\n// @viji-color:circleColor label:\"Circle Color\" default:#44ffaa\r\n// @viji-color:gridColor label:\"Grid Color\" default:#334466\r\n\r\nvoid main() {\r\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\r\n\r\n float d = length(uv);\r\n float circle = smoothstep(radius, radius - 2.0 / u_resolution.y, d);\r\n\r\n vec2 grid = abs(fract(uv * gridScale) - 0.5);\r\n float line = 1.0 - smoothstep(0.0, 2.0 / u_resolution.y * gridScale, min(grid.x, grid.y));\r\n\r\n vec3 bg = vec3(0.04, 0.04, 0.1);\r\n vec3 color = bg;\r\n color = mix(color, gridColor * 0.5, line * 0.4);\r\n color = mix(color, circleColor, circle);\r\n\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
7503
+ "sceneCode": "// @renderer shader\n// @viji-slider:gridScale label:\"Grid Scale\" default:6.0 min:2.0 max:16.0 step:1.0\n// @viji-slider:radius label:\"Circle Radius\" default:0.3 min:0.05 max:0.45 step:0.01\n// @viji-color:circleColor label:\"Circle Color\" default:#44ffaa\n// @viji-color:gridColor label:\"Grid Color\" default:#334466\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\n\n float d = length(uv);\n float circle = smoothstep(radius, radius - 2.0 / u_resolution.y, d);\n\n vec2 grid = abs(fract(uv * gridScale) - 0.5);\n float line = 1.0 - smoothstep(0.0, 2.0 / u_resolution.y * gridScale, min(grid.x, grid.y));\n\n vec3 bg = vec3(0.04, 0.04, 0.1);\n vec3 color = bg;\n color = mix(color, gridColor * 0.5, line * 0.4);\n color = mix(color, circleColor, circle);\n\n gl_FragColor = vec4(color, 1.0);\n}\n",
7504
7504
  "sceneFile": "resolution-aspect.scene.glsl"
7505
7505
  },
7506
7506
  {
@@ -7593,7 +7593,7 @@ export const docsApi = {
7593
7593
  {
7594
7594
  "type": "live-example",
7595
7595
  "title": "u_time: Plasma Effect",
7596
- "sceneCode": "// @renderer shader\r\n// @viji-slider:scale label:\"Scale\" default:4.0 min:1.0 max:10.0 step:0.5\r\n// @viji-slider:speed label:\"Speed\" default:1.5 min:0.2 max:5.0\r\n// @viji-color:colorA label:\"Color A\" default:#ff2266\r\n// @viji-color:colorB label:\"Color B\" default:#2266ff\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float t = u_time * speed;\r\n\r\n float v1 = sin(uv.x * scale * 3.14159 + t);\r\n float v2 = cos(uv.y * scale * 3.14159 - t * 0.7);\r\n float v3 = sin((uv.x + uv.y) * scale * 2.0 + t * 1.3);\r\n float plasma = (v1 + v2 + v3) / 3.0 * 0.5 + 0.5;\r\n\r\n vec3 color = mix(colorA, colorB, plasma);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
7596
+ "sceneCode": "// @renderer shader\n// @viji-slider:scale label:\"Scale\" default:4.0 min:1.0 max:10.0 step:0.5\n// @viji-slider:speed label:\"Speed\" default:1.5 min:0.2 max:5.0\n// @viji-color:colorA label:\"Color A\" default:#ff2266\n// @viji-color:colorB label:\"Color B\" default:#2266ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float t = u_time * speed;\n\n float v1 = sin(uv.x * scale * 3.14159 + t);\n float v2 = cos(uv.y * scale * 3.14159 - t * 0.7);\n float v3 = sin((uv.x + uv.y) * scale * 2.0 + t * 1.3);\n float plasma = (v1 + v2 + v3) / 3.0 * 0.5 + 0.5;\n\n vec3 color = mix(colorA, colorB, plasma);\n gl_FragColor = vec4(color, 1.0);\n}\n",
7597
7597
  "sceneFile": "timing-patterns.scene.glsl"
7598
7598
  },
7599
7599
  {
@@ -7603,7 +7603,7 @@ export const docsApi = {
7603
7603
  {
7604
7604
  "type": "live-example",
7605
7605
  "title": "Accumulator: Smooth Spiral",
7606
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-accumulator:phase rate:speed\r\n// @viji-color:color1 label:\"Color A\" default:#ff6633\r\n// @viji-color:color2 label:\"Color B\" default:#3366ff\r\n\r\nvoid main() {\r\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\r\n float angle = atan(uv.y, uv.x);\r\n float dist = length(uv);\r\n\r\n float spiral = sin(angle * 4.0 + dist * 20.0 - phase * 6.28);\r\n float pattern = smoothstep(-0.2, 0.2, spiral);\r\n\r\n vec3 color = mix(color1, color2, pattern);\r\n float vignette = 1.0 - dist * 0.8;\r\n color *= vignette;\r\n\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
7606
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-accumulator:phase rate:speed\n// @viji-color:color1 label:\"Color A\" default:#ff6633\n// @viji-color:color2 label:\"Color B\" default:#3366ff\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\n float angle = atan(uv.y, uv.x);\n float dist = length(uv);\n\n float spiral = sin(angle * 4.0 + dist * 20.0 - phase * 6.28);\n float pattern = smoothstep(-0.2, 0.2, spiral);\n\n vec3 color = mix(color1, color2, pattern);\n float vignette = 1.0 - dist * 0.8;\n color *= vignette;\n\n gl_FragColor = vec4(color, 1.0);\n}\n",
7607
7607
  "sceneFile": "timing-accumulator.scene.glsl"
7608
7608
  },
7609
7609
  {
@@ -7656,7 +7656,7 @@ export const docsApi = {
7656
7656
  "content": [
7657
7657
  {
7658
7658
  "type": "text",
7659
- "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"
7659
+ "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 the 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"
7660
7660
  }
7661
7661
  ]
7662
7662
  },
@@ -7709,7 +7709,7 @@ export const docsApi = {
7709
7709
  {
7710
7710
  "type": "live-example",
7711
7711
  "title": "Slider Controls",
7712
- "sceneCode": "// @renderer shader\r\n// @viji-slider:zoom label:\"Zoom\" default:1 min:0.1 max:5 step:0.1\r\n// @viji-slider:speed label:\"Speed\" default:3 min:0 max:10 step:0.5\r\n// @viji-color:ringColor label:\"Color\" default:#44ddff\r\n\r\nvoid main() {\r\n vec2 uv = (gl_FragCoord.xy / u_resolution - 0.5) * zoom;\r\n float aspect = u_resolution.x / u_resolution.y;\r\n uv.x *= aspect;\r\n\r\n float d = length(uv);\r\n float ring = sin(d * 20.0 - u_time * speed) * 0.5 + 0.5;\r\n vec3 col = ringColor * ring;\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
7712
+ "sceneCode": "// @renderer shader\n// @viji-slider:zoom label:\"Zoom\" default:1 min:0.1 max:5 step:0.1\n// @viji-slider:speed label:\"Speed\" default:3 min:0 max:10 step:0.5\n// @viji-color:ringColor label:\"Color\" default:#44ddff\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy / u_resolution - 0.5) * zoom;\n float aspect = u_resolution.x / u_resolution.y;\n uv.x *= aspect;\n\n float d = length(uv);\n float ring = sin(d * 20.0 - u_time * speed) * 0.5 + 0.5;\n vec3 col = ringColor * ring;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
7713
7713
  "sceneFile": "slider-shader.scene.glsl"
7714
7714
  },
7715
7715
  {
@@ -7767,7 +7767,7 @@ export const docsApi = {
7767
7767
  {
7768
7768
  "type": "live-example",
7769
7769
  "title": "Color Blending",
7770
- "sceneCode": "// @renderer shader\r\n// @viji-color:color1 label:\"Color 1 (hex)\" default:#ff4488 group:\"colors\"\r\n// @viji-color:color2 label:\"Color 2 (vec3)\" default:vec3(0.27, 0.53, 1.0) group:\"colors\"\r\n// @viji-color:accent label:\"Accent (rgb)\" default:rgb(68, 255, 136) group:\"colors\"\r\n// @viji-color:bgColor label:\"Background (hsb)\" default:hsb(240, 60, 12)\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float t = sin(uv.x * 6.2832 + u_time) * 0.5 + 0.5;\r\n vec3 gradient = mix(color1, color2, t);\r\n\r\n float d = distance(uv, vec2(0.5));\r\n vec3 col = mix(gradient, bgColor, smoothstep(0.2, 0.5, d));\r\n\r\n float ring = smoothstep(0.02, 0.0, abs(d - 0.32));\r\n col = mix(col, accent, ring);\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
7770
+ "sceneCode": "// @renderer shader\n// @viji-color:color1 label:\"Color 1 (hex)\" default:#ff4488 group:\"colors\"\n// @viji-color:color2 label:\"Color 2 (vec3)\" default:vec3(0.27, 0.53, 1.0) group:\"colors\"\n// @viji-color:accent label:\"Accent (rgb)\" default:rgb(68, 255, 136) group:\"colors\"\n// @viji-color:bgColor label:\"Background (hsb)\" default:hsb(240, 60, 12)\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float t = sin(uv.x * 6.2832 + u_time) * 0.5 + 0.5;\n vec3 gradient = mix(color1, color2, t);\n\n float d = distance(uv, vec2(0.5));\n vec3 col = mix(gradient, bgColor, smoothstep(0.2, 0.5, d));\n\n float ring = smoothstep(0.02, 0.0, abs(d - 0.32));\n col = mix(col, accent, ring);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
7771
7771
  "sceneFile": "color-shader.scene.glsl"
7772
7772
  },
7773
7773
  {
@@ -7873,7 +7873,7 @@ export const docsApi = {
7873
7873
  {
7874
7874
  "type": "live-example",
7875
7875
  "title": "Toggle Inversion",
7876
- "sceneCode": "// @renderer shader\r\n// @viji-toggle:invert label:\"Invert\" default:false\r\n// @viji-toggle:animate label:\"Animate\" default:true\r\n// @viji-color:baseColor label:\"Base Color\" default:#4488ff\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float t = animate ? u_time : 0.0;\r\n float pattern = sin(uv.x * 10.0 + t * 2.0) * sin(uv.y * 10.0 + t * 1.5);\r\n vec3 col = baseColor * (pattern * 0.5 + 0.5);\r\n if (invert) {\r\n col = 1.0 - col;\r\n }\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
7876
+ "sceneCode": "// @renderer shader\n// @viji-toggle:invert label:\"Invert\" default:false\n// @viji-toggle:animate label:\"Animate\" default:true\n// @viji-color:baseColor label:\"Base Color\" default:#4488ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float t = animate ? u_time : 0.0;\n float pattern = sin(uv.x * 10.0 + t * 2.0) * sin(uv.y * 10.0 + t * 1.5);\n vec3 col = baseColor * (pattern * 0.5 + 0.5);\n if (invert) {\n col = 1.0 - col;\n }\n gl_FragColor = vec4(col, 1.0);\n}\n",
7877
7877
  "sceneFile": "toggle-shader.scene.glsl"
7878
7878
  },
7879
7879
  {
@@ -7916,17 +7916,17 @@ export const docsApi = {
7916
7916
  "content": [
7917
7917
  {
7918
7918
  "type": "text",
7919
- "markdown": "# @viji-select\n\n```glsl\n// @viji-select:mode label:\"Mode\" options:[\"Wave\",\"Circles\",\"Grid\"] default:0\nuniform int mode;\n```\n\nDeclares a dropdown selection parameter. The host renders it as a dropdown menu. The selected option's **index** is injected as a `uniform int`.\n\n## Directive Syntax\n\n```\n// @viji-select:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | | Display name in the parameter UI |\n| `options` | Yes | | Array of string choices: `options:[\"A\",\"B\",\"C\"]` |\n| `default` | Yes | | Initially selected index (0-based integer) |\n| `description` | No | | Tooltip text (use quotes) |\n| `group` | No | `general` | Group name (use quotes) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe select value is injected as an `int`: the **zero-based index** of the selected option:\n\n| Selected Option | GLSL Value |\n|-----------------|------------|\n| First option | `0` |\n| Second option | `1` |\n| Third option | `2` |\n\nThis differs from Native and P5 renderers, where `.value` is the option string itself.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-select:pattern label:\"Pattern\" options:[\"Stripes\",\"Dots\",\"Checker\"] default:0\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float v = 0.0;\n\n if (pattern == 0) {\n v = step(0.5, fract(uv.x * 10.0 + u_time));\n } else if (pattern == 1) {\n v = 1.0 - step(0.3, length(fract(uv * 5.0) - 0.5));\n } else if (pattern == 2) {\n v = mod(floor(uv.x * 8.0) + floor(uv.y * 8.0), 2.0);\n }\n\n gl_FragColor = vec4(vec3(v), 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."
7919
+ "markdown": "# @viji-select\n\n```glsl\n// @viji-select:mode label:\"Mode\" options:[\"Wave\",\"Circles\",\"Grid\"] default:\"Wave\"\nuniform int mode;\n```\n\nDeclares a dropdown selection parameter. The host renders it as a dropdown menu. The directive and the dropdown work with the option itself (`\"Wave\"`). The GLSL uniform is that option's position in `options` (`0`).\n\n## Directive Syntax\n\n```\n// @viji-select:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | | Display name in the parameter UI |\n| `options` | Yes | | Array of choices, all strings or all numbers: `options:[\"A\",\"B\",\"C\"]` |\n| `default` | Yes | | Initially selected option, one of `options`: `default:\"A\"` |\n| `description` | No | | Tooltip text (use quotes) |\n| `group` | No | `general` | Group name (use quotes) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe uniform is an `int` holding the **zero-based index** of the selected option. With `options:[\"Wave\",\"Circles\",\"Grid\"]`:\n\n| Where | Selected \"Circles\" appears as |\n|-------|-------------------------------|\n| Directive `default:` | `\"Circles\"` |\n| Parameter panel and presets | `\"Circles\"` |\n| GLSL `uniform int` | `1` |\n\nReordering `options` changes the numbers the shader sees.\n\nNumeric options follow the same rule. With `options:[2,4,8] default:4` the uniform starts at `1`, not `4`. To use the number itself, look it up in GLSL:\n\n```glsl\n// @viji-select:steps label:\"Steps\" options:[2,4,8] default:4\n\nfloat stepCount = steps == 0 ? 2.0 : steps == 1 ? 4.0 : 8.0;\n```\n\nThis differs from Native and P5 renderers, where `.value` is the option value itself.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-select:pattern label:\"Pattern\" options:[\"Stripes\",\"Dots\",\"Checker\"] default:\"Stripes\"\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float v = 0.0;\n\n if (pattern == 0) {\n v = step(0.5, fract(uv.x * 10.0 + u_time));\n } else if (pattern == 1) {\n v = 1.0 - step(0.3, length(fract(uv * 5.0) - 0.5));\n } else if (pattern == 2) {\n v = mod(floor(uv.x * 8.0) + floor(uv.y * 8.0), 2.0);\n }\n\n gl_FragColor = vec4(vec3(v), 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."
7920
7920
  },
7921
7921
  {
7922
7922
  "type": "live-example",
7923
7923
  "title": "Pattern Selector",
7924
- "sceneCode": "// @renderer shader\r\n// @viji-select:pattern label:\"Pattern\" options:[\"Stripes\",\"Dots\",\"Checker\"] default:0\r\n// @viji-color:color1 label:\"Color\" default:#ff4488\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float v = 0.0;\r\n\r\n if (pattern == 0) {\r\n v = step(0.5, fract(uv.x * 10.0 + u_time));\r\n } else if (pattern == 1) {\r\n v = 1.0 - step(0.3, length(fract(uv * 5.0) - 0.5));\r\n } else {\r\n v = mod(floor(uv.x * 8.0) + floor(uv.y * 8.0), 2.0);\r\n }\r\n\r\n vec3 col = mix(vec3(0.06), color1, v);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
7924
+ "sceneCode": "// @renderer shader\n// @viji-select:pattern label:\"Pattern\" options:[\"Stripes\",\"Dots\",\"Checker\"] default:\"Stripes\"\n// @viji-color:color1 label:\"Color\" default:#ff4488\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float v = 0.0;\n\n if (pattern == 0) {\n v = step(0.5, fract(uv.x * 10.0 + u_time));\n } else if (pattern == 1) {\n v = 1.0 - step(0.3, length(fract(uv * 5.0) - 0.5));\n } else {\n v = mod(floor(uv.x * 8.0) + floor(uv.y * 8.0), 2.0);\n }\n\n vec3 col = mix(vec3(0.06), color1, v);\n gl_FragColor = vec4(col, 1.0);\n}\n",
7925
7925
  "sceneFile": "select-shader.scene.glsl"
7926
7926
  },
7927
7927
  {
7928
7928
  "type": "text",
7929
- "markdown": "## Rules\n\n- Options must be a JSON-style array with quoted strings: `options:[\"A\",\"B\",\"C\"]`\n- The `default` value is a 0-based index (integer), not a string\n- String values use quotes: `label:\"Pattern\"`\n- The `label`, `options`, and `default` keys are all required\n\n## Related\n\n- [Shader Basics](/shader/basics): shader file structure and directives\n- [Toggle](../toggle/): two-state `uniform bool`\n- [Number](../number/): continuous `uniform float`\n- [Color](../color/): color picker `uniform vec3`\n- [Native Select](/native/parameters/select): equivalent for the Native renderer\n- [P5 Select](/p5/parameters/select): equivalent for the P5 renderer"
7929
+ "markdown": "## Rules\n\n- Options must be a JSON-style array, all quoted strings or all numbers: `options:[\"A\",\"B\",\"C\"]` or `options:[2,4,8]`\n- The `default` value is one of the options, written the same way: `default:\"A\"`, not its index. A default outside the options is a parse error\n- The shader receives the index of the selected option, never the option itself\n- String values use quotes: `label:\"Pattern\"`\n- The `label`, `options`, and `default` keys are all required\n\n## Related\n\n- [Shader Basics](/shader/basics): shader file structure and directives\n- [Toggle](../toggle/): two-state `uniform bool`\n- [Number](../number/): continuous `uniform float`\n- [Color](../color/): color picker `uniform vec3`\n- [Native Select](/native/parameters/select): equivalent for the Native renderer\n- [P5 Select](/p5/parameters/select): equivalent for the P5 renderer"
7930
7930
  }
7931
7931
  ]
7932
7932
  },
@@ -7979,7 +7979,7 @@ export const docsApi = {
7979
7979
  {
7980
7980
  "type": "live-example",
7981
7981
  "title": "Ring Count",
7982
- "sceneCode": "// @renderer shader\r\n// @viji-number:rings label:\"Rings\" default:5 min:1 max:15 step:1\r\n// @viji-number:speed label:\"Speed\" default:3 min:0 max:10 step:0.5\r\n// @viji-color:ringColor label:\"Color\" default:#44ddff\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n float aspect = u_resolution.x / u_resolution.y;\r\n center.x *= aspect;\r\n\r\n float d = length(center);\r\n float wave = sin(d * rings * 6.2832 - u_time * speed) * 0.5 + 0.5;\r\n vec3 col = ringColor * wave;\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
7982
+ "sceneCode": "// @renderer shader\n// @viji-number:rings label:\"Rings\" default:5 min:1 max:15 step:1\n// @viji-number:speed label:\"Speed\" default:3 min:0 max:10 step:0.5\n// @viji-color:ringColor label:\"Color\" default:#44ddff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 center = uv - 0.5;\n float aspect = u_resolution.x / u_resolution.y;\n center.x *= aspect;\n\n float d = length(center);\n float wave = sin(d * rings * 6.2832 - u_time * speed) * 0.5 + 0.5;\n vec3 col = ringColor * wave;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
7983
7983
  "sceneFile": "number-shader.scene.glsl"
7984
7984
  },
7985
7985
  {
@@ -8032,7 +8032,7 @@ export const docsApi = {
8032
8032
  {
8033
8033
  "type": "live-example",
8034
8034
  "title": "Image Texture",
8035
- "sceneCode": "// @renderer shader\r\n// @viji-image:tex label:\"Texture\"\r\n// @viji-slider:distort label:\"Distortion\" default:0.02 min:0 max:0.1 step:0.005\r\n// @viji-color:tint label:\"Tint\" default:#ffffff\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 d = vec2(\r\n sin(uv.y * 10.0 + u_time * 2.0) * distort,\r\n cos(uv.x * 10.0 + u_time * 2.0) * distort\r\n );\r\n vec4 img = texture2D(tex, uv + d);\r\n\r\n float hasImage = step(0.001, img.a);\r\n vec3 fallback = vec3(uv, sin(u_time) * 0.5 + 0.5);\r\n vec3 col = mix(fallback, img.rgb * tint, hasImage);\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
8035
+ "sceneCode": "// @renderer shader\n// @viji-image:tex label:\"Texture\"\n// @viji-slider:distort label:\"Distortion\" default:0.02 min:0 max:0.1 step:0.005\n// @viji-color:tint label:\"Tint\" default:#ffffff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 d = vec2(\n sin(uv.y * 10.0 + u_time * 2.0) * distort,\n cos(uv.x * 10.0 + u_time * 2.0) * distort\n );\n vec4 img = texture2D(tex, uv + d);\n\n float hasImage = step(0.001, img.a);\n vec3 fallback = vec3(uv, sin(u_time) * 0.5 + 0.5);\n vec3 col = mix(fallback, img.rgb * tint, hasImage);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
8036
8036
  "sceneFile": "image-shader.scene.glsl"
8037
8037
  },
8038
8038
  {
@@ -8143,7 +8143,7 @@ export const docsApi = {
8143
8143
  {
8144
8144
  "type": "live-example",
8145
8145
  "title": "Basic Coordinate",
8146
- "sceneCode": "// @renderer shader\r\n// @viji-coordinate:origin label:\"Effect Origin\" default:[0,0]\r\n// @viji-color:tint label:\"Tint\" default:#ff6b6b\r\n// @viji-slider:radius label:\"Radius\" default:0.3 min:0.05 max:0.8 step:0.01\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 centered = uv * 2.0 - 1.0;\r\n centered.x *= u_resolution.x / u_resolution.y;\r\n\r\n // Grid lines\r\n vec2 grid = abs(fract(centered * 4.0) - 0.5);\r\n float gridLine = 1.0 - smoothstep(0.0, 0.02, min(grid.x, grid.y));\r\n\r\n // Crosshair at center\r\n float cross = 1.0 - smoothstep(0.0, 0.005, min(abs(centered.x), abs(centered.y)));\r\n\r\n // Circle at origin coordinate\r\n float d = length(centered - origin);\r\n float circle = smoothstep(radius, radius - 0.01, d);\r\n float glow = 0.05 / (d + 0.01);\r\n\r\n // Background\r\n vec3 bg = vec3(0.1, 0.1, 0.18);\r\n bg += gridLine * 0.10;\r\n bg += cross * 0.18;\r\n\r\n // Composite\r\n vec3 col = bg + tint * (circle + glow * 0.3);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
8146
+ "sceneCode": "// @renderer shader\n// @viji-coordinate:origin label:\"Effect Origin\" default:[0,0]\n// @viji-color:tint label:\"Tint\" default:#ff6b6b\n// @viji-slider:radius label:\"Radius\" default:0.3 min:0.05 max:0.8 step:0.01\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 centered = uv * 2.0 - 1.0;\n centered.x *= u_resolution.x / u_resolution.y;\n\n // Grid lines\n vec2 grid = abs(fract(centered * 4.0) - 0.5);\n float gridLine = 1.0 - smoothstep(0.0, 0.02, min(grid.x, grid.y));\n\n // Crosshair at center\n float cross = 1.0 - smoothstep(0.0, 0.005, min(abs(centered.x), abs(centered.y)));\n\n // Circle at origin coordinate\n float d = length(centered - origin);\n float circle = smoothstep(radius, radius - 0.01, d);\n float glow = 0.05 / (d + 0.01);\n\n // Background\n vec3 bg = vec3(0.1, 0.1, 0.18);\n bg += gridLine * 0.10;\n bg += cross * 0.18;\n\n // Composite\n vec3 col = bg + tint * (circle + glow * 0.3);\n gl_FragColor = vec4(col, 1.0);\n}\n",
8147
8147
  "sceneFile": "coordinate-shader.scene.glsl"
8148
8148
  },
8149
8149
  {
@@ -8211,7 +8211,7 @@ export const docsApi = {
8211
8211
  {
8212
8212
  "type": "live-example",
8213
8213
  "title": "Speed-Driven Accumulator",
8214
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-color:tint label:\"Tint\" default:#00ffcc\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\r\n gl_FragColor = vec4(tint * wave, 1.0);\r\n}\r\n",
8214
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-color:tint label:\"Tint\" default:#00ffcc\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 gl_FragColor = vec4(tint * wave, 1.0);\n}\n",
8215
8215
  "sceneFile": "acc-basic.scene.glsl"
8216
8216
  },
8217
8217
  {
@@ -8221,7 +8221,7 @@ export const docsApi = {
8221
8221
  {
8222
8222
  "type": "live-example",
8223
8223
  "title": "Constant-Rate Accumulator",
8224
- "sceneCode": "// @renderer shader\r\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.1 max:2.0 step:0.1\r\n// @viji-accumulator:drift rate:0.5\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n center.x *= u_resolution.x / u_resolution.y;\r\n\r\n float d = length(center);\r\n float ring = smoothstep(0.02, 0.0, abs(d - mod(drift * 0.3, 0.5)));\r\n\r\n vec3 color = vec3(0.1, 0.05, 0.2) + ring * brightness * vec3(0.9, 0.4, 1.0);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
8224
+ "sceneCode": "// @renderer shader\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.1 max:2.0 step:0.1\n// @viji-accumulator:drift rate:0.5\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 center = uv - 0.5;\n center.x *= u_resolution.x / u_resolution.y;\n\n float d = length(center);\n float ring = smoothstep(0.02, 0.0, abs(d - mod(drift * 0.3, 0.5)));\n\n vec3 color = vec3(0.1, 0.05, 0.2) + ring * brightness * vec3(0.9, 0.4, 1.0);\n gl_FragColor = vec4(color, 1.0);\n}\n",
8225
8225
  "sceneFile": "acc-constant.scene.glsl"
8226
8226
  },
8227
8227
  {
@@ -8231,7 +8231,7 @@ export const docsApi = {
8231
8231
  {
8232
8232
  "type": "live-example",
8233
8233
  "title": "Multi-Axis Accumulator",
8234
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speedX label:\"Horizontal Speed\" default:1.0 min:0.0 max:5.0 step:0.1\r\n// @viji-slider:speedY label:\"Vertical Speed\" default:0.7 min:0.0 max:5.0 step:0.1\r\n// @viji-slider:radius label:\"Radius\" default:0.3 min:0.05 max:0.5 step:0.01\r\n// @viji-color:dotColor label:\"Color\" default:#ff6600\r\n// @viji-accumulator:phaseX rate:speedX\r\n// @viji-accumulator:phaseY rate:speedY\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n center.x *= u_resolution.x / u_resolution.y;\r\n\r\n vec2 pos = vec2(cos(phaseX), sin(phaseY)) * 0.3;\r\n float d = length(center - pos);\r\n float circle = smoothstep(radius + 0.01, radius - 0.01, d);\r\n\r\n vec3 bg = vec3(0.05);\r\n vec3 color = mix(bg, dotColor, circle);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
8234
+ "sceneCode": "// @renderer shader\n// @viji-slider:speedX label:\"Horizontal Speed\" default:1.0 min:0.0 max:5.0 step:0.1\n// @viji-slider:speedY label:\"Vertical Speed\" default:0.7 min:0.0 max:5.0 step:0.1\n// @viji-slider:radius label:\"Radius\" default:0.3 min:0.05 max:0.5 step:0.01\n// @viji-color:dotColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phaseX rate:speedX\n// @viji-accumulator:phaseY rate:speedY\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 center = uv - 0.5;\n center.x *= u_resolution.x / u_resolution.y;\n\n vec2 pos = vec2(cos(phaseX), sin(phaseY)) * 0.3;\n float d = length(center - pos);\n float circle = smoothstep(radius + 0.01, radius - 0.01, d);\n\n vec3 bg = vec3(0.05);\n vec3 color = mix(bg, dotColor, circle);\n gl_FragColor = vec4(color, 1.0);\n}\n",
8235
8235
  "sceneFile": "acc-multi.scene.glsl"
8236
8236
  },
8237
8237
  {
@@ -8284,12 +8284,12 @@ export const docsApi = {
8284
8284
  {
8285
8285
  "type": "live-example",
8286
8286
  "title": "Shader Grouped Parameters",
8287
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 group:animation\r\n// @viji-toggle:invert label:\"Invert\" default:false group:animation\r\n// @viji-slider:rings label:\"Ring Count\" default:10.0 min:2.0 max:30.0 step:1.0 group:shape\r\n// @viji-color:tint label:\"Tint\" default:#ff6600 group:shape\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\r\n float d = length(uv);\r\n\r\n float wave = sin(d * rings - phase * 4.0) * 0.5 + 0.5;\r\n vec3 col = tint * wave;\r\n\r\n if (invert) col = 1.0 - col;\r\n\r\n col *= smoothstep(1.5, 0.5, d);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
8287
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 group:animation\n// @viji-toggle:invert label:\"Invert\" default:false group:animation\n// @viji-slider:rings label:\"Ring Count\" default:10.0 min:2.0 max:30.0 step:1.0 group:shape\n// @viji-color:tint label:\"Tint\" default:#ff6600 group:shape\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\n float d = length(uv);\n\n float wave = sin(d * rings - phase * 4.0) * 0.5 + 0.5;\n vec3 col = tint * wave;\n\n if (invert) col = 1.0 - col;\n\n col *= smoothstep(1.5, 0.5, d);\n gl_FragColor = vec4(col, 1.0);\n}\n",
8288
8288
  "sceneFile": "grouping-demo.scene.glsl"
8289
8289
  },
8290
8290
  {
8291
8291
  "type": "text",
8292
- "markdown": "## Mixing Types\n\nGroups can contain any mix of parameter types: [`@viji-slider`](../slider/), [`@viji-color`](../color/), [`@viji-toggle`](../toggle/), [`@viji-select`](../select/), and more. There is no restriction on which types can share a group:\n\n```glsl\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0 group:visual\n// @viji-color:tint label:\"Tint\" default:#ffffff group:visual\n// @viji-toggle:showGrid label:\"Grid Overlay\" default:false group:visual\n// @viji-select:blendMode label:\"Blend\" default:0.0 options:normal|add|multiply group:visual\n```\n\n## Groups and Categories\n\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. Combine them when a grouped parameter depends on an external input:\n\n```glsl\n// @viji-slider:bassReact label:\"Bass React\" default:0.5 min:0.0 max:1.0 group:effects category:audio\n// @viji-slider:colorShift label:\"Color Shift\" default:0.2 min:0.0 max:1.0 group:effects category:general\n```\n\nBoth appear in the \"effects\" group, but `bassReact` only shows when audio is connected.\n\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\n\n## Related\n\n- [Parameters Overview](../): all shader parameter types\n- [Categories](../categories/): visibility based on active capabilities\n- [Native Grouping](/native/parameters/grouping): same concept in JavaScript\n- [P5 Grouping](/p5/parameters/grouping): same concept in P5 renderer\n- [Slider](../slider/): the most common parameter type"
8292
+ "markdown": "## Mixing Types\n\nGroups can contain any mix of parameter types: [`@viji-slider`](../slider/), [`@viji-color`](../color/), [`@viji-toggle`](../toggle/), [`@viji-select`](../select/), and more. There is no restriction on which types can share a group:\n\n```glsl\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0 group:visual\n// @viji-color:tint label:\"Tint\" default:#ffffff group:visual\n// @viji-toggle:showGrid label:\"Grid Overlay\" default:false group:visual\n// @viji-select:blendMode label:\"Blend\" default:\"normal\" options:[\"normal\",\"add\",\"multiply\"] group:visual\n```\n\n## Groups and Categories\n\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. Combine them when a grouped parameter depends on an external input:\n\n```glsl\n// @viji-slider:bassReact label:\"Bass React\" default:0.5 min:0.0 max:1.0 group:effects category:audio\n// @viji-slider:colorShift label:\"Color Shift\" default:0.2 min:0.0 max:1.0 group:effects category:general\n```\n\nBoth appear in the \"effects\" group, but `bassReact` only shows when audio is connected.\n\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\n\n## Related\n\n- [Parameters Overview](../): all shader parameter types\n- [Categories](../categories/): visibility based on active capabilities\n- [Native Grouping](/native/parameters/grouping): same concept in JavaScript\n- [P5 Grouping](/p5/parameters/grouping): same concept in P5 renderer\n- [Slider](../slider/): the most common parameter type"
8293
8293
  }
8294
8294
  ]
8295
8295
  },
@@ -9200,7 +9200,7 @@ export const docsApi = {
9200
9200
  {
9201
9201
  "type": "live-example",
9202
9202
  "title": "Pointer: Click Flash & Glow",
9203
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 pNorm = u_pointer / u_resolution;\r\n\r\n vec3 col = vec3(0.04, 0.04, 0.08);\r\n\r\n float dist = length(uv - pNorm);\r\n float glow = 0.015 / (dist + 0.01);\r\n vec3 glowColor = u_pointerDown\r\n ? vec3(0.3, 0.7, 1.0)\r\n : vec3(0.5, 0.5, 0.6);\r\n col += glow * glowColor;\r\n\r\n float flash = u_pointerWasPressed ? 1.0 : 0.0;\r\n col += vec3(0.2, 0.5, 0.8) * flash * smoothstep(0.3, 0.0, dist);\r\n\r\n float release = u_pointerWasReleased ? 1.0 : 0.0;\r\n float ring = smoothstep(0.002, 0.0, abs(dist - 0.15)) * release;\r\n col += vec3(0.8, 0.4, 0.2) * ring;\r\n\r\n if (!u_pointerInCanvas) {\r\n col *= 0.5;\r\n }\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9203
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 pNorm = u_pointer / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n float dist = length(uv - pNorm);\n float glow = 0.015 / (dist + 0.01);\n vec3 glowColor = u_pointerDown\n ? vec3(0.3, 0.7, 1.0)\n : vec3(0.5, 0.5, 0.6);\n col += glow * glowColor;\n\n float flash = u_pointerWasPressed ? 1.0 : 0.0;\n col += vec3(0.2, 0.5, 0.8) * flash * smoothstep(0.3, 0.0, dist);\n\n float release = u_pointerWasReleased ? 1.0 : 0.0;\n float ring = smoothstep(0.002, 0.0, abs(dist - 0.15)) * release;\n col += vec3(0.8, 0.4, 0.2) * ring;\n\n if (!u_pointerInCanvas) {\n col *= 0.5;\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9204
9204
  "sceneFile": "pointer-shader-demo.scene.glsl",
9205
9205
  "capabilities": {
9206
9206
  "interaction": true
@@ -9291,7 +9291,7 @@ export const docsApi = {
9291
9291
  {
9292
9292
  "type": "live-example",
9293
9293
  "title": "Mouse: Glow & Button States",
9294
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 mNorm = u_mouse / u_resolution;\r\n\r\n vec3 col = vec3(0.04, 0.04, 0.08);\r\n\r\n float dist = length(uv - mNorm);\r\n float glow = 0.012 / (dist + 0.01);\r\n\r\n vec3 btnColor = vec3(0.5);\r\n if (u_mouseLeft) btnColor = vec3(0.3, 0.8, 1.0);\r\n if (u_mouseRight) btnColor = vec3(1.0, 0.4, 0.3);\r\n if (u_mouseMiddle) btnColor = vec3(0.3, 1.0, 0.5);\r\n col += glow * btnColor;\r\n\r\n float flash = u_mouseWasPressed ? 1.0 : 0.0;\r\n col += vec3(0.3) * flash * smoothstep(0.2, 0.0, dist);\r\n\r\n float speed = length(u_mouseDelta) / max(u_resolution.x, 1.0);\r\n col += vec3(0.0, 0.2, 0.5) * speed * 5.0 * smoothstep(0.3, 0.0, dist);\r\n\r\n float wheelVis = clamp(u_mouseWheel * 0.001, -1.0, 1.0);\r\n col.r += abs(wheelVis) * 0.15 * smoothstep(0.5, 0.0, dist);\r\n\r\n if (!u_mouseInCanvas) {\r\n col *= 0.4;\r\n }\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9294
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 mNorm = u_mouse / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n float dist = length(uv - mNorm);\n float glow = 0.012 / (dist + 0.01);\n\n vec3 btnColor = vec3(0.5);\n if (u_mouseLeft) btnColor = vec3(0.3, 0.8, 1.0);\n if (u_mouseRight) btnColor = vec3(1.0, 0.4, 0.3);\n if (u_mouseMiddle) btnColor = vec3(0.3, 1.0, 0.5);\n col += glow * btnColor;\n\n float flash = u_mouseWasPressed ? 1.0 : 0.0;\n col += vec3(0.3) * flash * smoothstep(0.2, 0.0, dist);\n\n float speed = length(u_mouseDelta) / max(u_resolution.x, 1.0);\n col += vec3(0.0, 0.2, 0.5) * speed * 5.0 * smoothstep(0.3, 0.0, dist);\n\n float wheelVis = clamp(u_mouseWheel * 0.001, -1.0, 1.0);\n col.r += abs(wheelVis) * 0.15 * smoothstep(0.5, 0.0, dist);\n\n if (!u_mouseInCanvas) {\n col *= 0.4;\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9295
9295
  "sceneFile": "mouse-shader-demo.scene.glsl",
9296
9296
  "capabilities": {
9297
9297
  "interaction": true
@@ -9397,7 +9397,7 @@ export const docsApi = {
9397
9397
  {
9398
9398
  "type": "live-example",
9399
9399
  "title": "Keyboard: WASD Movement",
9400
- "sceneCode": "// @renderer shader\r\n\r\n// Smooth WASD/arrow movement via accumulators\r\n// @viji-accumulator:moveRight rate:u_keyD\r\n// @viji-accumulator:moveLeft rate:u_keyA\r\n// @viji-accumulator:moveUp rate:u_keyW\r\n// @viji-accumulator:moveDown rate:u_keyS\r\n// @viji-accumulator:arRight rate:u_keyRight\r\n// @viji-accumulator:arLeft rate:u_keyLeft\r\n// @viji-accumulator:arUp rate:u_keyUp\r\n// @viji-accumulator:arDown rate:u_keyDown\r\n\r\nvoid main() {\r\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\r\n\r\n float speed = u_keyShift ? 0.5 : 0.2;\r\n vec2 offset = vec2(\r\n (moveRight + arRight - moveLeft - arLeft) * speed,\r\n (moveUp + arUp - moveDown - arDown) * speed\r\n );\r\n\r\n vec2 p = uv - offset;\r\n\r\n float d = length(p) - 0.1;\r\n vec3 col = vec3(0.04, 0.04, 0.08);\r\n\r\n vec3 dotColor = u_keySpace ? vec3(1.0, 0.5, 0.2) : vec3(0.3, 0.7, 1.0);\r\n col += dotColor * smoothstep(0.01, 0.0, d);\r\n\r\n float glow = 0.005 / (abs(d) + 0.005);\r\n col += dotColor * glow * 0.3;\r\n\r\n float grid = step(0.98, fract(uv.x * 10.0)) + step(0.98, fract(uv.y * 10.0));\r\n col += grid * 0.08;\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9400
+ "sceneCode": "// @renderer shader\n\n// Smooth WASD/arrow movement via accumulators\n// @viji-accumulator:moveRight rate:u_keyD\n// @viji-accumulator:moveLeft rate:u_keyA\n// @viji-accumulator:moveUp rate:u_keyW\n// @viji-accumulator:moveDown rate:u_keyS\n// @viji-accumulator:arRight rate:u_keyRight\n// @viji-accumulator:arLeft rate:u_keyLeft\n// @viji-accumulator:arUp rate:u_keyUp\n// @viji-accumulator:arDown rate:u_keyDown\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n\n float speed = u_keyShift ? 0.5 : 0.2;\n vec2 offset = vec2(\n (moveRight + arRight - moveLeft - arLeft) * speed,\n (moveUp + arUp - moveDown - arDown) * speed\n );\n\n vec2 p = uv - offset;\n\n float d = length(p) - 0.1;\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n vec3 dotColor = u_keySpace ? vec3(1.0, 0.5, 0.2) : vec3(0.3, 0.7, 1.0);\n col += dotColor * smoothstep(0.01, 0.0, d);\n\n float glow = 0.005 / (abs(d) + 0.005);\n col += dotColor * glow * 0.3;\n\n float grid = step(0.98, fract(uv.x * 10.0)) + step(0.98, fract(uv.y * 10.0));\n col += grid * 0.08;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9401
9401
  "sceneFile": "keyboard-shader-demo.scene.glsl",
9402
9402
  "capabilities": {
9403
9403
  "interaction": true
@@ -9458,7 +9458,7 @@ export const docsApi = {
9458
9458
  {
9459
9459
  "type": "live-example",
9460
9460
  "title": "Touch: Multi-Point Glow",
9461
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy;\r\n\r\n vec3 col = vec3(0.04, 0.04, 0.08);\r\n\r\n vec3 colors[5];\r\n colors[0] = vec3(0.3, 0.7, 1.0);\r\n colors[1] = vec3(0.3, 1.0, 0.5);\r\n colors[2] = vec3(1.0, 0.5, 0.3);\r\n colors[3] = vec3(1.0, 0.8, 0.2);\r\n colors[4] = vec3(0.8, 0.3, 1.0);\r\n\r\n for (int i = 0; i < 5; i++) {\r\n if (i >= u_touchCount) break;\r\n vec2 tp;\r\n if (i == 0) tp = u_touch0;\r\n else if (i == 1) tp = u_touch1;\r\n else if (i == 2) tp = u_touch2;\r\n else if (i == 3) tp = u_touch3;\r\n else tp = u_touch4;\r\n\r\n float d = length(uv - tp);\r\n col += colors[i] * 20.0 / (d + 8.0);\r\n\r\n float ring = smoothstep(2.0, 0.0, abs(d - 50.0));\r\n col += colors[i] * ring * 0.5;\r\n }\r\n\r\n if (u_touchCount == 0) {\r\n vec2 center = u_resolution * 0.5;\r\n float pulse = 0.5 + 0.5 * sin(u_time * 2.0);\r\n float d = length(uv - center);\r\n col += vec3(0.2, 0.3, 0.5) * pulse * 10.0 / (d + 20.0);\r\n }\r\n\r\n if (u_touchCount >= 2) {\r\n float dist = length(u_touch0 - u_touch1);\r\n vec2 mid = (u_touch0 + u_touch1) * 0.5;\r\n float dMid = length(uv - mid);\r\n float ring = smoothstep(2.0, 0.0, abs(dMid - dist * 0.5));\r\n col += vec3(0.5, 0.5, 0.8) * ring * 0.4;\r\n }\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9461
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy;\n\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n vec3 colors[5];\n colors[0] = vec3(0.3, 0.7, 1.0);\n colors[1] = vec3(0.3, 1.0, 0.5);\n colors[2] = vec3(1.0, 0.5, 0.3);\n colors[3] = vec3(1.0, 0.8, 0.2);\n colors[4] = vec3(0.8, 0.3, 1.0);\n\n for (int i = 0; i < 5; i++) {\n if (i >= u_touchCount) break;\n vec2 tp;\n if (i == 0) tp = u_touch0;\n else if (i == 1) tp = u_touch1;\n else if (i == 2) tp = u_touch2;\n else if (i == 3) tp = u_touch3;\n else tp = u_touch4;\n\n float d = length(uv - tp);\n col += colors[i] * 20.0 / (d + 8.0);\n\n float ring = smoothstep(2.0, 0.0, abs(d - 50.0));\n col += colors[i] * ring * 0.5;\n }\n\n if (u_touchCount == 0) {\n vec2 center = u_resolution * 0.5;\n float pulse = 0.5 + 0.5 * sin(u_time * 2.0);\n float d = length(uv - center);\n col += vec3(0.2, 0.3, 0.5) * pulse * 10.0 / (d + 20.0);\n }\n\n if (u_touchCount >= 2) {\n float dist = length(u_touch0 - u_touch1);\n vec2 mid = (u_touch0 + u_touch1) * 0.5;\n float dMid = length(uv - mid);\n float ring = smoothstep(2.0, 0.0, abs(dMid - dist * 0.5));\n col += vec3(0.5, 0.5, 0.8) * ring * 0.4;\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9462
9462
  "sceneFile": "touch-shader-demo.scene.glsl",
9463
9463
  "capabilities": {
9464
9464
  "interaction": true
@@ -9539,7 +9539,7 @@ export const docsApi = {
9539
9539
  {
9540
9540
  "type": "live-example",
9541
9541
  "title": "Sensor Uniforms: Tilt Shader",
9542
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n float tiltX = u_deviceOrientation.z / 90.0;\r\n float tiltY = u_deviceOrientation.y / 180.0;\r\n\r\n vec2 center = vec2(0.5 + tiltX * 0.35, 0.5 + tiltY * 0.35);\r\n float d = distance(uv, center);\r\n\r\n float accelMag = length(u_deviceAccelerationGravity) / 15.0;\r\n float radius = 0.15 + accelMag * 0.1;\r\n\r\n float hue = u_deviceOrientation.x / 360.0;\r\n vec3 hsvColor = vec3(hue, 0.7, 0.8);\r\n float c = hsvColor.z * hsvColor.y;\r\n float x = c * (1.0 - abs(mod(hsvColor.x * 6.0, 2.0) - 1.0));\r\n float m = hsvColor.z - c;\r\n vec3 rgb;\r\n float h6 = hsvColor.x * 6.0;\r\n if (h6 < 1.0) rgb = vec3(c, x, 0.0);\r\n else if (h6 < 2.0) rgb = vec3(x, c, 0.0);\r\n else if (h6 < 3.0) rgb = vec3(0.0, c, x);\r\n else if (h6 < 4.0) rgb = vec3(0.0, x, c);\r\n else if (h6 < 5.0) rgb = vec3(x, 0.0, c);\r\n else rgb = vec3(c, 0.0, x);\r\n rgb += m;\r\n\r\n float glow = smoothstep(radius + 0.05, radius - 0.05, d);\r\n vec3 col = mix(vec3(0.04, 0.04, 0.1), rgb, glow);\r\n\r\n float crosshair = step(abs(uv.x - 0.5), 0.001) + step(abs(uv.y - 0.5), 0.001);\r\n col += vec3(crosshair * 0.08);\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9542
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float tiltX = u_deviceOrientation.z / 90.0;\n float tiltY = u_deviceOrientation.y / 180.0;\n\n vec2 center = vec2(0.5 + tiltX * 0.35, 0.5 + tiltY * 0.35);\n float d = distance(uv, center);\n\n float accelMag = length(u_deviceAccelerationGravity) / 15.0;\n float radius = 0.15 + accelMag * 0.1;\n\n float hue = u_deviceOrientation.x / 360.0;\n vec3 hsvColor = vec3(hue, 0.7, 0.8);\n float c = hsvColor.z * hsvColor.y;\n float x = c * (1.0 - abs(mod(hsvColor.x * 6.0, 2.0) - 1.0));\n float m = hsvColor.z - c;\n vec3 rgb;\n float h6 = hsvColor.x * 6.0;\n if (h6 < 1.0) rgb = vec3(c, x, 0.0);\n else if (h6 < 2.0) rgb = vec3(x, c, 0.0);\n else if (h6 < 3.0) rgb = vec3(0.0, c, x);\n else if (h6 < 4.0) rgb = vec3(0.0, x, c);\n else if (h6 < 5.0) rgb = vec3(x, 0.0, c);\n else rgb = vec3(c, 0.0, x);\n rgb += m;\n\n float glow = smoothstep(radius + 0.05, radius - 0.05, d);\n vec3 col = mix(vec3(0.04, 0.04, 0.1), rgb, glow);\n\n float crosshair = step(abs(uv.x - 0.5), 0.001) + step(abs(uv.y - 0.5), 0.001);\n col += vec3(crosshair * 0.08);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9543
9543
  "sceneFile": "sensors-shader-demo.scene.glsl",
9544
9544
  "capabilities": {
9545
9545
  "interaction": true
@@ -9620,7 +9620,7 @@ export const docsApi = {
9620
9620
  {
9621
9621
  "type": "live-example",
9622
9622
  "title": "External Devices: Multi-Device Overview",
9623
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n vec3 col = vec3(0.04, 0.04, 0.1);\r\n\r\n float deviceIndicator = float(u_externalDeviceCount) / 8.0;\r\n col += vec3(0.0, deviceIndicator * 0.15, deviceIndicator * 0.3);\r\n\r\n float videoIndicator = float(u_deviceCount) / 8.0;\r\n float bar = step(0.92, uv.y) * step(uv.x, videoIndicator);\r\n col = mix(col, vec3(0.3, 0.8, 0.4), bar);\r\n\r\n float sensorBar = step(uv.y, 0.08) * step(uv.x, deviceIndicator);\r\n col = mix(col, vec3(0.3, 0.5, 1.0), sensorBar);\r\n\r\n if (u_device0Connected) {\r\n vec2 thumbUV = (uv - vec2(0.05, 0.15)) / vec2(0.25, 0.35);\r\n if (thumbUV.x >= 0.0 && thumbUV.x <= 1.0 && thumbUV.y >= 0.0 && thumbUV.y <= 1.0) {\r\n col = texture2D(u_device0, thumbUV).rgb;\r\n }\r\n }\r\n\r\n if (u_device1Connected) {\r\n vec2 thumbUV = (uv - vec2(0.35, 0.15)) / vec2(0.25, 0.35);\r\n if (thumbUV.x >= 0.0 && thumbUV.x <= 1.0 && thumbUV.y >= 0.0 && thumbUV.y <= 1.0) {\r\n col = texture2D(u_device1, thumbUV).rgb;\r\n }\r\n }\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9623
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.1);\n\n float deviceIndicator = float(u_externalDeviceCount) / 8.0;\n col += vec3(0.0, deviceIndicator * 0.15, deviceIndicator * 0.3);\n\n float videoIndicator = float(u_deviceCount) / 8.0;\n float bar = step(0.92, uv.y) * step(uv.x, videoIndicator);\n col = mix(col, vec3(0.3, 0.8, 0.4), bar);\n\n float sensorBar = step(uv.y, 0.08) * step(uv.x, deviceIndicator);\n col = mix(col, vec3(0.3, 0.5, 1.0), sensorBar);\n\n if (u_device0Connected) {\n vec2 thumbUV = (uv - vec2(0.05, 0.15)) / vec2(0.25, 0.35);\n if (thumbUV.x >= 0.0 && thumbUV.x <= 1.0 && thumbUV.y >= 0.0 && thumbUV.y <= 1.0) {\n col = texture2D(u_device0, thumbUV).rgb;\n }\n }\n\n if (u_device1Connected) {\n vec2 thumbUV = (uv - vec2(0.35, 0.15)) / vec2(0.25, 0.35);\n if (thumbUV.x >= 0.0 && thumbUV.x <= 1.0 && thumbUV.y >= 0.0 && thumbUV.y <= 1.0) {\n col = texture2D(u_device1, thumbUV).rgb;\n }\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9624
9624
  "sceneFile": "overview-demo.scene.glsl",
9625
9625
  "capabilities": {
9626
9626
  "interaction": true
@@ -9701,7 +9701,7 @@ export const docsApi = {
9701
9701
  {
9702
9702
  "type": "live-example",
9703
9703
  "title": "Device Video: Camera Grid Shader",
9704
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n vec3 col = vec3(0.04, 0.04, 0.1);\r\n\r\n if (u_deviceCount == 0) {\r\n float grid = step(0.98, fract(uv.x * 8.0)) + step(0.98, fract(uv.y * 8.0));\r\n col += vec3(grid * 0.10);\r\n gl_FragColor = vec4(col, 1.0);\r\n return;\r\n }\r\n\r\n int cols = u_deviceCount <= 1 ? 1 : 2;\r\n int rows = (u_deviceCount + cols - 1) / cols;\r\n vec2 cell = vec2(float(cols), float(rows));\r\n vec2 cellUV = fract(uv * cell);\r\n ivec2 cellIdx = ivec2(floor(uv * cell));\r\n int idx = cellIdx.y * cols + cellIdx.x;\r\n\r\n float pad = 0.02;\r\n bool inPad = cellUV.x < pad || cellUV.x > (1.0 - pad) || cellUV.y < pad || cellUV.y > (1.0 - pad);\r\n vec2 innerUV = (cellUV - pad) / (1.0 - 2.0 * pad);\r\n\r\n if (inPad || idx >= u_deviceCount) {\r\n gl_FragColor = vec4(col, 1.0);\r\n return;\r\n }\r\n\r\n vec3 deviceCol = vec3(0.06);\r\n if (idx == 0 && u_device0Connected) deviceCol = texture2D(u_device0, innerUV).rgb;\r\n else if (idx == 1 && u_device1Connected) deviceCol = texture2D(u_device1, innerUV).rgb;\r\n else if (idx == 2 && u_device2Connected) deviceCol = texture2D(u_device2, innerUV).rgb;\r\n else if (idx == 3 && u_device3Connected) deviceCol = texture2D(u_device3, innerUV).rgb;\r\n\r\n gl_FragColor = vec4(deviceCol, 1.0);\r\n}\r\n",
9704
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.1);\n\n if (u_deviceCount == 0) {\n float grid = step(0.98, fract(uv.x * 8.0)) + step(0.98, fract(uv.y * 8.0));\n col += vec3(grid * 0.10);\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n\n int cols = u_deviceCount <= 1 ? 1 : 2;\n int rows = (u_deviceCount + cols - 1) / cols;\n vec2 cell = vec2(float(cols), float(rows));\n vec2 cellUV = fract(uv * cell);\n ivec2 cellIdx = ivec2(floor(uv * cell));\n int idx = cellIdx.y * cols + cellIdx.x;\n\n float pad = 0.02;\n bool inPad = cellUV.x < pad || cellUV.x > (1.0 - pad) || cellUV.y < pad || cellUV.y > (1.0 - pad);\n vec2 innerUV = (cellUV - pad) / (1.0 - 2.0 * pad);\n\n if (inPad || idx >= u_deviceCount) {\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n\n vec3 deviceCol = vec3(0.06);\n if (idx == 0 && u_device0Connected) deviceCol = texture2D(u_device0, innerUV).rgb;\n else if (idx == 1 && u_device1Connected) deviceCol = texture2D(u_device1, innerUV).rgb;\n else if (idx == 2 && u_device2Connected) deviceCol = texture2D(u_device2, innerUV).rgb;\n else if (idx == 3 && u_device3Connected) deviceCol = texture2D(u_device3, innerUV).rgb;\n\n gl_FragColor = vec4(deviceCol, 1.0);\n}\n",
9705
9705
  "sceneFile": "video-demo.scene.glsl",
9706
9706
  "capabilities": {
9707
9707
  "interaction": true
@@ -9787,7 +9787,7 @@ export const docsApi = {
9787
9787
  {
9788
9788
  "type": "live-example",
9789
9789
  "title": "External Device Sensors: Tilt Shader",
9790
- "sceneCode": "// @renderer shader\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n vec3 col = vec3(0.04, 0.04, 0.1);\r\n\r\n if (u_externalDeviceCount == 0) {\r\n float grid = step(0.98, fract(uv.x * 10.0)) + step(0.98, fract(uv.y * 10.0));\r\n col += vec3(grid * 0.08);\r\n gl_FragColor = vec4(col, 1.0);\r\n return;\r\n }\r\n\r\n float tiltX = u_device0Orientation.z / 90.0;\r\n float tiltY = u_device0Orientation.y / 180.0;\r\n\r\n vec2 center = vec2(0.5 + tiltX * 0.35, 0.5 + tiltY * 0.35);\r\n float d = distance(uv, center);\r\n\r\n float accelMag = length(u_device0AccelerationGravity) / 15.0;\r\n float radius = 0.12 + accelMag * 0.08;\r\n\r\n float glow = smoothstep(radius + 0.04, radius - 0.04, d);\r\n vec3 orb = mix(vec3(0.2, 0.5, 1.0), vec3(0.5, 0.8, 1.0), glow);\r\n col = mix(col, orb, glow);\r\n\r\n float heading = u_device0Orientation.x / 360.0;\r\n col.r += heading * 0.15;\r\n\r\n float cross = step(abs(uv.x - 0.5), 0.001) + step(abs(uv.y - 0.5), 0.001);\r\n col += vec3(cross * 0.20);\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
9790
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.1);\n\n if (u_externalDeviceCount == 0) {\n float grid = step(0.98, fract(uv.x * 10.0)) + step(0.98, fract(uv.y * 10.0));\n col += vec3(grid * 0.08);\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n\n float tiltX = u_device0Orientation.z / 90.0;\n float tiltY = u_device0Orientation.y / 180.0;\n\n vec2 center = vec2(0.5 + tiltX * 0.35, 0.5 + tiltY * 0.35);\n float d = distance(uv, center);\n\n float accelMag = length(u_device0AccelerationGravity) / 15.0;\n float radius = 0.12 + accelMag * 0.08;\n\n float glow = smoothstep(radius + 0.04, radius - 0.04, d);\n vec3 orb = mix(vec3(0.2, 0.5, 1.0), vec3(0.5, 0.8, 1.0), glow);\n col = mix(col, orb, glow);\n\n float heading = u_device0Orientation.x / 360.0;\n col.r += heading * 0.15;\n\n float cross = step(abs(uv.x - 0.5), 0.001) + step(abs(uv.y - 0.5), 0.001);\n col += vec3(cross * 0.20);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
9791
9791
  "sceneFile": "sensors-demo.scene.glsl",
9792
9792
  "capabilities": {
9793
9793
  "interaction": true
@@ -9926,7 +9926,7 @@ export const docsApi = {
9926
9926
  {
9927
9927
  "type": "live-example",
9928
9928
  "title": "Trail Effect",
9929
- "sceneCode": "// @renderer shader\r\n// @viji-slider:decay label:\"Trail Decay\" default:3.0 min:0.5 max:20.0 step:0.1\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec4 prev = texture2D(backbuffer, uv);\r\n\r\n float fade = 1.0 - exp(-decay * u_deltaTime);\r\n\r\n vec2 mouse = u_mouse / u_resolution;\r\n float dist = length(uv - mouse);\r\n float spot = smoothstep(0.08, 0.0, dist);\r\n\r\n float hue = fract(u_time * 0.1);\r\n vec3 spotColor = vec3(\r\n abs(hue * 6.0 - 3.0) - 1.0,\r\n 2.0 - abs(hue * 6.0 - 2.0),\r\n 2.0 - abs(hue * 6.0 - 4.0)\r\n );\r\n spotColor = clamp(spotColor, 0.0, 1.0);\r\n\r\n vec3 current = spotColor * spot;\r\n vec3 result = mix(prev.rgb, current, max(fade, spot));\r\n\r\n gl_FragColor = vec4(result, 1.0);\r\n}\r\n",
9929
+ "sceneCode": "// @renderer shader\n// @viji-slider:decay label:\"Trail Decay\" default:3.0 min:0.5 max:20.0 step:0.1\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec4 prev = texture2D(backbuffer, uv);\n\n float fade = 1.0 - exp(-decay * u_deltaTime);\n\n vec2 mouse = u_mouse / u_resolution;\n float dist = length(uv - mouse);\n float spot = smoothstep(0.08, 0.0, dist);\n\n float hue = fract(u_time * 0.1);\n vec3 spotColor = vec3(\n abs(hue * 6.0 - 3.0) - 1.0,\n 2.0 - abs(hue * 6.0 - 2.0),\n 2.0 - abs(hue * 6.0 - 4.0)\n );\n spotColor = clamp(spotColor, 0.0, 1.0);\n\n vec3 current = spotColor * spot;\n vec3 result = mix(prev.rgb, current, max(fade, spot));\n\n gl_FragColor = vec4(result, 1.0);\n}\n",
9930
9930
  "sceneFile": "backbuffer-trail.scene.glsl",
9931
9931
  "capabilities": {
9932
9932
  "interaction": true
@@ -9939,7 +9939,7 @@ export const docsApi = {
9939
9939
  {
9940
9940
  "type": "live-example",
9941
9941
  "title": "Paint Effect",
9942
- "sceneCode": "// @renderer shader\r\n// @viji-slider:brushSize label:\"Brush Size\" default:0.04 min:0.01 max:0.15 step:0.005\r\n// @viji-color:brushColor label:\"Brush Color\" default:\"#ff6622\"\r\n// @viji-button:clearCanvas label:\"Clear Canvas\"\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec4 prev = texture2D(backbuffer, uv);\r\n\r\n if (clearCanvas) {\r\n gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\r\n return;\r\n }\r\n\r\n vec2 mouse = u_mouse / u_resolution;\r\n float dist = length(uv - mouse);\r\n float brush = smoothstep(brushSize, brushSize * 0.3, dist);\r\n\r\n bool isDrawing = u_mouse.x > 0.0 || u_mouse.y > 0.0;\r\n float stroke = isDrawing ? brush : 0.0;\r\n\r\n vec3 result = mix(prev.rgb, brushColor, stroke);\r\n gl_FragColor = vec4(result, 1.0);\r\n}\r\n",
9942
+ "sceneCode": "// @renderer shader\n// @viji-slider:brushSize label:\"Brush Size\" default:0.04 min:0.01 max:0.15 step:0.005\n// @viji-color:brushColor label:\"Brush Color\" default:\"#ff6622\"\n// @viji-button:clearCanvas label:\"Clear Canvas\"\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec4 prev = texture2D(backbuffer, uv);\n\n if (clearCanvas) {\n gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n return;\n }\n\n vec2 mouse = u_mouse / u_resolution;\n float dist = length(uv - mouse);\n float brush = smoothstep(brushSize, brushSize * 0.3, dist);\n\n bool isDrawing = u_mouse.x > 0.0 || u_mouse.y > 0.0;\n float stroke = isDrawing ? brush : 0.0;\n\n vec3 result = mix(prev.rgb, brushColor, stroke);\n gl_FragColor = vec4(result, 1.0);\n}\n",
9943
9943
  "sceneFile": "backbuffer-paint.scene.glsl",
9944
9944
  "capabilities": {
9945
9945
  "interaction": true
@@ -10065,7 +10065,7 @@ export const docsApi = {
10065
10065
  {
10066
10066
  "type": "live-example",
10067
10067
  "title": "Shadertoy Style: Animated Rings",
10068
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-accumulator:phase rate:speed\r\n\r\n#define iTime phase\r\n#define iResolution vec3(u_resolution, u_resolution.x / u_resolution.y)\r\n\r\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord);\r\nvoid main() { mainImage(gl_FragColor, gl_FragCoord.xy); }\r\n\r\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\r\n vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\r\n\r\n float d = length(uv);\r\n float rings = sin(d * 20.0 - iTime * 4.0) * 0.5 + 0.5;\r\n\r\n vec3 col = mix(\r\n vec3(0.1, 0.2, 0.4),\r\n vec3(1.0, 0.6, 0.2),\r\n rings\r\n );\r\n\r\n col *= smoothstep(1.8, 0.5, d);\r\n\r\n fragColor = vec4(col, 1.0);\r\n}\r\n",
10068
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-accumulator:phase rate:speed\n\n#define iTime phase\n#define iResolution vec3(u_resolution, u_resolution.x / u_resolution.y)\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord);\nvoid main() { mainImage(gl_FragColor, gl_FragCoord.xy); }\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n\n float d = length(uv);\n float rings = sin(d * 20.0 - iTime * 4.0) * 0.5 + 0.5;\n\n vec3 col = mix(\n vec3(0.1, 0.2, 0.4),\n vec3(1.0, 0.6, 0.2),\n rings\n );\n\n col *= smoothstep(1.8, 0.5, d);\n\n fragColor = vec4(col, 1.0);\n}\n",
10069
10069
  "sceneFile": "shadertoy-compat.scene.glsl"
10070
10070
  },
10071
10071
  {
@@ -10075,7 +10075,7 @@ export const docsApi = {
10075
10075
  {
10076
10076
  "type": "live-example",
10077
10077
  "title": "Native Viji: Animated Rings",
10078
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\r\n\r\n float d = length(uv);\r\n float rings = sin(d * 20.0 - phase * 4.0) * 0.5 + 0.5;\r\n\r\n vec3 col = mix(\r\n vec3(0.1, 0.2, 0.4),\r\n vec3(1.0, 0.6, 0.2),\r\n rings\r\n );\r\n\r\n col *= smoothstep(1.8, 0.5, d);\r\n\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
10078
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\n\n float d = length(uv);\n float rings = sin(d * 20.0 - phase * 4.0) * 0.5 + 0.5;\n\n vec3 col = mix(\n vec3(0.1, 0.2, 0.4),\n vec3(1.0, 0.6, 0.2),\n rings\n );\n\n col *= smoothstep(1.8, 0.5, d);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
10079
10079
  "sceneFile": "shadertoy-native.scene.glsl"
10080
10080
  },
10081
10081
  {
@@ -10087,14 +10087,14 @@ export const docsApi = {
10087
10087
  },
10088
10088
  "ai": {
10089
10089
  "systemPrompts": {
10090
- "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",
10090
+ "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:\"A\" 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",
10091
10091
  "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",
10092
10092
  "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",
10093
- "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"
10093
+ "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:\"Solid\" options:[\"Solid\",\"Gradient\",\"Noise\"]\n// -> uniform int name; (0-based index of the selected option)\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"
10094
10094
  },
10095
10095
  "conversionGuides": {
10096
10096
  "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",
10097
- "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",
10097
+ "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:\"A\" 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",
10098
10098
  "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"
10099
10099
  }
10100
10100
  }