@viji-dev/core 0.3.34 → 0.3.36

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.0.0",
3
- "coreVersion": "0.3.33",
4
- "generatedAt": "2026-04-08T11:36:34.004Z",
3
+ "coreVersion": "0.3.35",
4
+ "generatedAt": "2026-04-10T20:09:47.653Z",
5
5
  "navigation": [
6
6
  {
7
7
  "id": "getting-started",
@@ -889,7 +889,7 @@ export const docsApi = {
889
889
  {
890
890
  "type": "live-example",
891
891
  "title": "P5.js — WEBGL",
892
- "sceneCode": "// @renderer p5 webgl\n\nconst speed = viji.slider(1.2, { min: 0.2, max: 4, step: 0.1, label: 'Spin' });\n\nfunction setup(viji, p5) {\n p5.angleMode(p5.RADIANS);\n}\n\nfunction render(viji, p5) {\n p5.background(24, 18, 40);\n p5.ambientLight(120);\n p5.directionalLight(255, 255, 255, 0.3, -0.4, -0.85);\n\n const t = viji.time * speed.value;\n const r = p5.min(p5.width, p5.height) * 0.22;\n\n p5.push();\n p5.rotateX(t * 0.7);\n p5.rotateY(t);\n p5.normalMaterial();\n p5.box(r);\n p5.pop();\n}\n",
892
+ "sceneCode": "// @renderer p5 webgl\n\nconst speed = viji.slider(1.2, { min: 0.2, max: 4, step: 0.1, label: 'Spin' });\nlet phase = 0;\n\nfunction setup(viji, p5) {\n p5.angleMode(p5.RADIANS);\n}\n\nfunction render(viji, p5) {\n p5.background(24, 18, 40);\n p5.ambientLight(120);\n p5.directionalLight(255, 255, 255, 0.3, -0.4, -0.85);\n\n phase += speed.value * viji.deltaTime;\n const t = phase;\n const r = p5.min(p5.width, p5.height) * 0.22;\n\n p5.push();\n p5.rotateX(t * 0.7);\n p5.rotateY(t);\n p5.normalMaterial();\n p5.box(r);\n p5.pop();\n}\n",
893
893
  "sceneFile": "renderers-p5-webgl.scene.js"
894
894
  },
895
895
  {
@@ -915,7 +915,7 @@ export const docsApi = {
915
915
  "content": [
916
916
  {
917
917
  "type": "text",
918
- "markdown": "# Best Practices\n\nThese practices apply to all three renderers (Native, P5, Shader). Following them ensures your scenes look correct at any resolution, run smoothly at any frame rate, and work reliably across devices.\n\n---\n\n## Use `viji.time` and `viji.deltaTime` for Animation\n\nViji provides two timing values. Use the right one for the job:\n\n- **[`viji.time`](/native/timing)** — seconds since the scene started. Use this for most animations (oscillations, rotations, color cycling). This is the most common choice.\n- **[`viji.deltaTime`](/native/timing)** — seconds since the last frame. Use this when you need to accumulate values smoothly regardless of frame rate (movement, physics, fading).\n\n```javascript\n// viji.time — animation that looks identical regardless of frame rate\nconst angle = viji.time * speed.value;\nconst x = Math.cos(angle) * radius;\n\n// viji.deltaTime — accumulation that stays smooth at any FPS\nposition += velocity * viji.deltaTime;\nopacity -= fadeRate * viji.deltaTime;\n```\n\nFor shaders, the equivalents are `u_time` and `u_deltaTime`. When animation speed is driven by a parameter, use an [**accumulator**](/shader/parameters/accumulator) to avoid jumps:\n\n```glsl\n// Instead of: float wave = sin(u_time * speed); ← jumps when slider moves\n// @viji-accumulator:phase rate:speed\nfloat wave = sin(phase + uv.x * 10.0); // smooth at any slider value\n```\n\n> [!NOTE]\n> Always use [`viji.time`](/native/timing) or [`viji.deltaTime`](/native/timing) for animation. Never count frames or assume a specific frame rate — the host application may run your scene at different rates (`full` or `half` mode) or the actual FPS may vary by device.\n\n---\n\n## Design for Any Resolution\n\nThe host application controls your scene's resolution. It may change at any time (window resize, resolution scaling for performance, high-DPI displays). Never hardcode pixel values.\n\n**Use [`viji.width`](/native/canvas-context) and [`viji.height`](/native/canvas-context)** for all positioning and sizing:\n\n```javascript\n// Good — scales to any resolution\nconst centerX = viji.width / 2;\nconst centerY = viji.height / 2;\nconst radius = Math.min(viji.width, viji.height) * 0.1;\n\n// Bad — breaks at different resolutions\nconst centerX = 960;\nconst centerY = 540;\nconst radius = 50;\n```\n\nFor parameters that control sizes, use normalized values (0–1) and multiply by canvas dimensions:\n\n```javascript\nconst size = viji.slider(0.15, { min: 0.02, max: 0.5, label: 'Size' });\n\nfunction render(viji) {\n const pixelSize = size.value * Math.min(viji.width, viji.height);\n}\n```\n\nFor shaders, use `u_resolution`:\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0–1 coordinates\n```\n\n> [!NOTE]\n> Always use [`viji.width`](/native/canvas-context) and [`viji.height`](/native/canvas-context) for positioning and sizing, and [`viji.deltaTime`](/native/timing) for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n---\n\n## Declare Parameters at the Top Level\n\nParameter functions ([`viji.slider()`](/native/parameters/slider), [`viji.color()`](/native/parameters/color), etc.) register controls with the host application. They must be called **once**, at the top level of your scene code — never inside `render()`.\n\n```javascript\n// Correct — declared once at top level\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\n\nfunction render(viji) {\n // Read current values inside render\n const s = speed.value;\n const bg = bgColor.value;\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n---\n\n## Avoid Allocations in the Render Loop\n\nCreating objects, arrays, or strings inside `render()` triggers garbage collection, causing frame drops and stuttering. Pre-allocate at the top level and reuse.\n\n> [!TIP]\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\n> ```javascript\n> // Good — pre-allocated\n> const pos = { x: 0, y: 0 };\n> function render(viji) {\n> pos.x = viji.width / 2;\n> pos.y = viji.height / 2;\n> }\n>\n> // Bad — creates a new object every frame\n> function render(viji) {\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\n> }\n> ```\n\nThis is especially important for particle systems, arrays of positions, or any data structure that persists across frames.\n\n---\n\n## No DOM APIs (but `fetch` Is Fine)\n\nYour scene runs in a Web Worker. Standard DOM APIs are not available:\n\n- No `window`, `document`, `Image()`, `localStorage`\n- No `createElement`, `querySelector`, `addEventListener`\n\nHowever, **`fetch()` works** and can be used to load JSON, text, or other data from external URLs:\n\n```javascript\n// This works — fetch is available in workers\nconst response = await fetch('https://cdn.example.com/data.json');\nconst data = await response.json();\n```\n\nFor images, use Viji's [`viji.image()`](/native/parameters/image) parameter — the host application handles file selection and transfers the image to the worker.\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. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\n\n---\n\n## Guard Audio and Video with `isConnected`\n\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` on the main audio stream, on each entry in `viji.audioStreams` you read from, and on `device.audio` when iterating devices, before using audio or video data:\n\n```javascript\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n // ... use audio data\n }\n\n if (viji.video.isConnected && viji.video.currentFrame) {\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nWithout this guard, your scene would reference undefined or zero values when no audio/video source is connected.\n\n---\n\n## Be Mindful of Computer Vision Costs\n\nCV features (face detection, hand tracking, pose detection, etc.) are powerful but expensive. Each feature runs ML inference in its own WebGL context.\n\n| Feature | Relative Cost | Notes |\n|---------|--------------|-------|\n| Face Detection | Low | Bounding box + basic landmarks only |\n| Face Mesh | Medium-High | 468 facial landmarks |\n| Emotion Detection | High | 7 expressions + 52 blendshape coefficients |\n| Hand Tracking | Medium | Up to 2 hands, 21 landmarks each |\n| Pose Detection | Medium | 33 body landmarks |\n| Body Segmentation | High | Per-pixel mask, large tensor output |\n\n> [!WARNING]\n> **WebGL Context Limits:** Each CV feature requires its own WebGL context for ML inference. Browsers typically allow 8-16 active WebGL contexts. Enabling too many CV features simultaneously can cause context eviction, potentially breaking the scene's own rendering. Use only the CV features you need.\n\n**Don't enable CV features by default.** Instead, expose a toggle parameter so users can activate them on capable devices:\n\n> [!TIP]\n> **Best practice:** Don't enable CV features by default. Instead, expose a toggle parameter so users can activate them on capable devices:\n> ```javascript\n> const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n> if (useFace.value) {\n> await viji.video.cv.enableFaceDetection(true);\n> }\n> ```\n\n---\n\n## Pick One Canvas Context Type\n\nThe native renderer lets you choose between 2D and WebGL contexts via [`viji.useContext()`](/native/canvas-context). Options are `'2d'`, `'webgl'` (WebGL 1), and `'webgl2'` (WebGL 2). Pick one and stick with it.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call [`useContext('2d')`](/native/canvas-context) and later call [`useContext('webgl')`](/native/canvas-context) (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/) — specific wrong/right code examples\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer"
918
+ "markdown": "# Best Practices\n\nThese practices apply to all three renderers (Native, P5, Shader). Following them ensures your scenes look correct at any resolution, run smoothly at any frame rate, and work reliably across devices.\n\n---\n\n## Use `viji.time` and `viji.deltaTime` for Animation\n\nViji provides two timing values. Use the right one for the job:\n\n- **[`viji.time`](/native/timing)** — seconds since the scene started. Use this for animations with a **constant** speed (oscillations, rotations, color cycling).\n- **[`viji.deltaTime`](/native/timing)** — seconds since the last frame. Use this when you need to accumulate values smoothly regardless of frame rate (movement, physics, fading), or when **animation speed is controlled by a parameter**.\n\n```javascript\n// viji.time — animation at a constant speed\nconst angle = viji.time * 2.0; // fixed 2 rad/s — no parameter involved\nconst x = Math.cos(angle) * radius;\n\n// viji.deltaTime — accumulation that stays smooth at any FPS\nposition += velocity * viji.deltaTime;\nopacity -= fadeRate * viji.deltaTime;\n```\n\n### Accumulator Pattern — Parameter-Driven Speed\n\nWhen animation speed is controlled by a user parameter, **never** multiply `viji.time` (or `u_time`) by the parameter directly — changing the slider recalculates the entire phase history and causes a visible jump. Instead, accumulate with `viji.deltaTime`:\n\n```javascript\n// Wrong — animation jumps when the speed slider moves\nconst angle = viji.time * speed.value;\n\n// Right — accumulate smoothly, slider only affects future rate\nlet phase = 0; // top-level state\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const x = Math.cos(phase) * radius;\n}\n```\n\nFor P5 scenes, the same pattern applies:\n\n```javascript\n// @renderer p5\nlet phase = 0;\nfunction render(viji, p5) {\n phase += rotationSpeed.value * viji.deltaTime;\n p5.rotate(phase);\n}\n```\n\nFor shaders, use a [`@viji-accumulator`](/shader/parameters/accumulator) — the runtime integrates `speed × deltaTime` into the uniform automatically:\n\n```glsl\n// Instead of: float wave = sin(u_time * speed); ← jumps when slider moves\n// @viji-accumulator:phase rate:speed\nfloat wave = sin(phase + uv.x * 10.0); // smooth at any slider value\n```\n\nThe same principle applies to **nested multiplications**: if you have an accumulated phase and multiply it by another parameter, changing that parameter causes the same jump. Each parameter-driven speed needs its own accumulator:\n\n```javascript\n// Wrong — rotation jumps when rotationSpeed slider changes\nphase += speed.value * viji.deltaTime;\nconst rotation = phase * rotationSpeed.value; // ← jumps\n\n// Right — separate accumulator for rotation\nrotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\nconst rotation = rotPhase; // smooth\n```\n\n> [!NOTE]\n> Always use [`viji.time`](/native/timing) or [`viji.deltaTime`](/native/timing) for animation. Never count frames or assume a specific frame rate — the host application may run your scene at different rates (`full` or `half` mode) or the actual FPS may vary by device. When speed is driven by a parameter, always use the accumulator pattern (`deltaTime` in JS/P5, `@viji-accumulator` in shaders) to prevent phase jumps. The rule extends to nested multiplications — never multiply an accumulated value by a parameter.\n\n---\n\n## Design for Any Resolution\n\nThe host application controls your scene's resolution. It may change at any time (window resize, resolution scaling for performance, high-DPI displays). Never hardcode pixel values.\n\n**Use [`viji.width`](/native/canvas-context) and [`viji.height`](/native/canvas-context)** for all positioning and sizing:\n\n```javascript\n// Good — scales to any resolution\nconst centerX = viji.width / 2;\nconst centerY = viji.height / 2;\nconst radius = Math.min(viji.width, viji.height) * 0.1;\n\n// Bad — breaks at different resolutions\nconst centerX = 960;\nconst centerY = 540;\nconst radius = 50;\n```\n\nFor parameters that control sizes, use normalized values (0–1) and multiply by canvas dimensions:\n\n```javascript\nconst size = viji.slider(0.15, { min: 0.02, max: 0.5, label: 'Size' });\n\nfunction render(viji) {\n const pixelSize = size.value * Math.min(viji.width, viji.height);\n}\n```\n\nFor shaders, use `u_resolution`:\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0–1 coordinates\n```\n\n> [!NOTE]\n> Always use [`viji.width`](/native/canvas-context) and [`viji.height`](/native/canvas-context) for positioning and sizing, and [`viji.deltaTime`](/native/timing) for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n---\n\n## Declare Parameters at the Top Level\n\nParameter functions ([`viji.slider()`](/native/parameters/slider), [`viji.color()`](/native/parameters/color), etc.) register controls with the host application. They must be called **once**, at the top level of your scene code — never inside `render()`.\n\n```javascript\n// Correct — declared once at top level\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\n\nfunction render(viji) {\n // Read current values inside render\n const s = speed.value;\n const bg = bgColor.value;\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n---\n\n## Avoid Allocations in the Render Loop\n\nCreating objects, arrays, or strings inside `render()` triggers garbage collection, causing frame drops and stuttering. Pre-allocate at the top level and reuse.\n\n> [!TIP]\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\n> ```javascript\n> // Good — pre-allocated\n> const pos = { x: 0, y: 0 };\n> function render(viji) {\n> pos.x = viji.width / 2;\n> pos.y = viji.height / 2;\n> }\n>\n> // Bad — creates a new object every frame\n> function render(viji) {\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\n> }\n> ```\n\nThis is especially important for particle systems, arrays of positions, or any data structure that persists across frames.\n\n---\n\n## No DOM APIs (but `fetch` Is Fine)\n\nYour scene runs in a Web Worker. Standard DOM APIs are not available:\n\n- No `window`, `document`, `Image()`, `localStorage`\n- No `createElement`, `querySelector`, `addEventListener`\n\nHowever, **`fetch()` works** and can be used to load JSON, text, or other data from external URLs:\n\n```javascript\n// This works — fetch is available in workers\nconst response = await fetch('https://cdn.example.com/data.json');\nconst data = await response.json();\n```\n\nFor images, use Viji's [`viji.image()`](/native/parameters/image) parameter — the host application handles file selection and transfers the image to the worker.\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. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\n\n---\n\n## Guard Audio and Video with `isConnected`\n\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` on the main audio stream, on each entry in `viji.audioStreams` you read from, and on `device.audio` when iterating devices, before using audio or video data:\n\n```javascript\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n // ... use audio data\n }\n\n if (viji.video.isConnected && viji.video.currentFrame) {\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nWithout this guard, your scene would reference undefined or zero values when no audio/video source is connected.\n\n---\n\n## Be Mindful of Computer Vision Costs\n\nCV features (face detection, hand tracking, pose detection, etc.) are powerful but expensive. Each feature runs ML inference in its own WebGL context.\n\n| Feature | Relative Cost | Notes |\n|---------|--------------|-------|\n| Face Detection | Low | Bounding box + basic landmarks only |\n| Face Mesh | Medium-High | 468 facial landmarks |\n| Emotion Detection | High | 7 expressions + 52 blendshape coefficients |\n| Hand Tracking | Medium | Up to 2 hands, 21 landmarks each |\n| Pose Detection | Medium | 33 body landmarks |\n| Body Segmentation | High | Per-pixel mask, large tensor output |\n\n> [!WARNING]\n> **WebGL Context Limits:** Each CV feature requires its own WebGL context for ML inference. Browsers typically allow 8-16 active WebGL contexts. Enabling too many CV features simultaneously can cause context eviction, potentially breaking the scene's own rendering. Use only the CV features you need.\n\n**Don't enable CV features by default.** Instead, expose a toggle parameter so users can activate them on capable devices:\n\n> [!TIP]\n> **Best practice:** Don't enable CV features by default. Instead, expose a toggle parameter so users can activate them on capable devices:\n> ```javascript\n> const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n> if (useFace.value) {\n> await viji.video.cv.enableFaceDetection(true);\n> }\n> ```\n\n---\n\n## Categorize Input-Related Parameters\n\nParameters that depend on an external input — audio, video/camera, or user interaction — should include the matching `category` property so the host UI can automatically show or hide them based on whether that input is active.\n\n```javascript\n// Wrong — parameter is always visible, even when no audio is connected\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio' });\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\n\n// Right — parameters only appear when the corresponding input is active\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n\nconst followMouse = viji.toggle(true, { label: 'Follow Mouse', group: 'interaction', category: 'interaction' });\nconst showVideo = viji.toggle(false, { label: 'Show Video', group: 'video', category: 'video' });\n```\n\n| Input type | `category` value | Visible when |\n|---|---|---|\n| Audio | `'audio'` | Audio source is connected |\n| Video / Camera / CV | `'video'` | Video/camera source is connected |\n| Mouse / Keyboard / Touch | `'interaction'` | User interaction is enabled |\n| Everything else | `'general'` (default) | Always |\n\n> [!TIP]\n> `group` and `category` are orthogonal. `group` controls which section a parameter appears in (layout). `category` controls whether it's visible at all (capability filtering). Use both when appropriate. See [Parameter Categories](/native/parameters/categories) for details.\n\n---\n\n## Use `viji.useContext()` for Canvas Access\n\nAlways use [`viji.useContext()`](/native/canvas-context) to obtain a rendering context — not `viji.canvas.getContext()`. The `useContext()` method ensures the runtime properly tracks the context and performs any necessary internal setup (e.g., WebGL viewport configuration).\n\n```javascript\n// Good\nconst ctx = viji.useContext('2d');\n\n// Avoid — bypasses runtime context tracking\nconst ctx = viji.canvas.getContext('2d');\n```\n\nOptions are `'2d'`, `'webgl'` (WebGL 1), and `'webgl2'` (WebGL 2). Pick one and stick with it — a canvas only supports one context type.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call [`useContext('2d')`](/native/canvas-context) and later call [`useContext('webgl')`](/native/canvas-context) (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/) — specific wrong/right code examples\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer"
919
919
  }
920
920
  ]
921
921
  },
@@ -926,7 +926,7 @@ export const docsApi = {
926
926
  "content": [
927
927
  {
928
928
  "type": "text",
929
- "markdown": "# Common Mistakes\r\n\r\nThis page collects the most frequent mistakes artists make when writing Viji scenes. Each section shows the wrong approach and the correct alternative.\r\n\r\n---\r\n\r\n## Using DOM APIs\r\n\r\nScenes run in a Web Worker. There is no DOM.\r\n\r\n```javascript\r\n// Wrong — DOM APIs don't exist in workers\r\nconst img = new Image();\r\nimg.src = 'photo.jpg';\r\n\r\ndocument.createElement('canvas');\r\nwindow.innerWidth;\r\nlocalStorage.setItem('key', 'value');\r\n```\r\n\r\n```javascript\r\n// Right — use Viji's API for inputs\r\nconst photo = viji.image(null, { label: 'Photo' });\r\n\r\n// Use viji.canvas, viji.width, viji.height instead\r\n// Use fetch() for loading external data:\r\nconst data = await fetch('https://cdn.example.com/data.json').then(r => r.json());\r\n```\r\n\r\n---\r\n\r\n## Declaring Parameters Inside `render()`\r\n\r\nParameter functions register UI controls with the host. Calling them in `render()` re-registers the parameter every frame, resetting its value to the default and making user changes ineffective.\r\n\r\n```javascript\r\n// Wrong — re-registers the slider every frame, resetting its value\r\nfunction render(viji) {\r\n const speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\r\n // ...\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — declare once at top level, read .value in render()\r\nconst speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\r\n\r\nfunction render(viji) {\r\n const s = speed.value;\r\n // ...\r\n}\r\n```\r\n\r\n---\r\n\r\n## Forgetting `.value` on Parameters\r\n\r\nParameter objects are not raw values. You need to access `.value` to get the current value.\r\n\r\n```javascript\r\n// Wrong — uses the parameter object, not its value\r\nconst radius = viji.slider(50, { min: 10, max: 200, label: 'Radius' });\r\n\r\nfunction render(viji) {\r\n ctx.arc(x, y, radius, 0, Math.PI * 2); // radius is an object, not a number\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — access .value\r\nfunction render(viji) {\r\n ctx.arc(x, y, radius.value, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Hardcoding Pixel Values\r\n\r\nThe host controls your scene's resolution. Hardcoded values break at different sizes.\r\n\r\n```javascript\r\n// Wrong — only looks right at one specific resolution\r\nfunction render(viji) {\r\n ctx.arc(960, 540, 50, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — adapts to any resolution\r\nfunction render(viji) {\r\n const cx = viji.width / 2;\r\n const cy = viji.height / 2;\r\n const r = Math.min(viji.width, viji.height) * 0.05;\r\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Frame-Rate-Dependent Animation\r\n\r\nCounting frames or using fixed increments makes animation speed depend on the device's frame rate.\r\n\r\n```javascript\r\n// Wrong — faster on 120Hz displays, slower on 30Hz\r\nlet angle = 0;\r\nfunction render(viji) {\r\n angle += 0.02;\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — use viji.time for consistent speed regardless of FPS\r\nfunction render(viji) {\r\n const angle = viji.time * speed.value;\r\n}\r\n\r\n// Or use viji.deltaTime for accumulation\r\nlet position = 0;\r\nfunction render(viji) {\r\n position += velocity * viji.deltaTime;\r\n}\r\n```\r\n\r\n---\r\n\r\n## Allocating Objects in `render()`\r\n\r\nCreating new objects every frame causes garbage collection pauses.\r\n\r\n```javascript\r\n// Wrong — new object every frame\r\nfunction render(viji) {\r\n const particles = [];\r\n for (let i = 0; i < 100; i++) {\r\n particles.push({ x: Math.random() * viji.width, y: Math.random() * viji.height });\r\n }\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — pre-allocate and reuse\r\nconst particles = Array.from({ length: 100 }, () => ({ x: 0, y: 0 }));\r\n\r\nfunction render(viji) {\r\n for (const p of particles) {\r\n p.x = Math.random() * viji.width;\r\n p.y = Math.random() * viji.height;\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Missing the `p5.` Prefix\r\n\r\nViji runs P5 in **instance mode**. All P5 functions must be called on the `p5` object.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — global P5 functions don't exist\r\nfunction render(viji, p5) {\r\n background(0); // ReferenceError\r\n fill(255, 0, 0); // ReferenceError\r\n circle(width / 2, height / 2, 100); // ReferenceError\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — use p5. prefix for P5 functions, viji.* for dimensions\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n p5.fill(255, 0, 0);\r\n p5.circle(viji.width / 2, viji.height / 2, 100);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Using `draw()` Instead of `render()`\r\n\r\nP5's built-in draw loop is disabled in Viji. Your function must be named `render`, not `draw`.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — Viji never calls draw()\r\nfunction draw(viji, p5) {\r\n p5.background(0);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — Viji calls render() every frame\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Calling `createCanvas()`\r\n\r\nThe canvas is created and managed by Viji. Calling `createCanvas()` creates a second canvas that won't be displayed. **WEBGL mode** is not enabled this way — use the directive `// @renderer p5 webgl` on the first line instead of passing `p5.WEBGL` here.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — creates a separate, invisible canvas\r\nfunction setup(viji, p5) {\r\n p5.createCanvas(800, 600);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — main canvas already exists; this does not switch it to WEBGL\r\nfunction setup(viji, p5) {\r\n p5.createCanvas(p5.width, p5.height, p5.WEBGL);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — canvas is already provided, just configure settings\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5 webgl\r\n\r\n// Right — WEBGL is selected by the first-line directive; never call createCanvas\r\nfunction setup(viji, p5) {\r\n p5.angleMode(p5.RADIANS);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Expecting WEBGL without `// @renderer p5 webgl`\r\n\r\n`// @renderer p5` alone gives a **2D** main canvas. For 3D / WEBGL, the first comment must include **`webgl`** after `p5`.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — this scene is 2D only; box() / WEBGL lighting won't work as intended\r\nfunction render(viji, p5) {\r\n p5.normalMaterial();\r\n p5.box(100);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5 webgl\r\n\r\n// Right — Viji creates the main canvas in WEBGL mode\r\nfunction render(viji, p5) {\r\n p5.background(32);\r\n p5.ambientLight(100);\r\n p5.normalMaterial();\r\n p5.box(100);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Using Event Callbacks\r\n\r\nP5 event callbacks like `mousePressed()`, `keyPressed()`, `touchStarted()` do not work in Viji's worker environment. Use Viji's interaction APIs instead.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — these callbacks are never called\r\nfunction mousePressed() {\r\n console.log('clicked');\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — check Viji's interaction state in render()\r\nfunction render(viji, p5) {\r\n if (viji.pointer.wasPressed) {\r\n console.log('clicked');\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Shader: Redeclaring Auto-Injected Code\r\n\r\nViji auto-injects `precision`, all built-in uniform declarations, and all parameter uniforms from `@viji-*` directives. Redeclaring any of them causes compilation errors.\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Wrong — these are already injected by Viji\r\nprecision mediump float;\r\nuniform vec2 u_resolution;\r\nuniform float u_time;\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\r\n}\r\n```\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Right — just write your code, uniforms are available automatically\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Shader: Using `u_` Prefix for Parameters\r\n\r\nThe `u_` prefix is reserved for Viji's built-in uniforms. Using it for your parameters risks naming collisions.\r\n\r\n```glsl\r\n// Wrong — u_ prefix is reserved\r\n// @viji-slider:u_speed label:\"Speed\" default:1.0\r\n```\r\n\r\n```glsl\r\n// Right — use descriptive names without u_ prefix\r\n// @viji-slider:speed label:\"Speed\" default:1.0\r\n```\r\n\r\n---\r\n\r\n## Shader: Missing `@renderer shader`\r\n\r\nWithout the directive, your GLSL code is treated as JavaScript and will throw syntax errors.\r\n\r\n```glsl\r\n// Wrong — no directive, treated as JavaScript\r\nvoid main() {\r\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n}\r\n```\r\n\r\n```glsl\r\n// Right — directive tells Viji to use the shader renderer\r\n// @renderer shader\r\n\r\nvoid main() {\r\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Shader: Using Block Comments for `@viji-*` Parameters\r\n\r\nThe `@viji-*` parameter declarations only work with single-line `//` comments. Block comments `/* */` are silently ignored.\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Wrong — block comments are not parsed for parameters\r\n/* @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0 */\r\n\r\nvoid main() {\r\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0); // speed is undefined\r\n}\r\n```\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Right — use single-line comments for parameter declarations\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0\r\n\r\nvoid main() {\r\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0);\r\n}\r\n```\r\n\r\n> [!NOTE]\r\n> The `@renderer` directive supports both `//` and `/* */` styles, but `@viji-*` parameter declarations require `//`.\r\n\r\n---\r\n\r\n## Shader: Using `u_time * speed` for Parameter-Driven Animation\r\n\r\nMultiplying `u_time` by a parameter causes the entire phase to jump when the slider moves, because the full history is recalculated instantly.\r\n\r\n```glsl\r\n// Wrong — animation jumps when speed slider changes\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\r\nvoid main() {\r\n float wave = sin(u_time * speed);\r\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\r\n}\r\n```\r\n\r\n```glsl\r\n// Right — accumulator integrates smoothly, no jumps\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\r\n// @viji-accumulator:phase rate:speed\r\nvoid main() {\r\n float wave = sin(phase);\r\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Not Checking `isConnected` for Audio/Video\r\n\r\nAudio and video streams may not be available. Accessing their properties without checking `isConnected` gives meaningless zero values with no indication that something is missing.\r\n\r\n```javascript\r\n// Wrong — no guard, silently uses zero values\r\nfunction render(viji) {\r\n const bass = viji.audio.bands.low;\r\n ctx.drawImage(viji.video.currentFrame, 0, 0);\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — check connection state first\r\nfunction render(viji) {\r\n if (viji.audio.isConnected) {\r\n const bass = viji.audio.bands.low;\r\n // ... react to audio\r\n }\r\n\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Enabling All CV Features by Default\r\n\r\nEnabling CV features without user consent wastes resources on devices that can't handle it, and risks WebGL context loss.\r\n\r\n```javascript\r\n// Wrong — activates expensive CV on every device\r\nawait viji.video.cv.enableFaceDetection(true);\r\nawait viji.video.cv.enableHandTracking(true);\r\nawait viji.video.cv.enablePoseDetection(true);\r\nawait viji.video.cv.enableBodySegmentation(true);\r\n```\r\n\r\n```javascript\r\n// Right — let the user opt in\r\nconst useFace = viji.toggle(false, { label: 'Enable Face Tracking', category: 'video' });\r\nconst useHands = viji.toggle(false, { label: 'Enable Hand Tracking', category: 'video' });\r\n\r\nfunction render(viji) {\r\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\r\n else await viji.video.cv.enableFaceDetection(false);\r\n\r\n if (useHands.value) await viji.video.cv.enableHandTracking(true);\r\n else await viji.video.cv.enableHandTracking(false);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Related\r\n\r\n- [Best Practices](../best-practices/) — positive guidance for writing robust scenes\r\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer\r\n- [Audio](/native/audio) — audio connection and analysis API\r\n- [Video & CV](/native/video) — video stream and computer vision features"
929
+ "markdown": "# Common Mistakes\r\n\r\nThis page collects the most frequent mistakes artists make when writing Viji scenes. Each section shows the wrong approach and the correct alternative.\r\n\r\n---\r\n\r\n## Using DOM APIs\r\n\r\nScenes run in a Web Worker. There is no DOM.\r\n\r\n```javascript\r\n// Wrong — DOM APIs don't exist in workers\r\nconst img = new Image();\r\nimg.src = 'photo.jpg';\r\n\r\ndocument.createElement('canvas');\r\nwindow.innerWidth;\r\nlocalStorage.setItem('key', 'value');\r\n```\r\n\r\n```javascript\r\n// Right — use Viji's API for inputs\r\nconst photo = viji.image(null, { label: 'Photo' });\r\n\r\n// Use viji.canvas, viji.width, viji.height instead\r\n// Use fetch() for loading external data:\r\nconst data = await fetch('https://cdn.example.com/data.json').then(r => r.json());\r\n```\r\n\r\n---\r\n\r\n## Declaring Parameters Inside `render()`\r\n\r\nParameter functions register UI controls with the host. Calling them in `render()` re-registers the parameter every frame, resetting its value to the default and making user changes ineffective.\r\n\r\n```javascript\r\n// Wrong — re-registers the slider every frame, resetting its value\r\nfunction render(viji) {\r\n const speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\r\n // ...\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — declare once at top level, read .value in render()\r\nconst speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\r\n\r\nfunction render(viji) {\r\n const s = speed.value;\r\n // ...\r\n}\r\n```\r\n\r\n---\r\n\r\n## Forgetting `.value` on Parameters\r\n\r\nParameter objects are not raw values. You need to access `.value` to get the current value.\r\n\r\n```javascript\r\n// Wrong — uses the parameter object, not its value\r\nconst radius = viji.slider(50, { min: 10, max: 200, label: 'Radius' });\r\n\r\nfunction render(viji) {\r\n ctx.arc(x, y, radius, 0, Math.PI * 2); // radius is an object, not a number\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — access .value\r\nfunction render(viji) {\r\n ctx.arc(x, y, radius.value, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Hardcoding Pixel Values\r\n\r\nThe host controls your scene's resolution. Hardcoded values break at different sizes.\r\n\r\n```javascript\r\n// Wrong — only looks right at one specific resolution\r\nfunction render(viji) {\r\n ctx.arc(960, 540, 50, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — adapts to any resolution\r\nfunction render(viji) {\r\n const cx = viji.width / 2;\r\n const cy = viji.height / 2;\r\n const r = Math.min(viji.width, viji.height) * 0.05;\r\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Frame-Rate-Dependent Animation\r\n\r\nCounting frames or using fixed increments makes animation speed depend on the device's frame rate.\r\n\r\n```javascript\r\n// Wrong — faster on 120Hz displays, slower on 30Hz\r\nlet angle = 0;\r\nfunction render(viji) {\r\n angle += 0.02;\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — use viji.time for constant-speed animation\r\nfunction render(viji) {\r\n const angle = viji.time * 2.0; // fixed speed, not a parameter\r\n}\r\n\r\n// Or use viji.deltaTime for accumulation\r\nlet position = 0;\r\nfunction render(viji) {\r\n position += velocity * viji.deltaTime;\r\n}\r\n```\r\n\r\n> [!TIP]\r\n> If animation speed is controlled by a **parameter** (e.g. a slider), do not use `viji.time * speed.value` — this causes jumps when the slider moves. Use the [accumulator pattern](#js--p5-using-vijitime--speedvalue-for-parameter-driven-animation) instead.\r\n\r\n---\r\n\r\n## Allocating Objects in `render()`\r\n\r\nCreating new objects every frame causes garbage collection pauses.\r\n\r\n```javascript\r\n// Wrong — new object every frame\r\nfunction render(viji) {\r\n const particles = [];\r\n for (let i = 0; i < 100; i++) {\r\n particles.push({ x: Math.random() * viji.width, y: Math.random() * viji.height });\r\n }\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — pre-allocate and reuse\r\nconst particles = Array.from({ length: 100 }, () => ({ x: 0, y: 0 }));\r\n\r\nfunction render(viji) {\r\n for (const p of particles) {\r\n p.x = Math.random() * viji.width;\r\n p.y = Math.random() * viji.height;\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Missing the `p5.` Prefix\r\n\r\nViji runs P5 in **instance mode**. All P5 functions must be called on the `p5` object.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — global P5 functions don't exist\r\nfunction render(viji, p5) {\r\n background(0); // ReferenceError\r\n fill(255, 0, 0); // ReferenceError\r\n circle(width / 2, height / 2, 100); // ReferenceError\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — use p5. prefix for P5 functions, viji.* for dimensions\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n p5.fill(255, 0, 0);\r\n p5.circle(viji.width / 2, viji.height / 2, 100);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Using `draw()` Instead of `render()`\r\n\r\nP5's built-in draw loop is disabled in Viji. Your function must be named `render`, not `draw`.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — Viji never calls draw()\r\nfunction draw(viji, p5) {\r\n p5.background(0);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — Viji calls render() every frame\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Calling `createCanvas()`\r\n\r\nThe canvas is created and managed by Viji. Calling `createCanvas()` creates a second canvas that won't be displayed. **WEBGL mode** is not enabled this way — use the directive `// @renderer p5 webgl` on the first line instead of passing `p5.WEBGL` here.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — creates a separate, invisible canvas\r\nfunction setup(viji, p5) {\r\n p5.createCanvas(800, 600);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — main canvas already exists; this does not switch it to WEBGL\r\nfunction setup(viji, p5) {\r\n p5.createCanvas(p5.width, p5.height, p5.WEBGL);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — canvas is already provided, just configure settings\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5 webgl\r\n\r\n// Right — WEBGL is selected by the first-line directive; never call createCanvas\r\nfunction setup(viji, p5) {\r\n p5.angleMode(p5.RADIANS);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Expecting WEBGL without `// @renderer p5 webgl`\r\n\r\n`// @renderer p5` alone gives a **2D** main canvas. For 3D / WEBGL, the first comment must include **`webgl`** after `p5`.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — this scene is 2D only; box() / WEBGL lighting won't work as intended\r\nfunction render(viji, p5) {\r\n p5.normalMaterial();\r\n p5.box(100);\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5 webgl\r\n\r\n// Right — Viji creates the main canvas in WEBGL mode\r\nfunction render(viji, p5) {\r\n p5.background(32);\r\n p5.ambientLight(100);\r\n p5.normalMaterial();\r\n p5.box(100);\r\n}\r\n```\r\n\r\n---\r\n\r\n## P5: Using Event Callbacks\r\n\r\nP5 event callbacks like `mousePressed()`, `keyPressed()`, `touchStarted()` do not work in Viji's worker environment. Use Viji's interaction APIs instead.\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Wrong — these callbacks are never called\r\nfunction mousePressed() {\r\n console.log('clicked');\r\n}\r\n```\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// Right — check Viji's interaction state in render()\r\nfunction render(viji, p5) {\r\n if (viji.pointer.wasPressed) {\r\n console.log('clicked');\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Shader: Redeclaring Auto-Injected Code\r\n\r\nViji auto-injects `precision`, all built-in uniform declarations, and all parameter uniforms from `@viji-*` directives. Redeclaring any of them causes compilation errors.\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Wrong — these are already injected by Viji\r\nprecision mediump float;\r\nuniform vec2 u_resolution;\r\nuniform float u_time;\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\r\n}\r\n```\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Right — just write your code, uniforms are available automatically\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Shader: Using `u_` Prefix for Parameters\r\n\r\nThe `u_` prefix is reserved for Viji's built-in uniforms. Using it for your parameters risks naming collisions.\r\n\r\n```glsl\r\n// Wrong — u_ prefix is reserved\r\n// @viji-slider:u_speed label:\"Speed\" default:1.0\r\n```\r\n\r\n```glsl\r\n// Right — use descriptive names without u_ prefix\r\n// @viji-slider:speed label:\"Speed\" default:1.0\r\n```\r\n\r\n---\r\n\r\n## Shader: Missing `@renderer shader`\r\n\r\nWithout the directive, your GLSL code is treated as JavaScript and will throw syntax errors.\r\n\r\n```glsl\r\n// Wrong — no directive, treated as JavaScript\r\nvoid main() {\r\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n}\r\n```\r\n\r\n```glsl\r\n// Right — directive tells Viji to use the shader renderer\r\n// @renderer shader\r\n\r\nvoid main() {\r\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Shader: Using Block Comments for `@viji-*` Parameters\r\n\r\nThe `@viji-*` parameter declarations only work with single-line `//` comments. Block comments `/* */` are silently ignored.\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Wrong — block comments are not parsed for parameters\r\n/* @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0 */\r\n\r\nvoid main() {\r\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0); // speed is undefined\r\n}\r\n```\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n// Right — use single-line comments for parameter declarations\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0\r\n\r\nvoid main() {\r\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0);\r\n}\r\n```\r\n\r\n> [!NOTE]\r\n> The `@renderer` directive supports both `//` and `/* */` styles, but `@viji-*` parameter declarations require `//`.\r\n\r\n---\r\n\r\n## JS / P5: Using `viji.time * speed.value` for Parameter-Driven Animation\r\n\r\nMultiplying `viji.time` by a parameter causes the entire phase to jump when the slider moves, because the full time history is recalculated instantly with the new value.\r\n\r\n```javascript\r\n// Wrong — animation jumps when speed slider changes\r\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\r\nfunction render(viji) {\r\n const angle = viji.time * speed.value; // ← jumps\r\n ctx.arc(x, y, Math.sin(angle) * r, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — accumulate with deltaTime, slider only affects future rate\r\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\r\nlet phase = 0;\r\nfunction render(viji) {\r\n phase += speed.value * viji.deltaTime;\r\n ctx.arc(x, y, Math.sin(phase) * r, 0, Math.PI * 2);\r\n}\r\n```\r\n\r\nThe same pattern applies to P5 scenes:\r\n\r\n```javascript\r\n// @renderer p5\r\nconst spin = viji.slider(1, { min: 0.1, max: 5 });\r\nlet phase = 0;\r\nfunction render(viji, p5) {\r\n phase += spin.value * viji.deltaTime;\r\n p5.rotate(phase);\r\n}\r\n```\r\n\r\n> [!TIP]\r\n> `viji.time * constant` is perfectly fine when the multiplier never changes at runtime. The accumulator is only needed when speed is a **user-controlled parameter**.\r\n\r\n### Nested Multiplication — Same Trap After Accumulating\r\n\r\nAfter adopting the accumulator pattern, a common follow-up mistake is multiplying the accumulated value by another parameter. This causes the same jump because the full accumulated history is rescaled:\r\n\r\n```javascript\r\n// Wrong — rotation jumps when rotationSpeed slider changes\r\nlet phase = 0;\r\nfunction render(viji) {\r\n phase += speed.value * viji.deltaTime; // ✓ accumulates correctly\r\n const rotation = phase * rotationSpeed.value; // ← jumps!\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — give each parameter-driven value its own accumulator\r\nlet phase = 0;\r\nlet rotPhase = 0;\r\nfunction render(viji) {\r\n phase += speed.value * viji.deltaTime;\r\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\r\n const rotation = rotPhase; // smooth\r\n}\r\n```\r\n\r\n**Rule of thumb:** any time a growing value (accumulated phase, elapsed time) is multiplied by a user parameter, the result will jump when that parameter changes. Each such product needs its own `deltaTime`-based accumulator.\r\n\r\n---\r\n\r\n## Shader: Using `u_time * speed` for Parameter-Driven Animation\r\n\r\nThe same principle applies to shaders — multiplying `u_time` by a parameter causes the entire phase to jump when the slider moves.\r\n\r\n```glsl\r\n// Wrong — animation jumps when speed slider changes\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\r\nvoid main() {\r\n float wave = sin(u_time * speed);\r\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\r\n}\r\n```\r\n\r\n```glsl\r\n// Right — accumulator integrates smoothly, no jumps\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\r\n// @viji-accumulator:phase rate:speed\r\nvoid main() {\r\n float wave = sin(phase);\r\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Using `viji.canvas.getContext()` Instead of `viji.useContext()`\r\n\r\nCalling `viji.canvas.getContext()` directly bypasses the Viji runtime's internal context tracking. While it works in practice (the runtime intercepts the call), it is not the canonical pattern and may miss future runtime setup such as WebGL viewport initialization.\r\n\r\n```javascript\r\n// Wrong — bypasses Viji's context management\r\nconst ctx = viji.canvas.getContext('2d');\r\n```\r\n\r\n```javascript\r\n// Right — use the Viji API, which tracks the context and sets up internals\r\nconst ctx = viji.useContext('2d');\r\n```\r\n\r\nSimilarly, use `viji.width` and `viji.height` instead of `viji.canvas.width` and `viji.canvas.height` — they are always in sync, and `viji.width` / `viji.height` are the documented, canonical properties.\r\n\r\n---\r\n\r\n## Not Checking `isConnected` for Audio/Video\r\n\r\nAudio and video streams may not be available. Accessing their properties without checking `isConnected` gives meaningless zero values with no indication that something is missing.\r\n\r\n```javascript\r\n// Wrong — no guard, silently uses zero values\r\nfunction render(viji) {\r\n const bass = viji.audio.bands.low;\r\n ctx.drawImage(viji.video.currentFrame, 0, 0);\r\n}\r\n```\r\n\r\n```javascript\r\n// Right — check connection state first\r\nfunction render(viji) {\r\n if (viji.audio.isConnected) {\r\n const bass = viji.audio.bands.low;\r\n // ... react to audio\r\n }\r\n\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\r\n }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Enabling All CV Features by Default\r\n\r\nEnabling CV features without user consent wastes resources on devices that can't handle it, and risks WebGL context loss.\r\n\r\n```javascript\r\n// Wrong — activates expensive CV on every device\r\nawait viji.video.cv.enableFaceDetection(true);\r\nawait viji.video.cv.enableHandTracking(true);\r\nawait viji.video.cv.enablePoseDetection(true);\r\nawait viji.video.cv.enableBodySegmentation(true);\r\n```\r\n\r\n```javascript\r\n// Right — let the user opt in\r\nconst useFace = viji.toggle(false, { label: 'Enable Face Tracking', category: 'video' });\r\nconst useHands = viji.toggle(false, { label: 'Enable Hand Tracking', category: 'video' });\r\n\r\nfunction render(viji) {\r\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\r\n else await viji.video.cv.enableFaceDetection(false);\r\n\r\n if (useHands.value) await viji.video.cv.enableHandTracking(true);\r\n else await viji.video.cv.enableHandTracking(false);\r\n}\r\n```\r\n\r\n---\r\n\r\n## Missing `category` on Input-Related Parameters\r\n\r\nParameters tied to audio, video, or interaction inputs should include `category` so the host UI hides them when the input is inactive. Without it, users see controls that do nothing until the corresponding input is connected.\r\n\r\n```javascript\r\n// Wrong — audio parameters are always visible, confusing when no audio is connected\r\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio' });\r\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\r\n```\r\n\r\n```javascript\r\n// Right — parameters are hidden until audio is available\r\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\r\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\r\n```\r\n\r\nThe same applies to `category: 'video'` for video/camera/CV parameters and `category: 'interaction'` for mouse/keyboard/touch parameters. See [Parameter Categories](/native/parameters/categories) for the full guide.\r\n\r\n> [!NOTE]\r\n> `group` and `category` serve different purposes. `group` organizes the UI layout. `category` controls visibility based on active inputs. Having `group: 'audio'` without `category: 'audio'` means the parameter is in the \"audio\" section but always visible — even when no audio source is connected.\r\n\r\n---\r\n\r\n## Related\r\n\r\n- [Best Practices](../best-practices/) — positive guidance for writing robust scenes\r\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer\r\n- [Audio](/native/audio) — audio connection and analysis API\r\n- [Video & CV](/native/video) — video stream and computer vision features"
930
930
  }
931
931
  ]
932
932
  },
@@ -937,7 +937,7 @@ export const docsApi = {
937
937
  "content": [
938
938
  {
939
939
  "type": "text",
940
- "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. Assess their experience level and recommend a renderer:\n - **No coding experience** → recommend **P5** (most approachable for beginners)\n - **Knows JavaScript/Canvas** → recommend **Native** (maximum control)\n - **Wants GPU effects, patterns, or has shader experience** → recommend **Shader**\n - **Wants 3D with Three.js or custom WebGL** → recommend **Native**\n - **Knows P5.js/Processing** → recommend **P5**\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.\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 (for AI assistants with web access)\n\nFor the latest API documentation, type definitions, and all uniform details:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference: https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n- NPM package: https://www.npmjs.com/package/@viji-dev/core\n\n## VIJI ARCHITECTURE (all renderers)\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is NO DOM access.\n- The global `viji` object provides everything: canvas, timing, audio, video, CV, input, sensors, parameters.\n- **Top-level code** runs once (parameters, state, imports).\n- **`render(viji)` / `render(viji, p5)` / `void main()`** runs every frame.\n- `fetch()` is available. `window`, `document`, `Image()` are NOT.\n\n## SCENE STRUCTURE PER RENDERER\n\n### Native\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx...\n}\n```\n\n### P5\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.circle(), p5.rect(), etc. (all need p5. prefix)\n}\n```\n\n### Shader (GLSL)\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## PARAMETERS (all renderers)\n\nArtists control scenes through parameters — declared once, shown as UI controls.\n\n**Native/P5 syntax** (top-level):\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // → .value: number\nviji.color(default, { label }) // → .value: '#rrggbb'\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)\n```\n\n**Shader syntax** (comment directives):\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 → uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 → uniform vec3 name;\n// @viji-toggle:name label:\"Toggle\" default:false → uniform bool name;\n// @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\"] → uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 → uniform float name;\n// @viji-image:name label:\"Texture\" → uniform sampler2D name;\n// @viji-button:name label:\"Reset\" → uniform bool name;\n// @viji-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`, `volume.current`, `volume.peak`, `volume.smoothed`\n- `bands.low`, `bands.lowMid`, `bands.mid`, `bands.highMid`, `bands.high` (+ smoothed variants)\n- `beat.kick`, `beat.snare`, `beat.hat`, `beat.any` (+ `.triggers.kick` etc. for single-frame)\n- `beat.bpm`, `beat.confidence`, `beat.isLocked`\n- `spectral.brightness`, `spectral.flatness`\n- `getFrequencyData()`, `getWaveform()`\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`.\n\n**Additional audio streams** (`viji.audioStreams[]`, and `device.audio` on each `viji.devices[]` entry): lightweight **`AudioStreamAPI`** — `isConnected`, `volume` (current/peak/smoothed), `bands` (low/lowMid/mid/highMid/high + smoothed variants), `spectral` (brightness/flatness), `getFrequencyData()`, `getWaveform()`. **No** beat detection, BPM, triggers, or events. Shader: `u_audioStreamCount`, `u_audioStream{i}Connected`, `u_audioStream{i}Volume`, `Low`, `LowMid`, `Mid`, `HighMid`, `High`, `Brightness`, `Flatness` for `i` = 0–7. **No** per-stream FFT or waveform textures (`u_audioFFT` / `u_audioWaveform` apply only to the main audio source).\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\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[]` (id, name, motion, orientation, video, audio) / Shader: `u_device0`–`u_device7` textures, sensors, connection status; device audio uses `u_audioStream{i}*` scalars (see Streams)\n**Streams**: `viji.videoStreams[]` (host-provided additional video) / Shader: `u_videoStream0`–`u_videoStream7`. **`viji.audioStreams[]`** (host-provided additional audio) / Shader: `u_audioStreamCount`, `u_audioStream{i}Connected`, `u_audioStream{i}Volume` / `Low` / `LowMid` / `Mid` / `HighMid` / `High` / `Brightness` / `Flatness` for `i` = 0–7 (scalars only — no per-stream FFT/waveform textures)\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 allocate objects/arrays inside `render()` — pre-allocate at top level.\n6. ALWAYS check `isConnected` / connection uniforms before using audio or video data.\n7. NEVER enable CV features by default — use toggle parameters.\n8. In P5: ALWAYS prefix every P5 function/constant with `p5.`. NEVER use `createCanvas()`.\n9. 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, 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 robust scenes\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid"
940
+ "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. Assess their experience level and recommend a renderer:\n - **No coding experience** → recommend **P5** (most approachable for beginners)\n - **Knows JavaScript/Canvas** → recommend **Native** (maximum control)\n - **Wants GPU effects, patterns, or has shader experience** → recommend **Shader**\n - **Wants 3D with Three.js or custom WebGL** → recommend **Native**\n - **Knows P5.js/Processing** → recommend **P5**\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.\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 (for AI assistants with web access)\n\nFor the latest API documentation, type definitions, and all uniform details:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference: https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n- NPM package: https://www.npmjs.com/package/@viji-dev/core\n\n## VIJI ARCHITECTURE (all renderers)\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is NO DOM access.\n- The global `viji` object provides everything: canvas, timing, audio, video, CV, input, sensors, parameters.\n- **Top-level code** runs once (parameters, state, imports).\n- **`render(viji)` / `render(viji, p5)` / `void main()`** runs every frame.\n- `fetch()` is available. `window`, `document`, `Image()` are NOT.\n\n## SCENE STRUCTURE PER RENDERER\n\n### Native\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx...\n}\n```\n\n### P5\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.circle(), p5.rect(), etc. (all need p5. prefix)\n}\n```\n\n### Shader (GLSL)\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## PARAMETERS (all renderers)\n\nArtists control scenes through parameters — declared once, shown as UI controls.\n\n**Native/P5 syntax** (top-level):\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // → .value: number\nviji.color(default, { label }) // → .value: '#rrggbb'\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)\n```\n\n**Shader syntax** (comment directives):\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 → uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 → uniform vec3 name;\n// @viji-toggle:name label:\"Toggle\" default:false → uniform bool name;\n// @viji-select:name label:\"Mode\" default:0 options:[\"A\",\"B\"] → uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 → uniform float name;\n// @viji-image:name label:\"Texture\" → uniform sampler2D name;\n// @viji-button:name label:\"Reset\" → uniform bool name;\n// @viji-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`, `volume.current`, `volume.peak`, `volume.smoothed`\n- `bands.low`, `bands.lowMid`, `bands.mid`, `bands.highMid`, `bands.high` (+ smoothed variants)\n- `beat.kick`, `beat.snare`, `beat.hat`, `beat.any` (+ `.triggers.kick` etc. for single-frame)\n- `beat.bpm`, `beat.confidence`, `beat.isLocked`\n- `spectral.brightness`, `spectral.flatness`\n- `getFrequencyData()`, `getWaveform()`\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`.\n\n**Additional audio streams** (`viji.audioStreams[]`, and `device.audio` on each `viji.devices[]` entry): lightweight **`AudioStreamAPI`** — `isConnected`, `volume` (current/peak/smoothed), `bands` (low/lowMid/mid/highMid/high + smoothed variants), `spectral` (brightness/flatness), `getFrequencyData()`, `getWaveform()`. **No** beat detection, BPM, triggers, or events. Shader: `u_audioStreamCount`, `u_audioStream{i}Connected`, `u_audioStream{i}Volume`, `Low`, `LowMid`, `Mid`, `HighMid`, `High`, `Brightness`, `Flatness` for `i` = 0–7. **No** per-stream FFT or waveform textures (`u_audioFFT` / `u_audioWaveform` apply only to the main audio source).\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\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[]` (id, name, motion, orientation, video, audio) / Shader: `u_device0`–`u_device7` textures, sensors, connection status; device audio uses `u_audioStream{i}*` scalars (see Streams)\n**Streams**: `viji.videoStreams[]` (host-provided additional video) / Shader: `u_videoStream0`–`u_videoStream7`. **`viji.audioStreams[]`** (host-provided additional audio) / Shader: `u_audioStreamCount`, `u_audioStream{i}Connected`, `u_audioStream{i}Volume` / `Low` / `LowMid` / `Mid` / `HighMid` / `High` / `Brightness` / `Flatness` for `i` = 0–7 (scalars only — no per-stream FFT/waveform textures)\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, 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 robust scenes\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid"
941
941
  }
942
942
  ]
943
943
  },
@@ -948,7 +948,7 @@ export const docsApi = {
948
948
  "content": [
949
949
  {
950
950
  "type": "text",
951
- "markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene — a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you produce complete, working scene code. Apply every rule below exactly.\n\n## REFERENCE (for AI assistants with web access)\n\nThis prompt is self-contained — all information needed is included below.\nFor the latest API documentation and type definitions:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- NPM package: https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports).\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- Optional **`async function setup(viji) { ... }`** runs once before the first `render`.\n- **Top-level `await`** is supported — you can dynamically import libraries.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time` — elapsed seconds, best for oscillations and direct time-based effects.\n - `viji.deltaTime` — seconds since last frame, best for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n7. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')` — Canvas 2D\n - `viji.useContext('webgl')` — WebGL 1\n - `viji.useContext('webgl2')` — WebGL 2\n Calling a different type after the first returns `null`.\n8. ALWAYS check `viji.audio.isConnected` before using audio data.\n9. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n10. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n11. Be mindful of WebGL context limits — each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n12. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\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` | Current frames per second |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb' }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\n```\n\n### Audio — `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0–1 |\n| `volume.peak` | `number` | Peak amplitude 0–1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20–120 Hz energy 0–1 |\n| `bands.lowMid` | `number` | 120–400 Hz energy 0–1 |\n| `bands.mid` | `number` | 400–1600 Hz energy 0–1 |\n| `bands.highMid` | `number` | 1600–6000 Hz energy 0–1 |\n| `bands.high` | `number` | 6000–16000 Hz energy 0–1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0–1 |\n| `beat.snare` | `number` | Snare energy 0–1 |\n| `beat.hat` | `number` | Hi-hat energy 0–1 |\n| `beat.any` | `number` | Any beat energy 0–1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60–240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0–1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0–1 |\n| `spectral.flatness` | `number` | Spectral flatness 0–1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0–255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`viji.audioStreams` & `device.audio`:** Host and external devices may expose additional sources as **`AudioStreamAPI`** — same `isConnected`, `volume`, `bands` (+ smoothed), `spectral`, `getFrequencyData()`, and `getWaveform()` as above, but **no** `beat`, BPM, triggers, or events (lightweight subset).\n\n### Video — `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video: `ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height)`\n\n### Computer Vision — `viji.video.cv` & `viji.video.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0–1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0–1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: 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 — all 0–1).\n\n**`viji.video.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0–1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0–1 confidence).\n\n**`viji.video.pose: PoseData | null`**\n`confidence` (0–1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input — Pointer (unified mouse/touch) — `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input — Mouse — `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input — Keyboard — `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input — Touch — `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors — `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0–360° compass), `beta` (−180–180° tilt), `gamma` (−90–90° tilt), `absolute` (boolean).\n\n### External Devices — `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null — same as viji.video but without CV), `audio` (AudioStreamAPI|null — lightweight analysis only; no beat/BPM/triggers).\n\n### Streams — `viji.videoStreams`\n\n`VideoAPI[]` — additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams — `viji.audioStreams`\n\n`AudioStreamAPI[]` — additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()` — **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. Use `viji.deltaTime` accumulators for parameter-driven animation to prevent jumps:\n ```javascript\n let angle = 0;\n function render(viji) { angle += speed.value * viji.deltaTime; }\n ```\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level — never inside `render()`.\n4. For CV, use toggle parameters — never enable by default.\n5. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow generate a Viji native scene based on the artist's description below. Return ONLY the scene code.\nFollow all rules. Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want — be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene) — guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips) — how to get better results from AI\n- [Native Quick Start](/native/quickstart) — your first Viji native scene\n- [Native API Reference](/native/api-reference) — full API reference\n- [Best Practices](/getting-started/best-practices) — essential patterns for robust scenes\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid"
951
+ "markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene — a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you produce complete, working scene code. Apply every rule below exactly.\n\n## REFERENCE (for AI assistants with web access)\n\nThis prompt is self-contained — all information needed is included below.\nFor the latest API documentation and type definitions:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- NPM package: https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time` — elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\n - `viji.deltaTime` — seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER multiply `viji.time` by a parameter for animation speed — it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\n ```javascript\n // WRONG — jumps when speed changes:\n const t = viji.time * speed.value;\n // RIGHT — smooth:\n let phase = 0; // top level\n phase += speed.value * viji.deltaTime; // in render()\n ```\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\n ```javascript\n // WRONG — jumps when rotSpeed changes:\n const rot = phase * rotSpeed.value;\n // RIGHT — give it its own accumulator:\n let rotPhase = 0; // top level\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\n ```\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')` — Canvas 2D\n - `viji.useContext('webgl')` — WebGL 1\n - `viji.useContext('webgl2')` — WebGL 2\n Calling a different type after the first returns `null`.\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n12. Be mindful of WebGL context limits — each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive.\n ```javascript\n const audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\n const followMouse = viji.toggle(true, { label: 'Follow Mouse', group: 'interaction', category: 'interaction' });\n ```\n14. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb' }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\n```\n\n### Audio — `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0–1 |\n| `volume.peak` | `number` | Peak amplitude 0–1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20–120 Hz energy 0–1 |\n| `bands.lowMid` | `number` | 120–400 Hz energy 0–1 |\n| `bands.mid` | `number` | 400–1600 Hz energy 0–1 |\n| `bands.highMid` | `number` | 1600–6000 Hz energy 0–1 |\n| `bands.high` | `number` | 6000–16000 Hz energy 0–1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0–1 |\n| `beat.snare` | `number` | Snare energy 0–1 |\n| `beat.hat` | `number` | Hi-hat energy 0–1 |\n| `beat.any` | `number` | Any beat energy 0–1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60–240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0–1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0–1 |\n| `spectral.flatness` | `number` | Spectral flatness 0–1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0–255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`viji.audioStreams` & `device.audio`:** Host and external devices may expose additional sources as **`AudioStreamAPI`** — same `isConnected`, `volume`, `bands` (+ smoothed), `spectral`, `getFrequencyData()`, and `getWaveform()` as above, but **no** `beat`, BPM, triggers, or events (lightweight subset).\n\n### Video — `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video: `ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height)`\n\n### Computer Vision — `viji.video.cv` & `viji.video.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0–1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0–1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: 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 — all 0–1).\n\n**`viji.video.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0–1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0–1 confidence).\n\n**`viji.video.pose: PoseData | null`**\n`confidence` (0–1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input — Pointer (unified mouse/touch) — `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input — Mouse — `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input — Keyboard — `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input — Touch — `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors — `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0–360° compass), `beta` (−180–180° tilt), `gamma` (−90–90° tilt), `absolute` (boolean).\n\n### External Devices — `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null — same as viji.video but without CV), `audio` (AudioStreamAPI|null — lightweight analysis only; no beat/BPM/triggers).\n\n### Streams — `viji.videoStreams`\n\n`VideoAPI[]` — additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams — `viji.audioStreams`\n\n`AudioStreamAPI[]` — additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()` — **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value` — use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level — never inside `render()`.\n4. For CV, use toggle parameters — never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow generate a Viji native scene based on the artist's description below. Return ONLY the scene code.\nFollow all rules. Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want — be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene) — guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips) — how to get better results from AI\n- [Native Quick Start](/native/quickstart) — your first Viji native scene\n- [Native API Reference](/native/api-reference) — full API reference\n- [Best Practices](/getting-started/best-practices) — essential patterns for robust scenes\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid"
952
952
  }
953
953
  ]
954
954
  },
@@ -959,7 +959,7 @@ export const docsApi = {
959
959
  "content": [
960
960
  {
961
961
  "type": "text",
962
- "markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene — a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js.\nArtists describe what they want; you produce complete, working scene code. Apply every rule below exactly.\n\n## REFERENCE (for AI assistants with web access)\n\nThis prompt is self-contained — all information needed is included below.\nFor the latest API documentation and type definitions:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- NPM package: https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode** — every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene’s needs.\n2. ALWAYS use `render(viji, p5)` — not `draw()`. ALWAYS use `setup(viji, p5)` — not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n17. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n18. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only — `p5.drawingContext` is WebGL, not Canvas 2D.\n ```javascript\n if (viji.video.isConnected && viji.video.currentFrame) {\n p5.image(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n }\n ```\n19. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n20. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n21. `p5.tint()` and `p5.blendMode()` work normally.\n22. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)` — Viji creates the canvas in the correct mode.\n23. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context — never use Canvas 2D–only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n24. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n25. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n26. ALWAYS check `viji.audio.isConnected` before using audio data.\n27. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n28. NEVER enable CV features by default — use toggle parameters for user opt-in.\n29. `viji.useContext()` is NOT available in P5 scenes — the canvas is managed by P5.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Current frames per second |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb' }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\n```\n\n### Audio — `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0–1 |\n| `volume.peak` | `number` | Peak amplitude 0–1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20–120 Hz energy 0–1 |\n| `bands.lowMid` | `number` | 120–400 Hz energy 0–1 |\n| `bands.mid` | `number` | 400–1600 Hz energy 0–1 |\n| `bands.highMid` | `number` | 1600–6000 Hz energy 0–1 |\n| `bands.high` | `number` | 6000–16000 Hz energy 0–1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0–1 |\n| `beat.snare` | `number` | Snare energy 0–1 |\n| `beat.hat` | `number` | Hi-hat energy 0–1 |\n| `beat.any` | `number` | Any beat energy 0–1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60–240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0–1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0–1 |\n| `spectral.flatness` | `number` | Spectral flatness 0–1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0–255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`viji.audioStreams` & `device.audio`:** Host and external devices may expose additional sources as **`AudioStreamAPI`** — same `isConnected`, `volume`, `bands` (+ smoothed), `spectral`, `getFrequencyData()`, and `getWaveform()` as above, but **no** `beat`, BPM, triggers, or events (lightweight subset).\n\n### Video — `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5: `p5.drawingContext.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height)`\n\n### Computer Vision — `viji.video.cv` & `viji.video.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0–1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0–1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: 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 — all 0–1).\n\n**`viji.video.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0–1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0–1 confidence).\n\n**`viji.video.pose: PoseData | null`**\n`confidence` (0–1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input — Pointer (unified mouse/touch) — `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input — Mouse — `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input — Keyboard — `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input — Touch — `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors — `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0–360° compass), `beta` (−180–180° tilt), `gamma` (−90–90° tilt), `absolute` (boolean).\n\n### External Devices — `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null — same as viji.video but without CV).\n\n### Streams — `viji.videoStreams`\n\n`VideoAPI[]` — additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove — host controls frame rate |\n| `createCanvas(w, h)` | Remove — canvas is provided |\n| `preload()` | Remove — use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove — host handles capture |\n\n## BEST PRACTICES\n\n1. Use `viji.deltaTime` accumulators for smooth, frame-rate-independent animation.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level — never inside `render()`.\n4. For CV, use toggle parameters — never enable by default.\n5. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n6. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow generate a Viji P5 scene based on the artist's description below. Return ONLY the scene code.\nFollow all rules. Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene) — guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips) — how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5) — convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart) — your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference) — full API reference\n- [Drawing with P5](/p5/drawing) — Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/) — full P5.js documentation"
962
+ "markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene — a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js.\nArtists describe what they want; you produce complete, working scene code. Apply every rule below exactly.\n\n## REFERENCE (for AI assistants with web access)\n\nThis prompt is self-contained — all information needed is included below.\nFor the latest API documentation and type definitions:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- NPM package: https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode** — every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene’s needs.\n2. ALWAYS use `render(viji, p5)` — not `draw()`. ALWAYS use `setup(viji, p5)` — not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed — it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications — never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG — jumps: const t = viji.time * speed.value;\n // WRONG — nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only — `p5.drawingContext` is WebGL, not Canvas 2D.\n ```javascript\n if (viji.video.isConnected && viji.video.currentFrame) {\n p5.image(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n }\n ```\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)` — Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context — never use Canvas 2D–only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default — use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive.\n ```javascript\n const audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\n const followMouse = viji.toggle(true, { label: 'Follow Mouse', group: 'interaction', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes — the canvas is managed by P5.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb' }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\n```\n\n### Audio — `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0–1 |\n| `volume.peak` | `number` | Peak amplitude 0–1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20–120 Hz energy 0–1 |\n| `bands.lowMid` | `number` | 120–400 Hz energy 0–1 |\n| `bands.mid` | `number` | 400–1600 Hz energy 0–1 |\n| `bands.highMid` | `number` | 1600–6000 Hz energy 0–1 |\n| `bands.high` | `number` | 6000–16000 Hz energy 0–1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0–1 |\n| `beat.snare` | `number` | Snare energy 0–1 |\n| `beat.hat` | `number` | Hi-hat energy 0–1 |\n| `beat.any` | `number` | Any beat energy 0–1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60–240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0–1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0–1 |\n| `spectral.flatness` | `number` | Spectral flatness 0–1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0–255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`viji.audioStreams` & `device.audio`:** Host and external devices may expose additional sources as **`AudioStreamAPI`** — same `isConnected`, `volume`, `bands` (+ smoothed), `spectral`, `getFrequencyData()`, and `getWaveform()` as above, but **no** `beat`, BPM, triggers, or events (lightweight subset).\n\n### Video — `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5: `p5.drawingContext.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height)`\n\n### Computer Vision — `viji.video.cv` & `viji.video.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0–1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0–1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: 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 — all 0–1).\n\n**`viji.video.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0–1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0–1 confidence).\n\n**`viji.video.pose: PoseData | null`**\n`confidence` (0–1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input — Pointer (unified mouse/touch) — `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input — Mouse — `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input — Keyboard — `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input — Touch — `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors — `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0–360° compass), `beta` (−180–180° tilt), `gamma` (−90–90° tilt), `absolute` (boolean).\n\n### External Devices — `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null — same as viji.video but without CV), `audio` (AudioStreamAPI|null — lightweight analysis only; no beat/BPM/triggers).\n\n### Streams — `viji.videoStreams`\n\n`VideoAPI[]` — additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams — `viji.audioStreams`\n\n`AudioStreamAPI[]` — additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()` — **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove — host controls frame rate |\n| `createCanvas(w, h)` | Remove — canvas is provided |\n| `preload()` | Remove — use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove — host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value` — use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level — never inside `render()`.\n4. For CV, use toggle parameters — never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow generate a Viji P5 scene based on the artist's description below. Return ONLY the scene code.\nFollow all rules. Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene) — guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips) — how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5) — convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart) — your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference) — full API reference\n- [Drawing with P5](/p5/drawing) — Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/) — full P5.js documentation"
963
963
  }
964
964
  ]
965
965
  },
@@ -970,7 +970,7 @@ export const docsApi = {
970
970
  "content": [
971
971
  {
972
972
  "type": "text",
973
- "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 produce complete, working GLSL code. Apply every rule below exactly.\n\n## REFERENCE (for AI assistants with web access)\n\nThis prompt is self-contained — all information needed is included below.\nFor the latest API documentation and type definitions:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference: https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n- NPM package: 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 ```\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.\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` | Current frames per second |\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 0–1 |\n| `u_audioSnare` | `float` | Snare energy 0–1 |\n| `u_audioHat` | `float` | Hi-hat energy 0–1 |\n| `u_audioAny` | `float` | Any beat energy 0–1 |\n| `u_audioKickSmoothed` – `u_audioAnySmoothed` | `float` | Smoothed beat values |\n| `u_audioKickTrigger` | `bool` | True on kick beat frame |\n| `u_audioSnareTrigger` | `bool` | True on snare beat frame |\n| `u_audioHatTrigger` | `bool` | True on hat beat frame |\n| `u_audioAnyTrigger` | `bool` | True on any beat frame |\n| `u_audioBPM` | `float` | Estimated BPM (60–240) |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0–1 |\n| `u_audioIsLocked` | `bool` | True when BPM is locked |\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 (−1 to 1) |\n\n> [!NOTE]\n> **`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** in this reference.\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| 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]\n> **Device audio** (when an external device provides an audio source) is exposed as **`u_audioStream{i}*`** scalar uniforms — same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0–7). There are **no** per-device or per-stream FFT/waveform textures; only the **main** audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0–8) |\n| `u_videoStream0` – `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` – `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` – `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0–8) |\n| `u_audioStream0Connected` – `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0–1 |\n| `u_audioStream{i}Low` – `u_audioStream{i}High` | `float` | Band energies 0–1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0–1 |\n\n(`i` = 0…7.) **Lightweight scalars only** — **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0–1)\n\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-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 generate a Viji shader scene based on the artist's description below. Return ONLY the GLSL code.\nFollow all rules. 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````\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"
973
+ "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 produce complete, working GLSL code. Apply every rule below exactly.\n\n## REFERENCE (for AI assistants with web access)\n\nThis prompt is self-contained — all information needed is included below.\nFor the latest API documentation and type definitions:\n- Complete docs (all pages + examples): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- TypeScript API types: https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference: https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n- NPM package: 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:\n ```glsl\n // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true group:audio category:audio\n // @viji-toggle:showVideo label:\"Show Video\" default:true group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 group:interaction category:interaction\n ```\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 0–1 |\n| `u_audioSnare` | `float` | Snare energy 0–1 |\n| `u_audioHat` | `float` | Hi-hat energy 0–1 |\n| `u_audioAny` | `float` | Any beat energy 0–1 |\n| `u_audioKickSmoothed` – `u_audioAnySmoothed` | `float` | Smoothed beat values |\n| `u_audioKickTrigger` | `bool` | True on kick beat frame |\n| `u_audioSnareTrigger` | `bool` | True on snare beat frame |\n| `u_audioHatTrigger` | `bool` | True on hat beat frame |\n| `u_audioAnyTrigger` | `bool` | True on any beat frame |\n| `u_audioBPM` | `float` | Estimated BPM (60–240) |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0–1 |\n| `u_audioIsLocked` | `bool` | True when BPM is locked |\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 (−1 to 1) |\n\n> [!NOTE]\n> **`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** in this reference.\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| 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]\n> **Device audio** (when an external device provides an audio source) is exposed as **`u_audioStream{i}*`** scalar uniforms — same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0–7). There are **no** per-device or per-stream FFT/waveform textures; only the **main** audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0–8) |\n| `u_videoStream0` – `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` – `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` – `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0–8) |\n| `u_audioStream0Connected` – `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0–1 |\n| `u_audioStream{i}Low` – `u_audioStream{i}High` | `float` | Band energies 0–1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0–1 |\n\n(`i` = 0…7.) **Lightweight scalars only** — **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0–1)\n\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-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 generate a Viji shader scene based on the artist's description below. Return ONLY the GLSL code.\nFollow all rules. 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````\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"
974
974
  }
975
975
  ]
976
976
  },
@@ -981,7 +981,7 @@ export const docsApi = {
981
981
  "content": [
982
982
  {
983
983
  "type": "text",
984
- "markdown": "# Prompting Tips\n\nThese tips help you communicate with AI assistants more effectively when creating Viji scenes. No coding knowledge required — just better descriptions lead to better results.\n\n## Start With What You See\n\nDescribe the visual, not the code. AI assistants are great at translating visual descriptions into working scenes.\n\n**Vague (harder for AI):**\n> \"Make something cool with audio\"\n\n**Specific (better results):**\n> \"A field of circles that pulse outward from the center when the bass hits. Use warm colors — oranges and reds. The circles should fade out as they expand.\"\n\n### What to Include in Your Description\n\n- **Shapes and objects**: circles, lines, particles, grid, waves, text, images\n- **Colors and mood**: warm, cool, neon, pastel, monochrome, gradient, dark background\n- **Motion**: spinning, pulsing, flowing, bouncing, drifting, exploding, morphing\n- **Data sources**: \"reacts to music,\" \"uses the camera,\" \"follows the mouse,\" \"responds to device tilt\"\n- **Controls**: \"let me adjust the speed,\" \"add a color picker,\" \"toggle the effect on/off\"\n\n## Build Up, Don't Over-specify\n\nStart with a simple version and iterate. This gives you a working base to refine.\n\n**Round 1** — Get the basics:\n> \"Colorful particles that float upward and fade out\"\n\n**Round 2** — Add reactivity:\n> \"Make them react to audio — spawn more particles on kick beats, and make particle size follow the bass level\"\n\n**Round 3** — Polish:\n> \"Add a slider for particle count, a color picker for the base color, and make the background slowly shift hue over time\"\n\nThis iterative approach works better than a single long prompt because you can see each step and correct course.\n\n## Request Specific Data Sources\n\nBe explicit about what should drive the visuals:\n\n| You want... | Say... |\n|---|---|\n| Audio-reactive | \"React to the music,\" \"pulse with the bass,\" \"follow the beat\" |\n| Camera/video | \"Use the camera feed,\" \"show the video as a background\" |\n| Face tracking | \"Track my face,\" \"use face expressions,\" \"follow eye blinks\" |\n| Hand tracking | \"Track my hands,\" \"respond to gestures,\" \"follow palm position\" |\n| Pose detection | \"Track my body,\" \"use body position,\" \"full body skeleton\" |\n| Mouse/touch | \"Follow the mouse,\" \"draw where I touch,\" \"drag to control\" |\n| Device tilt | \"Respond to tilting the phone,\" \"use accelerometer\" |\n| Keyboard | \"Use arrow keys to move,\" \"press space to trigger\" |\n\n## Request Parameters\n\nParameters give you sliders, toggles, and other controls in the Viji UI. Ask for them explicitly:\n\n> \"Add a slider for speed from 0.1 to 5\"\n> \"Add a color picker for the main color\"\n> \"Add a toggle to enable/disable the camera effect\"\n> \"Let me choose between 3 blend modes with a dropdown\"\n> \"Add an image upload for a texture\"\n\n## Choose the Right Renderer\n\nIf you're not sure which renderer to use, the [Create Your First Scene](/ai-prompts/create-first-scene) prompt will help you decide. But here's a quick guide:\n\n| If you want... | Use... |\n|---|---|\n| Familiar creative coding (shapes, colors, transforms) | **P5** |\n| Full control, Three.js, complex state | **Native** |\n| GPU patterns, fractals, raymarching | **Shader** |\n| Video post-processing | **Shader** or **Native** |\n| Particle systems with physics | **Native** |\n| Audio visualizer with bars/circles | **P5** or **Native** |\n| Abstract generative art | **Shader** |\n\n## Troubleshoot Common AI Mistakes\n\nIf the generated scene doesn't work, here are common issues and how to fix them:\n\n| Symptom | Likely cause | What to tell the AI |\n|---|---|---|\n| Jerky or speed-dependent animation | Using frame count instead of time | \"Use `viji.deltaTime` for animation, not frame counting\" |\n| Nothing renders / blank screen | Missing `isConnected` check | \"Check `isConnected` before using audio/video data\" |\n| P5 functions don't work | Missing `p5.` prefix | \"Every P5 function needs the `p5.` prefix — it's instance mode\" |\n| Shader compilation error | Redeclared uniforms | \"Don't declare `precision`, `u_time`, or other built-in uniforms — Viji injects them\" |\n| Shader animation jumps when slider moves | Using `u_time * speed` | \"Use `@viji-accumulator` instead of multiplying `u_time` by a parameter\" |\n| Parameters not appearing | Declared inside render | \"Move parameter declarations to the top level, outside `render()`\" |\n| Images/fonts won't load | Using `loadImage()`/`loadFont()` | \"Use `viji.image()` for images. For fonts, use CSS generic names like `monospace`\" |\n| DOM errors | Accessing `window`/`document` | \"The scene runs in a Web Worker — there's no DOM. Use `viji.*` for everything\" |\n| P5 canvas wrong size | Called `createCanvas()` | \"Don't call `createCanvas()` — Viji manages the canvas\" |\n| Camera not showing | Not using Viji video API | \"Use `viji.video.currentFrame` to get the camera feed, not `createCapture()`\" |\n\n## Tips for Non-Coders\n\nIf you've never written code:\n\n1. **Use the guided prompt**: Start with [Create Your First Scene](/ai-prompts/create-first-scene) — it asks you questions and guides the process.\n2. **Describe what you see**: Talk about colors, shapes, and motion. Don't worry about code.\n3. **Ask for explanations**: Say \"explain what each part does\" and the AI will add comments.\n4. **Iterate**: \"Make the circles bigger,\" \"change the color to blue,\" \"make it faster.\"\n5. **Ask for parameters**: \"Add controls so I can adjust the speed and colors without code.\"\n\n## Advanced Prompting\n\n### Combining Effects\n\nYou can describe multiple layers of behavior:\n\n> \"A particle field that reacts to audio beats — particles explode outward on kick beats and slowly drift back. Overlay the camera feed in the background with a slight blur. Add a slider for particle count and a toggle to show/hide the camera.\"\n\n### Converting Existing Code\n\nIf you have code from other platforms:\n\n- **P5.js sketches** → Use [Convert: P5 Sketches](/ai-prompts/convert-p5)\n- **Shadertoy shaders** → Use [Convert: Shadertoy](/ai-prompts/convert-shadertoy)\n- **Three.js scenes** → Use [Convert: Three.js](/ai-prompts/convert-threejs)\n\n### Getting Maximum Quality\n\nFor the most accurate results, use the full renderer-specific prompts instead of the beginner prompt:\n\n- [Prompt: Native Scenes](/ai-prompts/native-prompt) — complete Native API\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt) — complete P5 API\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt) — complete Shader API (270+ uniforms)\n\nThese contain the entire API surface, ensuring the AI never misses a feature or uses incorrect syntax.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene) — guided prompt for beginners\n- [Best Practices](/getting-started/best-practices) — essential patterns for robust scenes\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid"
984
+ "markdown": "# Prompting Tips\n\nThese tips help you communicate with AI assistants more effectively when creating Viji scenes. No coding knowledge required — just better descriptions lead to better results.\n\n## Start With What You See\n\nDescribe the visual, not the code. AI assistants are great at translating visual descriptions into working scenes.\n\n**Vague (harder for AI):**\n> \"Make something cool with audio\"\n\n**Specific (better results):**\n> \"A field of circles that pulse outward from the center when the bass hits. Use warm colors — oranges and reds. The circles should fade out as they expand.\"\n\n### What to Include in Your Description\n\n- **Shapes and objects**: circles, lines, particles, grid, waves, text, images\n- **Colors and mood**: warm, cool, neon, pastel, monochrome, gradient, dark background\n- **Motion**: spinning, pulsing, flowing, bouncing, drifting, exploding, morphing\n- **Data sources**: \"reacts to music,\" \"uses the camera,\" \"follows the mouse,\" \"responds to device tilt\"\n- **Controls**: \"let me adjust the speed,\" \"add a color picker,\" \"toggle the effect on/off\"\n\n## Build Up, Don't Over-specify\n\nStart with a simple version and iterate. This gives you a working base to refine.\n\n**Round 1** — Get the basics:\n> \"Colorful particles that float upward and fade out\"\n\n**Round 2** — Add reactivity:\n> \"Make them react to audio — spawn more particles on kick beats, and make particle size follow the bass level\"\n\n**Round 3** — Polish:\n> \"Add a slider for particle count, a color picker for the base color, and make the background slowly shift hue over time\"\n\nThis iterative approach works better than a single long prompt because you can see each step and correct course.\n\n## Request Specific Data Sources\n\nBe explicit about what should drive the visuals:\n\n| You want... | Say... |\n|---|---|\n| Audio-reactive | \"React to the music,\" \"pulse with the bass,\" \"follow the beat\" |\n| Camera/video | \"Use the camera feed,\" \"show the video as a background\" |\n| Face tracking | \"Track my face,\" \"use face expressions,\" \"follow eye blinks\" |\n| Hand tracking | \"Track my hands,\" \"respond to gestures,\" \"follow palm position\" |\n| Pose detection | \"Track my body,\" \"use body position,\" \"full body skeleton\" |\n| Mouse/touch | \"Follow the mouse,\" \"draw where I touch,\" \"drag to control\" |\n| Device tilt | \"Respond to tilting the phone,\" \"use accelerometer\" |\n| Keyboard | \"Use arrow keys to move,\" \"press space to trigger\" |\n\n## Request Parameters\n\nParameters give you sliders, toggles, and other controls in the Viji UI. Ask for them explicitly:\n\n> \"Add a slider for speed from 0.1 to 5\"\n> \"Add a color picker for the main color\"\n> \"Add a toggle to enable/disable the camera effect\"\n> \"Let me choose between 3 blend modes with a dropdown\"\n> \"Add an image upload for a texture\"\n\n## Choose the Right Renderer\n\nIf you're not sure which renderer to use, the [Create Your First Scene](/ai-prompts/create-first-scene) prompt will help you decide. But here's a quick guide:\n\n| If you want... | Use... |\n|---|---|\n| Familiar creative coding (shapes, colors, transforms) | **P5** |\n| Full control, Three.js, complex state | **Native** |\n| GPU patterns, fractals, raymarching | **Shader** |\n| Video post-processing | **Shader** or **Native** |\n| Particle systems with physics | **Native** |\n| Audio visualizer with bars/circles | **P5** or **Native** |\n| Abstract generative art | **Shader** |\n\n## Troubleshoot Common AI Mistakes\n\nIf the generated scene doesn't work, here are common issues and how to fix them:\n\n| Symptom | Likely cause | What to tell the AI |\n|---|---|---|\n| Jerky or speed-dependent animation | Using frame count instead of time | \"Use `viji.deltaTime` for animation, not frame counting\" |\n| Nothing renders / blank screen | Missing `isConnected` check | \"Check `isConnected` before using audio/video data\" |\n| P5 functions don't work | Missing `p5.` prefix | \"Every P5 function needs the `p5.` prefix — it's instance mode\" |\n| Shader compilation error | Redeclared uniforms | \"Don't declare `precision`, `u_time`, or other built-in uniforms — Viji injects them\" |\n| Shader animation jumps when slider moves | Using `u_time * speed` | \"Use `@viji-accumulator` instead of multiplying `u_time` by a parameter\" |\n| JS/P5 animation jumps when slider moves | Using `viji.time * speed.value` | \"Use a `deltaTime` accumulator: `phase += speed.value * viji.deltaTime` instead of `viji.time * speed.value`\" |\n| Animation still jumps after accumulator fix | Multiplying accumulator by another parameter | \"Give each speed its own accumulator — never multiply an accumulated phase by another parameter\" |\n| Parameters not appearing | Declared inside render | \"Move parameter declarations to the top level, outside `render()`\" |\n| Images/fonts won't load | Using `loadImage()`/`loadFont()` | \"Use `viji.image()` for images. For fonts, use CSS generic names like `monospace`\" |\n| DOM errors | Accessing `window`/`document` | \"The scene runs in a Web Worker — there's no DOM. Use `viji.*` for everything\" |\n| P5 canvas wrong size | Called `createCanvas()` | \"Don't call `createCanvas()` — Viji manages the canvas\" |\n| Camera not showing | Not using Viji video API | \"Use `viji.video.currentFrame` to get the camera feed, not `createCapture()`\" |\n| Audio/video controls visible when input is off | Missing `category` on parameters | \"Add `category: 'audio'` to audio parameters, `category: 'video'` to video parameters, `category: 'interaction'` to interaction parameters\" |\n\n## Tips for Non-Coders\n\nIf you've never written code:\n\n1. **Use the guided prompt**: Start with [Create Your First Scene](/ai-prompts/create-first-scene) — it asks you questions and guides the process.\n2. **Describe what you see**: Talk about colors, shapes, and motion. Don't worry about code.\n3. **Ask for explanations**: Say \"explain what each part does\" and the AI will add comments.\n4. **Iterate**: \"Make the circles bigger,\" \"change the color to blue,\" \"make it faster.\"\n5. **Ask for parameters**: \"Add controls so I can adjust the speed and colors without code.\"\n\n## Advanced Prompting\n\n### Combining Effects\n\nYou can describe multiple layers of behavior:\n\n> \"A particle field that reacts to audio beats — particles explode outward on kick beats and slowly drift back. Overlay the camera feed in the background with a slight blur. Add a slider for particle count and a toggle to show/hide the camera.\"\n\n### Converting Existing Code\n\nIf you have code from other platforms:\n\n- **P5.js sketches** → Use [Convert: P5 Sketches](/ai-prompts/convert-p5)\n- **Shadertoy shaders** → Use [Convert: Shadertoy](/ai-prompts/convert-shadertoy)\n- **Three.js scenes** → Use [Convert: Three.js](/ai-prompts/convert-threejs)\n\n### Getting Maximum Quality\n\nFor the most accurate results, use the full renderer-specific prompts instead of the beginner prompt:\n\n- [Prompt: Native Scenes](/ai-prompts/native-prompt) — complete Native API\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt) — complete P5 API\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt) — complete Shader API (270+ uniforms)\n\nThese contain the entire API surface, ensuring the AI never misses a feature or uses incorrect syntax.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene) — guided prompt for beginners\n- [Best Practices](/getting-started/best-practices) — essential patterns for robust scenes\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid"
985
985
  }
986
986
  ]
987
987
  },
@@ -992,7 +992,7 @@ export const docsApi = {
992
992
  "content": [
993
993
  {
994
994
  "type": "text",
995
- "markdown": "# Convert: P5 Sketches to Viji\r\n\r\nCopy the prompt below and paste it into your AI assistant along with the P5.js sketch you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji-P5 scene.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are converting a standard P5.js sketch into a Viji-P5 scene.\r\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\r\n\r\n## RULES\r\n\r\n1. ALWAYS set the first line from the sketch's canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or 3D primitives on the main canvas. NEVER keep `createCanvas()` — Viji creates the canvas.\r\n2. ALWAYS rename `draw()` to `render(viji, p5)`.\r\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it doesn't exist, do NOT add one.\r\n4. ALWAYS prefix every P5 function and constant with `p5.`:\r\n - `background(0)` → `p5.background(0)`\r\n - `fill(255)` → `p5.fill(255)`\r\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\r\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\r\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\r\n - `noise(x)` → `p5.noise(x)`\r\n This applies to ALL P5 functions and constants without exception.\r\n5. NEVER call `createCanvas()`. The canvas is created and managed by Viji. WEBGL is selected only with `// @renderer p5 webgl`, not with `createCanvas(..., p5.WEBGL)`.\r\n6. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in an async `setup()` for data.\r\n7. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Instead, check state in `render()`:\r\n - `mouseIsPressed` → `viji.pointer.isDown` (works for both mouse and touch) or `viji.mouse.isPressed`\r\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` (works for both mouse and touch) or `viji.mouse.x` / `viji.mouse.y`\r\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\r\n - For press-edge detection: use `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\r\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. These are host-level concerns.\r\n9. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\r\n10. NEVER use `createCapture()` or `createVideo()`. Use `viji.video.*` instead.\r\n11. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\r\n12. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\r\n13. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\r\n ```javascript\r\n // CORRECT\r\n const size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\r\n function render(viji, p5) { p5.circle(0, 0, size.value); }\r\n\r\n // WRONG — creates a new parameter every frame\r\n function render(viji, p5) { const size = viji.slider(50, { ... }); }\r\n ```\r\n14. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`.\r\n15. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\r\n16. ALWAYS use `viji.deltaTime` for frame-rate-independent animation. Replace `frameCount * 0.01` patterns with a deltaTime accumulator:\r\n ```javascript\r\n let angle = 0;\r\n function render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n }\r\n ```\r\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\r\n18. For image parameters displayed with P5, use `photo.p5` (not `photo.value`) with `p5.image()`:\r\n ```javascript\r\n const photo = viji.image(null, { label: 'Photo' });\r\n function render(viji, p5) {\r\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\r\n }\r\n ```\r\n\r\n## API MAPPING\r\n\r\n| Standard P5.js | Viji-P5 |\r\n|---|---|\r\n| `width` / `height` | `viji.width` / `viji.height` |\r\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\r\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\r\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\r\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\r\n| `key` | `viji.keyboard.lastKeyPressed` |\r\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\r\n| `frameRate(n)` | Remove — host controls frame rate |\r\n| `createCanvas(w, h)` / `createCanvas(w, h, WEBGL)` | Remove — use `// @renderer p5` or `// @renderer p5 webgl` |\r\n| `preload()` | Remove — use `viji.image()` or `fetch()` in `setup()` |\r\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\r\n| `save()` | Remove — host uses `captureFrame()` |\r\n\r\n## PARAMETER TYPES\r\n\r\n```javascript\r\nviji.slider(default, { min, max, step, label, group, category }) // returns { value: number }\r\nviji.color(default, { label, group, category }) // returns { value: '#rrggbb' }\r\nviji.toggle(default, { label, group, category }) // returns { value: boolean }\r\nviji.select(default, { options: [...], label, group, category }) // returns { value: string }\r\nviji.number(default, { min, max, step, label, group, category }) // returns { value: number }\r\nviji.text(default, { label, group, category }) // returns { value: string }\r\nviji.image(default, { label, group, category }) // returns { value: ImageBitmap|null, p5: P5Image }\r\nviji.button({ label, description, group, category }) // returns { value: boolean }\r\n```\r\n\r\n## TEMPLATE\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\n\r\nlet angle = 0;\r\n\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB, 360, 100, 100);\r\n}\r\n\r\nfunction render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n p5.background(0, 0, 10);\r\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\r\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\r\n p5.noStroke();\r\n p5.fill(angle * 30 % 360, 80, 100);\r\n p5.circle(x, y, viji.width * 0.05);\r\n}\r\n```\r\n\r\nNow convert the P5.js sketch I provide. Return ONLY the converted Viji-P5 scene code.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant.\r\n3. After the prompt, paste the P5.js sketch you want to convert.\r\n4. The AI will return a Viji-compatible scene.\r\n\r\nFor a detailed human-readable guide, see [Converting P5 Sketches](/p5/converting-sketches).\r\n\r\n## Related\r\n\r\n- [Converting P5 Sketches](/p5/converting-sketches) — step-by-step manual conversion guide\r\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt) — AI prompt for creating new P5 scenes from scratch\r\n- [P5 Quick Start](/p5/quickstart) — your first Viji-P5 scene"
995
+ "markdown": "# Convert: P5 Sketches to Viji\r\n\r\nCopy the prompt below and paste it into your AI assistant along with the P5.js sketch you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji-P5 scene.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are converting a standard P5.js sketch into a Viji-P5 scene.\r\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\r\n\r\n## RULES\r\n\r\n1. ALWAYS set the first line from the sketch's canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or 3D primitives on the main canvas. NEVER keep `createCanvas()` — Viji creates the canvas.\r\n2. ALWAYS rename `draw()` to `render(viji, p5)`.\r\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it doesn't exist, do NOT add one.\r\n4. ALWAYS prefix every P5 function and constant with `p5.`:\r\n - `background(0)` → `p5.background(0)`\r\n - `fill(255)` → `p5.fill(255)`\r\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\r\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\r\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\r\n - `noise(x)` → `p5.noise(x)`\r\n This applies to ALL P5 functions and constants without exception.\r\n5. NEVER call `createCanvas()`. The canvas is created and managed by Viji. WEBGL is selected only with `// @renderer p5 webgl`, not with `createCanvas(..., p5.WEBGL)`.\r\n6. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in an async `setup()` for data.\r\n7. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Instead, check state in `render()`:\r\n - `mouseIsPressed` → `viji.pointer.isDown` (works for both mouse and touch) or `viji.mouse.isPressed`\r\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` (works for both mouse and touch) or `viji.mouse.x` / `viji.mouse.y`\r\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\r\n - For press-edge detection: use `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\r\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. These are host-level concerns.\r\n9. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\r\n10. NEVER use `createCapture()` or `createVideo()`. Use `viji.video.*` instead.\r\n11. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\r\n12. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\r\n13. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\r\n ```javascript\r\n // CORRECT\r\n const size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\r\n function render(viji, p5) { p5.circle(0, 0, size.value); }\r\n\r\n // WRONG — creates a new parameter every frame\r\n function render(viji, p5) { const size = viji.slider(50, { ... }); }\r\n ```\r\n14. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`.\r\n15. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\r\n16. ALWAYS use `viji.deltaTime` for frame-rate-independent animation. Replace `frameCount * 0.01` patterns with a deltaTime accumulator:\r\n ```javascript\r\n let angle = 0;\r\n function render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n }\r\n ```\r\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`) — it causes jumps when the parameter changes. Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\r\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\r\n18. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\r\n19. For image parameters displayed with P5, use `photo.p5` (not `photo.value`) with `p5.image()`:\r\n ```javascript\r\n const photo = viji.image(null, { label: 'Photo' });\r\n function render(viji, p5) {\r\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\r\n }\r\n ```\r\n\r\n## API MAPPING\r\n\r\n| Standard P5.js | Viji-P5 |\r\n|---|---|\r\n| `width` / `height` | `viji.width` / `viji.height` |\r\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\r\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\r\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\r\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\r\n| `key` | `viji.keyboard.lastKeyPressed` |\r\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\r\n| `frameRate(n)` | Remove — host controls frame rate |\r\n| `createCanvas(w, h)` / `createCanvas(w, h, WEBGL)` | Remove — use `// @renderer p5` or `// @renderer p5 webgl` |\r\n| `preload()` | Remove — use `viji.image()` or `fetch()` in `setup()` |\r\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\r\n| `save()` | Remove — host uses `captureFrame()` |\r\n\r\n## PARAMETER TYPES\r\n\r\n```javascript\r\nviji.slider(default, { min, max, step, label, group, category }) // returns { value: number }\r\nviji.color(default, { label, group, category }) // returns { value: '#rrggbb' }\r\nviji.toggle(default, { label, group, category }) // returns { value: boolean }\r\nviji.select(default, { options: [...], label, group, category }) // returns { value: string }\r\nviji.number(default, { min, max, step, label, group, category }) // returns { value: number }\r\nviji.text(default, { label, group, category }) // returns { value: string }\r\nviji.image(default, { label, group, category }) // returns { value: ImageBitmap|null, p5: P5Image }\r\nviji.button({ label, description, group, category }) // returns { value: boolean }\r\n```\r\n\r\n## TEMPLATE\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\n\r\nlet angle = 0;\r\n\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB, 360, 100, 100);\r\n}\r\n\r\nfunction render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n p5.background(0, 0, 10);\r\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\r\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\r\n p5.noStroke();\r\n p5.fill(angle * 30 % 360, 80, 100);\r\n p5.circle(x, y, viji.width * 0.05);\r\n}\r\n```\r\n\r\nNow convert the P5.js sketch I provide. Return ONLY the converted Viji-P5 scene code.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant.\r\n3. After the prompt, paste the P5.js sketch you want to convert.\r\n4. The AI will return a Viji-compatible scene.\r\n\r\nFor a detailed human-readable guide, see [Converting P5 Sketches](/p5/converting-sketches).\r\n\r\n## Related\r\n\r\n- [Converting P5 Sketches](/p5/converting-sketches) — step-by-step manual conversion guide\r\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt) — AI prompt for creating new P5 scenes from scratch\r\n- [P5 Quick Start](/p5/quickstart) — your first Viji-P5 scene"
996
996
  }
997
997
  ]
998
998
  },
@@ -1003,7 +1003,7 @@ export const docsApi = {
1003
1003
  "content": [
1004
1004
  {
1005
1005
  "type": "text",
1006
- "markdown": "# Convert: Shadertoy to Viji\r\n\r\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.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are converting a Shadertoy shader into a Viji shader scene.\r\nViji runs GLSL fragment shaders with automatic uniform injection. Apply every rule below exactly.\r\n\r\n## RULES\r\n\r\n1. ALWAYS add `// @renderer shader` as the very first line (or after `#version 300 es` if using GLSL ES 3.00).\r\n2. NEVER declare `precision mediump float;` or `precision highp float;` — Viji auto-injects precision.\r\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.) — they are auto-injected.\r\n4. NEVER redeclare parameter uniforms — they are auto-generated from `@viji-*` directives.\r\n5. Convert the `mainImage` signature to a standard `void main()`:\r\n - Replace `fragCoord` with `gl_FragCoord.xy`\r\n - Replace `fragColor` with `gl_FragColor`\r\n - Remove the `mainImage` wrapper entirely.\r\n6. ALWAYS replace Shadertoy uniforms with Viji equivalents using this mapping:\r\n\r\n | Shadertoy | Viji | Notes |\r\n |---|---|---|\r\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. For `.z` (aspect ratio), use `u_resolution.x / u_resolution.y`. |\r\n | `iResolution.x`, `.y` | `u_resolution.x`, `.y` | Direct match. |\r\n | `iTime` | `u_time` | Elapsed seconds. |\r\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\r\n | `iFrame` | `u_frame` | Frame counter (`int`). |\r\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\r\n | `iMouse.z` | Approximate with `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click-origin. |\r\n | `iMouse.w` | Approximate with `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click-origin. |\r\n | `iChannel0`–`3` | Declare `@viji-image` parameters | See texture rules below. |\r\n | `iChannelResolution` | Not available | Track dimensions manually if needed. |\r\n | `iDate` | Not available | Use `u_time` for elapsed time. |\r\n | `iSampleRate` | Not available | Not applicable. |\r\n\r\n7. For `iChannel` textures, declare `@viji-image` parameters:\r\n ```glsl\r\n // @viji-image:channel0 label:\"Texture 1\"\r\n // @viji-image:channel1 label:\"Texture 2\"\r\n ```\r\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (or `texture(channel0, uv)` in ES 3.00).\r\n\r\n8. If an `iChannel` is set to **Keyboard** input in Shadertoy, replace it with `u_keyboard`:\r\n ```glsl\r\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\r\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\r\n ```\r\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.\r\n\r\n9. For `iResolution` used as `vec3`, replace with:\r\n ```glsl\r\n vec3(u_resolution, u_resolution.x / u_resolution.y)\r\n ```\r\n Or refactor to use `u_resolution` as `vec2` directly — most code only needs `.xy`.\r\n\r\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:\r\n ```glsl\r\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\r\n // @viji-accumulator:phase rate:speed\r\n ```\r\n Then replace `iTime` with `phase`.\r\n\r\n11. If adding artist-controllable parameters, ALWAYS use `// @viji-*` directives:\r\n ```glsl\r\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\r\n // @viji-color:name label:\"Label\" default:#ff6600\r\n // @viji-toggle:name label:\"Label\" default:true\r\n // @viji-select:name label:\"Label\" default:0 options:[\"A\",\"B\",\"C\"]\r\n // @viji-image:name label:\"Label\"\r\n // @viji-button:name label:\"Label\"\r\n // @viji-accumulator:name rate:source\r\n ```\r\n NEVER use the `u_` prefix for parameter names — it is reserved for built-in uniforms.\r\n\r\n12. Parameter directives ONLY work with `//` comments. NEVER use `/* */` for `@viji-*` directives.\r\n\r\n13. If the shader uses `#version 300 es`:\r\n - Keep it as the very first line (before `// @renderer shader`).\r\n - Replace `gl_FragColor = ...` with `out vec4 fragColor;` (declared before `main`) and `fragColor = ...`.\r\n - Replace `texture2D()` with `texture()`.\r\n - Default (no `#version`) uses GLSL ES 1.00 for maximum compatibility.\r\n\r\n14. Remove any `#ifdef GL_ES` / `precision` blocks — Viji handles this.\r\n\r\n15. For feedback effects that used Shadertoy's Buffer tabs, use Viji's backbuffer:\r\n ```glsl\r\n vec4 prev = texture2D(backbuffer, uv);\r\n ```\r\n `backbuffer` is auto-detected and enabled. This replaces simple Buffer A patterns.\r\n Viji does NOT support multi-buffer pipelines (Buffer A feeding Buffer B). Only single-pass feedback is available.\r\n\r\n16. UNSUPPORTED Shadertoy features — if the source shader uses any of these, warn the user:\r\n - **Multi-buffer pipelines** (Buffer A→B→C→D): only single `backbuffer` available.\r\n - **CubeMap buffer** (`samplerCube`): not supported.\r\n - **3D textures** (`sampler3D`): not supported.\r\n - **`iChannelTime`**: not available.\r\n - **`iChannelResolution`**: not available.\r\n - **Sound output buffer**: not supported.\r\n - **`mainVR()`**: not supported.\r\n - **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\r\n\r\n## UNIFORM QUICK REFERENCE\r\n\r\nThese are always available — do NOT declare them:\r\n- `u_resolution` (vec2), `u_time` (float), `u_deltaTime` (float), `u_frame` (int)\r\n- `u_mouse` (vec2), `u_mousePressed` (bool), `u_mouseLeft` (bool)\r\n- `u_keyboard` (sampler2D, 256x3 keyboard state texture)\r\n- `u_audioVolume` (float), `u_audioLow/Mid/High` (float), `u_audioKick/Snare/Hat` (float)\r\n- `u_video` (sampler2D), `backbuffer` (sampler2D, auto-enabled if used)\r\n\r\n## PARAMETER TYPE → UNIFORM MAPPING\r\n\r\n| Directive | GLSL Type | Example |\r\n|---|---|---|\r\n| `@viji-slider` | `uniform float` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\r\n| `@viji-number` | `uniform float` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\r\n| `@viji-color` | `uniform vec3` | `// @viji-color:tint label:\"Tint\" default:#00ffcc` |\r\n| `@viji-toggle` | `uniform bool` | `// @viji-toggle:invert label:\"Invert\" default:false` |\r\n| `@viji-select` | `uniform int` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"A\",\"B\"]` |\r\n| `@viji-image` | `uniform sampler2D` | `// @viji-image:tex label:\"Texture\"` |\r\n| `@viji-button` | `uniform bool` | `// @viji-button:trigger label:\"Trigger\"` |\r\n| `@viji-accumulator` | `uniform float` | `// @viji-accumulator:phase rate:speed` |\r\n\r\n## CONVERSION TEMPLATE\r\n\r\n```glsl\r\n// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\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 // ... converted shader logic ...\r\n // Use `phase` instead of `iTime`\r\n // Use `u_resolution` instead of `iResolution.xy`\r\n // Use `gl_FragCoord.xy` instead of `fragCoord`\r\n\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n```\r\n\r\nNow convert the Shadertoy shader I provide. Return ONLY the converted Viji shader code.\r\nApply all rules. Do NOT use a compatibility layer — convert directly to native Viji uniforms.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant.\r\n3. After the prompt, paste the Shadertoy shader code (the `mainImage` function).\r\n4. The AI will return a native Viji shader scene.\r\n\r\n> [!TIP]\r\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).\r\n\r\n## Related\r\n\r\n- [Shadertoy Compatibility](/shader/shadertoy) — manual conversion guide with compatibility layer\r\n- [Accumulator](/shader/parameters/accumulator) — how accumulators prevent animation jumps\r\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt) — AI prompt for creating new shaders from scratch\r\n- [Shader Quick Start](/shader/quickstart) — your first Viji shader"
1006
+ "markdown": "# Convert: Shadertoy to Viji\r\n\r\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.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are converting a Shadertoy shader into a Viji shader scene.\r\nViji runs GLSL fragment shaders with automatic uniform injection. Apply every rule below exactly.\r\n\r\n## RULES\r\n\r\n1. ALWAYS add `// @renderer shader` as the very first line (or after `#version 300 es` if using GLSL ES 3.00).\r\n2. NEVER declare `precision mediump float;` or `precision highp float;` — Viji auto-injects precision.\r\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.) — they are auto-injected.\r\n4. NEVER redeclare parameter uniforms — they are auto-generated from `@viji-*` directives.\r\n5. Convert the `mainImage` signature to a standard `void main()`:\r\n - Replace `fragCoord` with `gl_FragCoord.xy`\r\n - Replace `fragColor` with `gl_FragColor`\r\n - Remove the `mainImage` wrapper entirely.\r\n6. ALWAYS replace Shadertoy uniforms with Viji equivalents using this mapping:\r\n\r\n | Shadertoy | Viji | Notes |\r\n |---|---|---|\r\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. For `.z` (aspect ratio), use `u_resolution.x / u_resolution.y`. |\r\n | `iResolution.x`, `.y` | `u_resolution.x`, `.y` | Direct match. |\r\n | `iTime` | `u_time` | Elapsed seconds. |\r\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\r\n | `iFrame` | `u_frame` | Frame counter (`int`). |\r\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\r\n | `iMouse.z` | Approximate with `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click-origin. |\r\n | `iMouse.w` | Approximate with `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click-origin. |\r\n | `iChannel0`–`3` | Declare `@viji-image` parameters | See texture rules below. |\r\n | `iChannelResolution` | Not available | Track dimensions manually if needed. |\r\n | `iDate` | Not available | Use `u_time` for elapsed time. |\r\n | `iSampleRate` | Not available | Not applicable. |\r\n\r\n7. For `iChannel` textures, declare `@viji-image` parameters:\r\n ```glsl\r\n // @viji-image:channel0 label:\"Texture 1\"\r\n // @viji-image:channel1 label:\"Texture 2\"\r\n ```\r\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (or `texture(channel0, uv)` in ES 3.00).\r\n\r\n8. If an `iChannel` is set to **Keyboard** input in Shadertoy, replace it with `u_keyboard`:\r\n ```glsl\r\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\r\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\r\n ```\r\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.\r\n\r\n9. For `iResolution` used as `vec3`, replace with:\r\n ```glsl\r\n vec3(u_resolution, u_resolution.x / u_resolution.y)\r\n ```\r\n Or refactor to use `u_resolution` as `vec2` directly — most code only needs `.xy`.\r\n\r\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:\r\n ```glsl\r\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\r\n // @viji-accumulator:phase rate:speed\r\n ```\r\n Then replace `iTime` with `phase`.\r\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.\r\n\r\n11. If adding artist-controllable parameters, ALWAYS use `// @viji-*` directives:\r\n ```glsl\r\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\r\n // @viji-color:name label:\"Label\" default:#ff6600\r\n // @viji-toggle:name label:\"Label\" default:true\r\n // @viji-select:name label:\"Label\" default:0 options:[\"A\",\"B\",\"C\"]\r\n // @viji-image:name label:\"Label\"\r\n // @viji-button:name label:\"Label\"\r\n // @viji-accumulator:name rate:source\r\n ```\r\n NEVER use the `u_` prefix for parameter names — it is reserved for built-in uniforms.\r\n\r\n12. Parameter directives ONLY work with `//` comments. NEVER use `/* */` for `@viji-*` directives.\r\n\r\n13. If the shader uses `#version 300 es`:\r\n - Keep it as the very first line (before `// @renderer shader`).\r\n - Replace `gl_FragColor = ...` with `out vec4 fragColor;` (declared before `main`) and `fragColor = ...`.\r\n - Replace `texture2D()` with `texture()`.\r\n - Default (no `#version`) uses GLSL ES 1.00 for maximum compatibility.\r\n\r\n14. Remove any `#ifdef GL_ES` / `precision` blocks — Viji handles this.\r\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.\r\n\r\n16. For feedback effects that used Shadertoy's Buffer tabs, use Viji's backbuffer:\r\n ```glsl\r\n vec4 prev = texture2D(backbuffer, uv);\r\n ```\r\n `backbuffer` is auto-detected and enabled. This replaces simple Buffer A patterns.\r\n Viji does NOT support multi-buffer pipelines (Buffer A feeding Buffer B). Only single-pass feedback is available.\r\n\r\n16. UNSUPPORTED Shadertoy features — if the source shader uses any of these, warn the user:\r\n - **Multi-buffer pipelines** (Buffer A→B→C→D): only single `backbuffer` available.\r\n - **CubeMap buffer** (`samplerCube`): not supported.\r\n - **3D textures** (`sampler3D`): not supported.\r\n - **`iChannelTime`**: not available.\r\n - **`iChannelResolution`**: not available.\r\n - **Sound output buffer**: not supported.\r\n - **`mainVR()`**: not supported.\r\n - **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\r\n\r\n## UNIFORM QUICK REFERENCE\r\n\r\nThese are always available — do NOT declare them:\r\n- `u_resolution` (vec2), `u_time` (float), `u_deltaTime` (float), `u_frame` (int)\r\n- `u_mouse` (vec2), `u_mousePressed` (bool), `u_mouseLeft` (bool)\r\n- `u_keyboard` (sampler2D, 256x3 keyboard state texture)\r\n- `u_audioVolume` (float), `u_audioLow/Mid/High` (float), `u_audioKick/Snare/Hat` (float)\r\n- `u_video` (sampler2D), `backbuffer` (sampler2D, auto-enabled if used)\r\n\r\n## PARAMETER TYPE → UNIFORM MAPPING\r\n\r\n| Directive | GLSL Type | Example |\r\n|---|---|---|\r\n| `@viji-slider` | `uniform float` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\r\n| `@viji-number` | `uniform float` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\r\n| `@viji-color` | `uniform vec3` | `// @viji-color:tint label:\"Tint\" default:#00ffcc` |\r\n| `@viji-toggle` | `uniform bool` | `// @viji-toggle:invert label:\"Invert\" default:false` |\r\n| `@viji-select` | `uniform int` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"A\",\"B\"]` |\r\n| `@viji-image` | `uniform sampler2D` | `// @viji-image:tex label:\"Texture\"` |\r\n| `@viji-button` | `uniform bool` | `// @viji-button:trigger label:\"Trigger\"` |\r\n| `@viji-accumulator` | `uniform float` | `// @viji-accumulator:phase rate:speed` |\r\n\r\n## CONVERSION TEMPLATE\r\n\r\n```glsl\r\n// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\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 // ... converted shader logic ...\r\n // Use `phase` instead of `iTime`\r\n // Use `u_resolution` instead of `iResolution.xy`\r\n // Use `gl_FragCoord.xy` instead of `fragCoord`\r\n\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n```\r\n\r\nNow convert the Shadertoy shader I provide. Return ONLY the converted Viji shader code.\r\nApply all rules. Do NOT use a compatibility layer — convert directly to native Viji uniforms.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant.\r\n3. After the prompt, paste the Shadertoy shader code (the `mainImage` function).\r\n4. The AI will return a native Viji shader scene.\r\n\r\n> [!TIP]\r\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).\r\n\r\n## Related\r\n\r\n- [Shadertoy Compatibility](/shader/shadertoy) — manual conversion guide with compatibility layer\r\n- [Accumulator](/shader/parameters/accumulator) — how accumulators prevent animation jumps\r\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt) — AI prompt for creating new shaders from scratch\r\n- [Shader Quick Start](/shader/quickstart) — your first Viji shader"
1007
1007
  }
1008
1008
  ]
1009
1009
  },
@@ -1014,7 +1014,7 @@ export const docsApi = {
1014
1014
  "content": [
1015
1015
  {
1016
1016
  "type": "text",
1017
- "markdown": "# Convert: Three.js to Viji\r\n\r\nCopy the prompt below and paste it into your AI assistant along with the Three.js code you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji native scene with Three.js.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are converting a standalone Three.js application into a Viji native scene.\r\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\r\n\r\n## RULES\r\n\r\n1. ALWAYS import Three.js dynamically at the top level using `await import()`:\r\n ```javascript\r\n const THREE = await import('https://esm.sh/three@0.160.0');\r\n ```\r\n NEVER use `<script>` tags, `require()`, or static `import` statements.\r\n ALWAYS pin the version number in the URL.\r\n\r\n2. ALWAYS use `viji.canvas` as the renderer's canvas:\r\n ```javascript\r\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\r\n renderer.setSize(viji.width, viji.height, false);\r\n ```\r\n ALWAYS pass `false` as the third argument to `setSize()` — this prevents Three.js from setting CSS styles, which would fail in the worker.\r\n\r\n3. NEVER use `requestAnimationFrame()`. Viji controls the render loop. Write all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\r\n\r\n4. ALWAYS handle resize by checking `viji.width` / `viji.height` in `render()`:\r\n ```javascript\r\n let prevWidth = viji.width;\r\n let prevHeight = viji.height;\r\n\r\n function 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 renderer.render(scene, camera);\r\n }\r\n ```\r\n\r\n5. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\r\n\r\n6. NEVER use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\r\n\r\n7. Replace hardcoded values with Viji parameters declared at the top level:\r\n ```javascript\r\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\n const color = viji.color('#049ef4', { label: 'Color' });\r\n ```\r\n NEVER declare parameters inside `render()`.\r\n ALWAYS read via `.value`: `speed.value`, `color.value`.\r\n\r\n8. ALWAYS use `viji.deltaTime` for animation timing:\r\n ```javascript\r\n cube.rotation.y += speed.value * viji.deltaTime;\r\n ```\r\n NEVER use `clock.getDelta()` or `Date.now()`. Remove any `THREE.Clock` usage.\r\n\r\n9. Replace mouse/keyboard event listeners with Viji APIs:\r\n - `event.clientX` → `viji.pointer.x` (works for both mouse and touch) or `viji.mouse.x`\r\n - `event.clientY` → `viji.pointer.y` or `viji.mouse.y`\r\n - Mouse buttons → `viji.mouse.leftButton`, `viji.mouse.rightButton`\r\n - Key presses → `viji.keyboard.isPressed('keyName')`\r\n\r\n10. `OrbitControls` and other controls that depend on DOM events will NOT work in the worker.\r\n For camera interaction, read `viji.pointer` (handles both mouse and touch) and update the camera manually.\r\n\r\n11. For Three.js addons, import from the examples directory:\r\n ```javascript\r\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\r\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\r\n ```\r\n ALWAYS use the same Three.js version for addons as for the main library.\r\n\r\n12. For textures from file inputs, use Viji's image parameters:\r\n ```javascript\r\n const photo = viji.image(null, { label: 'Texture' });\r\n // In render():\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\r\n13. For video textures, use `viji.video`:\r\n ```javascript\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n if (!videoTexture) {\r\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\r\n material.map = videoTexture;\r\n }\r\n videoTexture.needsUpdate = true;\r\n }\r\n ```\r\n\r\n14. NEVER allocate new objects inside `render()`. Pre-create vectors, colors, and materials at the top level.\r\n\r\n15. Remove any `window.addEventListener('resize', ...)` — resize is handled in `render()` (see rule 4).\r\n\r\n16. Remove any CSS, HTML, or DOM manipulation code. Viji scenes produce only canvas output.\r\n\r\n## PARAMETER TYPES\r\n\r\n```javascript\r\nviji.slider(default, { min, max, step, label, group, category }) // { value: number }\r\nviji.color(default, { label, group, category }) // { value: '#rrggbb' }\r\nviji.toggle(default, { label, group, category }) // { value: boolean }\r\nviji.select(default, { options: [...], label, group, category }) // { value: string }\r\nviji.number(default, { min, max, step, label, group, category }) // { value: number }\r\nviji.image(default, { label, group, category }) // { value: ImageBitmap|null }\r\nviji.button({ label, description, group, category }) // { value: boolean }\r\n```\r\n\r\n## VIJI API QUICK REFERENCE\r\n\r\n- `viji.canvas` — the OffscreenCanvas (pass to Three.js renderer)\r\n- `viji.width`, `viji.height` — current canvas dimensions\r\n- `viji.time` — elapsed seconds since scene start\r\n- `viji.deltaTime` — seconds since last frame\r\n- `viji.pointer.x`, `.y`, `.isDown`, `.deltaX`, `.deltaY` — unified mouse/touch input\r\n- `viji.mouse.x`, `.y`, `.isPressed`, `.leftButton`, `.rightButton`, `.wheelDelta`\r\n- `viji.keyboard.isPressed('key')`, `.lastKeyPressed`\r\n- `viji.audio.isConnected`, `.volume.current`, `.bands.low/mid/high`, `.beat.kick/snare/hat`\r\n- `viji.video.isConnected`, `.currentFrame`\r\n\r\n## TEMPLATE\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#049ef4', { label: 'Color' });\r\n\r\nconst scene = new THREE.Scene();\r\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\r\ncamera.position.set(0, 1, 3);\r\ncamera.lookAt(0, 0, 0);\r\n\r\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\r\nrenderer.setSize(viji.width, viji.height, false);\r\n\r\nconst geometry = new THREE.BoxGeometry();\r\nconst material = new THREE.MeshStandardMaterial({ color: color.value });\r\nconst mesh = new THREE.Mesh(geometry, material);\r\nscene.add(mesh);\r\n\r\nscene.add(new THREE.DirectionalLight(0xffffff, 1.5));\r\nscene.add(new THREE.AmbientLight(0x404040));\r\n\r\nlet prevWidth = viji.width;\r\nlet prevHeight = viji.height;\r\n\r\nfunction render(viji) {\r\n mesh.rotation.y += speed.value * viji.deltaTime;\r\n material.color.set(color.value);\r\n\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\nNow convert the Three.js code I provide. Return ONLY the converted Viji scene code.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant.\r\n3. After the prompt, paste the Three.js code you want to convert.\r\n4. The AI will return a Viji-compatible native scene.\r\n\r\n> [!NOTE]\r\n> This prompt handles standard Three.js scenes. If the original code uses a framework (React Three Fiber, Drei, etc.), you may need to manually extract the Three.js scene setup first.\r\n\r\n## Related\r\n\r\n- [External Libraries](/native/external-libraries) — detailed guide for using Three.js and other libraries in Viji\r\n- [Prompt: Native Scenes](/ai-prompts/native-prompt) — AI prompt for creating new native scenes from scratch\r\n- [Native Quick Start](/native/quickstart) — your first Viji native scene"
1017
+ "markdown": "# Convert: Three.js to Viji\r\n\r\nCopy the prompt below and paste it into your AI assistant along with the Three.js code you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji native scene with Three.js.\r\n\r\n## The Prompt\r\n\r\n````\r\nYou are converting a standalone Three.js application into a Viji native scene.\r\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\r\n\r\n## RULES\r\n\r\n1. ALWAYS import Three.js dynamically at the top level using `await import()`:\r\n ```javascript\r\n const THREE = await import('https://esm.sh/three@0.160.0');\r\n ```\r\n NEVER use `<script>` tags, `require()`, or static `import` statements.\r\n ALWAYS pin the version number in the URL.\r\n\r\n2. ALWAYS use `viji.canvas` as the renderer's canvas:\r\n ```javascript\r\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\r\n renderer.setSize(viji.width, viji.height, false);\r\n ```\r\n ALWAYS pass `false` as the third argument to `setSize()` — this prevents Three.js from setting CSS styles, which would fail in the worker.\r\n\r\n3. NEVER use `requestAnimationFrame()`. Viji controls the render loop. Write all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\r\n\r\n4. ALWAYS handle resize by checking `viji.width` / `viji.height` in `render()`:\r\n ```javascript\r\n let prevWidth = viji.width;\r\n let prevHeight = viji.height;\r\n\r\n function 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 renderer.render(scene, camera);\r\n }\r\n ```\r\n\r\n5. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\r\n\r\n6. NEVER use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\r\n\r\n7. Replace hardcoded values with Viji parameters declared at the top level:\r\n ```javascript\r\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\n const color = viji.color('#049ef4', { label: 'Color' });\r\n ```\r\n NEVER declare parameters inside `render()`.\r\n ALWAYS read via `.value`: `speed.value`, `color.value`.\r\n\r\n8. ALWAYS use `viji.deltaTime` for animation timing:\r\n ```javascript\r\n cube.rotation.y += speed.value * viji.deltaTime;\r\n ```\r\n NEVER use `clock.getDelta()` or `Date.now()`. Remove any `THREE.Clock` usage.\r\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`) — it causes animation jumps when the parameter changes. Same for nested multiplications: never multiply an accumulator by another parameter; give each speed its own accumulator.\r\n\r\n9. Replace mouse/keyboard event listeners with Viji APIs:\r\n - `event.clientX` → `viji.pointer.x` (works for both mouse and touch) or `viji.mouse.x`\r\n - `event.clientY` → `viji.pointer.y` or `viji.mouse.y`\r\n - Mouse buttons → `viji.mouse.leftButton`, `viji.mouse.rightButton`\r\n - Key presses → `viji.keyboard.isPressed('keyName')`\r\n\r\n10. `OrbitControls` and other controls that depend on DOM events will NOT work in the worker.\r\n For camera interaction, read `viji.pointer` (handles both mouse and touch) and update the camera manually.\r\n\r\n11. For Three.js addons, import from the examples directory:\r\n ```javascript\r\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\r\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\r\n ```\r\n ALWAYS use the same Three.js version for addons as for the main library.\r\n\r\n12. For textures from file inputs, use Viji's image parameters:\r\n ```javascript\r\n const photo = viji.image(null, { label: 'Texture' });\r\n // In render():\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\r\n13. For video textures, use `viji.video`:\r\n ```javascript\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n if (!videoTexture) {\r\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\r\n material.map = videoTexture;\r\n }\r\n videoTexture.needsUpdate = true;\r\n }\r\n ```\r\n\r\n14. NEVER allocate new objects inside `render()`. Pre-create vectors, colors, and materials at the top level.\r\n\r\n15. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\r\n\r\n16. Remove any `window.addEventListener('resize', ...)` — resize is handled in `render()` (see rule 4).\r\n\r\n16. Remove any CSS, HTML, or DOM manipulation code. Viji scenes produce only canvas output.\r\n\r\n## PARAMETER TYPES\r\n\r\n```javascript\r\nviji.slider(default, { min, max, step, label, group, category }) // { value: number }\r\nviji.color(default, { label, group, category }) // { value: '#rrggbb' }\r\nviji.toggle(default, { label, group, category }) // { value: boolean }\r\nviji.select(default, { options: [...], label, group, category }) // { value: string }\r\nviji.number(default, { min, max, step, label, group, category }) // { value: number }\r\nviji.image(default, { label, group, category }) // { value: ImageBitmap|null }\r\nviji.button({ label, description, group, category }) // { value: boolean }\r\n```\r\n\r\n## VIJI API QUICK REFERENCE\r\n\r\n- `viji.canvas` — the OffscreenCanvas (pass to Three.js renderer)\r\n- `viji.width`, `viji.height` — current canvas dimensions\r\n- `viji.time` — elapsed seconds since scene start\r\n- `viji.deltaTime` — seconds since last frame\r\n- `viji.pointer.x`, `.y`, `.isDown`, `.deltaX`, `.deltaY` — unified mouse/touch input\r\n- `viji.mouse.x`, `.y`, `.isPressed`, `.leftButton`, `.rightButton`, `.wheelDelta`\r\n- `viji.keyboard.isPressed('key')`, `.lastKeyPressed`\r\n- `viji.audio.isConnected`, `.volume.current`, `.bands.low/mid/high`, `.beat.kick/snare/hat`\r\n- `viji.video.isConnected`, `.currentFrame`\r\n\r\n## TEMPLATE\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#049ef4', { label: 'Color' });\r\n\r\nconst scene = new THREE.Scene();\r\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\r\ncamera.position.set(0, 1, 3);\r\ncamera.lookAt(0, 0, 0);\r\n\r\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\r\nrenderer.setSize(viji.width, viji.height, false);\r\n\r\nconst geometry = new THREE.BoxGeometry();\r\nconst material = new THREE.MeshStandardMaterial({ color: color.value });\r\nconst mesh = new THREE.Mesh(geometry, material);\r\nscene.add(mesh);\r\n\r\nscene.add(new THREE.DirectionalLight(0xffffff, 1.5));\r\nscene.add(new THREE.AmbientLight(0x404040));\r\n\r\nlet prevWidth = viji.width;\r\nlet prevHeight = viji.height;\r\n\r\nfunction render(viji) {\r\n mesh.rotation.y += speed.value * viji.deltaTime;\r\n material.color.set(color.value);\r\n\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\nNow convert the Three.js code I provide. Return ONLY the converted Viji scene code.\r\n````\r\n\r\n## Usage\r\n\r\n1. Copy the entire prompt block above.\r\n2. Paste it into your AI assistant.\r\n3. After the prompt, paste the Three.js code you want to convert.\r\n4. The AI will return a Viji-compatible native scene.\r\n\r\n> [!NOTE]\r\n> This prompt handles standard Three.js scenes. If the original code uses a framework (React Three Fiber, Drei, etc.), you may need to manually extract the Three.js scene setup first.\r\n\r\n## Related\r\n\r\n- [External Libraries](/native/external-libraries) — detailed guide for using Three.js and other libraries in Viji\r\n- [Prompt: Native Scenes](/ai-prompts/native-prompt) — AI prompt for creating new native scenes from scratch\r\n- [Native Quick Start](/native/quickstart) — your first Viji native scene"
1018
1018
  }
1019
1019
  ]
1020
1020
  },
@@ -1030,7 +1030,7 @@ export const docsApi = {
1030
1030
  {
1031
1031
  "type": "live-example",
1032
1032
  "title": "Native — Dot Field",
1033
- "sceneCode": "const bg = viji.color('#0a0a2e', { label: 'Background' });\r\nconst dotColor = viji.color('#00ffcc', { label: 'Dot Color' });\r\nconst count = viji.slider(80, { min: 10, max: 200, step: 1, label: 'Dot Count' });\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\n\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 const r = Math.min(w, h) * 0.01;\r\n\r\n ctx.fillStyle = bg.value;\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n ctx.fillStyle = dotColor.value;\r\n for (let i = 0; i < count.value; i++) {\r\n const t = viji.time * speed.value + i * 0.3;\r\n const x = w / 2 + Math.cos(t + i) * Math.sin(t * 0.3) * w * 0.35;\r\n const y = h / 2 + Math.sin(t + i * 0.7) * Math.cos(t * 0.2) * h * 0.35;\r\n ctx.beginPath();\r\n ctx.arc(x, y, r, 0, Math.PI * 2);\r\n ctx.fill();\r\n }\r\n}\r\n",
1033
+ "sceneCode": "const bg = viji.color('#0a0a2e', { label: 'Background' });\r\nconst dotColor = viji.color('#00ffcc', { label: 'Dot Color' });\r\nconst count = viji.slider(80, { min: 10, max: 200, step: 1, label: 'Dot Count' });\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nlet phase = 0;\r\n\r\nfunction render(viji) {\r\n phase += speed.value * viji.deltaTime;\r\n const ctx = viji.useContext('2d');\r\n const w = viji.width;\r\n const h = viji.height;\r\n const r = Math.min(w, h) * 0.01;\r\n\r\n ctx.fillStyle = bg.value;\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n ctx.fillStyle = dotColor.value;\r\n for (let i = 0; i < count.value; i++) {\r\n const t = phase + i * 0.3;\r\n const x = w / 2 + Math.cos(t + i) * Math.sin(t * 0.3) * w * 0.35;\r\n const y = h / 2 + Math.sin(t + i * 0.7) * Math.cos(t * 0.2) * h * 0.35;\r\n ctx.beginPath();\r\n ctx.arc(x, y, r, 0, Math.PI * 2);\r\n ctx.fill();\r\n }\r\n}\r\n",
1034
1034
  "sceneFile": "quickstart-native.scene.js"
1035
1035
  },
1036
1036
  {
@@ -1056,7 +1056,7 @@ export const docsApi = {
1056
1056
  "content": [
1057
1057
  {
1058
1058
  "type": "text",
1059
- "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your scene functions. Use it as a quick lookup — each entry links to its dedicated documentation page for full details, examples, and patterns.\n\nNew to Viji? Start with the [Quick Start](/native/quickstart) instead.\n\n## Entry Points\n\n```javascript\nfunction setup(viji) {\n // Called once when the scene starts (optional)\n}\n\nfunction render(viji) {\n // Called every frame\n}\n```\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/native/canvas-context) | `OffscreenCanvas` | The rendering canvas | [Canvas & Context](/native/canvas-context) |\n| [`viji.ctx`](/native/canvas-context) | `OffscreenCanvasRenderingContext2D` | 2D context (after `useContext('2d')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.gl`](/native/canvas-context) | `WebGLRenderingContext \\| WebGL2RenderingContext` | WebGL context (after `useContext('webgl'\\|'webgl2')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.width`](/native/canvas-context) | `number` | Canvas width in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.height`](/native/canvas-context) | `number` | Canvas height in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.useContext(type)`](/native/canvas-context) | `Method` | Request a rendering context: `'2d'`, `'webgl'`, `'webgl2'` | [Canvas & Context](/native/canvas-context) |\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/native/timing) | `number` | Seconds elapsed since the scene started | [Timing](/native/timing) |\n| [`viji.deltaTime`](/native/timing) | `number` | Seconds since the previous frame | [Timing](/native/timing) |\n| [`viji.frameCount`](/native/timing) | `number` | Monotonically increasing frame counter | [Timing](/native/timing) |\n| [`viji.fps`](/native/timing) | `number` | Target FPS based on the host's frame rate mode | [Timing](/native/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/native/parameters/slider) | `SliderParameter` | `number` | [Slider](/native/parameters/slider) |\n| [`viji.color(default, config)`](/native/parameters/color) | `ColorParameter` | `string` (hex) | [Color](/native/parameters/color) |\n| [`viji.toggle(default, config)`](/native/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/native/parameters/toggle) |\n| [`viji.select(default, config)`](/native/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/native/parameters/select) |\n| [`viji.number(default, config)`](/native/parameters/number) | `NumberParameter` | `number` | [Number](/native/parameters/number) |\n| [`viji.text(default, config)`](/native/parameters/text) | `TextParameter` | `string` | [Text](/native/parameters/text) |\n| [`viji.image(null, config)`](/native/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/native/parameters/image) |\n| [`viji.button(config)`](/native/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/native/parameters/button) |\n\nSee [Parameters Overview](/native/parameters) for the declaration pattern, [Grouping](/native/parameters/grouping) and [Categories](/native/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/native/audio) | `boolean` | Whether an audio source is active | [Overview](/native/audio) |\n| [`viji.audio.volume.current`](/native/audio/volume) | `number` | Current RMS volume 0–1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.peak`](/native/audio/volume) | `number` | Peak volume 0–1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.smoothed`](/native/audio/volume) | `number` | Smoothed volume 0–1 | [Volume](/native/audio/volume) |\n| [`viji.audio.bands.low`](/native/audio/bands) | `number` | Low frequency band energy (20–120 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMid`](/native/audio/bands) | `number` | Low-mid band energy (120–500 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.mid`](/native/audio/bands) | `number` | Mid band energy (500–2 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMid`](/native/audio/bands) | `number` | High-mid band energy (2–6 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.high`](/native/audio/bands) | `number` | High band energy (6–16 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/native/audio/bands) | `number` | Smoothed low band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/native/audio/bands) | `number` | Smoothed low-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/native/audio/bands) | `number` | Smoothed mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/native/audio/bands) | `number` | Smoothed high-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/native/audio/bands) | `number` | Smoothed high band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.beat.kick`](/native/audio/beat) | `number` | Kick beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snare`](/native/audio/beat) | `number` | Snare beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hat`](/native/audio/beat) | `number` | Hi-hat beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.any`](/native/audio/beat) | `number` | Combined beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/native/audio/beat) | `number` | Smoothed kick | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/native/audio/beat) | `number` | Smoothed snare | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/native/audio/beat) | `number` | Smoothed hi-hat | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/native/audio/beat) | `number` | Smoothed combined | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/native/audio/beat) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/native/audio/beat) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/native/audio/beat) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/native/audio/beat) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.events`](/native/audio/beat) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.bpm`](/native/audio/beat) | `number` | Tracked BPM | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.confidence`](/native/audio/beat) | `number` | Beat-tracker confidence 0–1 | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.isLocked`](/native/audio/beat) | `boolean` | Whether beat tracking is locked | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.spectral.brightness`](/native/audio/spectral) | `number` | Spectral brightness 0–1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/native/audio/spectral) | `number` | Spectral flatness 0–1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/native/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0–255) | [Frequency Data](/native/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/native/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/native/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/native/video) | `boolean` | Whether a video source is active | [Overview](/native/video) |\n| [`viji.video.currentFrame`](/native/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/native/video/basics) |\n| [`viji.video.frameWidth`](/native/video/basics) | `number` | Frame width in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameHeight`](/native/video/basics) | `number` | Frame height in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameRate`](/native/video/basics) | `number` | Video frame rate | [Video Basics](/native/video/basics) |\n| [`viji.video.getFrameData()`](/native/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/native/video/basics) |\n| [`viji.video.faces`](/native/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/native/video/face-detection) |\n| [`viji.video.hands`](/native/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.pose`](/native/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.segmentation`](/native/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/native/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/native/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/native/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/native/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/native/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/native/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/native/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/native/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/native/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/native/video) | `() => CVFeature[]` | List of active CV features | [Overview](/native/video) |\n| [`viji.video.cv.isProcessing()`](/native/video) | `() => boolean` | Whether CV is currently processing | [Overview](/native/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/native/mouse) | `number` | Cursor X position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.y`](/native/mouse) | `number` | Cursor Y position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.isInCanvas`](/native/mouse) | `boolean` | Whether cursor is inside the canvas | [Mouse](/native/mouse) |\n| [`viji.mouse.isPressed`](/native/mouse) | `boolean` | Whether any button is pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.leftButton`](/native/mouse) | `boolean` | Left button state | [Mouse](/native/mouse) |\n| [`viji.mouse.rightButton`](/native/mouse) | `boolean` | Right button state | [Mouse](/native/mouse) |\n| [`viji.mouse.middleButton`](/native/mouse) | `boolean` | Middle button state | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaX`](/native/mouse) | `number` | Horizontal movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaY`](/native/mouse) | `number` | Vertical movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelDelta`](/native/mouse) | `number` | Scroll wheel delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelX`](/native/mouse) | `number` | Horizontal scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelY`](/native/mouse) | `number` | Vertical scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wasPressed`](/native/mouse) | `boolean` | True for one frame when pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.wasReleased`](/native/mouse) | `boolean` | True for one frame when released | [Mouse](/native/mouse) |\n| [`viji.mouse.wasMoved`](/native/mouse) | `boolean` | True for one frame when moved | [Mouse](/native/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/native/keyboard) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/native/keyboard) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/native/keyboard) | `(string) => boolean` | True for one frame when released | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.activeKeys`](/native/keyboard) | `Set<string>` | All currently held keys | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/native/keyboard) | `Set<string>` | Keys pressed this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/native/keyboard) | `Set<string>` | Keys released this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/native/keyboard) | `string` | Most recently pressed key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/native/keyboard) | `string` | Most recently released key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.shift`](/native/keyboard) | `boolean` | Shift key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.ctrl`](/native/keyboard) | `boolean` | Ctrl/Cmd key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.alt`](/native/keyboard) | `boolean` | Alt/Option key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.meta`](/native/keyboard) | `boolean` | Meta/Win key state | [Keyboard](/native/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/native/touch) | `TouchPoint[]` | All active touch points | [Touch](/native/touch) |\n| [`viji.touches.count`](/native/touch) | `number` | Number of active touches | [Touch](/native/touch) |\n| [`viji.touches.started`](/native/touch) | `TouchPoint[]` | Touch points that started this frame | [Touch](/native/touch) |\n| [`viji.touches.moved`](/native/touch) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/native/touch) |\n| [`viji.touches.ended`](/native/touch) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/native/touch) |\n| [`viji.touches.primary`](/native/touch) | `TouchPoint \\| null` | The first active touch point | [Touch](/native/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/native/pointer) | `number` | Primary pointer X position | [Pointer](/native/pointer) |\n| [`viji.pointer.y`](/native/pointer) | `number` | Primary pointer Y position | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaX`](/native/pointer) | `number` | Horizontal movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaY`](/native/pointer) | `number` | Vertical movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.isDown`](/native/pointer) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/native/pointer) |\n| [`viji.pointer.wasPressed`](/native/pointer) | `boolean` | True for one frame when pressed | [Pointer](/native/pointer) |\n| [`viji.pointer.wasReleased`](/native/pointer) | `boolean` | True for one frame when released | [Pointer](/native/pointer) |\n| [`viji.pointer.isInCanvas`](/native/pointer) | `boolean` | Whether pointer is inside the canvas | [Pointer](/native/pointer) |\n| [`viji.pointer.type`](/native/pointer) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/native/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/native/sensors) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/native/sensors) |\n| [`viji.device.orientation`](/native/sensors) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/native/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/native/external-devices) | `DeviceState[]` | Connected external devices | [Overview](/native/external-devices) |\n| [`viji.devices[i].id`](/native/external-devices) | `string` | Unique device identifier | [Overview](/native/external-devices) |\n| [`viji.devices[i].name`](/native/external-devices) | `string` | User-friendly device name | [Overview](/native/external-devices) |\n| [`viji.devices[i].motion`](/native/external-devices/sensors) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/native/external-devices/sensors) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].video`](/native/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/native/external-devices/video) |\n| [`viji.devices[i].audio`](/native/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/native/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.videoStreams` | `VideoAPI[]` | Additional video streams provided by the host |\n| `viji.audioStreams` | `AudioStreamAPI[]` | Additional audio streams provided by the host |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/native/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking — see [`viji.audio`](/native/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/native/quickstart) — getting started with the Native renderer\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid\n- [P5 API Reference](/p5/api-reference) — the same API in the P5 renderer\n- [Shader API Reference](/shader/api-reference) — built-in uniforms for shaders"
1059
+ "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your scene functions. Use it as a quick lookup — each entry links to its dedicated documentation page for full details, examples, and patterns.\n\nNew to Viji? Start with the [Quick Start](/native/quickstart) instead.\n\n## Entry Points\n\n```javascript\n// Top-level code runs once (initialization, parameters, state, imports)\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\n\nfunction render(viji) {\n // Called every frame — this is where you draw\n}\n```\n\nThere is no `setup()` function in native scenes. All initialization goes at the top level. Top-level `await` is supported for dynamic imports.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/native/canvas-context) | `OffscreenCanvas` | The rendering canvas | [Canvas & Context](/native/canvas-context) |\n| [`viji.ctx`](/native/canvas-context) | `OffscreenCanvasRenderingContext2D` | 2D context (after `useContext('2d')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.gl`](/native/canvas-context) | `WebGLRenderingContext \\| WebGL2RenderingContext` | WebGL context (after `useContext('webgl'\\|'webgl2')`) | [Canvas & Context](/native/canvas-context) |\n| [`viji.width`](/native/canvas-context) | `number` | Canvas width in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.height`](/native/canvas-context) | `number` | Canvas height in pixels | [Canvas & Context](/native/canvas-context) |\n| [`viji.useContext(type)`](/native/canvas-context) | `Method` | Request a rendering context: `'2d'`, `'webgl'`, `'webgl2'` | [Canvas & Context](/native/canvas-context) |\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/native/timing) | `number` | Seconds elapsed since the scene started | [Timing](/native/timing) |\n| [`viji.deltaTime`](/native/timing) | `number` | Seconds since the previous frame | [Timing](/native/timing) |\n| [`viji.frameCount`](/native/timing) | `number` | Monotonically increasing frame counter | [Timing](/native/timing) |\n| [`viji.fps`](/native/timing) | `number` | Target FPS based on the host's frame rate mode | [Timing](/native/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/native/parameters/slider) | `SliderParameter` | `number` | [Slider](/native/parameters/slider) |\n| [`viji.color(default, config)`](/native/parameters/color) | `ColorParameter` | `string` (hex) | [Color](/native/parameters/color) |\n| [`viji.toggle(default, config)`](/native/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/native/parameters/toggle) |\n| [`viji.select(default, config)`](/native/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/native/parameters/select) |\n| [`viji.number(default, config)`](/native/parameters/number) | `NumberParameter` | `number` | [Number](/native/parameters/number) |\n| [`viji.text(default, config)`](/native/parameters/text) | `TextParameter` | `string` | [Text](/native/parameters/text) |\n| [`viji.image(null, config)`](/native/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/native/parameters/image) |\n| [`viji.button(config)`](/native/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/native/parameters/button) |\n\nSee [Parameters Overview](/native/parameters) for the declaration pattern, [Grouping](/native/parameters/grouping) and [Categories](/native/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/native/audio) | `boolean` | Whether an audio source is active | [Overview](/native/audio) |\n| [`viji.audio.volume.current`](/native/audio/volume) | `number` | Current RMS volume 0–1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.peak`](/native/audio/volume) | `number` | Peak volume 0–1 | [Volume](/native/audio/volume) |\n| [`viji.audio.volume.smoothed`](/native/audio/volume) | `number` | Smoothed volume 0–1 | [Volume](/native/audio/volume) |\n| [`viji.audio.bands.low`](/native/audio/bands) | `number` | Low frequency band energy (20–120 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMid`](/native/audio/bands) | `number` | Low-mid band energy (120–500 Hz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.mid`](/native/audio/bands) | `number` | Mid band energy (500–2 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMid`](/native/audio/bands) | `number` | High-mid band energy (2–6 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.high`](/native/audio/bands) | `number` | High band energy (6–16 kHz) | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/native/audio/bands) | `number` | Smoothed low band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/native/audio/bands) | `number` | Smoothed low-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/native/audio/bands) | `number` | Smoothed mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/native/audio/bands) | `number` | Smoothed high-mid band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/native/audio/bands) | `number` | Smoothed high band | [Frequency Bands](/native/audio/bands) |\n| [`viji.audio.beat.kick`](/native/audio/beat) | `number` | Kick beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snare`](/native/audio/beat) | `number` | Snare beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hat`](/native/audio/beat) | `number` | Hi-hat beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.any`](/native/audio/beat) | `number` | Combined beat energy | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/native/audio/beat) | `number` | Smoothed kick | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/native/audio/beat) | `number` | Smoothed snare | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/native/audio/beat) | `number` | Smoothed hi-hat | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/native/audio/beat) | `number` | Smoothed combined | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/native/audio/beat) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/native/audio/beat) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/native/audio/beat) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/native/audio/beat) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.events`](/native/audio/beat) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.bpm`](/native/audio/beat) | `number` | Tracked BPM | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.confidence`](/native/audio/beat) | `number` | Beat-tracker confidence 0–1 | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.beat.isLocked`](/native/audio/beat) | `boolean` | Whether beat tracking is locked | [Beat Detection](/native/audio/beat) |\n| [`viji.audio.spectral.brightness`](/native/audio/spectral) | `number` | Spectral brightness 0–1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/native/audio/spectral) | `number` | Spectral flatness 0–1 | [Spectral Analysis](/native/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/native/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0–255) | [Frequency Data](/native/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/native/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/native/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/native/video) | `boolean` | Whether a video source is active | [Overview](/native/video) |\n| [`viji.video.currentFrame`](/native/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/native/video/basics) |\n| [`viji.video.frameWidth`](/native/video/basics) | `number` | Frame width in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameHeight`](/native/video/basics) | `number` | Frame height in pixels | [Video Basics](/native/video/basics) |\n| [`viji.video.frameRate`](/native/video/basics) | `number` | Video frame rate | [Video Basics](/native/video/basics) |\n| [`viji.video.getFrameData()`](/native/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/native/video/basics) |\n| [`viji.video.faces`](/native/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/native/video/face-detection) |\n| [`viji.video.hands`](/native/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.pose`](/native/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.segmentation`](/native/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/native/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/native/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/native/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/native/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/native/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/native/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/native/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/native/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/native/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/native/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/native/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/native/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/native/video) | `() => CVFeature[]` | List of active CV features | [Overview](/native/video) |\n| [`viji.video.cv.isProcessing()`](/native/video) | `() => boolean` | Whether CV is currently processing | [Overview](/native/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/native/mouse) | `number` | Cursor X position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.y`](/native/mouse) | `number` | Cursor Y position in pixels | [Mouse](/native/mouse) |\n| [`viji.mouse.isInCanvas`](/native/mouse) | `boolean` | Whether cursor is inside the canvas | [Mouse](/native/mouse) |\n| [`viji.mouse.isPressed`](/native/mouse) | `boolean` | Whether any button is pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.leftButton`](/native/mouse) | `boolean` | Left button state | [Mouse](/native/mouse) |\n| [`viji.mouse.rightButton`](/native/mouse) | `boolean` | Right button state | [Mouse](/native/mouse) |\n| [`viji.mouse.middleButton`](/native/mouse) | `boolean` | Middle button state | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaX`](/native/mouse) | `number` | Horizontal movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.deltaY`](/native/mouse) | `number` | Vertical movement since last frame | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelDelta`](/native/mouse) | `number` | Scroll wheel delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelX`](/native/mouse) | `number` | Horizontal scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wheelY`](/native/mouse) | `number` | Vertical scroll delta | [Mouse](/native/mouse) |\n| [`viji.mouse.wasPressed`](/native/mouse) | `boolean` | True for one frame when pressed | [Mouse](/native/mouse) |\n| [`viji.mouse.wasReleased`](/native/mouse) | `boolean` | True for one frame when released | [Mouse](/native/mouse) |\n| [`viji.mouse.wasMoved`](/native/mouse) | `boolean` | True for one frame when moved | [Mouse](/native/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/native/keyboard) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/native/keyboard) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/native/keyboard) | `(string) => boolean` | True for one frame when released | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.activeKeys`](/native/keyboard) | `Set<string>` | All currently held keys | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/native/keyboard) | `Set<string>` | Keys pressed this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/native/keyboard) | `Set<string>` | Keys released this frame | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/native/keyboard) | `string` | Most recently pressed key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/native/keyboard) | `string` | Most recently released key | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.shift`](/native/keyboard) | `boolean` | Shift key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.ctrl`](/native/keyboard) | `boolean` | Ctrl/Cmd key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.alt`](/native/keyboard) | `boolean` | Alt/Option key state | [Keyboard](/native/keyboard) |\n| [`viji.keyboard.meta`](/native/keyboard) | `boolean` | Meta/Win key state | [Keyboard](/native/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/native/touch) | `TouchPoint[]` | All active touch points | [Touch](/native/touch) |\n| [`viji.touches.count`](/native/touch) | `number` | Number of active touches | [Touch](/native/touch) |\n| [`viji.touches.started`](/native/touch) | `TouchPoint[]` | Touch points that started this frame | [Touch](/native/touch) |\n| [`viji.touches.moved`](/native/touch) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/native/touch) |\n| [`viji.touches.ended`](/native/touch) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/native/touch) |\n| [`viji.touches.primary`](/native/touch) | `TouchPoint \\| null` | The first active touch point | [Touch](/native/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/native/pointer) | `number` | Primary pointer X position | [Pointer](/native/pointer) |\n| [`viji.pointer.y`](/native/pointer) | `number` | Primary pointer Y position | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaX`](/native/pointer) | `number` | Horizontal movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.deltaY`](/native/pointer) | `number` | Vertical movement since last frame | [Pointer](/native/pointer) |\n| [`viji.pointer.isDown`](/native/pointer) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/native/pointer) |\n| [`viji.pointer.wasPressed`](/native/pointer) | `boolean` | True for one frame when pressed | [Pointer](/native/pointer) |\n| [`viji.pointer.wasReleased`](/native/pointer) | `boolean` | True for one frame when released | [Pointer](/native/pointer) |\n| [`viji.pointer.isInCanvas`](/native/pointer) | `boolean` | Whether pointer is inside the canvas | [Pointer](/native/pointer) |\n| [`viji.pointer.type`](/native/pointer) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/native/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/native/sensors) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/native/sensors) |\n| [`viji.device.orientation`](/native/sensors) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/native/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/native/external-devices) | `DeviceState[]` | Connected external devices | [Overview](/native/external-devices) |\n| [`viji.devices[i].id`](/native/external-devices) | `string` | Unique device identifier | [Overview](/native/external-devices) |\n| [`viji.devices[i].name`](/native/external-devices) | `string` | User-friendly device name | [Overview](/native/external-devices) |\n| [`viji.devices[i].motion`](/native/external-devices/sensors) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/native/external-devices/sensors) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/native/external-devices/sensors) |\n| [`viji.devices[i].video`](/native/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/native/external-devices/video) |\n| [`viji.devices[i].audio`](/native/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/native/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.videoStreams` | `VideoAPI[]` | Additional video streams provided by the host |\n| `viji.audioStreams` | `AudioStreamAPI[]` | Additional audio streams provided by the host |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/native/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking — see [`viji.audio`](/native/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/native/quickstart) — getting started with the Native renderer\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers\n- [Common Mistakes](/getting-started/common-mistakes) — pitfalls to avoid\n- [P5 API Reference](/p5/api-reference) — the same API in the P5 renderer\n- [Shader API Reference](/shader/api-reference) — built-in uniforms for shaders"
1060
1060
  }
1061
1061
  ]
1062
1062
  },
@@ -1103,7 +1103,7 @@ export const docsApi = {
1103
1103
  {
1104
1104
  "type": "live-example",
1105
1105
  "title": "Time-Based — Orbiting Dots",
1106
- "sceneCode": "const bg = viji.color('#0a0a1a', { label: 'Background' });\nconst orbitColor = viji.color('#ff6644', { label: 'Dot Color' });\nconst count = viji.slider(6, { min: 2, max: 16, step: 1, label: 'Dot Count' });\nconst speed = viji.slider(1, { min: 0.2, max: 4, label: 'Speed' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const cx = w / 2;\n const cy = h / 2;\n const radius = Math.min(w, h) * 0.3;\n const dotR = Math.min(w, h) * 0.02;\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n ctx.fillStyle = orbitColor.value;\n for (let i = 0; i < count.value; i++) {\n const angle = viji.time * speed.value + (i / count.value) * Math.PI * 2;\n const x = cx + Math.cos(angle) * radius;\n const y = cy + Math.sin(angle) * radius;\n ctx.beginPath();\n ctx.arc(x, y, dotR, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n",
1106
+ "sceneCode": "const bg = viji.color('#0a0a1a', { label: 'Background' });\nconst orbitColor = viji.color('#ff6644', { label: 'Dot Color' });\nconst count = viji.slider(6, { min: 2, max: 16, step: 1, label: 'Dot Count' });\nconst speed = viji.slider(1, { min: 0.2, max: 4, label: 'Speed' });\nlet phase = 0;\n\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const cx = w / 2;\n const cy = h / 2;\n const radius = Math.min(w, h) * 0.3;\n const dotR = Math.min(w, h) * 0.02;\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n ctx.fillStyle = orbitColor.value;\n for (let i = 0; i < count.value; i++) {\n const angle = phase + (i / count.value) * Math.PI * 2;\n const x = cx + Math.cos(angle) * radius;\n const y = cy + Math.sin(angle) * radius;\n ctx.beginPath();\n ctx.arc(x, y, dotR, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n",
1107
1107
  "sceneFile": "timing-time.scene.js"
1108
1108
  },
1109
1109
  {
@@ -1118,7 +1118,7 @@ export const docsApi = {
1118
1118
  },
1119
1119
  {
1120
1120
  "type": "text",
1121
- "markdown": "## When to Use `viji.time` vs `viji.deltaTime`\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| Oscillation (`sin`, `cos`) | `viji.time` | Depends on absolute position in time |\n| Hue cycling | `viji.time` | Needs a continuous, monotonic input |\n| Position accumulation | `viji.deltaTime` | Must advance the same amount per second, regardless of FPS |\n| Physics / velocity | `viji.deltaTime` | Distance = speed × time elapsed |\n| Fading / easing | `viji.deltaTime` | Progress should be per-second, not per-frame |\n| Periodic events | `viji.time` | Trigger at fixed time intervals |\n\n**Rule of thumb:** if you multiply by a speed and _add_ the result to a running total, use `viji.deltaTime`. If you pass time directly into a periodic function, use `viji.time`.\n\n## `viji.frameCount`\n\nAn integer that starts at 0 and increments by 1 every frame. Useful for alternating patterns, sampling every N frames, or seeding frame-unique randomness:\n\n```javascript\nif (viji.frameCount % 60 === 0) {\n // runs once per ~60 frames\n}\n```\n\n## `viji.fps` — Target Frame Rate\n\n`viji.fps` is the **target** frame rate based on the host's frame rate mode and screen refresh rate — it is **not** a measured FPS value:\n\n- `frameRateMode: 'full'` → `viji.fps` equals the screen refresh rate (typically 60 or 120)\n- `frameRateMode: 'half'` → `viji.fps` equals half the screen refresh rate (typically 30 or 60)\n\nThis value is stable and does not fluctuate frame-to-frame. It's useful for informational display or calculations that need the intended rate, but **not** for driving animation — use `viji.time` or `viji.deltaTime` instead.\n\n## Frame-Rate Independence\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\nA scene running at 30 FPS should look the same as one running at 120 FPS. The only way to achieve this is to scale all per-frame accumulation by `viji.deltaTime`:\n\n```javascript\n// Bad — moves faster at higher frame rates\nposition += 5;\n\n// Good — moves at 5 units per second regardless of frame rate\nposition += 5 * viji.deltaTime;\n```\n\n## Next Steps\n\n- [Canvas & Context](/native/canvas-context) — [`viji.canvas`](/native/canvas-context), [`viji.useContext()`](/native/canvas-context), dimensions\n- [Parameters](/native/parameters) — sliders, colors, toggles\n- [P5 Timing](/p5/timing) — timing in the P5 renderer\n- [Shader Timing](/shader/timing) — `u_time`, `u_deltaTime`, `u_frame`, `u_fps`\n- [API Reference](/native/api-reference) — full list of everything available"
1121
+ "markdown": "## When to Use `viji.time` vs `viji.deltaTime`\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| Oscillation at **constant** speed | `viji.time` | Depends on absolute position in time |\n| Hue cycling at constant rate | `viji.time` | Needs a continuous, monotonic input |\n| Oscillation at **parameter-driven** speed | `viji.deltaTime` (accumulator) | Avoids phase jumps when the slider changes |\n| Position accumulation | `viji.deltaTime` | Must advance the same amount per second, regardless of FPS |\n| Physics / velocity | `viji.deltaTime` | Distance = speed × time elapsed |\n| Fading / easing | `viji.deltaTime` | Progress should be per-second, not per-frame |\n| Periodic events | `viji.time` | Trigger at fixed time intervals |\n\n**Rule of thumb:** if the speed multiplier is a **constant**, `viji.time * constant` is fine. If the speed comes from a **parameter** (slider, etc.), accumulate with `viji.deltaTime` to prevent jumps.\n\n### Accumulator Pattern\n\nWhen animation speed is driven by a user parameter, multiplying `viji.time * speed.value` recalculates the entire phase history every frame — changing the slider causes a visible jump. Instead, accumulate the phase incrementally:\n\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet phase = 0;\n\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const x = Math.cos(phase) * radius;\n}\n```\n\nThe slider now only affects future frames — past phase is preserved, so the animation transitions smoothly.\n\n> [!NOTE]\n> For shaders, use [`@viji-accumulator`](/shader/parameters/accumulator) instead — it performs the same integration (`speed × deltaTime`) on the host side and provides the accumulated value as a uniform.\n\n## `viji.frameCount`\n\nAn integer that starts at 0 and increments by 1 every frame. Useful for alternating patterns, sampling every N frames, or seeding frame-unique randomness:\n\n```javascript\nif (viji.frameCount % 60 === 0) {\n // runs once per ~60 frames\n}\n```\n\n## `viji.fps` — Target Frame Rate\n\n`viji.fps` is the **target** frame rate based on the host's frame rate mode and screen refresh rate — it is **not** a measured FPS value:\n\n- `frameRateMode: 'full'` → `viji.fps` equals the screen refresh rate (typically 60 or 120)\n- `frameRateMode: 'half'` → `viji.fps` equals half the screen refresh rate (typically 30 or 60)\n\nThis value is stable and does not fluctuate frame-to-frame. It's useful for informational display or calculations that need the intended rate, but **not** for driving animation — use `viji.time` or `viji.deltaTime` instead.\n\n## Frame-Rate Independence\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\nA scene running at 30 FPS should look the same as one running at 120 FPS. The only way to achieve this is to scale all per-frame accumulation by `viji.deltaTime`:\n\n```javascript\n// Bad — moves faster at higher frame rates\nposition += 5;\n\n// Good — moves at 5 units per second regardless of frame rate\nposition += 5 * viji.deltaTime;\n```\n\n## Next Steps\n\n- [Canvas & Context](/native/canvas-context) — [`viji.canvas`](/native/canvas-context), [`viji.useContext()`](/native/canvas-context), dimensions\n- [Parameters](/native/parameters) — sliders, colors, toggles\n- [P5 Timing](/p5/timing) — timing in the P5 renderer\n- [Shader Timing](/shader/timing) — `u_time`, `u_deltaTime`, `u_frame`, `u_fps`\n- [API Reference](/native/api-reference) — full list of everything available"
1122
1122
  }
1123
1123
  ]
1124
1124
  },
@@ -1166,7 +1166,7 @@ export const docsApi = {
1166
1166
  {
1167
1167
  "type": "live-example",
1168
1168
  "title": "Basic Slider",
1169
- "sceneCode": "const radius = viji.slider(0.15, {\r\n min: 0.02,\r\n max: 0.4,\r\n step: 0.01,\r\n label: 'Radius',\r\n description: 'Size of the circle relative to canvas',\r\n group: 'shape'\r\n});\r\n\r\nconst speed = viji.slider(1.0, {\r\n min: 0,\r\n max: 3.0,\r\n step: 0.1,\r\n label: 'Pulse Speed',\r\n description: 'Speed of the breathing animation',\r\n group: 'animation'\r\n});\r\n\r\nconst fillColor = viji.color('#4ecdc4', {\r\n label: 'Fill Color',\r\n group: 'style'\r\n});\r\n\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 = '#1a1a2e';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n const baseRadius = radius.value * Math.min(w, h);\r\n const pulse = 1 + 0.15 * Math.sin(viji.time * speed.value * Math.PI * 2);\r\n const r = baseRadius * pulse;\r\n\r\n ctx.beginPath();\r\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\r\n ctx.fillStyle = fillColor.value;\r\n ctx.fill();\r\n\r\n ctx.beginPath();\r\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\r\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';\r\n ctx.lineWidth = Math.max(1, Math.min(w, h) * 0.003);\r\n ctx.stroke();\r\n}\r\n",
1169
+ "sceneCode": "const radius = viji.slider(0.15, {\r\n min: 0.02,\r\n max: 0.4,\r\n step: 0.01,\r\n label: 'Radius',\r\n description: 'Size of the circle relative to canvas',\r\n group: 'shape'\r\n});\r\n\r\nconst speed = viji.slider(1.0, {\r\n min: 0,\r\n max: 3.0,\r\n step: 0.1,\r\n label: 'Pulse Speed',\r\n description: 'Speed of the breathing animation',\r\n group: 'animation'\r\n});\r\n\r\nconst fillColor = viji.color('#4ecdc4', {\r\n label: 'Fill Color',\r\n group: 'style'\r\n});\r\nlet phase = 0;\r\n\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 = '#1a1a2e';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n const baseRadius = radius.value * Math.min(w, h);\r\n phase += speed.value * viji.deltaTime * Math.PI * 2;\r\n const pulse = 1 + 0.15 * Math.sin(phase);\r\n const r = baseRadius * pulse;\r\n\r\n ctx.beginPath();\r\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\r\n ctx.fillStyle = fillColor.value;\r\n ctx.fill();\r\n\r\n ctx.beginPath();\r\n ctx.arc(w / 2, h / 2, r, 0, Math.PI * 2);\r\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';\r\n ctx.lineWidth = Math.max(1, Math.min(w, h) * 0.003);\r\n ctx.stroke();\r\n}\r\n",
1170
1170
  "sceneFile": "slider-basic.scene.js"
1171
1171
  },
1172
1172
  {
@@ -1850,7 +1850,7 @@ export const docsApi = {
1850
1850
  {
1851
1851
  "type": "live-example",
1852
1852
  "title": "Device Sensors — Tilt Visualization",
1853
- "sceneCode": "function render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const orient = viji.device.orientation;\n const motion = viji.device.motion;\n\n const beta = orient?.beta ?? 0;\n const gamma = orient?.gamma ?? 0;\n const alpha = orient?.alpha ?? 0;\n\n const normX = gamma / 90;\n const normY = beta / 180;\n\n const cx = w / 2 + normX * (w * 0.35);\n const cy = h / 2 + normY * (h * 0.35);\n\n const accel = motion?.accelerationIncludingGravity;\n const ax = accel?.x ?? 0;\n const ay = accel?.y ?? 0;\n const magnitude = Math.sqrt(ax * ax + ay * ay);\n const radius = 20 + Math.min(magnitude, 15) * 3;\n\n const hue = (alpha % 360);\n ctx.fillStyle = `hsl(${hue}, 70%, 60%)`;\n ctx.beginPath();\n ctx.arc(cx, cy, radius, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(w / 2, 0);\n ctx.lineTo(w / 2, h);\n ctx.moveTo(0, h / 2);\n ctx.lineTo(w, h / 2);\n ctx.stroke();\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n ctx.font = '12px monospace';\n ctx.textAlign = 'left';\n ctx.fillText(`beta: ${beta.toFixed(1)}° gamma: ${gamma.toFixed(1)}°`, 10, 20);\n ctx.fillText(`alpha: ${alpha.toFixed(1)}°`, 10, 36);\n ctx.fillText(`accel: ${magnitude.toFixed(2)} m/s²`, 10, 52);\n}\n",
1853
+ "sceneCode": "function render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const orient = viji.device.orientation;\n const motion = viji.device.motion;\n\n const beta = orient?.beta ?? 0;\n const gamma = orient?.gamma ?? 0;\n const alpha = orient?.alpha ?? 0;\n\n const normX = gamma / 90;\n const normY = beta / 180;\n\n const cx = w / 2 + normX * (w * 0.35);\n const cy = h / 2 + normY * (h * 0.35);\n\n const accel = motion?.accelerationIncludingGravity;\n const ax = accel?.x ?? 0;\n const ay = accel?.y ?? 0;\n const magnitude = Math.sqrt(ax * ax + ay * ay);\n const radius = 20 + Math.min(magnitude, 15) * 3;\n\n const hue = (alpha % 360);\n ctx.fillStyle = `hsl(${hue}, 70%, 60%)`;\n ctx.beginPath();\n ctx.arc(cx, cy, radius, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(w / 2, 0);\n ctx.lineTo(w / 2, h);\n ctx.moveTo(0, h / 2);\n ctx.lineTo(w, h / 2);\n ctx.stroke();\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n ctx.font = '12px monospace';\n ctx.textAlign = 'left';\n ctx.fillText(`beta: ${beta.toFixed(1)}° gamma: ${gamma.toFixed(1)}°`, 10, 20);\n ctx.fillText(`alpha: ${alpha.toFixed(1)}°`, 10, 36);\n ctx.fillText(`accel: ${magnitude.toFixed(2)} m/s²`, 10, 52);\n}\n",
1854
1854
  "sceneFile": "sensors-demo.scene.js",
1855
1855
  "capabilities": {
1856
1856
  "interaction": true
@@ -1858,7 +1858,7 @@ export const docsApi = {
1858
1858
  },
1859
1859
  {
1860
1860
  "type": "text",
1861
- "markdown": "## Common Patterns\n\n### Tilt-Reactive Position\n\n```javascript\nfunction render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n const orient = viji.device.orientation;\n const tiltX = orient?.gamma ?? 0; // -90 to 90\n const tiltY = orient?.beta ?? 0; // -180 to 180\n\n const x = w / 2 + (tiltX / 90) * (w / 2);\n const y = h / 2 + (tiltY / 180) * (h / 2);\n\n ctx.clearRect(0, 0, w, h);\n ctx.beginPath();\n ctx.arc(x, y, 30, 0, Math.PI * 2);\n ctx.fill();\n}\n```\n\n### Shake Detection\n\n```javascript\nlet lastAccel = 0;\n\nfunction render(viji) {\n const accel = viji.device.motion?.acceleration;\n if (accel) {\n const magnitude = Math.sqrt(\n (accel.x ?? 0) ** 2 +\n (accel.y ?? 0) ** 2 +\n (accel.z ?? 0) ** 2\n );\n if (magnitude > 15 && magnitude - lastAccel > 5) {\n triggerShakeEffect();\n }\n lastAccel = magnitude;\n }\n}\n```\n\n### Compass Heading\n\n```javascript\nfunction render(viji) {\n const heading = viji.device.orientation?.alpha ?? 0;\n const radians = (heading * Math.PI) / 180;\n // Rotate a compass needle by `radians`\n}\n```\n\n## Related\n\n- [Pointer](../pointer/) — unified click/drag input\n- [Touch](../touch/) — multi-touch with pressure and radius\n- [External Device Sensors](../external-devices/sensors/) — sensor data from connected external devices\n- [P5 Device Sensors](/p5/sensors) — same API in the P5 renderer\n- [Shader Sensor Uniforms](/shader/sensors) — GLSL uniforms for device sensors"
1861
+ "markdown": "## Common Patterns\n\n### Tilt-Reactive Position\n\n```javascript\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n const orient = viji.device.orientation;\n const tiltX = orient?.gamma ?? 0; // -90 to 90\n const tiltY = orient?.beta ?? 0; // -180 to 180\n\n const x = w / 2 + (tiltX / 90) * (w / 2);\n const y = h / 2 + (tiltY / 180) * (h / 2);\n\n ctx.clearRect(0, 0, w, h);\n ctx.beginPath();\n ctx.arc(x, y, 30, 0, Math.PI * 2);\n ctx.fill();\n}\n```\n\n### Shake Detection\n\n```javascript\nlet lastAccel = 0;\n\nfunction render(viji) {\n const accel = viji.device.motion?.acceleration;\n if (accel) {\n const magnitude = Math.sqrt(\n (accel.x ?? 0) ** 2 +\n (accel.y ?? 0) ** 2 +\n (accel.z ?? 0) ** 2\n );\n if (magnitude > 15 && magnitude - lastAccel > 5) {\n triggerShakeEffect();\n }\n lastAccel = magnitude;\n }\n}\n```\n\n### Compass Heading\n\n```javascript\nfunction render(viji) {\n const heading = viji.device.orientation?.alpha ?? 0;\n const radians = (heading * Math.PI) / 180;\n // Rotate a compass needle by `radians`\n}\n```\n\n## Related\n\n- [Pointer](../pointer/) — unified click/drag input\n- [Touch](../touch/) — multi-touch with pressure and radius\n- [External Device Sensors](../external-devices/sensors/) — sensor data from connected external devices\n- [P5 Device Sensors](/p5/sensors) — same API in the P5 renderer\n- [Shader Sensor Uniforms](/shader/sensors) — GLSL uniforms for device sensors"
1862
1862
  }
1863
1863
  ]
1864
1864
  },
@@ -1874,7 +1874,7 @@ export const docsApi = {
1874
1874
  {
1875
1875
  "type": "live-example",
1876
1876
  "title": "External Devices — Connected Devices",
1877
- "sceneCode": "function render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const devices = viji.devices;\n const count = devices.length;\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 20px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(\n `${count} device${count !== 1 ? 's' : ''} connected`,\n w / 2, 40\n );\n\n if (count === 0) {\n ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.font = '14px sans-serif';\n ctx.fillText('Waiting for external devices...', w / 2, h / 2);\n return;\n }\n\n const cardW = Math.min(200, (w - 40) / Math.min(count, 4));\n const startX = (w - cardW * Math.min(count, 4)) / 2;\n\n devices.forEach((device, i) => {\n const col = i % 4;\n const row = Math.floor(i / 4);\n const x = startX + col * cardW;\n const y = 70 + row * 120;\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';\n ctx.beginPath();\n ctx.roundRect(x + 4, y, cardW - 8, 100, 8);\n ctx.fill();\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 13px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(device.name || device.id, x + cardW / 2, y + 25);\n\n ctx.font = '11px monospace';\n ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n\n const hasVideo = device.video?.isConnected ? '● Video' : '○ No video';\n const hasSensors = device.motion ? '● Sensors' : '○ No sensors';\n\n ctx.fillText(hasVideo, x + cardW / 2, y + 55);\n ctx.fillText(hasSensors, x + cardW / 2, y + 72);\n });\n}\n",
1877
+ "sceneCode": "function render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const devices = viji.devices;\n const count = devices.length;\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 20px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(\n `${count} device${count !== 1 ? 's' : ''} connected`,\n w / 2, 40\n );\n\n if (count === 0) {\n ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.font = '14px sans-serif';\n ctx.fillText('Waiting for external devices...', w / 2, h / 2);\n return;\n }\n\n const cardW = Math.min(200, (w - 40) / Math.min(count, 4));\n const startX = (w - cardW * Math.min(count, 4)) / 2;\n\n devices.forEach((device, i) => {\n const col = i % 4;\n const row = Math.floor(i / 4);\n const x = startX + col * cardW;\n const y = 70 + row * 120;\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';\n ctx.beginPath();\n ctx.roundRect(x + 4, y, cardW - 8, 100, 8);\n ctx.fill();\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 13px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(device.name || device.id, x + cardW / 2, y + 25);\n\n ctx.font = '11px monospace';\n ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n\n const hasVideo = device.video?.isConnected ? '● Video' : '○ No video';\n const hasSensors = device.motion ? '● Sensors' : '○ No sensors';\n\n ctx.fillText(hasVideo, x + cardW / 2, y + 55);\n ctx.fillText(hasSensors, x + cardW / 2, y + 72);\n });\n}\n",
1878
1878
  "sceneFile": "overview-demo.scene.js",
1879
1879
  "capabilities": {
1880
1880
  "interaction": true
@@ -1882,7 +1882,7 @@ export const docsApi = {
1882
1882
  },
1883
1883
  {
1884
1884
  "type": "text",
1885
- "markdown": "## Common Patterns\n\n### Display Device Count\n\n```javascript\nfunction render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const count = viji.devices.length;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, viji.canvas.width, viji.canvas.height);\n\n ctx.fillStyle = '#fff';\n ctx.font = '24px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(\n `${count} device${count !== 1 ? 's' : ''} connected`,\n viji.canvas.width / 2,\n viji.canvas.height / 2\n );\n}\n```\n\n### Find Device by Name\n\n```javascript\nfunction render(viji) {\n const phone = viji.devices.find(d => d.name.includes('Phone'));\n if (phone) {\n // Use phone-specific data\n }\n}\n```\n\n### Iterate All Devices\n\n```javascript\nfunction render(viji) {\n viji.devices.forEach((device, index) => {\n const hasVideo = device.video?.isConnected ?? false;\n const hasSensors = device.motion !== null;\n // Render device status at position based on index\n });\n}\n```\n\n## What's Available on Each Device\n\n| Feature | Access | Notes |\n|---------|--------|-------|\n| **Identity** | `device.id`, `device.name` | Always available |\n| **Sensors** | `device.motion`, `device.orientation` | See [Device Sensors](sensors/) |\n| **Video** | `device.video` | See [Device Video](video/) |\n| **Audio** | `device.audio` | See [Device Audio](audio/) |\n\n> [!WARNING]\n> Device video does **not** support Computer Vision (CV) features. CV processing (face detection, hand tracking, etc.) is only available on the main video stream (`viji.video`). The `device.video` object provides video frames only.\n\n> [!NOTE]\n> Device audio provides **lightweight analysis** only — volume, frequency bands, and spectral features. Beat detection, BPM tracking, and onset events are only available on the main audio stream (`viji.audio`).\n\n## Related\n\n- [Device Audio](audio/) — audio analysis from connected devices\n- [Device Video](video/) — accessing camera feeds from connected devices\n- [Device Sensors](sensors/) — accelerometer and orientation from connected devices\n- [Device Sensors (Internal)](../sensors/) — sensors from the device running the scene\n- [P5 External Devices](/p5/external-devices) — same API in the P5 renderer\n- [Shader External Device Uniforms](/shader/external-devices) — GLSL uniforms for external devices"
1885
+ "markdown": "## Common Patterns\n\n### Display Device Count\n\n```javascript\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const count = viji.devices.length;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n ctx.fillStyle = '#fff';\n ctx.font = '24px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(\n `${count} device${count !== 1 ? 's' : ''} connected`,\n viji.width / 2,\n viji.height / 2\n );\n}\n```\n\n### Find Device by Name\n\n```javascript\nfunction render(viji) {\n const phone = viji.devices.find(d => d.name.includes('Phone'));\n if (phone) {\n // Use phone-specific data\n }\n}\n```\n\n### Iterate All Devices\n\n```javascript\nfunction render(viji) {\n viji.devices.forEach((device, index) => {\n const hasVideo = device.video?.isConnected ?? false;\n const hasSensors = device.motion !== null;\n // Render device status at position based on index\n });\n}\n```\n\n## What's Available on Each Device\n\n| Feature | Access | Notes |\n|---------|--------|-------|\n| **Identity** | `device.id`, `device.name` | Always available |\n| **Sensors** | `device.motion`, `device.orientation` | See [Device Sensors](sensors/) |\n| **Video** | `device.video` | See [Device Video](video/) |\n| **Audio** | `device.audio` | See [Device Audio](audio/) |\n\n> [!WARNING]\n> Device video does **not** support Computer Vision (CV) features. CV processing (face detection, hand tracking, etc.) is only available on the main video stream (`viji.video`). The `device.video` object provides video frames only.\n\n> [!NOTE]\n> Device audio provides **lightweight analysis** only — volume, frequency bands, and spectral features. Beat detection, BPM tracking, and onset events are only available on the main audio stream (`viji.audio`).\n\n## Related\n\n- [Device Audio](audio/) — audio analysis from connected devices\n- [Device Video](video/) — accessing camera feeds from connected devices\n- [Device Sensors](sensors/) — accelerometer and orientation from connected devices\n- [Device Sensors (Internal)](../sensors/) — sensors from the device running the scene\n- [P5 External Devices](/p5/external-devices) — same API in the P5 renderer\n- [Shader External Device Uniforms](/shader/external-devices) — GLSL uniforms for external devices"
1886
1886
  }
1887
1887
  ]
1888
1888
  },
@@ -1898,7 +1898,7 @@ export const docsApi = {
1898
1898
  {
1899
1899
  "type": "live-example",
1900
1900
  "title": "Device Video — Camera Grid",
1901
- "sceneCode": "function render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const cameras = viji.devices.filter(\n d => d.video?.isConnected && d.video.currentFrame\n );\n\n if (cameras.length === 0) {\n ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.font = '14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('No device cameras connected', w / 2, h / 2);\n return;\n }\n\n const cols = Math.ceil(Math.sqrt(cameras.length));\n const rows = Math.ceil(cameras.length / cols);\n const cellW = w / cols;\n const cellH = h / rows;\n const pad = 4;\n\n cameras.forEach((device, i) => {\n const col = i % cols;\n const row = Math.floor(i / cols);\n const x = col * cellW + pad;\n const y = row * cellH + pad;\n const cw = cellW - pad * 2;\n const ch = cellH - pad * 2;\n\n ctx.drawImage(device.video.currentFrame, x, y, cw, ch);\n\n ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';\n ctx.fillRect(x, y + ch - 24, cw, 24);\n ctx.fillStyle = '#fff';\n ctx.font = '11px sans-serif';\n ctx.textAlign = 'left';\n ctx.fillText(\n `${device.name} — ${device.video.frameWidth}×${device.video.frameHeight}`,\n x + 6, y + ch - 8\n );\n });\n}\n",
1901
+ "sceneCode": "function render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const cameras = viji.devices.filter(\n d => d.video?.isConnected && d.video.currentFrame\n );\n\n if (cameras.length === 0) {\n ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.font = '14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('No device cameras connected', w / 2, h / 2);\n return;\n }\n\n const cols = Math.ceil(Math.sqrt(cameras.length));\n const rows = Math.ceil(cameras.length / cols);\n const cellW = w / cols;\n const cellH = h / rows;\n const pad = 4;\n\n cameras.forEach((device, i) => {\n const col = i % cols;\n const row = Math.floor(i / cols);\n const x = col * cellW + pad;\n const y = row * cellH + pad;\n const cw = cellW - pad * 2;\n const ch = cellH - pad * 2;\n\n ctx.drawImage(device.video.currentFrame, x, y, cw, ch);\n\n ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';\n ctx.fillRect(x, y + ch - 24, cw, 24);\n ctx.fillStyle = '#fff';\n ctx.font = '11px sans-serif';\n ctx.textAlign = 'left';\n ctx.fillText(\n `${device.name} — ${device.video.frameWidth}×${device.video.frameHeight}`,\n x + 6, y + ch - 8\n );\n });\n}\n",
1902
1902
  "sceneFile": "video-demo.scene.js",
1903
1903
  "capabilities": {
1904
1904
  "interaction": true
@@ -1906,7 +1906,7 @@ export const docsApi = {
1906
1906
  },
1907
1907
  {
1908
1908
  "type": "text",
1909
- "markdown": "## Common Patterns\n\n### Draw All Device Cameras in a Grid\n\n```javascript\nfunction render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, w, h);\n\n const cameras = viji.devices.filter(\n d => d.video?.isConnected && d.video.currentFrame\n );\n if (cameras.length === 0) return;\n\n const cols = Math.ceil(Math.sqrt(cameras.length));\n const rows = Math.ceil(cameras.length / cols);\n const cellW = w / cols;\n const cellH = h / rows;\n\n cameras.forEach((device, i) => {\n const col = i % cols;\n const row = Math.floor(i / cols);\n ctx.drawImage(\n device.video.currentFrame,\n col * cellW, row * cellH, cellW, cellH\n );\n });\n}\n```\n\n### Picture-in-Picture from a Device Camera\n\n```javascript\nfunction render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n // Main scene drawing\n ctx.fillStyle = '#1a1a2e';\n ctx.fillRect(0, 0, w, h);\n\n // Overlay first device camera as PiP\n const device = viji.devices[0];\n if (device?.video?.isConnected && device.video.currentFrame) {\n const pipW = w * 0.3;\n const pipH = pipW * (device.video.frameHeight / device.video.frameWidth);\n ctx.drawImage(\n device.video.currentFrame,\n w - pipW - 10, 10, pipW, pipH\n );\n\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.lineWidth = 2;\n ctx.strokeRect(w - pipW - 10, 10, pipW, pipH);\n }\n}\n```\n\n### Read Pixel Data from a Device Camera\n\n```javascript\nfunction render(viji) {\n const device = viji.devices[0];\n if (!device?.video?.isConnected) return;\n\n const imageData = device.video.getFrameData();\n if (!imageData) return;\n\n const pixels = imageData.data; // Uint8ClampedArray — RGBA\n let totalR = 0, totalG = 0, totalB = 0;\n const pixelCount = imageData.width * imageData.height;\n\n for (let i = 0; i < pixels.length; i += 4) {\n totalR += pixels[i];\n totalG += pixels[i + 1];\n totalB += pixels[i + 2];\n }\n\n const avgR = totalR / pixelCount;\n const avgG = totalG / pixelCount;\n const avgB = totalB / pixelCount;\n // Use average color for background or effects\n}\n```\n\n## Related\n\n- [External Devices — Overview](../) — device identity, connection lifecycle, and guard patterns\n- [External Device Sensors](../sensors/) — accelerometer and orientation from connected devices\n- [Video & CV — Video Basics](../../video/basics/) — main camera video API (with CV support)\n- [P5 Device Video](/p5/external-devices/video) — same API in the P5 renderer\n- [Shader Device Video Textures](/shader/external-devices/video) — GLSL uniforms for device camera textures"
1909
+ "markdown": "## Common Patterns\n\n### Draw All Device Cameras in a Grid\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 = '#000';\n ctx.fillRect(0, 0, w, h);\n\n const cameras = viji.devices.filter(\n d => d.video?.isConnected && d.video.currentFrame\n );\n if (cameras.length === 0) return;\n\n const cols = Math.ceil(Math.sqrt(cameras.length));\n const rows = Math.ceil(cameras.length / cols);\n const cellW = w / cols;\n const cellH = h / rows;\n\n cameras.forEach((device, i) => {\n const col = i % cols;\n const row = Math.floor(i / cols);\n ctx.drawImage(\n device.video.currentFrame,\n col * cellW, row * cellH, cellW, cellH\n );\n });\n}\n```\n\n### Picture-in-Picture from a Device Camera\n\n```javascript\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n // Main scene drawing\n ctx.fillStyle = '#1a1a2e';\n ctx.fillRect(0, 0, w, h);\n\n // Overlay first device camera as PiP\n const device = viji.devices[0];\n if (device?.video?.isConnected && device.video.currentFrame) {\n const pipW = w * 0.3;\n const pipH = pipW * (device.video.frameHeight / device.video.frameWidth);\n ctx.drawImage(\n device.video.currentFrame,\n w - pipW - 10, 10, pipW, pipH\n );\n\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.lineWidth = 2;\n ctx.strokeRect(w - pipW - 10, 10, pipW, pipH);\n }\n}\n```\n\n### Read Pixel Data from a Device Camera\n\n```javascript\nfunction render(viji) {\n const device = viji.devices[0];\n if (!device?.video?.isConnected) return;\n\n const imageData = device.video.getFrameData();\n if (!imageData) return;\n\n const pixels = imageData.data; // Uint8ClampedArray — RGBA\n let totalR = 0, totalG = 0, totalB = 0;\n const pixelCount = imageData.width * imageData.height;\n\n for (let i = 0; i < pixels.length; i += 4) {\n totalR += pixels[i];\n totalG += pixels[i + 1];\n totalB += pixels[i + 2];\n }\n\n const avgR = totalR / pixelCount;\n const avgG = totalG / pixelCount;\n const avgB = totalB / pixelCount;\n // Use average color for background or effects\n}\n```\n\n## Related\n\n- [External Devices — Overview](../) — device identity, connection lifecycle, and guard patterns\n- [External Device Sensors](../sensors/) — accelerometer and orientation from connected devices\n- [Video & CV — Video Basics](../../video/basics/) — main camera video API (with CV support)\n- [P5 Device Video](/p5/external-devices/video) — same API in the P5 renderer\n- [Shader Device Video Textures](/shader/external-devices/video) — GLSL uniforms for device camera textures"
1910
1910
  }
1911
1911
  ]
1912
1912
  },
@@ -1922,7 +1922,7 @@ export const docsApi = {
1922
1922
  {
1923
1923
  "type": "live-example",
1924
1924
  "title": "External Device Sensors — Tilt Bars",
1925
- "sceneCode": "function render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const devices = viji.devices;\n\n if (devices.length === 0) {\n ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.font = '14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('No external devices connected', w / 2, h / 2);\n return;\n }\n\n const barH = 16;\n const barW = w * 0.5;\n const startX = w * 0.35;\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('External Device Sensors', w / 2, 30);\n\n devices.forEach((device, di) => {\n const baseY = 60 + di * 140;\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 12px sans-serif';\n ctx.textAlign = 'left';\n ctx.fillText(device.name || device.id, 10, baseY);\n\n const orient = device.orientation;\n const axes = [\n { label: 'alpha', value: orient?.alpha ?? 0, max: 360 },\n { label: 'beta', value: orient?.beta ?? 0, max: 180 },\n { label: 'gamma', value: orient?.gamma ?? 0, max: 90 }\n ];\n\n axes.forEach((axis, ai) => {\n const y = baseY + 18 + ai * (barH + 8);\n const norm = axis.value / axis.max;\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';\n ctx.fillRect(startX, y, barW, barH);\n\n const fillW = Math.abs(norm) * (barW / 2);\n const fillX = norm >= 0 ? startX + barW / 2 : startX + barW / 2 - fillW;\n ctx.fillStyle = norm >= 0 ? '#4af' : '#f64';\n ctx.fillRect(fillX, y, fillW, barH);\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';\n ctx.font = '11px monospace';\n ctx.textAlign = 'right';\n ctx.fillText(`${axis.label}: ${axis.value.toFixed(1)}°`, startX - 8, y + 12);\n });\n\n const accel = device.motion?.accelerationIncludingGravity;\n if (accel) {\n const y = baseY + 18 + 3 * (barH + 8);\n const mag = Math.sqrt(\n (accel.x ?? 0) ** 2 + (accel.y ?? 0) ** 2 + (accel.z ?? 0) ** 2\n );\n ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';\n ctx.font = '11px monospace';\n ctx.textAlign = 'right';\n ctx.fillText(`accel: ${mag.toFixed(1)} m/s²`, startX - 8, y + 12);\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';\n ctx.fillRect(startX, y, barW, barH);\n const normMag = Math.min(mag / 20, 1);\n ctx.fillStyle = '#6f4';\n ctx.fillRect(startX, y, normMag * barW, barH);\n }\n });\n}\n",
1925
+ "sceneCode": "function render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#0a0a1a';\n ctx.fillRect(0, 0, w, h);\n\n const devices = viji.devices;\n\n if (devices.length === 0) {\n ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n ctx.font = '14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('No external devices connected', w / 2, h / 2);\n return;\n }\n\n const barH = 16;\n const barW = w * 0.5;\n const startX = w * 0.35;\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('External Device Sensors', w / 2, 30);\n\n devices.forEach((device, di) => {\n const baseY = 60 + di * 140;\n\n ctx.fillStyle = '#fff';\n ctx.font = 'bold 12px sans-serif';\n ctx.textAlign = 'left';\n ctx.fillText(device.name || device.id, 10, baseY);\n\n const orient = device.orientation;\n const axes = [\n { label: 'alpha', value: orient?.alpha ?? 0, max: 360 },\n { label: 'beta', value: orient?.beta ?? 0, max: 180 },\n { label: 'gamma', value: orient?.gamma ?? 0, max: 90 }\n ];\n\n axes.forEach((axis, ai) => {\n const y = baseY + 18 + ai * (barH + 8);\n const norm = axis.value / axis.max;\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';\n ctx.fillRect(startX, y, barW, barH);\n\n const fillW = Math.abs(norm) * (barW / 2);\n const fillX = norm >= 0 ? startX + barW / 2 : startX + barW / 2 - fillW;\n ctx.fillStyle = norm >= 0 ? '#4af' : '#f64';\n ctx.fillRect(fillX, y, fillW, barH);\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';\n ctx.font = '11px monospace';\n ctx.textAlign = 'right';\n ctx.fillText(`${axis.label}: ${axis.value.toFixed(1)}°`, startX - 8, y + 12);\n });\n\n const accel = device.motion?.accelerationIncludingGravity;\n if (accel) {\n const y = baseY + 18 + 3 * (barH + 8);\n const mag = Math.sqrt(\n (accel.x ?? 0) ** 2 + (accel.y ?? 0) ** 2 + (accel.z ?? 0) ** 2\n );\n ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';\n ctx.font = '11px monospace';\n ctx.textAlign = 'right';\n ctx.fillText(`accel: ${mag.toFixed(1)} m/s²`, startX - 8, y + 12);\n\n ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';\n ctx.fillRect(startX, y, barW, barH);\n const normMag = Math.min(mag / 20, 1);\n ctx.fillStyle = '#6f4';\n ctx.fillRect(startX, y, normMag * barW, barH);\n }\n });\n}\n",
1926
1926
  "sceneFile": "sensors-demo.scene.js",
1927
1927
  "capabilities": {
1928
1928
  "interaction": true
@@ -1930,7 +1930,7 @@ export const docsApi = {
1930
1930
  },
1931
1931
  {
1932
1932
  "type": "text",
1933
- "markdown": "## Common Patterns\n\n### Tilt-Reactive Effect from an External Device\n\n```javascript\nfunction render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n const device = viji.devices[0];\n if (!device?.orientation) return;\n\n const tiltX = device.orientation.gamma ?? 0;\n const tiltY = device.orientation.beta ?? 0;\n\n const x = w / 2 + (tiltX / 90) * (w * 0.4);\n const y = h / 2 + (tiltY / 180) * (h * 0.4);\n\n ctx.fillStyle = '#4af';\n ctx.beginPath();\n ctx.arc(x, y, 25, 0, Math.PI * 2);\n ctx.fill();\n}\n```\n\n### Compare Internal vs External Device Tilt\n\n```javascript\nfunction render(viji) {\n const ctx = viji.canvas.getContext('2d');\n const w = viji.canvas.width;\n const h = viji.canvas.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n // Internal device tilt\n const selfTilt = viji.device.orientation?.gamma ?? 0;\n ctx.fillStyle = '#f44';\n ctx.beginPath();\n ctx.arc(w * 0.3, h / 2 + (selfTilt / 90) * (h * 0.4), 20, 0, Math.PI * 2);\n ctx.fill();\n\n // External device tilt\n const extDevice = viji.devices[0];\n const extTilt = extDevice?.orientation?.gamma ?? 0;\n ctx.fillStyle = '#4af';\n ctx.beginPath();\n ctx.arc(w * 0.7, h / 2 + (extTilt / 90) * (h * 0.4), 20, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.fillStyle = '#fff';\n ctx.font = '12px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('This device', w * 0.3, h - 20);\n ctx.fillText('External device', w * 0.7, h - 20);\n}\n```\n\n### Shake Detection from an External Device\n\n```javascript\nlet prevMag = 0;\n\nfunction render(viji) {\n const device = viji.devices[0];\n const accel = device?.motion?.acceleration;\n if (!accel) return;\n\n const mag = Math.sqrt(\n (accel.x ?? 0) ** 2 +\n (accel.y ?? 0) ** 2 +\n (accel.z ?? 0) ** 2\n );\n\n if (mag > 15 && mag - prevMag > 5) {\n triggerShakeEffect();\n }\n prevMag = mag;\n}\n```\n\n## Related\n\n- [External Devices — Overview](../) — device identity, connection lifecycle, and guard patterns\n- [Device Video](../video/) — camera feeds from connected devices\n- [Device Sensors (Internal)](../../sensors/) — sensors from the device running the scene\n- [P5 External Device Sensors](/p5/external-devices/sensors) — same API in the P5 renderer\n- [Shader External Device Sensor Uniforms](/shader/external-devices/sensors) — GLSL uniforms for external device sensors"
1933
+ "markdown": "## Common Patterns\n\n### Tilt-Reactive Effect from an External Device\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 const device = viji.devices[0];\n if (!device?.orientation) return;\n\n const tiltX = device.orientation.gamma ?? 0;\n const tiltY = device.orientation.beta ?? 0;\n\n const x = w / 2 + (tiltX / 90) * (w * 0.4);\n const y = h / 2 + (tiltY / 180) * (h * 0.4);\n\n ctx.fillStyle = '#4af';\n ctx.beginPath();\n ctx.arc(x, y, 25, 0, Math.PI * 2);\n ctx.fill();\n}\n```\n\n### Compare Internal vs External Device Tilt\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 // Internal device tilt\n const selfTilt = viji.device.orientation?.gamma ?? 0;\n ctx.fillStyle = '#f44';\n ctx.beginPath();\n ctx.arc(w * 0.3, h / 2 + (selfTilt / 90) * (h * 0.4), 20, 0, Math.PI * 2);\n ctx.fill();\n\n // External device tilt\n const extDevice = viji.devices[0];\n const extTilt = extDevice?.orientation?.gamma ?? 0;\n ctx.fillStyle = '#4af';\n ctx.beginPath();\n ctx.arc(w * 0.7, h / 2 + (extTilt / 90) * (h * 0.4), 20, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.fillStyle = '#fff';\n ctx.font = '12px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('This device', w * 0.3, h - 20);\n ctx.fillText('External device', w * 0.7, h - 20);\n}\n```\n\n### Shake Detection from an External Device\n\n```javascript\nlet prevMag = 0;\n\nfunction render(viji) {\n const device = viji.devices[0];\n const accel = device?.motion?.acceleration;\n if (!accel) return;\n\n const mag = Math.sqrt(\n (accel.x ?? 0) ** 2 +\n (accel.y ?? 0) ** 2 +\n (accel.z ?? 0) ** 2\n );\n\n if (mag > 15 && mag - prevMag > 5) {\n triggerShakeEffect();\n }\n prevMag = mag;\n}\n```\n\n## Related\n\n- [External Devices — Overview](../) — device identity, connection lifecycle, and guard patterns\n- [Device Video](../video/) — camera feeds from connected devices\n- [Device Sensors (Internal)](../../sensors/) — sensors from the device running the scene\n- [P5 External Device Sensors](/p5/external-devices/sensors) — same API in the P5 renderer\n- [Shader External Device Sensor Uniforms](/shader/external-devices/sensors) — GLSL uniforms for external device sensors"
1934
1934
  }
1935
1935
  ]
1936
1936
  },
@@ -1957,7 +1957,7 @@ export const docsApi = {
1957
1957
  {
1958
1958
  "type": "live-example",
1959
1959
  "title": "P5 — Rainbow Trail",
1960
- "sceneCode": "// @renderer p5\r\n\r\nconst trailLength = viji.slider(40, { min: 5, max: 100, step: 1, label: 'Trail Length' });\r\nconst hueSpeed = viji.slider(30, { min: 5, max: 100, label: 'Hue Speed' });\r\n\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\r\n}\r\n\r\nfunction render(viji, p5) {\r\n p5.background(0, 0, 10, 15);\r\n\r\n for (let i = 0; i < trailLength.value; i++) {\r\n const t = viji.time - i * 0.02;\r\n const x = viji.width / 2 + p5.cos(t * 1.5) * viji.width * 0.3;\r\n const y = viji.height / 2 + p5.sin(t * 2.0) * viji.height * 0.25;\r\n const hue = (viji.time * hueSpeed.value + i * 3) % 360;\r\n const size = p5.map(i, 0, trailLength.value, viji.width * 0.04, viji.width * 0.005);\r\n\r\n p5.noStroke();\r\n p5.fill(hue, 80, 100, p5.map(i, 0, trailLength.value, 100, 0));\r\n p5.circle(x, y, size);\r\n }\r\n}\r\n",
1960
+ "sceneCode": "// @renderer p5\r\n\r\nconst trailLength = viji.slider(40, { min: 5, max: 100, step: 1, label: 'Trail Length' });\r\nconst hueSpeed = viji.slider(30, { min: 5, max: 100, label: 'Hue Speed' });\r\nlet huePhase = 0;\r\n\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\r\n}\r\n\r\nfunction render(viji, p5) {\r\n huePhase += hueSpeed.value * viji.deltaTime;\r\n p5.background(0, 0, 10, 15);\r\n\r\n for (let i = 0; i < trailLength.value; i++) {\r\n const t = viji.time - i * 0.02;\r\n const x = viji.width / 2 + p5.cos(t * 1.5) * viji.width * 0.3;\r\n const y = viji.height / 2 + p5.sin(t * 2.0) * viji.height * 0.25;\r\n const hue = (huePhase + i * 3) % 360;\r\n const size = p5.map(i, 0, trailLength.value, viji.width * 0.04, viji.width * 0.005);\r\n\r\n p5.noStroke();\r\n p5.fill(hue, 80, 100, p5.map(i, 0, trailLength.value, 100, 0));\r\n p5.circle(x, y, size);\r\n }\r\n}\r\n",
1961
1961
  "sceneFile": "quickstart-p5.scene.js"
1962
1962
  },
1963
1963
  {
@@ -1973,7 +1973,7 @@ export const docsApi = {
1973
1973
  "content": [
1974
1974
  {
1975
1975
  "type": "text",
1976
- "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your P5 scene functions. The `viji` API is identical to the [Native renderer](/native/api-reference) — the difference is that P5 scenes also receive a `p5` instance as the second argument.\n\nNew to Viji P5? Start with the [Quick Start](/p5/quickstart) instead.\n\n## Entry Points\n\n```javascript\n// @renderer p5\n\nfunction setup(viji, p5) {\n // Called once when the scene starts (optional)\n}\n\nfunction render(viji, p5) {\n // Called every frame\n}\n```\n\nThe `p5` parameter is a full [P5.js](https://p5js.org/reference/) instance (v1.9.4) in instance mode. All P5 drawing methods (`p5.rect()`, `p5.fill()`, `p5.ellipse()`, etc.) are accessed through it. See [Drawing with P5](/p5/drawing) for Viji-specific drawing patterns.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/p5/canvas-resolution) | `OffscreenCanvas` | The rendering canvas | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.width`](/p5/canvas-resolution) | `number` | Canvas width in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.height`](/p5/canvas-resolution) | `number` | Canvas height in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n\n> [!WARNING]\n> `viji.useContext()` is **not available** in P5 scenes. The canvas and its rendering context (2D or WEBGL, depending on the directive) are managed by P5 internally. Calling `useContext()` would conflict with P5's rendering pipeline.\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/p5/timing) | `number` | Seconds elapsed since the scene started | [Timing](/p5/timing) |\n| [`viji.deltaTime`](/p5/timing) | `number` | Seconds since the previous frame | [Timing](/p5/timing) |\n| [`viji.frameCount`](/p5/timing) | `number` | Monotonically increasing frame counter | [Timing](/p5/timing) |\n| [`viji.fps`](/p5/timing) | `number` | Target FPS based on the host's frame rate mode | [Timing](/p5/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/p5/parameters/slider) | `SliderParameter` | `number` | [Slider](/p5/parameters/slider) |\n| [`viji.color(default, config)`](/p5/parameters/color) | `ColorParameter` | `string` (hex) | [Color](/p5/parameters/color) |\n| [`viji.toggle(default, config)`](/p5/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/p5/parameters/toggle) |\n| [`viji.select(default, config)`](/p5/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/p5/parameters/select) |\n| [`viji.number(default, config)`](/p5/parameters/number) | `NumberParameter` | `number` | [Number](/p5/parameters/number) |\n| [`viji.text(default, config)`](/p5/parameters/text) | `TextParameter` | `string` | [Text](/p5/parameters/text) |\n| [`viji.image(null, config)`](/p5/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/p5/parameters/image) |\n| [`viji.button(config)`](/p5/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/p5/parameters/button) |\n\n> [!NOTE]\n> Image parameters have a `.value.p5` property for use with `p5.image()`. See [Drawing with P5 — Image Parameters](/p5/drawing) for the pattern.\n\nSee [Parameters Overview](/p5/parameters) for the declaration pattern, [Grouping](/p5/parameters/grouping) and [Categories](/p5/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/p5/audio) | `boolean` | Whether an audio source is active | [Overview](/p5/audio) |\n| [`viji.audio.volume.current`](/p5/audio/volume) | `number` | Current RMS volume 0–1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.peak`](/p5/audio/volume) | `number` | Peak volume 0–1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.smoothed`](/p5/audio/volume) | `number` | Smoothed volume 0–1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.bands.low`](/p5/audio/bands) | `number` | Low frequency band energy (20–120 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMid`](/p5/audio/bands) | `number` | Low-mid band energy (120–500 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.mid`](/p5/audio/bands) | `number` | Mid band energy (500–2 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMid`](/p5/audio/bands) | `number` | High-mid band energy (2–6 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.high`](/p5/audio/bands) | `number` | High band energy (6–16 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/p5/audio/bands) | `number` | Smoothed low band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/p5/audio/bands) | `number` | Smoothed low-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/p5/audio/bands) | `number` | Smoothed mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/p5/audio/bands) | `number` | Smoothed high-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/p5/audio/bands) | `number` | Smoothed high band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.beat.kick`](/p5/audio/beat) | `number` | Kick beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snare`](/p5/audio/beat) | `number` | Snare beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hat`](/p5/audio/beat) | `number` | Hi-hat beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.any`](/p5/audio/beat) | `number` | Combined beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/p5/audio/beat) | `number` | Smoothed kick | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/p5/audio/beat) | `number` | Smoothed snare | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/p5/audio/beat) | `number` | Smoothed hi-hat | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/p5/audio/beat) | `number` | Smoothed combined | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/p5/audio/beat) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/p5/audio/beat) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/p5/audio/beat) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/p5/audio/beat) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.events`](/p5/audio/beat) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.bpm`](/p5/audio/beat) | `number` | Tracked BPM | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.confidence`](/p5/audio/beat) | `number` | Beat-tracker confidence 0–1 | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.isLocked`](/p5/audio/beat) | `boolean` | Whether beat tracking is locked | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.spectral.brightness`](/p5/audio/spectral) | `number` | Spectral brightness 0–1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/p5/audio/spectral) | `number` | Spectral flatness 0–1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/p5/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0–255) | [Frequency Data](/p5/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/p5/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/p5/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/p5/video) | `boolean` | Whether a video source is active | [Overview](/p5/video) |\n| [`viji.video.currentFrame`](/p5/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameWidth`](/p5/video/basics) | `number` | Frame width in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameHeight`](/p5/video/basics) | `number` | Frame height in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameRate`](/p5/video/basics) | `number` | Video frame rate | [Video Basics](/p5/video/basics) |\n| [`viji.video.getFrameData()`](/p5/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.faces`](/p5/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.hands`](/p5/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.pose`](/p5/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.segmentation`](/p5/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/p5/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/p5/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/p5/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/p5/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/p5/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/p5/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/p5/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/p5/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/p5/video) | `() => CVFeature[]` | List of active CV features | [Overview](/p5/video) |\n| [`viji.video.cv.isProcessing()`](/p5/video) | `() => boolean` | Whether CV is currently processing | [Overview](/p5/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/p5/mouse) | `number` | Cursor X position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.y`](/p5/mouse) | `number` | Cursor Y position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.isInCanvas`](/p5/mouse) | `boolean` | Whether cursor is inside the canvas | [Mouse](/p5/mouse) |\n| [`viji.mouse.isPressed`](/p5/mouse) | `boolean` | Whether any button is pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.leftButton`](/p5/mouse) | `boolean` | Left button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.rightButton`](/p5/mouse) | `boolean` | Right button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.middleButton`](/p5/mouse) | `boolean` | Middle button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaX`](/p5/mouse) | `number` | Horizontal movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaY`](/p5/mouse) | `number` | Vertical movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelDelta`](/p5/mouse) | `number` | Scroll wheel delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelX`](/p5/mouse) | `number` | Horizontal scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelY`](/p5/mouse) | `number` | Vertical scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasPressed`](/p5/mouse) | `boolean` | True for one frame when pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasReleased`](/p5/mouse) | `boolean` | True for one frame when released | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasMoved`](/p5/mouse) | `boolean` | True for one frame when moved | [Mouse](/p5/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/p5/keyboard) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/p5/keyboard) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/p5/keyboard) | `(string) => boolean` | True for one frame when released | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.activeKeys`](/p5/keyboard) | `Set<string>` | All currently held keys | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/p5/keyboard) | `Set<string>` | Keys pressed this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/p5/keyboard) | `Set<string>` | Keys released this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/p5/keyboard) | `string` | Most recently pressed key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/p5/keyboard) | `string` | Most recently released key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.shift`](/p5/keyboard) | `boolean` | Shift key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.ctrl`](/p5/keyboard) | `boolean` | Ctrl/Cmd key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.alt`](/p5/keyboard) | `boolean` | Alt/Option key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.meta`](/p5/keyboard) | `boolean` | Meta/Win key state | [Keyboard](/p5/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/p5/touch) | `TouchPoint[]` | All active touch points | [Touch](/p5/touch) |\n| [`viji.touches.count`](/p5/touch) | `number` | Number of active touches | [Touch](/p5/touch) |\n| [`viji.touches.started`](/p5/touch) | `TouchPoint[]` | Touch points that started this frame | [Touch](/p5/touch) |\n| [`viji.touches.moved`](/p5/touch) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/p5/touch) |\n| [`viji.touches.ended`](/p5/touch) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/p5/touch) |\n| [`viji.touches.primary`](/p5/touch) | `TouchPoint \\| null` | The first active touch point | [Touch](/p5/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/p5/pointer) | `number` | Primary pointer X position | [Pointer](/p5/pointer) |\n| [`viji.pointer.y`](/p5/pointer) | `number` | Primary pointer Y position | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaX`](/p5/pointer) | `number` | Horizontal movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaY`](/p5/pointer) | `number` | Vertical movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.isDown`](/p5/pointer) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasPressed`](/p5/pointer) | `boolean` | True for one frame when pressed | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasReleased`](/p5/pointer) | `boolean` | True for one frame when released | [Pointer](/p5/pointer) |\n| [`viji.pointer.isInCanvas`](/p5/pointer) | `boolean` | Whether pointer is inside the canvas | [Pointer](/p5/pointer) |\n| [`viji.pointer.type`](/p5/pointer) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/p5/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/p5/sensors) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/p5/sensors) |\n| [`viji.device.orientation`](/p5/sensors) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/p5/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/p5/external-devices) | `DeviceState[]` | Connected external devices | [Overview](/p5/external-devices) |\n| [`viji.devices[i].id`](/p5/external-devices) | `string` | Unique device identifier | [Overview](/p5/external-devices) |\n| [`viji.devices[i].name`](/p5/external-devices) | `string` | User-friendly device name | [Overview](/p5/external-devices) |\n| [`viji.devices[i].motion`](/p5/external-devices/sensors) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/p5/external-devices/sensors) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].video`](/p5/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/p5/external-devices/video) |\n| [`viji.devices[i].audio`](/p5/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/p5/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.videoStreams` | `VideoAPI[]` | Additional video streams provided by the host |\n| `viji.audioStreams` | `AudioStreamAPI[]` | Additional audio streams provided by the host |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/p5/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking — see [`viji.audio`](/p5/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/p5/quickstart) — getting started with the P5 renderer\n- [Scene Structure](/p5/scene-structure) — setup/render pattern and instance mode\n- [Drawing with P5](/p5/drawing) — Viji-specific drawing patterns\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers\n- [Native API Reference](/native/api-reference) — the same API in the Native renderer\n- [Shader API Reference](/shader/api-reference) — built-in uniforms for shaders\n- [P5.js Reference](https://p5js.org/reference/) — official P5.js documentation"
1976
+ "markdown": "# API Reference\n\nThis page lists every property and method available on the `viji` object passed to your P5 scene functions. The `viji` API is identical to the [Native renderer](/native/api-reference) — the difference is that P5 scenes also receive a `p5` instance as the second argument.\n\nNew to Viji P5? Start with the [Quick Start](/p5/quickstart) instead.\n\n## Entry Points\n\n```javascript\n// @renderer p5\n\nfunction setup(viji, p5) {\n // Called once when the scene starts (optional)\n}\n\nfunction render(viji, p5) {\n // Called every frame\n}\n```\n\nThe `p5` parameter is a full [P5.js](https://p5js.org/reference/) instance (v1.9.4) in instance mode. All P5 drawing methods (`p5.rect()`, `p5.fill()`, `p5.ellipse()`, etc.) are accessed through it. See [Drawing with P5](/p5/drawing) for Viji-specific drawing patterns.\n\n## Canvas & Context\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.canvas`](/p5/canvas-resolution) | `OffscreenCanvas` | The rendering canvas | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.width`](/p5/canvas-resolution) | `number` | Canvas width in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n| [`viji.height`](/p5/canvas-resolution) | `number` | Canvas height in pixels | [Canvas & Resolution](/p5/canvas-resolution) |\n\n> [!WARNING]\n> `viji.useContext()` is **not available** in P5 scenes. The canvas and its rendering context (2D or WEBGL, depending on the directive) are managed by P5 internally. Calling `useContext()` would conflict with P5's rendering pipeline.\n\n## Timing\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.time`](/p5/timing) | `number` | Seconds elapsed since the scene started | [Timing](/p5/timing) |\n| [`viji.deltaTime`](/p5/timing) | `number` | Seconds since the previous frame | [Timing](/p5/timing) |\n| [`viji.frameCount`](/p5/timing) | `number` | Monotonically increasing frame counter | [Timing](/p5/timing) |\n| [`viji.fps`](/p5/timing) | `number` | Target FPS based on the host's frame rate mode | [Timing](/p5/timing) |\n\n## Parameters\n\nAll parameter methods are called at the top level of your scene file. Read `.value` inside `render()` to get the current value.\n\n| Method | Returns | `.value` Type | Details |\n|--------|---------|---------------|---------|\n| [`viji.slider(default, config)`](/p5/parameters/slider) | `SliderParameter` | `number` | [Slider](/p5/parameters/slider) |\n| [`viji.color(default, config)`](/p5/parameters/color) | `ColorParameter` | `string` (hex) | [Color](/p5/parameters/color) |\n| [`viji.toggle(default, config)`](/p5/parameters/toggle) | `ToggleParameter` | `boolean` | [Toggle](/p5/parameters/toggle) |\n| [`viji.select(default, config)`](/p5/parameters/select) | `SelectParameter` | `string \\| number` | [Select](/p5/parameters/select) |\n| [`viji.number(default, config)`](/p5/parameters/number) | `NumberParameter` | `number` | [Number](/p5/parameters/number) |\n| [`viji.text(default, config)`](/p5/parameters/text) | `TextParameter` | `string` | [Text](/p5/parameters/text) |\n| [`viji.image(null, config)`](/p5/parameters/image) | `ImageParameter` | `ImageBitmap \\| null` | [Image](/p5/parameters/image) |\n| [`viji.button(config)`](/p5/parameters/button) | `ButtonParameter` | `boolean` (true for one frame) | [Button](/p5/parameters/button) |\n\n> [!NOTE]\n> Image parameters have a `.p5` property for use with `p5.image()`. See [Drawing with P5 — Image Parameters](/p5/drawing) for the pattern.\n\nSee [Parameters Overview](/p5/parameters) for the declaration pattern, [Grouping](/p5/parameters/grouping) and [Categories](/p5/parameters/categories) for organization.\n\n## Audio\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.audio.isConnected`](/p5/audio) | `boolean` | Whether an audio source is active | [Overview](/p5/audio) |\n| [`viji.audio.volume.current`](/p5/audio/volume) | `number` | Current RMS volume 0–1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.peak`](/p5/audio/volume) | `number` | Peak volume 0–1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.volume.smoothed`](/p5/audio/volume) | `number` | Smoothed volume 0–1 | [Volume](/p5/audio/volume) |\n| [`viji.audio.bands.low`](/p5/audio/bands) | `number` | Low frequency band energy (20–120 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMid`](/p5/audio/bands) | `number` | Low-mid band energy (120–500 Hz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.mid`](/p5/audio/bands) | `number` | Mid band energy (500–2 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMid`](/p5/audio/bands) | `number` | High-mid band energy (2–6 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.high`](/p5/audio/bands) | `number` | High band energy (6–16 kHz) | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowSmoothed`](/p5/audio/bands) | `number` | Smoothed low band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.lowMidSmoothed`](/p5/audio/bands) | `number` | Smoothed low-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.midSmoothed`](/p5/audio/bands) | `number` | Smoothed mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highMidSmoothed`](/p5/audio/bands) | `number` | Smoothed high-mid band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.bands.highSmoothed`](/p5/audio/bands) | `number` | Smoothed high band | [Frequency Bands](/p5/audio/bands) |\n| [`viji.audio.beat.kick`](/p5/audio/beat) | `number` | Kick beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snare`](/p5/audio/beat) | `number` | Snare beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hat`](/p5/audio/beat) | `number` | Hi-hat beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.any`](/p5/audio/beat) | `number` | Combined beat energy | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.kickSmoothed`](/p5/audio/beat) | `number` | Smoothed kick | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.snareSmoothed`](/p5/audio/beat) | `number` | Smoothed snare | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.hatSmoothed`](/p5/audio/beat) | `number` | Smoothed hi-hat | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.anySmoothed`](/p5/audio/beat) | `number` | Smoothed combined | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.kick`](/p5/audio/beat) | `boolean` | Kick trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.snare`](/p5/audio/beat) | `boolean` | Snare trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.hat`](/p5/audio/beat) | `boolean` | Hi-hat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.triggers.any`](/p5/audio/beat) | `boolean` | Any beat trigger (true for one frame) | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.events`](/p5/audio/beat) | `Array<{ type, time, strength }>` | Beat events this frame | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.bpm`](/p5/audio/beat) | `number` | Tracked BPM | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.confidence`](/p5/audio/beat) | `number` | Beat-tracker confidence 0–1 | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.beat.isLocked`](/p5/audio/beat) | `boolean` | Whether beat tracking is locked | [Beat Detection](/p5/audio/beat) |\n| [`viji.audio.spectral.brightness`](/p5/audio/spectral) | `number` | Spectral brightness 0–1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.spectral.flatness`](/p5/audio/spectral) | `number` | Spectral flatness 0–1 | [Spectral Analysis](/p5/audio/spectral) |\n| [`viji.audio.getFrequencyData()`](/p5/audio/frequency-data) | `() => Uint8Array` | Raw FFT frequency bins (0–255) | [Frequency Data](/p5/audio/frequency-data) |\n| [`viji.audio.getWaveform()`](/p5/audio/waveform) | `() => Float32Array` | Time-domain waveform (-1 to 1) | [Waveform](/p5/audio/waveform) |\n\n## Video & CV\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.video.isConnected`](/p5/video) | `boolean` | Whether a video source is active | [Overview](/p5/video) |\n| [`viji.video.currentFrame`](/p5/video/basics) | `OffscreenCanvas \\| ImageBitmap \\| null` | Current video frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameWidth`](/p5/video/basics) | `number` | Frame width in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameHeight`](/p5/video/basics) | `number` | Frame height in pixels | [Video Basics](/p5/video/basics) |\n| [`viji.video.frameRate`](/p5/video/basics) | `number` | Video frame rate | [Video Basics](/p5/video/basics) |\n| [`viji.video.getFrameData()`](/p5/video/basics) | `() => ImageData \\| null` | Pixel data for the current frame | [Video Basics](/p5/video/basics) |\n| [`viji.video.faces`](/p5/video/face-detection) | `FaceData[]` | Detected faces | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.hands`](/p5/video/hand-tracking) | `HandData[]` | Detected hands | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.pose`](/p5/video/pose-detection) | `PoseData \\| null` | Detected body pose | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.segmentation`](/p5/video/body-segmentation) | `SegmentationData \\| null` | Body segmentation mask | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.enableFaceDetection(enabled)`](/p5/video/face-detection) | `(boolean) => Promise<void>` | Enable/disable face detection | [Face Detection](/p5/video/face-detection) |\n| [`viji.video.cv.enableFaceMesh(enabled)`](/p5/video/face-mesh) | `(boolean) => Promise<void>` | Enable/disable face mesh | [Face Mesh](/p5/video/face-mesh) |\n| [`viji.video.cv.enableEmotionDetection(enabled)`](/p5/video/emotion-detection) | `(boolean) => Promise<void>` | Enable/disable emotion detection | [Emotion Detection](/p5/video/emotion-detection) |\n| [`viji.video.cv.enableHandTracking(enabled)`](/p5/video/hand-tracking) | `(boolean) => Promise<void>` | Enable/disable hand tracking | [Hand Tracking](/p5/video/hand-tracking) |\n| [`viji.video.cv.enablePoseDetection(enabled)`](/p5/video/pose-detection) | `(boolean) => Promise<void>` | Enable/disable pose detection | [Pose Detection](/p5/video/pose-detection) |\n| [`viji.video.cv.enableBodySegmentation(enabled)`](/p5/video/body-segmentation) | `(boolean) => Promise<void>` | Enable/disable body segmentation | [Body Segmentation](/p5/video/body-segmentation) |\n| [`viji.video.cv.getActiveFeatures()`](/p5/video) | `() => CVFeature[]` | List of active CV features | [Overview](/p5/video) |\n| [`viji.video.cv.isProcessing()`](/p5/video) | `() => boolean` | Whether CV is currently processing | [Overview](/p5/video) |\n\n## Mouse\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.mouse.x`](/p5/mouse) | `number` | Cursor X position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.y`](/p5/mouse) | `number` | Cursor Y position in pixels | [Mouse](/p5/mouse) |\n| [`viji.mouse.isInCanvas`](/p5/mouse) | `boolean` | Whether cursor is inside the canvas | [Mouse](/p5/mouse) |\n| [`viji.mouse.isPressed`](/p5/mouse) | `boolean` | Whether any button is pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.leftButton`](/p5/mouse) | `boolean` | Left button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.rightButton`](/p5/mouse) | `boolean` | Right button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.middleButton`](/p5/mouse) | `boolean` | Middle button state | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaX`](/p5/mouse) | `number` | Horizontal movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.deltaY`](/p5/mouse) | `number` | Vertical movement since last frame | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelDelta`](/p5/mouse) | `number` | Scroll wheel delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelX`](/p5/mouse) | `number` | Horizontal scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wheelY`](/p5/mouse) | `number` | Vertical scroll delta | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasPressed`](/p5/mouse) | `boolean` | True for one frame when pressed | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasReleased`](/p5/mouse) | `boolean` | True for one frame when released | [Mouse](/p5/mouse) |\n| [`viji.mouse.wasMoved`](/p5/mouse) | `boolean` | True for one frame when moved | [Mouse](/p5/mouse) |\n\n## Keyboard\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.keyboard.isPressed(key)`](/p5/keyboard) | `(string) => boolean` | Whether a key is currently held | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasPressed(key)`](/p5/keyboard) | `(string) => boolean` | True for one frame when pressed | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.wasReleased(key)`](/p5/keyboard) | `(string) => boolean` | True for one frame when released | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.activeKeys`](/p5/keyboard) | `Set<string>` | All currently held keys | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.pressedThisFrame`](/p5/keyboard) | `Set<string>` | Keys pressed this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.releasedThisFrame`](/p5/keyboard) | `Set<string>` | Keys released this frame | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyPressed`](/p5/keyboard) | `string` | Most recently pressed key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.lastKeyReleased`](/p5/keyboard) | `string` | Most recently released key | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.shift`](/p5/keyboard) | `boolean` | Shift key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.ctrl`](/p5/keyboard) | `boolean` | Ctrl/Cmd key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.alt`](/p5/keyboard) | `boolean` | Alt/Option key state | [Keyboard](/p5/keyboard) |\n| [`viji.keyboard.meta`](/p5/keyboard) | `boolean` | Meta/Win key state | [Keyboard](/p5/keyboard) |\n\n## Touch\n\n> **Note:** The property is `viji.touches` (plural), not `viji.touch`.\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.touches.points`](/p5/touch) | `TouchPoint[]` | All active touch points | [Touch](/p5/touch) |\n| [`viji.touches.count`](/p5/touch) | `number` | Number of active touches | [Touch](/p5/touch) |\n| [`viji.touches.started`](/p5/touch) | `TouchPoint[]` | Touch points that started this frame | [Touch](/p5/touch) |\n| [`viji.touches.moved`](/p5/touch) | `TouchPoint[]` | Touch points that moved this frame | [Touch](/p5/touch) |\n| [`viji.touches.ended`](/p5/touch) | `TouchPoint[]` | Touch points that ended this frame | [Touch](/p5/touch) |\n| [`viji.touches.primary`](/p5/touch) | `TouchPoint \\| null` | The first active touch point | [Touch](/p5/touch) |\n\n**`TouchPoint` fields:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force` (numbers); `isInCanvas`, `isNew`, `isActive`, `isEnding` (booleans); `deltaX`, `deltaY` (numbers); `velocity` `{ x, y }`.\n\n## Pointer (Unified)\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.pointer.x`](/p5/pointer) | `number` | Primary pointer X position | [Pointer](/p5/pointer) |\n| [`viji.pointer.y`](/p5/pointer) | `number` | Primary pointer Y position | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaX`](/p5/pointer) | `number` | Horizontal movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.deltaY`](/p5/pointer) | `number` | Vertical movement since last frame | [Pointer](/p5/pointer) |\n| [`viji.pointer.isDown`](/p5/pointer) | `boolean` | Whether the pointer is active (click or touch) | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasPressed`](/p5/pointer) | `boolean` | True for one frame when pressed | [Pointer](/p5/pointer) |\n| [`viji.pointer.wasReleased`](/p5/pointer) | `boolean` | True for one frame when released | [Pointer](/p5/pointer) |\n| [`viji.pointer.isInCanvas`](/p5/pointer) | `boolean` | Whether pointer is inside the canvas | [Pointer](/p5/pointer) |\n| [`viji.pointer.type`](/p5/pointer) | `'mouse' \\| 'touch' \\| 'none'` | Current input source | [Pointer](/p5/pointer) |\n\n## Device Sensors\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.device.motion`](/p5/sensors) | `DeviceMotionData \\| null` | Accelerometer and gyroscope data | [Device Sensors](/p5/sensors) |\n| [`viji.device.orientation`](/p5/sensors) | `DeviceOrientationData \\| null` | Device orientation (alpha, beta, gamma) | [Device Sensors](/p5/sensors) |\n\n**`DeviceMotionData`:** `acceleration` `{ x, y, z }`, `accelerationIncludingGravity` `{ x, y, z }`, `rotationRate` `{ alpha, beta, gamma }`, `interval`.\n\n**`DeviceOrientationData`:** `alpha`, `beta`, `gamma` (numbers or null), `absolute` (boolean).\n\n## External Devices\n\n| Member | Type | Description | Details |\n|--------|------|-------------|---------|\n| [`viji.devices`](/p5/external-devices) | `DeviceState[]` | Connected external devices | [Overview](/p5/external-devices) |\n| [`viji.devices[i].id`](/p5/external-devices) | `string` | Unique device identifier | [Overview](/p5/external-devices) |\n| [`viji.devices[i].name`](/p5/external-devices) | `string` | User-friendly device name | [Overview](/p5/external-devices) |\n| [`viji.devices[i].motion`](/p5/external-devices/sensors) | `DeviceMotionData \\| null` | Device accelerometer/gyroscope | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].orientation`](/p5/external-devices/sensors) | `DeviceOrientationData \\| null` | Device orientation | [Device Sensors](/p5/external-devices/sensors) |\n| [`viji.devices[i].video`](/p5/external-devices/video) | `VideoAPI \\| null` | Device camera video | [Device Video](/p5/external-devices/video) |\n| [`viji.devices[i].audio`](/p5/external-devices/audio) | `AudioStreamAPI \\| null` | Device audio stream | [Device Audio](/p5/external-devices/audio) |\n\n## Streams\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.videoStreams` | `VideoAPI[]` | Additional video streams provided by the host |\n| `viji.audioStreams` | `AudioStreamAPI[]` | Additional audio streams provided by the host |\n\nEach `videoStreams` element has the same shape as [`viji.video`](/p5/video). Each `audioStreams` element provides lightweight audio analysis (volume, frequency bands, spectral features) but does **not** include beat detection or BPM tracking — see [`viji.audio`](/p5/audio) for full analysis on the main stream.\n\nBoth arrays may be empty if the host does not provide additional streams. Audio streams from external devices appear on `device.audio`, not in `viji.audioStreams`.\n\n## Related\n\n- [Quick Start](/p5/quickstart) — getting started with the P5 renderer\n- [Scene Structure](/p5/scene-structure) — setup/render pattern and instance mode\n- [Drawing with P5](/p5/drawing) — Viji-specific drawing patterns\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers\n- [Native API Reference](/native/api-reference) — the same API in the Native renderer\n- [Shader API Reference](/shader/api-reference) — built-in uniforms for shaders\n- [P5.js Reference](https://p5js.org/reference/) — official P5.js documentation"
1977
1977
  }
1978
1978
  ]
1979
1979
  },
@@ -1989,7 +1989,7 @@ export const docsApi = {
1989
1989
  {
1990
1990
  "type": "live-example",
1991
1991
  "title": "P5 Lifecycle — Expanding Rings",
1992
- "sceneCode": "// @renderer p5\n\nconst ringCount = viji.slider(5, { min: 1, max: 12, step: 1, label: 'Ring Count' });\nconst speed = viji.slider(1, { min: 0.2, max: 3, label: 'Speed' });\nconst strokeColor = viji.color('#ff44aa', { label: 'Ring Color' });\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n p5.noFill();\n}\n\nfunction render(viji, p5) {\n p5.background(0, 0, 5);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const maxR = Math.min(viji.width, viji.height) * 0.45;\n\n for (let i = 0; i < ringCount.value; i++) {\n const t = (viji.time * speed.value + i * 0.4) % 3;\n const radius = (t / 3) * maxR;\n const alpha = p5.map(t, 0, 3, 100, 0);\n const sw = p5.map(t, 0, 3, Math.min(viji.width, viji.height) * 0.01, 1);\n\n p5.stroke(strokeColor.value);\n p5.drawingContext.globalAlpha = alpha / 100;\n p5.strokeWeight(sw);\n p5.circle(cx, cy, radius * 2);\n }\n\n p5.drawingContext.globalAlpha = 1;\n}\n",
1992
+ "sceneCode": "// @renderer p5\n\nconst ringCount = viji.slider(5, { min: 1, max: 12, step: 1, label: 'Ring Count' });\nconst speed = viji.slider(1, { min: 0.2, max: 3, label: 'Speed' });\nconst strokeColor = viji.color('#ff44aa', { label: 'Ring Color' });\nlet phase = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n p5.noFill();\n}\n\nfunction render(viji, p5) {\n phase += speed.value * viji.deltaTime;\n p5.background(0, 0, 5);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const maxR = Math.min(viji.width, viji.height) * 0.45;\n\n for (let i = 0; i < ringCount.value; i++) {\n const t = (phase + i * 0.4) % 3;\n const radius = (t / 3) * maxR;\n const alpha = p5.map(t, 0, 3, 100, 0);\n const sw = p5.map(t, 0, 3, Math.min(viji.width, viji.height) * 0.01, 1);\n\n p5.stroke(strokeColor.value);\n p5.drawingContext.globalAlpha = alpha / 100;\n p5.strokeWeight(sw);\n p5.circle(cx, cy, radius * 2);\n }\n\n p5.drawingContext.globalAlpha = 1;\n}\n",
1993
1993
  "sceneFile": "scene-structure-lifecycle.scene.js"
1994
1994
  },
1995
1995
  {
@@ -2026,12 +2026,12 @@ export const docsApi = {
2026
2026
  "content": [
2027
2027
  {
2028
2028
  "type": "text",
2029
- "markdown": "# Drawing with P5\n\nViji gives you a full P5.js instance in every P5 scene. All standard P5 drawing functions — shapes, colors, transforms, typography, pixel manipulation, math utilities — work as documented in the official reference.\n\n> **P5.js Reference**: Viji loads **P5.js v1.9.4**. For the full drawing API, see the [P5.js Reference](https://p5js.org/reference/).\n\nThis page covers only what is **different or specific to Viji** — how to draw images and video, off-screen buffers, font limitations, and what is not supported.\n\n## Instance Mode\n\n> [!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\nConstants are also namespaced: `p5.PI`, `p5.TWO_PI`, `p5.HSB`, `p5.CENTER`, `p5.BLEND`, etc.\n\n## Drawing Images\n\n### Image Parameters\n\nUse `.value.p5` (not `.value`) when passing image parameters to `p5.image()`:\n\n```javascript\n// @renderer p5\n\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji, p5) {\n p5.background(0);\n if (tex.value) {\n p5.image(tex.value.p5, 0, 0, p5.width, p5.height);\n }\n}\n```\n\n> [!WARNING]\n> Passing `tex.value` directly to `p5.image()` will not work. The raw `ImageBitmap` is not P5-compatible. Always use `.value.p5`.\n\nThe `.p5` wrapper is cached — accessing it multiple times per frame has no overhead. See [Image Parameter](../parameters/image/) for full details.\n\n### Video Frames\n\nVideo frames are automatically wrapped for P5 compatibility. Pass `viji.video.currentFrame` directly to `p5.image()`:\n\n```javascript\n// @renderer p5\n\nfunction render(viji, p5) {\n p5.background(0);\n if (viji.video.isConnected && viji.video.currentFrame) {\n p5.image(viji.video.currentFrame, 0, 0, p5.width, p5.height);\n }\n}\n```\n\nDevice video frames work the same way:\n\n```javascript\nfor (const device of viji.devices) {\n if (device.video?.isConnected && device.video.currentFrame) {\n p5.image(device.video.currentFrame, x, y, w, h);\n }\n}\n```\n\nSee [Video Basics](../video/basics/) for aspect-ratio-correct drawing and `getFrameData()`.\n\n### Tint\n\n`p5.tint()` works as expected for coloring or fading images:\n\n```javascript\n// @renderer p5\n\nconst tex = viji.image(null, { label: 'Image' });\nconst fade = viji.slider(255, { min: 0, max: 255, label: 'Fade' });\n\nfunction render(viji, p5) {\n p5.background(0);\n if (tex.value) {\n p5.tint(255, fade.value);\n p5.image(tex.value.p5, 0, 0, p5.width, p5.height);\n p5.noTint();\n }\n}\n```\n\n## Off-Screen Buffers\n\n`p5.createGraphics(w, h)` works in Viji. Each call creates a real `OffscreenCanvas` buffer you can draw to independently and then composite onto the main canvas:\n\n```javascript\n// @renderer p5\n\nlet buffer;\n\nfunction setup(viji, p5) {\n buffer = p5.createGraphics(p5.width, p5.height);\n}\n\nfunction render(viji, p5) {\n buffer.background(0, 10);\n buffer.noStroke();\n buffer.fill(255);\n buffer.ellipse(\n buffer.width / 2 + p5.sin(viji.time) * 100,\n buffer.height / 2,\n 20, 20\n );\n\n p5.image(buffer, 0, 0);\n}\n```\n\n> [!NOTE]\n> Off-screen buffers from `createGraphics(w, h)` are **2D only**. `createGraphics(w, h, p5.WEBGL)` is not supported.\n\n## Fonts\n\n`p5.loadFont()` is not available in the worker environment. Use system fonts instead:\n\n```javascript\n// @renderer p5\n\nfunction setup(viji, p5) {\n p5.textFont('monospace');\n}\n\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255);\n p5.textSize(24);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.text('Hello, Viji', p5.width / 2, p5.height / 2);\n}\n```\n\nAvailable system font families: `'monospace'`, `'sans-serif'`, `'serif'`. You can also try specific system fonts like `'Courier New'`, `'Arial'`, `'Georgia'`, but availability depends on the device.\n\n## Blend Modes\n\n`p5.blendMode()` works as expected. All standard P5 blend modes are available:\n\n```javascript\np5.blendMode(p5.ADD);\np5.blendMode(p5.MULTIPLY);\np5.blendMode(p5.SCREEN);\np5.blendMode(p5.BLEND); // default\n```\n\n## WEBGL main canvas\n\nUse the **first line** of the scene (same place as the P5 renderer directive):\n\n```javascript\n// @renderer p5 webgl\n```\n\nViji creates the main canvas with P5’s WEBGL renderer. You still **must not** call `createCanvas()` — the directive selects 2D vs WEBGL, not your `setup()` code.\n\nIn WEBGL mode, `p5.drawingContext` is a **WebGL** context. Do not use Canvas 2D–only APIs on it. Prefer P5’s 3D drawing APIs, `p5.image()` / textures for images and video (including `viji.video.currentFrame` and image parameters’ `.p5` wrapper), and follow the [P5.js WEBGL reference](https://p5js.org/reference/#/p5/WEBGL).\n\n---\n\n## Known Limitations\n\n| Feature | Status | Alternative |\n|---------|--------|-------------|\n| WEBGL main canvas | Supported via `// @renderer p5 webgl` | Do not call `createCanvas(..., p5.WEBGL)` |\n| WEBGL `createGraphics` | Not supported | Use 2D `createGraphics(w, h)` only, or [Shader](/shader/quickstart) / [Three.js (Native)](/native/external-libraries) for advanced GPU pipelines |\n| `p5.loadFont()` | Not available | Use system fonts: `p5.textFont('monospace')` |\n| `p5.loadImage()` | Not available | Use [`viji.image()`](../parameters/image/) parameters |\n| `p5.createCapture()` | Not available | Use [`viji.video`](../video/) |\n\n> [!NOTE]\n> For the full list of unavailable P5 features (event callbacks, `save()`, `frameRate()`, etc.), see [Converting P5 Sketches](../converting-sketches/).\n\n## Basic Example"
2029
+ "markdown": "# Drawing with P5\n\nViji gives you a full P5.js instance in every P5 scene. All standard P5 drawing functions — shapes, colors, transforms, typography, pixel manipulation, math utilities — work as documented in the official reference.\n\n> **P5.js Reference**: Viji loads **P5.js v1.9.4**. For the full drawing API, see the [P5.js Reference](https://p5js.org/reference/).\n\nThis page covers only what is **different or specific to Viji** — how to draw images and video, off-screen buffers, font limitations, and what is not supported.\n\n## Instance Mode\n\n> [!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\nConstants are also namespaced: `p5.PI`, `p5.TWO_PI`, `p5.HSB`, `p5.CENTER`, `p5.BLEND`, etc.\n\n## Drawing Images\n\n### Image Parameters\n\nUse `.p5` (not `.value`) when passing image parameters to `p5.image()`:\n\n```javascript\n// @renderer p5\n\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji, p5) {\n p5.background(0);\n if (tex.value) {\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n }\n}\n```\n\n> [!WARNING]\n> Passing `tex.value` directly to `p5.image()` will not work. The raw `ImageBitmap` is not P5-compatible. Always use `.p5`.\n\nThe `.p5` wrapper is cached — accessing it multiple times per frame has no overhead. See [Image Parameter](../parameters/image/) for full details.\n\n### Video Frames\n\nVideo frames are automatically wrapped for P5 compatibility. Pass `viji.video.currentFrame` directly to `p5.image()`:\n\n```javascript\n// @renderer p5\n\nfunction render(viji, p5) {\n p5.background(0);\n if (viji.video.isConnected && viji.video.currentFrame) {\n p5.image(viji.video.currentFrame, 0, 0, p5.width, p5.height);\n }\n}\n```\n\nDevice video frames work the same way:\n\n```javascript\nfor (const device of viji.devices) {\n if (device.video?.isConnected && device.video.currentFrame) {\n p5.image(device.video.currentFrame, x, y, w, h);\n }\n}\n```\n\nSee [Video Basics](../video/basics/) for aspect-ratio-correct drawing and `getFrameData()`.\n\n### Tint\n\n`p5.tint()` works as expected for coloring or fading images:\n\n```javascript\n// @renderer p5\n\nconst tex = viji.image(null, { label: 'Image' });\nconst fade = viji.slider(255, { min: 0, max: 255, label: 'Fade' });\n\nfunction render(viji, p5) {\n p5.background(0);\n if (tex.value) {\n p5.tint(255, fade.value);\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n p5.noTint();\n }\n}\n```\n\n## Off-Screen Buffers\n\n`p5.createGraphics(w, h)` works in Viji. Each call creates a real `OffscreenCanvas` buffer you can draw to independently and then composite onto the main canvas:\n\n```javascript\n// @renderer p5\n\nlet buffer;\n\nfunction setup(viji, p5) {\n buffer = p5.createGraphics(p5.width, p5.height);\n}\n\nfunction render(viji, p5) {\n buffer.background(0, 10);\n buffer.noStroke();\n buffer.fill(255);\n buffer.ellipse(\n buffer.width / 2 + p5.sin(viji.time) * 100,\n buffer.height / 2,\n 20, 20\n );\n\n p5.image(buffer, 0, 0);\n}\n```\n\n> [!NOTE]\n> Off-screen buffers from `createGraphics(w, h)` are **2D only**. `createGraphics(w, h, p5.WEBGL)` is not supported.\n\n## Fonts\n\n`p5.loadFont()` is not available in the worker environment. Use system fonts instead:\n\n```javascript\n// @renderer p5\n\nfunction setup(viji, p5) {\n p5.textFont('monospace');\n}\n\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255);\n p5.textSize(24);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.text('Hello, Viji', p5.width / 2, p5.height / 2);\n}\n```\n\nAvailable system font families: `'monospace'`, `'sans-serif'`, `'serif'`. You can also try specific system fonts like `'Courier New'`, `'Arial'`, `'Georgia'`, but availability depends on the device.\n\n## Blend Modes\n\n`p5.blendMode()` works as expected. All standard P5 blend modes are available:\n\n```javascript\np5.blendMode(p5.ADD);\np5.blendMode(p5.MULTIPLY);\np5.blendMode(p5.SCREEN);\np5.blendMode(p5.BLEND); // default\n```\n\n## WEBGL main canvas\n\nUse the **first line** of the scene (same place as the P5 renderer directive):\n\n```javascript\n// @renderer p5 webgl\n```\n\nViji creates the main canvas with P5’s WEBGL renderer. You still **must not** call `createCanvas()` — the directive selects 2D vs WEBGL, not your `setup()` code.\n\nIn WEBGL mode, `p5.drawingContext` is a **WebGL** context. Do not use Canvas 2D–only APIs on it. Prefer P5’s 3D drawing APIs, `p5.image()` / textures for images and video (including `viji.video.currentFrame` and image parameters’ `.p5` wrapper), and follow the [P5.js WEBGL reference](https://p5js.org/reference/#/p5/WEBGL).\n\n---\n\n## Known Limitations\n\n| Feature | Status | Alternative |\n|---------|--------|-------------|\n| WEBGL main canvas | Supported via `// @renderer p5 webgl` | Do not call `createCanvas(..., p5.WEBGL)` |\n| WEBGL `createGraphics` | Not supported | Use 2D `createGraphics(w, h)` only, or [Shader](/shader/quickstart) / [Three.js (Native)](/native/external-libraries) for advanced GPU pipelines |\n| `p5.loadFont()` | Not available | Use system fonts: `p5.textFont('monospace')` |\n| `p5.loadImage()` | Not available | Use [`viji.image()`](../parameters/image/) parameters |\n| `p5.createCapture()` | Not available | Use [`viji.video`](../video/) |\n\n> [!NOTE]\n> For the full list of unavailable P5 features (event callbacks, `save()`, `frameRate()`, etc.), see [Converting P5 Sketches](../converting-sketches/).\n\n## Basic Example"
2030
2030
  },
2031
2031
  {
2032
2032
  "type": "live-example",
2033
2033
  "title": "Drawing with P5 — Shapes & Transforms",
2034
- "sceneCode": "// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 4, step: 0.1, label: 'Speed' });\nconst count = viji.slider(6, { min: 3, max: 16, step: 1, label: 'Shape Count' });\nconst hueShift = viji.slider(0, { min: 0, max: 360, step: 1, label: 'Hue Offset' });\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n}\n\nfunction render(viji, p5) {\n p5.background(0, 0, 5);\n\n const cx = p5.width / 2;\n const cy = p5.height / 2;\n const radius = Math.min(p5.width, p5.height) * 0.3;\n const t = viji.time * speed.value;\n const n = count.value;\n\n p5.noStroke();\n\n for (let i = 0; i < n; i++) {\n const angle = (i / n) * p5.TWO_PI + t;\n const x = cx + p5.cos(angle) * radius;\n const y = cy + p5.sin(angle) * radius;\n const hue = (hueShift.value + (i / n) * 360) % 360;\n const size = 20 + p5.sin(t * 2 + i) * 10;\n\n p5.push();\n p5.translate(x, y);\n p5.rotate(angle + t * 0.5);\n\n p5.fill(hue, 70, 90, 80);\n p5.rectMode(p5.CENTER);\n p5.rect(0, 0, size, size, 4);\n\n p5.fill(hue, 40, 100);\n p5.ellipse(0, 0, size * 0.4, size * 0.4);\n\n p5.pop();\n }\n\n p5.push();\n p5.translate(cx, cy);\n p5.rotate(-t * 0.3);\n p5.stroke(0, 0, 100, 30);\n p5.strokeWeight(1);\n p5.noFill();\n const innerR = radius * 0.4;\n for (let i = 0; i < n; i++) {\n const a1 = (i / n) * p5.TWO_PI;\n const a2 = ((i + 1) / n) * p5.TWO_PI;\n p5.line(\n p5.cos(a1) * innerR, p5.sin(a1) * innerR,\n p5.cos(a2) * innerR, p5.sin(a2) * innerR\n );\n }\n p5.pop();\n\n p5.noStroke();\n p5.fill(0, 0, 50);\n p5.textFont('monospace');\n p5.textSize(11);\n p5.textAlign(p5.LEFT);\n p5.text(`shapes: ${n} speed: ${speed.value.toFixed(1)}x`, 8, p5.height - 8);\n}\n",
2034
+ "sceneCode": "// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 4, step: 0.1, label: 'Speed' });\nconst count = viji.slider(6, { min: 3, max: 16, step: 1, label: 'Shape Count' });\nconst hueShift = viji.slider(0, { min: 0, max: 360, step: 1, label: 'Hue Offset' });\nlet phase = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n}\n\nfunction render(viji, p5) {\n p5.background(0, 0, 5);\n\n const cx = p5.width / 2;\n const cy = p5.height / 2;\n const radius = Math.min(p5.width, p5.height) * 0.3;\n phase += speed.value * viji.deltaTime;\n const t = phase;\n const n = count.value;\n\n p5.noStroke();\n\n for (let i = 0; i < n; i++) {\n const angle = (i / n) * p5.TWO_PI + t;\n const x = cx + p5.cos(angle) * radius;\n const y = cy + p5.sin(angle) * radius;\n const hue = (hueShift.value + (i / n) * 360) % 360;\n const size = 20 + p5.sin(t * 2 + i) * 10;\n\n p5.push();\n p5.translate(x, y);\n p5.rotate(angle + t * 0.5);\n\n p5.fill(hue, 70, 90, 80);\n p5.rectMode(p5.CENTER);\n p5.rect(0, 0, size, size, 4);\n\n p5.fill(hue, 40, 100);\n p5.ellipse(0, 0, size * 0.4, size * 0.4);\n\n p5.pop();\n }\n\n p5.push();\n p5.translate(cx, cy);\n p5.rotate(-t * 0.3);\n p5.stroke(0, 0, 100, 30);\n p5.strokeWeight(1);\n p5.noFill();\n const innerR = radius * 0.4;\n for (let i = 0; i < n; i++) {\n const a1 = (i / n) * p5.TWO_PI;\n const a2 = ((i + 1) / n) * p5.TWO_PI;\n p5.line(\n p5.cos(a1) * innerR, p5.sin(a1) * innerR,\n p5.cos(a2) * innerR, p5.sin(a2) * innerR\n );\n }\n p5.pop();\n\n p5.noStroke();\n p5.fill(0, 0, 50);\n p5.textFont('monospace');\n p5.textSize(11);\n p5.textAlign(p5.LEFT);\n p5.text(`shapes: ${n} speed: ${speed.value.toFixed(1)}x`, 8, p5.height - 8);\n}\n",
2035
2035
  "sceneFile": "drawing-demo.scene.js"
2036
2036
  },
2037
2037
  {
@@ -2088,7 +2088,7 @@ export const docsApi = {
2088
2088
  },
2089
2089
  {
2090
2090
  "type": "text",
2091
- "markdown": "## When to Use `viji.time` vs `viji.deltaTime`\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| `p5.sin()` / `p5.cos()` animation | `viji.time` | Periodic functions need absolute time |\n| Hue cycling, color animation | `viji.time` | Continuous monotonic input |\n| Position += velocity | `viji.deltaTime` | Distance = speed × elapsed time |\n| Rotation += angular speed | `viji.deltaTime` | Angle increments must be per-second |\n| Opacity fading | `viji.deltaTime` | Fade rate is per-second |\n\n## `viji.frameCount` vs `p5.frameCount`\n\nBoth exist but have different origins:\n\n| Property | Source | Starts At |\n|----------|--------|-----------|\n| `viji.frameCount` | Viji runtime | 0 |\n| `p5.frameCount` | P5 internal | 1 |\n\n`viji.frameCount` is the canonical frame counter across all renderers. It increments by 1 every frame and is consistent whether you're in a native, P5, or shader scene. `p5.frameCount` is maintained by P5 internally and may differ by 1. Use `viji.frameCount` for consistency.\n\n## `viji.fps` — Target Frame Rate\n\n`viji.fps` is the **target** frame rate based on the host's configuration, not a measured value:\n\n- `frameRateMode: 'full'` → screen refresh rate (typically 60 or 120)\n- `frameRateMode: 'half'` → half the screen refresh rate (typically 30 or 60)\n\nThis value is stable and does not fluctuate. Don't use it for animation timing — use `viji.time` or `viji.deltaTime` instead.\n\n> [!NOTE]\n> P5's `frameRate()` function is not available in Viji — the host controls the render loop.\n\n## Frame-Rate Independence\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```javascript\n// Bad — speed depends on frame rate\nangle += 0.02;\n\n// Good — same visual speed at any frame rate\nangle += 1.2 * viji.deltaTime; // 1.2 radians per second\n```\n\n## Next Steps\n\n- [Canvas & Resolution](/p5/canvas-resolution) — [`viji.width`](/p5/canvas-resolution), [`viji.height`](/p5/canvas-resolution), responsive layouts\n- [Scene Structure](/p5/scene-structure) — `setup()`, `render()`, lifecycle\n- [Parameters](/p5/parameters) — sliders, colors, toggles\n- [Native Timing](/native/timing) — timing in the native renderer\n- [Shader Timing](/shader/timing) — `u_time`, `u_deltaTime`, `u_frame`, `u_fps`\n- [API Reference](/p5/api-reference) — full list of everything available"
2091
+ "markdown": "## When to Use `viji.time` vs `viji.deltaTime`\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| `p5.sin()` / `p5.cos()` at **constant** speed | `viji.time` | Periodic functions need absolute time |\n| Hue cycling at constant rate | `viji.time` | Continuous monotonic input |\n| Oscillation at **parameter-driven** speed | `viji.deltaTime` (accumulator) | Avoids phase jumps when the slider changes |\n| Position += velocity | `viji.deltaTime` | Distance = speed × elapsed time |\n| Rotation += angular speed | `viji.deltaTime` | Angle increments must be per-second |\n| Opacity fading | `viji.deltaTime` | Fade rate is per-second |\n\n### Accumulator Pattern — Parameter-Driven Speed\n\nWhen animation speed is controlled by a user parameter, do **not** use `viji.time * speed.value` — changing the slider recalculates the full phase history and causes a visible jump. Instead, accumulate incrementally:\n\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet phase = 0;\n\nfunction render(viji, p5) {\n phase += speed.value * viji.deltaTime;\n p5.rotate(phase);\n}\n```\n\n> [!TIP]\n> `viji.time * constant` is fine when the multiplier never changes at runtime. The accumulator is only needed when speed is a **user-controlled parameter**. See [Best Practices](/getting-started/best-practices) for the full explanation.\n\n## `viji.frameCount` vs `p5.frameCount`\n\nBoth exist but have different origins:\n\n| Property | Source | Starts At |\n|----------|--------|-----------|\n| `viji.frameCount` | Viji runtime | 0 |\n| `p5.frameCount` | P5 internal | 1 |\n\n`viji.frameCount` is the canonical frame counter across all renderers. It increments by 1 every frame and is consistent whether you're in a native, P5, or shader scene. `p5.frameCount` is maintained by P5 internally and may differ by 1. Use `viji.frameCount` for consistency.\n\n## `viji.fps` — Target Frame Rate\n\n`viji.fps` is the **target** frame rate based on the host's configuration, not a measured value:\n\n- `frameRateMode: 'full'` → screen refresh rate (typically 60 or 120)\n- `frameRateMode: 'half'` → half the screen refresh rate (typically 30 or 60)\n\nThis value is stable and does not fluctuate. Don't use it for animation timing — use `viji.time` or `viji.deltaTime` instead.\n\n> [!NOTE]\n> P5's `frameRate()` function is not available in Viji — the host controls the render loop.\n\n## Frame-Rate Independence\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```javascript\n// Bad — speed depends on frame rate\nangle += 0.02;\n\n// Good — same visual speed at any frame rate\nangle += 1.2 * viji.deltaTime; // 1.2 radians per second\n```\n\n## Next Steps\n\n- [Canvas & Resolution](/p5/canvas-resolution) — [`viji.width`](/p5/canvas-resolution), [`viji.height`](/p5/canvas-resolution), responsive layouts\n- [Scene Structure](/p5/scene-structure) — `setup()`, `render()`, lifecycle\n- [Parameters](/p5/parameters) — sliders, colors, toggles\n- [Native Timing](/native/timing) — timing in the native renderer\n- [Shader Timing](/shader/timing) — `u_time`, `u_deltaTime`, `u_frame`, `u_fps`\n- [API Reference](/p5/api-reference) — full list of everything available"
2092
2092
  }
2093
2093
  ]
2094
2094
  },
@@ -2115,7 +2115,7 @@ export const docsApi = {
2115
2115
  {
2116
2116
  "type": "live-example",
2117
2117
  "title": "Slider Control",
2118
- "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst dotColor = viji.color('#44ddff', { label: 'Color' });\nconst radius = viji.slider(0.25, { min: 0.05, max: 0.45, step: 0.01, label: 'Radius' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\nconst speed = viji.slider(1, { min: 0, max: 5, step: 0.1, label: 'Speed' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n p5.fill(dotColor.value);\n p5.noStroke();\n\n const unit = Math.min(p5.width, p5.height);\n const r = unit * radius.value;\n const n = count.value;\n const dotR = unit * 0.02;\n\n for (let i = 0; i < n; i++) {\n const a = (i / n) * p5.TWO_PI + viji.time * speed.value;\n const x = p5.width / 2 + Math.cos(a) * r;\n const y = p5.height / 2 + Math.sin(a) * r;\n p5.ellipse(x, y, dotR * 2);\n }\n}\n",
2118
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst dotColor = viji.color('#44ddff', { label: 'Color' });\nconst radius = viji.slider(0.25, { min: 0.05, max: 0.45, step: 0.01, label: 'Radius' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\nconst speed = viji.slider(1, { min: 0, max: 5, step: 0.1, label: 'Speed' });\nlet phase = 0;\n\nfunction render(viji, p5) {\n phase += speed.value * viji.deltaTime;\n p5.background(bg.value);\n p5.fill(dotColor.value);\n p5.noStroke();\n\n const unit = Math.min(p5.width, p5.height);\n const r = unit * radius.value;\n const n = count.value;\n const dotR = unit * 0.02;\n\n for (let i = 0; i < n; i++) {\n const a = (i / n) * p5.TWO_PI + phase;\n const x = p5.width / 2 + Math.cos(a) * r;\n const y = p5.height / 2 + Math.sin(a) * r;\n p5.ellipse(x, y, dotR * 2);\n }\n}\n",
2119
2119
  "sceneFile": "slider-p5.scene.js"
2120
2120
  },
2121
2121
  {
@@ -2236,17 +2236,17 @@ export const docsApi = {
2236
2236
  "content": [
2237
2237
  {
2238
2238
  "type": "text",
2239
- "markdown": "# viji.image()\n\n```\nimage(defaultValue: null, config: ImageConfig): ImageParameter\n```\n\nCreates an image upload parameter. The host renders it as a file picker or drag-and-drop area. In P5.js scenes, uploaded images include a special `.p5` property.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | — | Always `null` — images are provided by the user |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns an `ImageParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ImageBitmap \\| null` | Raw image, or `null` if none provided |\n| `value.p5` | `p5.Image` | P5-compatible image object (only in P5 renderer) |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## The `.p5` Property\n\nIn P5.js scenes, every non-null image value has a `.p5` property that returns a `p5.Image` object. This lets you use native P5 drawing functions like `p5.image()` and `p5.tint()`:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.image(tex.value.p5, 0, 0, p5.width, p5.height);\n }\n}\n```\n\n> [!WARNING]\n> Always use `tex.value.p5` (not `tex.value` directly) when passing to P5 drawing functions. Using the raw `ImageBitmap` will not work with `p5.image()`.\n\nThe `.p5` image is cached internally — accessing it multiple times per frame does not create new objects.\n\n## Usage\n\nAlways check for `null` before drawing — the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Image' });\nconst opacity = viji.slider(255, { min: 0, max: 255, step: 1, label: 'Opacity' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.tint(255, opacity.value);\n p5.image(tex.value.p5, 0, 0, p5.width, p5.height);\n p5.noTint();\n }\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Defining them inside `setup()` would register the parameter too late — no UI control would appear. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default."
2239
+ "markdown": "# viji.image()\n\n```\nimage(defaultValue: null, config: ImageConfig): ImageParameter\n```\n\nCreates an image upload parameter. The host renders it as a file picker or drag-and-drop area. In P5.js scenes, uploaded images include a special `.p5` property.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | — | Always `null` — images are provided by the user |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns an `ImageParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ImageBitmap \\| null` | Raw image, or `null` if none provided |\n| `p5` | `p5.Image` | P5-compatible image object (only in P5 renderer) |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## The `.p5` Property\n\nIn P5.js scenes, every image parameter with a non-null value has a `.p5` property that returns a `p5.Image` object. This lets you use native P5 drawing functions like `p5.image()` and `p5.tint()`:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n }\n}\n```\n\n> [!WARNING]\n> Always use `tex.p5` (not `tex.value` directly) when passing to P5 drawing functions. The raw `ImageBitmap` from `.value` is not P5-compatible.\n\nThe `.p5` image is cached internally — accessing it multiple times per frame does not create new objects.\n\n## Usage\n\nAlways check for `null` before drawing — the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Image' });\nconst opacity = viji.slider(255, { min: 0, max: 255, step: 1, label: 'Opacity' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.tint(255, opacity.value);\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n p5.noTint();\n }\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Defining them inside `setup()` would register the parameter too late — no UI control would appear. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default."
2240
2240
  },
2241
2241
  {
2242
2242
  "type": "live-example",
2243
2243
  "title": "Image Upload",
2244
- "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst tex = viji.image(null, { label: 'Image' });\nconst opacity = viji.slider(255, { min: 0, max: 255, step: 1, label: 'Opacity' });\nconst scale = viji.slider(1, { min: 0.2, max: 2, step: 0.05, label: 'Scale' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n\n if (tex.value) {\n const img = tex.value.p5;\n const imgAspect = img.width / img.height;\n const canvasAspect = p5.width / p5.height;\n let dw, dh;\n if (imgAspect > canvasAspect) { dw = p5.width; dh = p5.width / imgAspect; }\n else { dh = p5.height; dw = p5.height * imgAspect; }\n\n dw *= scale.value;\n dh *= scale.value;\n\n p5.tint(255, opacity.value);\n p5.imageMode(p5.CENTER);\n p5.image(img, p5.width / 2, p5.height / 2, dw, dh);\n p5.noTint();\n } else {\n p5.fill(255, 40);\n p5.noStroke();\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(p5.width, p5.height) * 0.04);\n p5.text('Upload an image above', p5.width / 2, p5.height / 2);\n }\n}\n",
2244
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst tex = viji.image(null, { label: 'Image' });\nconst opacity = viji.slider(255, { min: 0, max: 255, step: 1, label: 'Opacity' });\nconst scale = viji.slider(1, { min: 0.2, max: 2, step: 0.05, label: 'Scale' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n\n if (tex.value) {\n const img = tex.p5;\n const imgAspect = img.width / img.height;\n const canvasAspect = p5.width / p5.height;\n let dw, dh;\n if (imgAspect > canvasAspect) { dw = p5.width; dh = p5.width / imgAspect; }\n else { dh = p5.height; dw = p5.height * imgAspect; }\n\n dw *= scale.value;\n dh *= scale.value;\n\n p5.tint(255, opacity.value);\n p5.imageMode(p5.CENTER);\n p5.image(img, p5.width / 2, p5.height / 2, dw, dh);\n p5.noTint();\n } else {\n p5.fill(255, 40);\n p5.noStroke();\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(p5.width, p5.height) * 0.04);\n p5.text('Upload an image above', p5.width / 2, p5.height / 2);\n }\n}\n",
2245
2245
  "sceneFile": "image-p5.scene.js"
2246
2246
  },
2247
2247
  {
2248
2248
  "type": "text",
2249
- "markdown": "## Aspect-Correct Drawing\n\n```javascript\nif (tex.value) {\n const img = tex.value.p5;\n const imgAspect = img.width / img.height;\n const canvasAspect = p5.width / p5.height;\n let dw, dh;\n\n if (imgAspect > canvasAspect) {\n dw = p5.width;\n dh = p5.width / imgAspect;\n } else {\n dh = p5.height;\n dw = p5.height * imgAspect;\n }\n\n p5.image(img, (p5.width - dw) / 2, (p5.height - dh) / 2, dw, dh);\n}\n```\n\n## Related\n\n- [Color](../color/) — color picker parameter\n- [Slider](../slider/) — numeric slider parameter\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Image](/native/parameters/image) — Native renderer equivalent (raw `ImageBitmap`)\n- [Shader Image](/shader/parameters/image) — Shader renderer equivalent (`sampler2D`)"
2249
+ "markdown": "## Aspect-Correct Drawing\n\n```javascript\nif (tex.value) {\n const img = tex.p5;\n const imgAspect = img.width / img.height;\n const canvasAspect = p5.width / p5.height;\n let dw, dh;\n\n if (imgAspect > canvasAspect) {\n dw = p5.width;\n dh = p5.width / imgAspect;\n } else {\n dh = p5.height;\n dw = p5.height * imgAspect;\n }\n\n p5.image(img, (p5.width - dw) / 2, (p5.height - dh) / 2, dw, dh);\n}\n```\n\n## Related\n\n- [Color](../color/) — color picker parameter\n- [Slider](../slider/) — numeric slider parameter\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Image](/native/parameters/image) — Native renderer equivalent (raw `ImageBitmap`)\n- [Shader Image](/shader/parameters/image) — Shader renderer equivalent (`sampler2D`)"
2250
2250
  }
2251
2251
  ]
2252
2252
  },