@viji-dev/core 0.3.23 → 0.3.25

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.23",
4
- "generatedAt": "2026-03-18T20:21:59.236Z",
3
+ "coreVersion": "0.3.24",
4
+ "generatedAt": "2026-03-27T11:44:58.441Z",
5
5
  "navigation": [
6
6
  {
7
7
  "id": "getting-started",
@@ -151,6 +151,11 @@ export const docsApi = {
151
151
  "title": "Image",
152
152
  "path": "native/parameters/image"
153
153
  },
154
+ {
155
+ "id": "native-param-button",
156
+ "title": "Button",
157
+ "path": "native/parameters/button"
158
+ },
154
159
  {
155
160
  "id": "native-param-grouping",
156
161
  "title": "Grouping",
@@ -260,6 +265,11 @@ export const docsApi = {
260
265
  }
261
266
  ]
262
267
  },
268
+ {
269
+ "id": "native-pointer",
270
+ "title": "Pointer (Unified)",
271
+ "path": "native/pointer"
272
+ },
263
273
  {
264
274
  "id": "native-mouse",
265
275
  "title": "Mouse",
@@ -272,7 +282,7 @@ export const docsApi = {
272
282
  },
273
283
  {
274
284
  "id": "native-touch",
275
- "title": "Touch & Gestures",
285
+ "title": "Touch",
276
286
  "path": "native/touch"
277
287
  },
278
288
  {
@@ -307,6 +317,11 @@ export const docsApi = {
307
317
  "title": "Scene Structure",
308
318
  "path": "p5/scene-structure"
309
319
  },
320
+ {
321
+ "id": "p5-canvas-resolution",
322
+ "title": "Canvas & Resolution",
323
+ "path": "p5/canvas-resolution"
324
+ },
310
325
  {
311
326
  "id": "p5-drawing",
312
327
  "title": "Drawing with P5",
@@ -366,6 +381,11 @@ export const docsApi = {
366
381
  "title": "Image",
367
382
  "path": "p5/parameters/image"
368
383
  },
384
+ {
385
+ "id": "p5-param-button",
386
+ "title": "Button",
387
+ "path": "p5/parameters/button"
388
+ },
369
389
  {
370
390
  "id": "p5-param-grouping",
371
391
  "title": "Grouping",
@@ -475,6 +495,11 @@ export const docsApi = {
475
495
  }
476
496
  ]
477
497
  },
498
+ {
499
+ "id": "p5-pointer",
500
+ "title": "Pointer (Unified)",
501
+ "path": "p5/pointer"
502
+ },
478
503
  {
479
504
  "id": "p5-mouse",
480
505
  "title": "Mouse",
@@ -487,7 +512,7 @@ export const docsApi = {
487
512
  },
488
513
  {
489
514
  "id": "p5-touch",
490
- "title": "Touch & Gestures",
515
+ "title": "Touch",
491
516
  "path": "p5/touch"
492
517
  },
493
518
  {
@@ -522,6 +547,16 @@ export const docsApi = {
522
547
  "title": "Shader Basics",
523
548
  "path": "shader/basics"
524
549
  },
550
+ {
551
+ "id": "shader-resolution",
552
+ "title": "Resolution & Coordinates",
553
+ "path": "shader/resolution"
554
+ },
555
+ {
556
+ "id": "shader-timing",
557
+ "title": "Timing & Animation",
558
+ "path": "shader/timing"
559
+ },
525
560
  {
526
561
  "id": "shader-parameters",
527
562
  "title": "Parameters",
@@ -561,6 +596,11 @@ export const docsApi = {
561
596
  "title": "Image",
562
597
  "path": "shader/parameters/image"
563
598
  },
599
+ {
600
+ "id": "shader-param-button",
601
+ "title": "Button",
602
+ "path": "shader/parameters/button"
603
+ },
564
604
  {
565
605
  "id": "shader-param-accumulator",
566
606
  "title": "Accumulator",
@@ -665,6 +705,11 @@ export const docsApi = {
665
705
  }
666
706
  ]
667
707
  },
708
+ {
709
+ "id": "shader-pointer",
710
+ "title": "Pointer Uniforms",
711
+ "path": "shader/pointer"
712
+ },
668
713
  {
669
714
  "id": "shader-mouse",
670
715
  "title": "Mouse Uniforms",
@@ -735,12 +780,12 @@ export const docsApi = {
735
780
  "pages": {
736
781
  "overview": {
737
782
  "id": "overview",
738
- "title": "overview",
783
+ "title": "Overview",
739
784
  "description": "Introduction to the Viji Artist API — write code that reacts to audio, video, user interaction, and more.",
740
785
  "content": [
741
786
  {
742
787
  "type": "text",
743
- "markdown": "# Viji Artist API\r\n\r\nViji is a creative platform for generative art and visuals. You write scene code, and Viji gives you a canvas, real-time audio analysis, video feeds, computer vision, user interaction, device sensors, and a parameter system that connects your code to a UI — all without any setup.\r\n\r\n## Your First Scene\r\n\r\nA circle orbiting the center, with a slider to control its speed — try editing the code:"
788
+ "markdown": "# Viji Artist API\n\nViji is a creative platform for generative art and visuals. You write scene code, and Viji gives you a canvas, real-time audio analysis, video feeds, computer vision, user interaction, device sensors, and a parameter system that connects your code to a UI — all without any setup.\n\n## Your First Scene\n\nA circle orbiting the center, with a slider to control its speed — try editing the code:"
744
789
  },
745
790
  {
746
791
  "type": "live-example",
@@ -750,18 +795,18 @@ export const docsApi = {
750
795
  },
751
796
  {
752
797
  "type": "text",
753
- "markdown": "A few things to notice:\r\n\r\n- **[`viji.slider()`](/native/parameters/slider)** creates a UI slider the user can adjust — defined once at the top level, read via `.value` inside `render()`.\r\n- **[`viji.deltaTime`](/native/timing)** is the time since the last frame in seconds — use it with an accumulator (`angle +=`) for smooth, frame-rate-independent animation that doesn't jump when you change parameters.\r\n- **[`viji.width`](/native/canvas-context) / [`viji.height`](/native/canvas-context)** keep your scene resolution-agnostic.\r\n- **`render(viji)`** is called every frame. This is where you draw.\r\n\r\n## What You Can Access\r\n\r\nEverything is available through the `viji` object:\r\n\r\n| Category | What It Gives You |\r\n|----------|------------------|\r\n| **Canvas** | [`viji.canvas`](/native/canvas-context), [`viji.width`](/native/canvas-context), [`viji.height`](/native/canvas-context), [`viji.pixelRatio`](/native/canvas-context) |\r\n| **Timing** | [`viji.time`](/native/timing), [`viji.deltaTime`](/native/timing), [`viji.frameCount`](/native/timing), [`viji.fps`](/native/timing) |\r\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), [`viji.color()`](/native/parameters/color), [`viji.toggle()`](/native/parameters/toggle), [`viji.select()`](/native/parameters/select), [`viji.number()`](/native/parameters/number), [`viji.text()`](/native/parameters/text), [`viji.image()`](/native/parameters/image) |\r\n| **Audio** | Volume, frequency bands, beat detection, spectral analysis, FFT & waveform data |\r\n| **Video & CV** | Video frames, face detection, face mesh, emotion detection, hand tracking, pose detection, body segmentation |\r\n| **Interaction** | Mouse position & buttons, keyboard state, multi-touch & gestures |\r\n| **Sensors** | Accelerometer, gyroscope, device orientation |\r\n\r\n## Three Ways to Create\r\n\r\nViji supports three rendering modes:\r\n\r\n| Renderer | Best For | Entry Point |\r\n|----------|----------|-------------|\r\n| **Native** | Full control — Canvas 2D, WebGL, Three.js | `render(viji)` |\r\n| **P5.js** | Artists familiar with Processing / P5.js | `render(viji, p5)` |\r\n| **Shader** | GPU effects, raymarching, generative patterns | `void main()` in GLSL |\r\n\r\nAll three share the same audio, video, parameter, and interaction APIs. See [Renderers Overview](../renderers-overview/) for how each works.\r\n\r\n## Next Steps\r\n\r\n- [Renderers Overview](../renderers-overview/) — how to choose and use each renderer\r\n- [Best Practices](../best-practices/) — essential patterns for robust, performant scenes\r\n- [Common Mistakes](../common-mistakes/) — pitfalls to avoid\r\n- [Native Quick Start](/native/quickstart) — build with JavaScript and full canvas control\r\n- [P5 Quick Start](/p5/quickstart) — build with the familiar P5.js API\r\n- [Shader Quick Start](/shader/quickstart) — build with GLSL fragment shaders"
798
+ "markdown": "A few things to notice:\n\n- **[`viji.slider()`](/native/parameters/slider)** creates a UI slider the user can adjust — defined once at the top level, read via `.value` inside `render()`.\n- **[`viji.deltaTime`](/native/timing)** is the time since the last frame in seconds — use it with an accumulator (`angle +=`) for smooth, frame-rate-independent animation that doesn't jump when you change parameters.\n- **[`viji.width`](/native/canvas-context) / [`viji.height`](/native/canvas-context)** keep your scene resolution-agnostic.\n- **`render(viji)`** is called every frame. This is where you draw.\n\n## What You Can Access\n\nEverything is available through the `viji` object:\n\n| Category | What It Gives You |\n|----------|------------------|\n| **Canvas** | [`viji.canvas`](/native/canvas-context), [`viji.width`](/native/canvas-context), [`viji.height`](/native/canvas-context) |\n| **Timing** | [`viji.time`](/native/timing), [`viji.deltaTime`](/native/timing), [`viji.frameCount`](/native/timing), [`viji.fps`](/native/timing) |\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), [`viji.color()`](/native/parameters/color), [`viji.toggle()`](/native/parameters/toggle), [`viji.select()`](/native/parameters/select), [`viji.number()`](/native/parameters/number), [`viji.text()`](/native/parameters/text), [`viji.image()`](/native/parameters/image), [`viji.button()`](/native/parameters/button) |\n| **Audio** | Volume, frequency bands, beat detection, spectral analysis, FFT & waveform data |\n| **Video & CV** | Video frames, face detection, face mesh, emotion detection, hand tracking, pose detection, body segmentation |\n| **Interaction** | Unified pointer, mouse buttons & wheel, keyboard state, multi-touch with pressure & velocity |\n| **Sensors** | Accelerometer, gyroscope, device orientation |\n\n## Three Ways to Create\n\nViji supports three rendering modes:\n\n| Renderer | Best For | Entry Point |\n|----------|----------|-------------|\n| **Native** | Full control — Canvas 2D, WebGL, Three.js | `render(viji)` |\n| **P5.js** | Artists familiar with Processing / P5.js | `render(viji, p5)` |\n| **Shader** | GPU effects, raymarching, generative patterns | `void main()` in GLSL |\n\nAll three share the same audio, video, parameter, and interaction APIs. See [Renderers Overview](../renderers-overview/) for how each works.\n\n## Next Steps\n\n- [Renderers Overview](../renderers-overview/) — how to choose and use each renderer\n- [Best Practices](../best-practices/) — essential patterns for robust, performant scenes\n- [Common Mistakes](../common-mistakes/) — pitfalls to avoid\n- [Native Quick Start](/native/quickstart) — build with JavaScript and full canvas control\n- [P5 Quick Start](/p5/quickstart) — build with the familiar P5.js API\n- [Shader Quick Start](/shader/quickstart) — build with GLSL fragment shaders"
754
799
  }
755
800
  ]
756
801
  },
757
802
  "renderers-overview": {
758
803
  "id": "renderers-overview",
759
- "title": "renderers-overview",
804
+ "title": "Renderers Overview",
760
805
  "description": "How to choose and activate each of Viji's three rendering modes — Native, P5.js, and Shader — and what makes each one unique.",
761
806
  "content": [
762
807
  {
763
808
  "type": "text",
764
- "markdown": "# Renderers Overview\r\n\r\nViji supports three rendering modes. Each produces visuals on the same canvas and shares the same Artist API for parameters, audio, video, interaction, and sensors. The difference is the language and paradigm you use to draw.\r\n\r\n## Choosing a Renderer\r\n\r\nThe renderer is selected by a **comment directive** at the top of your scene code:\r\n\r\n```javascript\r\n// @renderer p5 → P5.js renderer\r\n// @renderer shader → Shader renderer\r\n// (no directive) → Native renderer (default)\r\n```\r\n\r\nIf no `@renderer` directive is present, the scene runs in **Native** mode. You can also write `// @renderer native` for clarity, but it's not required.\r\n\r\n> [!IMPORTANT]\r\n> P5 and shader scenes must declare their renderer type as the first comment:\r\n> ```\r\n> // @renderer p5\r\n> ```\r\n> or\r\n> ```\r\n> // @renderer shader\r\n> ```\r\n> Without this directive, the scene defaults to the native renderer.\r\n\r\n---\r\n\r\n## Native Renderer\r\n\r\nThe native renderer gives you direct access to the canvas. Write standard JavaScript (or TypeScript) and use any rendering approach: Canvas 2D, WebGL, or external libraries like Three.js.\r\n\r\n**Entry point:** `render(viji)`"
809
+ "markdown": "# Renderers Overview\n\nViji supports three rendering modes. Each produces visuals on the same canvas and shares the same Artist API for parameters, audio, video, interaction, and sensors. The difference is the language and paradigm you use to draw.\n\n## Choosing a Renderer\n\nThe renderer is selected by a **comment directive** at the top of your scene code:\n\n```javascript\n// @renderer p5 → P5.js renderer\n// @renderer shader → Shader renderer\n// (no directive) → Native renderer (default)\n```\n\nIf no `@renderer` directive is present, the scene runs in **Native** mode. You can also write `// @renderer native` for clarity, but it's not required.\n\n> [!IMPORTANT]\n> P5 and shader scenes must declare their renderer type as the first comment:\n> ```\n> // @renderer p5\n> ```\n> or\n> ```\n> // @renderer shader\n> ```\n> Without this directive, the scene defaults to the native renderer.\n\n---\n\n## Native Renderer\n\nThe native renderer gives you direct access to the canvas. Write standard JavaScript (or TypeScript) and use any rendering approach: Canvas 2D, WebGL, or external libraries like Three.js.\n\n**Entry point:** `render(viji)`"
765
810
  },
766
811
  {
767
812
  "type": "live-example",
@@ -771,7 +816,7 @@ export const docsApi = {
771
816
  },
772
817
  {
773
818
  "type": "text",
774
- "markdown": "**Key characteristics:**\r\n\r\n- **No setup function.** All initialization happens at the top level of your scene code. Top-level `await` is supported, which enables dynamic imports.\r\n- **Full canvas control.** Call [`viji.useContext('2d')`](/native/canvas-context) for Canvas 2D, [`viji.useContext('webgl')`](/native/canvas-context) for WebGL 1, or [`viji.useContext('webgl2')`](/native/canvas-context) for WebGL 2. Choose one and use it consistently — switching context types discards the previous one.\r\n- **External libraries** can be loaded via dynamic import from a CDN. Here's a full Three.js scene running inside the native renderer:"
819
+ "markdown": "**Key characteristics:**\n\n- **No setup function.** All initialization happens at the top level of your scene code. Top-level `await` is supported, which enables dynamic imports.\n- **Full canvas control.** Call [`viji.useContext('2d')`](/native/canvas-context) for Canvas 2D, [`viji.useContext('webgl')`](/native/canvas-context) for WebGL 1, or [`viji.useContext('webgl2')`](/native/canvas-context) for WebGL 2. Choose one and use it consistently — switching context types discards the previous one.\n- **External libraries** can be loaded via dynamic import from a CDN. Here's a full Three.js scene running inside the native renderer:"
775
820
  },
776
821
  {
777
822
  "type": "live-example",
@@ -781,7 +826,7 @@ export const docsApi = {
781
826
  },
782
827
  {
783
828
  "type": "text",
784
- "markdown": "See [External Libraries](/native/external-libraries) for detailed patterns with Three.js and other libraries.\r\n\r\n---\r\n\r\n## P5.js Renderer\r\n\r\nThe P5.js renderer provides the familiar Processing/P5.js creative coding API. Viji loads P5.js automatically when you use `// @renderer p5` — no installation or setup required.\r\n\r\n**Entry points:** `render(viji, p5)` (required), `setup(viji, p5)` (optional)"
829
+ "markdown": "See [External Libraries](/native/external-libraries) for detailed patterns with Three.js and other libraries.\n\n---\n\n## P5.js Renderer\n\nThe P5.js renderer provides the familiar Processing/P5.js creative coding API. Viji loads P5.js automatically when you use `// @renderer p5` — no installation or setup required.\n\n**Entry points:** `render(viji, p5)` (required), `setup(viji, p5)` (optional)"
785
830
  },
786
831
  {
787
832
  "type": "live-example",
@@ -791,7 +836,7 @@ export const docsApi = {
791
836
  },
792
837
  {
793
838
  "type": "text",
794
- "markdown": "> [!WARNING]\r\n> Viji uses P5 in **instance mode**. All P5 functions require the `p5.` prefix:\r\n> ```javascript\r\n> // Correct\r\n> p5.background(0);\r\n> p5.circle(p5.width / 2, p5.height / 2, 100);\r\n>\r\n> // Wrong — will throw ReferenceError\r\n> background(0);\r\n> circle(width / 2, height / 2, 100);\r\n> ```\r\n\r\n**Key characteristics:**\r\n\r\n- **`setup()` is optional.** Use it for one-time configuration like `p5.colorMode()`. If you don't need it, omit it entirely.\r\n- **`render()` replaces `draw()`.** P5's built-in draw loop is disabled; Viji calls your `render()` function each frame.\r\n- **No `createCanvas()`.** The canvas is created and managed by Viji.\r\n- **Viji APIs for input.** Use `viji.mouse`, `viji.keyboard`, `viji.touches` instead of P5's `mouseX`, `keyIsPressed`, etc.\r\n- **No `preload()`.** Load assets using Viji's [`viji.image()`](/native/parameters/image) parameter, or use `fetch()` in `setup()`.\r\n\r\nIf you have existing P5.js sketches, see [Converting P5 Sketches](/p5/converting-sketches) for a step-by-step migration guide.\r\n\r\n---\r\n\r\n## Shader Renderer\r\n\r\nThe shader renderer lets you write GLSL fragment shaders that run directly on the GPU. Viji automatically injects all uniform declarations — you write only your helper functions and `void main()`.\r\n\r\n**Entry point:** `void main()` (GLSL)"
839
+ "markdown": "> [!WARNING]\n> Viji uses P5 in **instance mode**. All P5 functions require the `p5.` prefix:\n> ```javascript\n> // Correct\n> p5.background(0);\n> p5.circle(p5.width / 2, p5.height / 2, 100);\n>\n> // Wrong — will throw ReferenceError\n> background(0);\n> circle(width / 2, height / 2, 100);\n> ```\n\n**Key characteristics:**\n\n- **`setup()` is optional.** Use it for one-time configuration like `p5.colorMode()`. If you don't need it, omit it entirely.\n- **`render()` replaces `draw()`.** P5's built-in draw loop is disabled; Viji calls your `render()` function each frame.\n- **No `createCanvas()`.** The canvas is created and managed by Viji.\n- **Viji APIs for input.** Use [`viji.pointer`](/p5/pointer) for cross-device interactions, or [`viji.mouse`](/p5/mouse), [`viji.keyboard`](/p5/keyboard), [`viji.touches`](/p5/touch) for device-specific access — instead of P5's `mouseX`, `keyIsPressed`, etc.\n- **No `preload()`.** Load assets using Viji's [`viji.image()`](/native/parameters/image) parameter, or use `fetch()` in `setup()`.\n\nIf you have existing P5.js sketches, see [Converting P5 Sketches](/p5/converting-sketches) for a step-by-step migration guide.\n\n---\n\n## Shader Renderer\n\nThe shader renderer lets you write GLSL fragment shaders that run directly on the GPU. Viji automatically injects all uniform declarations — you write only your helper functions and `void main()`.\n\n**Entry point:** `void main()` (GLSL)"
795
840
  },
796
841
  {
797
842
  "type": "live-example",
@@ -801,18 +846,18 @@ export const docsApi = {
801
846
  },
802
847
  {
803
848
  "type": "text",
804
- "markdown": "> [!NOTE]\r\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or built-in uniforms — they will conflict.\r\n\r\n**Key characteristics:**\r\n\r\n- **Fragment shader only.** Viji renders a fullscreen quad; your shader defines the color of every pixel.\r\n- **GLSL ES 1.00 by default.** If you add `#version 300 es` as the first line, Viji switches to WebGL 2. Note that ES 3.00 requires `out vec4` for output instead of `gl_FragColor`, and `texture()` instead of `texture2D()`. ES 1.00 is recommended for maximum compatibility.\r\n- **Built-in uniforms** like `u_time`, `u_resolution`, `u_mouse`, `u_audioVolume`, `u_video`, and many more are always available — no declaration needed.\r\n- **Parameters via comments.** Declare parameters with `// @viji-TYPE:uniformName key:value` syntax. They become uniforms automatically.\r\n- **Accumulators for smooth animation.** Use `// @viji-accumulator:phase rate:speed` instead of `u_time * speed` — the value grows smoothly without jumping when the rate parameter changes.\r\n- **No `u_` prefix for your parameters.** The `u_` prefix is reserved for Viji's built-in uniforms. Name your parameters descriptively: `speed`, `colorMix`, `intensity`.\r\n\r\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy) for a compatibility layer that lets you paste code with minimal changes.\r\n\r\n---\r\n\r\n## Comparison\r\n\r\n| | Native | P5.js | Shader |\r\n|---|--------|-------|--------|\r\n| **Language** | JavaScript / TypeScript | JavaScript with P5 API | GLSL ES 1.00 (or 3.00 with `#version 300 es`) |\r\n| **Directive** | None (default) | `// @renderer p5` | `// @renderer shader` |\r\n| **Entry point** | `render(viji)` | `render(viji, p5)` | `void main()` |\r\n| **Setup** | Top-level code + `await` | Optional `setup(viji, p5)` | N/A |\r\n| **Canvas access** | [`viji.useContext('2d'/'webgl'/'webgl2')`](/native/canvas-context) | P5 drawing functions | Automatic fullscreen quad |\r\n| **External libraries** | Yes (`await import(...)`) | P5.js only | No |\r\n| **Best for** | Full control, WebGL, Three.js | Familiar P5 workflows | GPU effects, raymarching |\r\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), etc. | [`viji.slider()`](/native/parameters/slider), etc. | `// @viji-slider:name ...` |\r\n\r\n## Next Steps\r\n\r\n- [Native Quick Start](/native/quickstart) — build your first native scene\r\n- [P5 Quick Start](/p5/quickstart) — build your first P5.js scene\r\n- [Shader Quick Start](/shader/quickstart) — build your first shader\r\n- [Best Practices](../best-practices/) — essential patterns all artists should follow"
849
+ "markdown": "> [!NOTE]\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations — both built-in uniforms (`u_resolution`, `u_time`, etc.) and parameter uniforms from `@viji-*` directives. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or any uniforms — they will conflict.\n\n**Key characteristics:**\n\n- **Fragment shader only.** Viji renders a fullscreen quad; your shader defines the color of every pixel.\n- **GLSL ES 1.00 by default.** If you add `#version 300 es` as the first line, Viji switches to WebGL 2. Note that ES 3.00 requires `out vec4` for output instead of `gl_FragColor`, and `texture()` instead of `texture2D()`. ES 1.00 is recommended for maximum compatibility.\n- **Built-in uniforms** like `u_time`, `u_resolution`, `u_mouse`, `u_audioVolume`, `u_video`, and many more are always available — no declaration needed.\n- **Parameters via comments.** Declare parameters with `// @viji-TYPE:uniformName key:value` syntax. They become uniforms automatically.\n- **Accumulators for smooth animation.** Use `// @viji-accumulator:phase rate:speed` instead of `u_time * speed` — the value grows smoothly without jumping when the rate parameter changes.\n- **No `u_` prefix for your parameters.** The `u_` prefix is reserved for Viji's built-in uniforms. Name your parameters descriptively: `speed`, `colorMix`, `intensity`.\n\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy) for a compatibility layer that lets you paste code with minimal changes.\n\n---\n\n## Comparison\n\n| | Native | P5.js | Shader |\n|---|--------|-------|--------|\n| **Language** | JavaScript / TypeScript | JavaScript with P5 API | GLSL ES 1.00 (or 3.00 with `#version 300 es`) |\n| **Directive** | None (default) | `// @renderer p5` | `// @renderer shader` |\n| **Entry point** | `render(viji)` | `render(viji, p5)` | `void main()` |\n| **Setup** | Top-level code + `await` | Optional `setup(viji, p5)` | N/A |\n| **Canvas access** | [`viji.useContext('2d'/'webgl'/'webgl2')`](/native/canvas-context) | P5 drawing functions | Automatic fullscreen quad |\n| **External libraries** | Yes (`await import(...)`) | P5.js only | No |\n| **Best for** | Full control, WebGL, Three.js | Familiar P5 workflows | GPU effects, raymarching |\n| **Parameters** | [`viji.slider()`](/native/parameters/slider), etc. | [`viji.slider()`](/native/parameters/slider), etc. | `// @viji-slider:name ...` |\n\n## Next Steps\n\n- [Native Quick Start](/native/quickstart) — build your first native scene\n- [P5 Quick Start](/p5/quickstart) — build your first P5.js scene\n- [Shader Quick Start](/shader/quickstart) — build your first shader\n- [Best Practices](../best-practices/) — essential patterns all artists should follow"
805
850
  }
806
851
  ]
807
852
  },
808
853
  "best-practices": {
809
854
  "id": "best-practices",
810
- "title": "best-practices",
855
+ "title": "Best Practices",
811
856
  "description": "Essential patterns every Viji artist should follow — resolution-agnostic design, time-based animation, memory management, and more.",
812
857
  "content": [
813
858
  {
814
859
  "type": "text",
815
- "markdown": "# Best Practices\r\n\r\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.\r\n\r\n---\r\n\r\n## Use `viji.time` and `viji.deltaTime` for Animation\r\n\r\nViji provides two timing values. Use the right one for the job:\r\n\r\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.\r\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).\r\n\r\n```javascript\r\n// viji.time — animation that looks identical regardless of frame rate\r\nconst angle = viji.time * speed.value;\r\nconst x = Math.cos(angle) * radius;\r\n\r\n// viji.deltaTime — accumulation that stays smooth at any FPS\r\nposition += velocity * viji.deltaTime;\r\nopacity -= fadeRate * viji.deltaTime;\r\n```\r\n\r\nFor shaders, the equivalents are `u_time` and `u_deltaTime`. When animation speed is driven by a parameter, use an **accumulator** to avoid jumps:\r\n\r\n```glsl\r\n// Instead of: float wave = sin(u_time * speed); ← jumps when slider moves\r\n// @viji-accumulator:phase rate:speed\r\nfloat wave = sin(phase + uv.x * 10.0); // smooth at any slider value\r\n```\r\n\r\n> [!NOTE]\r\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.\r\n\r\n---\r\n\r\n## Design for Any Resolution\r\n\r\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.\r\n\r\n**Use [`viji.width`](/native/canvas-context) and [`viji.height`](/native/canvas-context)** for all positioning and sizing:\r\n\r\n```javascript\r\n// Good — scales to any resolution\r\nconst centerX = viji.width / 2;\r\nconst centerY = viji.height / 2;\r\nconst radius = Math.min(viji.width, viji.height) * 0.1;\r\n\r\n// Bad — breaks at different resolutions\r\nconst centerX = 960;\r\nconst centerY = 540;\r\nconst radius = 50;\r\n```\r\n\r\nFor parameters that control sizes, use normalized values (0–1) and multiply by canvas dimensions:\r\n\r\n```javascript\r\nconst size = viji.slider(0.15, { min: 0.02, max: 0.5, label: 'Size' });\r\n\r\nfunction render(viji) {\r\n const pixelSize = size.value * Math.min(viji.width, viji.height);\r\n}\r\n```\r\n\r\nFor shaders, use `u_resolution`:\r\n\r\n```glsl\r\nvec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0–1 coordinates\r\n```\r\n\r\n> [!NOTE]\r\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.\r\n\r\n---\r\n\r\n## Declare Parameters at the Top Level\r\n\r\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()`.\r\n\r\n```javascript\r\n// Correct — declared once at top level\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\r\n\r\nfunction render(viji) {\r\n // Read current values inside render\r\n const s = speed.value;\r\n const bg = bgColor.value;\r\n}\r\n```\r\n\r\n> [!NOTE]\r\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 create duplicate parameters on every frame.\r\n\r\n---\r\n\r\n## Avoid Allocations in the Render Loop\r\n\r\nCreating objects, arrays, or strings inside `render()` triggers garbage collection, causing frame drops and stuttering. Pre-allocate at the top level and reuse.\r\n\r\n> [!TIP]\r\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\r\n> ```javascript\r\n> // Good — pre-allocated\r\n> const pos = { x: 0, y: 0 };\r\n> function render(viji) {\r\n> pos.x = viji.width / 2;\r\n> pos.y = viji.height / 2;\r\n> }\r\n>\r\n> // Bad — creates a new object every frame\r\n> function render(viji) {\r\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\r\n> }\r\n> ```\r\n\r\nThis is especially important for particle systems, arrays of positions, or any data structure that persists across frames.\r\n\r\n---\r\n\r\n## No DOM APIs (but `fetch` Is Fine)\r\n\r\nYour scene runs in a Web Worker. Standard DOM APIs are not available:\r\n\r\n- No `window`, `document`, `Image()`, `localStorage`\r\n- No `createElement`, `querySelector`, `addEventListener`\r\n\r\nHowever, **`fetch()` works** and can be used to load JSON, text, or other data from external URLs:\r\n\r\n```javascript\r\n// This works — fetch is available in workers\r\nconst response = await fetch('https://cdn.example.com/data.json');\r\nconst data = await response.json();\r\n```\r\n\r\nFor images, use Viji's [`viji.image()`](/native/parameters/image) parameter — the host application handles file selection and transfers the image to the worker.\r\n\r\n> [!WARNING]\r\n> Scenes run in a Web Worker — there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\r\n\r\n---\r\n\r\n## Guard Audio and Video with `isConnected`\r\n\r\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` before using audio or video data:\r\n\r\n```javascript\r\nfunction render(viji) {\r\n if (viji.audio.isConnected) {\r\n const bass = viji.audio.bands.low;\r\n // ... use audio data\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\nWithout this guard, your scene would reference undefined or zero values when no audio/video source is connected.\r\n\r\n---\r\n\r\n## Be Mindful of Computer Vision Costs\r\n\r\nCV features (face detection, hand tracking, pose detection, etc.) are powerful but expensive. Each feature runs ML inference in its own WebGL context.\r\n\r\n| Feature | Relative Cost | Notes |\r\n|---------|--------------|-------|\r\n| Face Detection | Low | Bounding box + basic landmarks only |\r\n| Face Mesh | Medium-High | 468 facial landmarks |\r\n| Emotion Detection | High | 7 expressions + 52 blendshape coefficients |\r\n| Hand Tracking | Medium | Up to 2 hands, 21 landmarks each |\r\n| Pose Detection | Medium | 33 body landmarks |\r\n| Body Segmentation | High | Per-pixel mask, large tensor output |\r\n\r\n> [!WARNING]\r\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.\r\n\r\n**Don't enable CV features by default.** Instead, expose a toggle parameter so users can activate them on capable devices:\r\n\r\n> [!TIP]\r\n> **Best practice:** Don't enable CV features by default. Instead, expose a toggle parameter so users can activate them on capable devices:\r\n> ```javascript\r\n> const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\r\n> if (useFace.value) {\r\n> await viji.video.cv.enableFaceDetection(true);\r\n> }\r\n> ```\r\n\r\n---\r\n\r\n## Pick One Canvas Context Type\r\n\r\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.\r\n\r\n> [!WARNING]\r\n> Calling [`useContext('2d')`](/native/canvas-context) and [`useContext('webgl')`](/native/canvas-context)/[`useContext('webgl2')`](/native/canvas-context) on the same canvas is mutually exclusive. Once a context type is obtained, switching to the other discards the previous one. Choose one context type and use it consistently.\r\n\r\n---\r\n\r\n## Related\r\n\r\n- [Common Mistakes](../common-mistakes/) — specific wrong/right code examples\r\n- [Performance](/advanced/performance) — deep dive into optimization\r\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer"
860
+ "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** 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` 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> Calling [`useContext('2d')`](/native/canvas-context) and [`useContext('webgl')`](/native/canvas-context)/[`useContext('webgl2')`](/native/canvas-context) on the same canvas is mutually exclusive. Once a context type is obtained, switching to the other discards the previous one. Choose one context type and use it consistently.\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/) — specific wrong/right code examples\n- [Performance](/advanced/performance) — deep dive into optimization\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer"
816
861
  }
817
862
  ]
818
863
  },
@@ -823,7 +868,7 @@ export const docsApi = {
823
868
  "content": [
824
869
  {
825
870
  "type": "text",
826
- "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()` creates duplicates every frame.\r\n\r\n```javascript\r\n// Wrong — creates a new slider every frame (60+ times per second)\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.\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// 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---\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.mouse.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` and all built-in uniform declarations. Redeclaring them causes conflicts.\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- [Performance](/advanced/performance) — deep dive into optimization\r\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer"
871
+ "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.\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// 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---\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- [Performance](/advanced/performance) — deep dive into optimization\r\n- [Renderers Overview](../renderers-overview/) — choosing the right renderer"
827
872
  }
828
873
  ]
829
874
  },
@@ -834,7 +879,7 @@ export const docsApi = {
834
879
  "content": [
835
880
  {
836
881
  "type": "text",
837
- "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 add `// @renderer p5` as the very first line.\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.\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.mouse.isPressed`\r\n - `mouseX` / `mouseY` → `viji.mouse.x` / `viji.mouse.y`\r\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\r\n - For press-edge detection: track a `wasPressed` variable and compare.\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.mouse.x` / `viji.mouse.y` |\r\n| `mouseIsPressed` | `viji.mouse.isPressed` |\r\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\r\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\r\n| `key` | `viji.keyboard.lastKey` |\r\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\r\n| `frameRate(n)` | Remove — host controls frame rate |\r\n| `createCanvas(w, h)` | Remove — canvas is provided |\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\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"
882
+ "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 add `// @renderer p5` as the very first line.\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.\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)` | Remove — canvas is provided |\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"
838
883
  }
839
884
  ]
840
885
  },
@@ -845,7 +890,7 @@ export const docsApi = {
845
890
  "content": [
846
891
  {
847
892
  "type": "text",
848
- "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. 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\n9. 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\n10. 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-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\n11. Parameter directives ONLY work with `//` comments. NEVER use `/* */` for `@viji-*` directives.\r\n\r\n12. 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\n13. Remove any `#ifdef GL_ES` / `precision` blocks — Viji handles this.\r\n\r\n14. 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\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_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-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"
893
+ "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"
849
894
  }
850
895
  ]
851
896
  },
@@ -856,7 +901,7 @@ export const docsApi = {
856
901
  "content": [
857
902
  {
858
903
  "type": "text",
859
- "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.mouse.x`\r\n - `event.clientY` → `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.mouse` 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\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.mouse.x`, `.y`, `.isPressed`, `.leftButton`, `.rightButton`\r\n- `viji.keyboard.isPressed('key')`, `.lastKey`\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"
904
+ "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"
860
905
  }
861
906
  ]
862
907
  },
@@ -877,7 +922,7 @@ export const docsApi = {
877
922
  },
878
923
  {
879
924
  "type": "text",
880
- "markdown": "### What's Happening\r\n\r\n**Top level — runs once:**\r\n\r\n- `viji.color()` and `viji.slider()` create parameters that appear in the UI. They must be declared at the top level, not inside `render()`.\r\n\r\n**`render(viji)` — called every frame:**\r\n\r\n- `viji.useContext('2d')` returns a standard `CanvasRenderingContext2D`. Call it once; the context is cached.\r\n- `viji.width` and `viji.height` give the current canvas size — use them instead of hardcoded pixels.\r\n- `viji.time` is elapsed seconds since the scene started — use it for animation.\r\n- Each parameter's `.value` is read inside `render()` and updates live as the user moves sliders.\r\n\r\n> [!NOTE]\r\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 create duplicate parameters on every frame.\r\n\r\n## Scene Structure\r\n\r\nA native scene has two parts:\r\n\r\n```javascript\r\n// 1. Top level — initialization, parameters, state\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nlet angle = 0;\r\n\r\n// 2. render() — called every frame\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n angle += speed.value * viji.deltaTime;\r\n // ... draw using ctx\r\n}\r\n```\r\n\r\n- **No `setup()` function.** All initialization happens at the top level. Top-level `await` is supported for dynamic imports.\r\n- **`render(viji)` is the only required function.** It receives the Viji API object with canvas, timing, audio, video, parameters, and interaction data.\r\n\r\n## Canvas Context\r\n\r\nUse `viji.useContext()` to get a rendering context:\r\n\r\n```javascript\r\nconst ctx = viji.useContext('2d'); // Canvas 2D\r\nconst gl = viji.useContext('webgl'); // WebGL 1\r\nconst gl2 = viji.useContext('webgl2'); // WebGL 2\r\n```\r\n\r\nPick one and use it consistently — switching context types discards the previous one.\r\n\r\n## Essential Patterns\r\n\r\n> [!NOTE]\r\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\r\n\r\n> [!WARNING]\r\n> Scenes run in a Web Worker — there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\r\n\r\n> [!TIP]\r\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\r\n> ```javascript\r\n> // Good — pre-allocated\r\n> const pos = { x: 0, y: 0 };\r\n> function render(viji) {\r\n> pos.x = viji.width / 2;\r\n> pos.y = viji.height / 2;\r\n> }\r\n>\r\n> // Bad — creates a new object every frame\r\n> function render(viji) {\r\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\r\n> }\r\n> ```\r\n\r\n## External Libraries\r\n\r\nTop-level `await` lets you load libraries from a CDN:\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\n// ... set up your Three.js scene at the top level\r\nfunction render(viji) { /* ... */ }\r\n```\r\n\r\nSee [External Libraries](/native/external-libraries) for detailed patterns.\r\n\r\n## TypeScript Support\r\n\r\nThe Viji editor provides full TypeScript support with autocomplete and type checking out of the box. All Viji types — `VijiAPI`, `SliderParameter`, `AudioAPI`, `MouseAPI`, and others — are available globally without imports. Add type annotations to get full IntelliSense:\r\n\r\n```typescript\r\nfunction render(viji: VijiAPI) {\r\n const ctx = viji.useContext('2d'); // ctx is typed as CanvasRenderingContext2D\r\n // ... full autocomplete for viji.audio, viji.mouse, etc.\r\n}\r\n```\r\n\r\nTypeScript is optional — the same code works as plain JavaScript without the annotations."
925
+ "markdown": "### What's Happening\r\n\r\n**Top level — runs once:**\r\n\r\n- `viji.color()` and `viji.slider()` create parameters that appear in the UI. They must be declared at the top level, not inside `render()`.\r\n\r\n**`render(viji)` — called every frame:**\r\n\r\n- `viji.useContext('2d')` returns a standard `CanvasRenderingContext2D`. Call it once; the context is cached.\r\n- `viji.width` and `viji.height` give the current canvas size — use them instead of hardcoded pixels.\r\n- `viji.time` is elapsed seconds since the scene started — use it for animation.\r\n- Each parameter's `.value` is read inside `render()` and updates live as the user moves sliders.\r\n\r\n> [!NOTE]\r\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.\r\n\r\n## Scene Structure\r\n\r\nA native scene has two parts:\r\n\r\n```javascript\r\n// 1. Top level — initialization, parameters, state\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nlet angle = 0;\r\n\r\n// 2. render() — called every frame\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n angle += speed.value * viji.deltaTime;\r\n // ... draw using ctx\r\n}\r\n```\r\n\r\n- **No `setup()` function.** All initialization happens at the top level. Top-level `await` is supported for dynamic imports.\r\n- **`render(viji)` is the only required function.** It receives the Viji API object with canvas, timing, audio, video, parameters, and interaction data.\r\n\r\n## Canvas Context\r\n\r\nUse `viji.useContext()` to get a rendering context:\r\n\r\n```javascript\r\nconst ctx = viji.useContext('2d'); // Canvas 2D\r\nconst gl = viji.useContext('webgl'); // WebGL 1\r\nconst gl2 = viji.useContext('webgl2'); // WebGL 2\r\n```\r\n\r\nPick one and use it consistently — switching context types discards the previous one.\r\n\r\n## Essential Patterns\r\n\r\n> [!NOTE]\r\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\r\n\r\n> [!WARNING]\r\n> Scenes run in a Web Worker — there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\r\n\r\n> [!TIP]\r\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\r\n> ```javascript\r\n> // Good — pre-allocated\r\n> const pos = { x: 0, y: 0 };\r\n> function render(viji) {\r\n> pos.x = viji.width / 2;\r\n> pos.y = viji.height / 2;\r\n> }\r\n>\r\n> // Bad — creates a new object every frame\r\n> function render(viji) {\r\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\r\n> }\r\n> ```\r\n\r\n## External Libraries\r\n\r\nTop-level `await` lets you load libraries from a CDN:\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\n// ... set up your Three.js scene at the top level\r\nfunction render(viji) { /* ... */ }\r\n```\r\n\r\nSee [External Libraries](/native/external-libraries) for detailed patterns.\r\n\r\n## TypeScript Support\r\n\r\nThe Viji editor provides full TypeScript support with autocomplete and type checking out of the box. All Viji types — `VijiAPI`, `SliderParameter`, `AudioAPI`, `MouseAPI`, and others — are available globally without imports. Add type annotations to get full IntelliSense:\r\n\r\n```typescript\r\nfunction render(viji: VijiAPI) {\r\n const ctx = viji.useContext('2d'); // ctx is typed as CanvasRenderingContext2D\r\n // ... full autocomplete for viji.audio, viji.mouse, etc.\r\n}\r\n```\r\n\r\nTypeScript is optional — the same code works as plain JavaScript without the annotations."
881
926
  },
882
927
  {
883
928
  "type": "live-example",
@@ -891,6 +936,68 @@ export const docsApi = {
891
936
  }
892
937
  ]
893
938
  },
939
+ "native-canvas": {
940
+ "id": "native-canvas",
941
+ "title": "Canvas & Context",
942
+ "description": "Access the canvas, choose a rendering context, and work with resolution-agnostic dimensions.",
943
+ "content": [
944
+ {
945
+ "type": "text",
946
+ "markdown": "# Canvas & Context\n\nEvery native scene gets a canvas and a set of properties for resolution-agnostic drawing. This page covers `viji.canvas`, `viji.useContext()`, `viji.width`, `viji.height`, `viji.ctx`, and `viji.gl`.\n\n## The Canvas\n\n`viji.canvas` is an [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) — not a DOM `<canvas>` element. It lives inside a Web Worker and has no associated DOM node. You never create or resize it yourself; the host application manages its lifecycle and dimensions.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The raw canvas. Rarely needed — use `viji.useContext()` instead. |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n`viji.width` and `viji.height` always reflect the actual pixel dimensions of the canvas and update automatically when the host resizes it. Use them for all positioning and sizing.\n\n## Choosing a Context\n\nCall `viji.useContext()` to get a rendering context:\n\n```javascript\nconst ctx = viji.useContext('2d'); // OffscreenCanvasRenderingContext2D\nconst gl = viji.useContext('webgl'); // WebGLRenderingContext\nconst gl2 = viji.useContext('webgl2'); // WebGL2RenderingContext\n```\n\n| Argument | Return Type | Also Sets |\n|----------|------------|-----------|\n| `'2d'` | `OffscreenCanvasRenderingContext2D` | `viji.ctx` |\n| `'webgl'` | `WebGLRenderingContext` | `viji.gl` |\n| `'webgl2'` | `WebGL2RenderingContext` | `viji.gl` |\n\nThe context is created once and cached — calling `useContext()` again with the same argument returns the same instance. Call it at the top of `render()` or once at the top level and store the result.\n\n> [!WARNING]\n> Calling `useContext('2d')` and `useContext('webgl')`/`useContext('webgl2')` on the same canvas is mutually exclusive. Once a context type is obtained, switching to the other discards the previous one. Choose one context type and use it consistently.\n\n## 2D Context\n\nThe most common choice. You get a standard [`CanvasRenderingContext2D`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) with full access to paths, fills, strokes, transforms, gradients, and compositing."
947
+ },
948
+ {
949
+ "type": "live-example",
950
+ "title": "2D — Pulsing Grid",
951
+ "sceneCode": "const bg = viji.color('#0a0a1a', { label: 'Background' });\nconst dotColor = viji.color('#4488ff', { label: 'Dot Color' });\nconst gridSize = viji.slider(12, { min: 4, max: 30, step: 1, label: 'Grid Size' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n const cols = gridSize.value;\n const rows = Math.round(cols * (h / w));\n const cellW = w / cols;\n const cellH = h / rows;\n const maxR = Math.min(cellW, cellH) * 0.35;\n\n ctx.fillStyle = dotColor.value;\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n const cx = (c + 0.5) * cellW;\n const cy = (r + 0.5) * cellH;\n const dist = Math.hypot(cx - w / 2, cy - h / 2);\n const pulse = Math.sin(viji.time * 3 - dist * 0.01) * 0.5 + 0.5;\n const radius = maxR * (0.2 + 0.8 * pulse);\n ctx.beginPath();\n ctx.arc(cx, cy, radius, 0, Math.PI * 2);\n ctx.fill();\n }\n }\n}\n",
952
+ "sceneFile": "canvas-context-2d.scene.js"
953
+ },
954
+ {
955
+ "type": "text",
956
+ "markdown": "### Shorthand: `viji.ctx`\n\nAfter calling `viji.useContext('2d')`, the same context is also available as `viji.ctx`:\n\n```javascript\nfunction render(viji) {\n viji.useContext('2d');\n viji.ctx.fillStyle = '#000';\n viji.ctx.fillRect(0, 0, viji.width, viji.height);\n}\n```\n\nBoth `viji.useContext('2d')` and `viji.ctx` reference the same object.\n\n## WebGL Context\n\nFor GPU-accelerated rendering, request a WebGL context. The returned object is a standard [`WebGLRenderingContext`](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) or [`WebGL2RenderingContext`](https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext) — you manage shaders, buffers, and draw calls yourself."
957
+ },
958
+ {
959
+ "type": "live-example",
960
+ "title": "WebGL 2 — Gradient Quad",
961
+ "sceneCode": "const topColor = viji.color('#ff3366', { label: 'Top Color' });\nconst bottomColor = viji.color('#3366ff', { label: 'Bottom Color' });\n\nconst vsSource = `\n attribute vec2 a_position;\n varying vec2 v_uv;\n void main() {\n v_uv = a_position * 0.5 + 0.5;\n gl_Position = vec4(a_position, 0.0, 1.0);\n }\n`;\n\nconst fsSource = `\n precision mediump float;\n varying vec2 v_uv;\n uniform vec3 u_topColor;\n uniform vec3 u_bottomColor;\n uniform float u_time;\n void main() {\n float wave = sin(v_uv.x * 6.28 + u_time * 2.0) * 0.05;\n float t = v_uv.y + wave;\n vec3 color = mix(u_bottomColor, u_topColor, t);\n gl_FragColor = vec4(color, 1.0);\n }\n`;\n\nlet program, timeLoc, topLoc, bottomLoc;\n\nfunction initGL(gl) {\n function compile(type, src) {\n const s = gl.createShader(type);\n gl.shaderSource(s, src);\n gl.compileShader(s);\n return s;\n }\n const vs = compile(gl.VERTEX_SHADER, vsSource);\n const fs = compile(gl.FRAGMENT_SHADER, fsSource);\n program = gl.createProgram();\n gl.attachShader(program, vs);\n gl.attachShader(program, fs);\n gl.linkProgram(program);\n gl.useProgram(program);\n\n const buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);\n const pos = gl.getAttribLocation(program, 'a_position');\n gl.enableVertexAttribArray(pos);\n gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);\n\n timeLoc = gl.getUniformLocation(program, 'u_time');\n topLoc = gl.getUniformLocation(program, 'u_topColor');\n bottomLoc = gl.getUniformLocation(program, 'u_bottomColor');\n}\n\nfunction hexToRGB(hex) {\n const n = parseInt(hex.slice(1), 16);\n return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];\n}\n\nfunction render(viji) {\n const gl = viji.useContext('webgl2');\n if (!program) initGL(gl);\n\n gl.viewport(0, 0, viji.width, viji.height);\n gl.uniform1f(timeLoc, viji.time);\n\n const top = hexToRGB(topColor.value);\n const bottom = hexToRGB(bottomColor.value);\n gl.uniform3f(topLoc, top[0], top[1], top[2]);\n gl.uniform3f(bottomLoc, bottom[0], bottom[1], bottom[2]);\n\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n}\n",
962
+ "sceneFile": "canvas-context-webgl.scene.js"
963
+ },
964
+ {
965
+ "type": "text",
966
+ "markdown": "### Shorthand: `viji.gl`\n\nAfter calling `viji.useContext('webgl')` or `viji.useContext('webgl2')`, the context is also available as `viji.gl`. Its runtime type matches whichever variant you requested.\n\n> [!TIP]\n> If you want full GPU control but don't want to manage WebGL boilerplate, consider the [Shader Renderer](/shader/quickstart) — it handles the fullscreen quad, uniform injection, and compilation for you.\n\n## Resolution-Agnostic Drawing\n\nThe canvas can be any size — from a small preview to fullscreen 4K — and the host can resize it at any time. Your scene must adapt automatically.\n\n> [!NOTE]\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n### Dimension Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\nBoth values update automatically when the host resizes the canvas. Read them every frame inside `render()` — don't cache them at the top level.\n\n### Centering and Proportional Sizing\n\nExpress all positions and sizes relative to the canvas dimensions:\n\n```javascript\n// Good — scales with canvas\nconst cx = viji.width / 2;\nconst cy = viji.height / 2;\nconst r = Math.min(viji.width, viji.height) * 0.3;\n\n// Bad — breaks at different resolutions\nconst cx = 400;\nconst cy = 300;\nconst r = 150;\n```\n\nUse `Math.min(viji.width, viji.height)` when you need uniform scaling — it ensures elements stay proportional regardless of aspect ratio. Use `viji.width` or `viji.height` individually when you want an element to stretch with one axis (e.g., a full-width bar).\n\n### Proportional Margins and Spacing\n\n```javascript\nconst margin = Math.min(viji.width, viji.height) * 0.05;\nconst usableW = viji.width - margin * 2;\nconst usableH = viji.height - margin * 2;\n```\n\n### Comparison Across Renderers\n\n| Concept | Native | P5 | Shader |\n|---------|--------|-----|--------|\n| Canvas width | `viji.width` | `viji.width` / `p5.width` | `u_resolution.x` |\n| Canvas height | `viji.height` | `viji.height` / `p5.height` | `u_resolution.y` |\n| Aspect ratio | `viji.width / viji.height` | `viji.width / viji.height` | `u_resolution.x / u_resolution.y` |\n| Uniform scale | `Math.min(viji.width, viji.height)` | `Math.min(viji.width, viji.height)` | `min(u_resolution.x, u_resolution.y)` |\n\n## Environment Constraints\n\n> [!WARNING]\n> Scenes run in a Web Worker — there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\n\n## Next Steps\n\n- [Timing](/native/timing) — [`viji.time`](/native/timing), [`viji.deltaTime`](/native/timing), frame counting\n- [Parameters](/native/parameters) — sliders, colors, toggles\n- [P5 Canvas & Resolution](/p5/canvas-resolution) — how P5 handles canvas and sizing\n- [Shader Resolution](/shader/resolution) — `u_resolution` and coordinate normalization\n- [API Reference](/native/api-reference) — full list of everything available"
967
+ }
968
+ ]
969
+ },
970
+ "native-timing": {
971
+ "id": "native-timing",
972
+ "title": "Timing",
973
+ "description": "Use viji.time, viji.deltaTime, viji.frameCount, and viji.fps for frame-rate-independent animation.",
974
+ "content": [
975
+ {
976
+ "type": "text",
977
+ "markdown": "# Timing\n\nViji provides four timing properties on every frame. Understanding when to use each one is the key to smooth, resolution-agnostic animation.\n\n## Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.time` | `number` | Seconds elapsed since the scene started |\n| `viji.deltaTime` | `number` | Seconds since the previous frame |\n| `viji.frameCount` | `number` | Integer frame counter (monotonically increasing) |\n| `viji.fps` | `number` | Target FPS based on the host's frame rate mode |\n\n## `viji.time` — Absolute Time\n\n`viji.time` is a monotonically increasing float representing seconds since the scene started. It is ideal for oscillations, rotations, and any effect driven directly by elapsed time:\n\n```javascript\nconst angle = viji.time * 2; // 2 radians per second\nconst pulse = Math.sin(viji.time * Math.PI); // oscillate every 2 seconds\nconst hue = (viji.time * 60) % 360; // cycle hue once per 6 seconds\n```"
978
+ },
979
+ {
980
+ "type": "live-example",
981
+ "title": "Time-Based — Orbiting Dots",
982
+ "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",
983
+ "sceneFile": "timing-time.scene.js"
984
+ },
985
+ {
986
+ "type": "text",
987
+ "markdown": "## `viji.deltaTime` — Frame Delta\n\n`viji.deltaTime` is the time in seconds since the last frame. Use it for accumulation-based animation — movement, physics, fading, and anything that should progress by a consistent amount regardless of frame rate:\n\n```javascript\nangle += speed.value * viji.deltaTime; // frame-rate-independent rotation\nposition.x += velocity * viji.deltaTime; // frame-rate-independent movement\nopacity -= fadeRate * viji.deltaTime; // frame-rate-independent fading\n```"
988
+ },
989
+ {
990
+ "type": "live-example",
991
+ "title": "DeltaTime — Bouncing Ball",
992
+ "sceneCode": "const bg = viji.color('#0a0a1a', { label: 'Background' });\nconst ballColor = viji.color('#44ffaa', { label: 'Ball Color' });\nconst speed = viji.slider(200, { min: 50, max: 600, label: 'Speed (px/s)' });\nconst ballSize = viji.slider(0.04, { min: 0.01, max: 0.1, label: 'Ball Size' });\n\nlet x = 0.5;\nlet y = 0.3;\nlet vx = 1;\nlet vy = 1;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const r = Math.min(w, h) * ballSize.value;\n const s = speed.value / Math.max(w, h);\n\n x += vx * s * viji.deltaTime;\n y += vy * s * viji.deltaTime;\n\n if (x * w - r < 0 || x * w + r > w) { vx = -vx; x = Math.max(r / w, Math.min(1 - r / w, x)); }\n if (y * h - r < 0 || y * h + r > h) { vy = -vy; y = Math.max(r / h, Math.min(1 - r / h, y)); }\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n ctx.fillStyle = ballColor.value;\n ctx.beginPath();\n ctx.arc(x * w, y * h, r, 0, Math.PI * 2);\n ctx.fill();\n}\n",
993
+ "sceneFile": "timing-delta.scene.js"
994
+ },
995
+ {
996
+ "type": "text",
997
+ "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"
998
+ }
999
+ ]
1000
+ },
894
1001
  "native-external-libs": {
895
1002
  "id": "native-external-libs",
896
1003
  "title": "native-external-libs",
@@ -908,29 +1015,29 @@ export const docsApi = {
908
1015
  },
909
1016
  {
910
1017
  "type": "text",
911
- "markdown": "### Key Integration Points\r\n\r\n**Canvas:** Pass `viji.canvas` to the Three.js renderer:\r\n\r\n```javascript\r\nconst renderer = new THREE.WebGLRenderer({\r\n canvas: viji.canvas,\r\n antialias: true\r\n});\r\n```\r\n\r\n**Resize:** Three.js doesn't auto-resize. Check for dimension changes in `render()`:\r\n\r\n```javascript\r\nlet prevWidth = viji.width;\r\nlet prevHeight = viji.height;\r\n\r\nfunction render(viji) {\r\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\r\n renderer.setSize(viji.width, viji.height, false);\r\n camera.aspect = viji.width / viji.height;\r\n camera.updateProjectionMatrix();\r\n prevWidth = viji.width;\r\n prevHeight = viji.height;\r\n }\r\n\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n> [!IMPORTANT]\r\n> Pass `false` as the third argument to `renderer.setSize()` — this prevents Three.js from setting CSS styles on the canvas, which would fail in the worker environment.\r\n\r\n**No `requestAnimationFrame`:** Viji controls the render loop. Write your animation logic inside `render(viji)` and call `renderer.render(scene, camera)` at the end.\r\n\r\n**Parameters:** All Viji parameter types work as expected:\r\n\r\n```javascript\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#049ef4', { label: 'Color' });\r\n\r\nfunction render(viji) {\r\n cube.rotation.y += speed.value * viji.deltaTime;\r\n material.color.set(color.value);\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n### Three.js Addons\r\n\r\nImport addons from the Three.js examples directory:\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\nconst { OrbitControls } = await import('https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js');\r\nconst { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\r\nconst { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\r\n```\r\n\r\n> [!NOTE]\r\n> `OrbitControls` requires DOM events and will not work as expected in the worker environment. For camera interaction, use `viji.mouse` to drive the camera manually.\r\n\r\n### Video and Image Textures\r\n\r\nThree.js can consume Viji's video frames and image parameters directly:\r\n\r\n```javascript\r\nconst photo = viji.image(null, { label: 'Texture' });\r\nlet texture = null;\r\n\r\nfunction render(viji) {\r\n if (photo.value && !texture) {\r\n texture = new THREE.CanvasTexture(photo.value);\r\n material.map = texture;\r\n material.needsUpdate = true;\r\n }\r\n\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n if (!videoTexture) {\r\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\r\n videoMaterial.map = videoTexture;\r\n }\r\n videoTexture.needsUpdate = true;\r\n }\r\n\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n## Other Libraries\r\n\r\nAny library published as an ES module works the same way. Examples:\r\n\r\n**Math / utility libraries:**\r\n\r\n```javascript\r\nconst { vec3, mat4 } = await import('https://esm.sh/gl-matrix@3.4.3');\r\nconst SimplexNoise = (await import('https://esm.sh/simplex-noise@4.0.1')).createNoise3D;\r\n```\r\n\r\n**2D rendering libraries:**\r\n\r\n```javascript\r\nconst PIXI = await import('https://esm.sh/pixi.js@7.3.2');\r\n```\r\n\r\n> [!NOTE]\r\n> Some libraries expect a DOM environment and may not work in the worker context. Libraries that accept an external canvas or work with pure data (math, noise, data structures) are the best candidates.\r\n\r\n## WebGL Context\r\n\r\nWhen using a library that creates its own WebGL context (like Three.js or Pixi.js), keep in mind:\r\n\r\n- The library consumes the canvas's WebGL context. You cannot also call `viji.useContext('2d')` or `viji.useContext('webgl')` for the same canvas — pick one rendering approach.\r\n- CV features (face detection, hand tracking, etc.) use additional WebGL contexts internally. Browsers have a limit of 8–16 contexts. Combining a WebGL library with multiple CV features may cause context loss.\r\n\r\nSee [Canvas & Context](/native/canvas-context) for details on `useContext()`.\r\n\r\n## Tips\r\n\r\n- **Pin library versions** in the import URL (e.g., `three@0.160.0`). Unpinned versions may break when the library updates.\r\n- **Import once at the top level.** Never put `await import()` inside `render()` — it would re-fetch the library every frame.\r\n- **Check worker compatibility** before choosing a library. Libraries that require `document`, `window`, or `Image()` will not work. `fetch()` is available.\r\n- **Use `viji.deltaTime`** for animation instead of the library's own timer, to stay frame-rate-independent and synchronized with other Viji features.\r\n\r\n## Related\r\n\r\n- [Canvas & Context](/native/canvas-context) — `useContext('2d')`, `useContext('webgl')`, `useContext('webgl2')`\r\n- [Native Quick Start](/native/quickstart) — building native scenes from scratch\r\n- [Parameters](/native/parameters) — sliders, colors, toggles\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1018
+ "markdown": "### Key Integration Points\r\n\r\n**Canvas:** Pass `viji.canvas` to the Three.js renderer:\r\n\r\n```javascript\r\nconst renderer = new THREE.WebGLRenderer({\r\n canvas: viji.canvas,\r\n antialias: true\r\n});\r\n```\r\n\r\n**Resize:** Three.js doesn't auto-resize. Check for dimension changes in `render()`:\r\n\r\n```javascript\r\nlet prevWidth = viji.width;\r\nlet prevHeight = viji.height;\r\n\r\nfunction render(viji) {\r\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\r\n renderer.setSize(viji.width, viji.height, false);\r\n camera.aspect = viji.width / viji.height;\r\n camera.updateProjectionMatrix();\r\n prevWidth = viji.width;\r\n prevHeight = viji.height;\r\n }\r\n\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n> [!IMPORTANT]\r\n> Pass `false` as the third argument to `renderer.setSize()` — this prevents Three.js from setting CSS styles on the canvas, which would fail in the worker environment.\r\n\r\n**No `requestAnimationFrame`:** Viji controls the render loop. Write your animation logic inside `render(viji)` and call `renderer.render(scene, camera)` at the end.\r\n\r\n**Parameters:** All Viji parameter types work as expected:\r\n\r\n```javascript\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#049ef4', { label: 'Color' });\r\n\r\nfunction render(viji) {\r\n cube.rotation.y += speed.value * viji.deltaTime;\r\n material.color.set(color.value);\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n### Three.js Addons\r\n\r\nImport addons from the Three.js examples directory:\r\n\r\n```javascript\r\nconst THREE = await import('https://esm.sh/three@0.160.0');\r\nconst { OrbitControls } = await import('https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js');\r\nconst { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\r\nconst { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\r\n```\r\n\r\n> [!NOTE]\r\n> `OrbitControls` requires DOM events and will not work as expected in the worker environment. For camera interaction, use [`viji.pointer`](/native/pointer) to drive the camera manually — it handles both mouse and touch input.\r\n\r\n### Video and Image Textures\r\n\r\nThree.js can consume Viji's video frames and image parameters directly:\r\n\r\n```javascript\r\nconst photo = viji.image(null, { label: 'Texture' });\r\nlet texture = null;\r\n\r\nfunction render(viji) {\r\n if (photo.value && !texture) {\r\n texture = new THREE.CanvasTexture(photo.value);\r\n material.map = texture;\r\n material.needsUpdate = true;\r\n }\r\n\r\n if (viji.video.isConnected && viji.video.currentFrame) {\r\n if (!videoTexture) {\r\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\r\n videoMaterial.map = videoTexture;\r\n }\r\n videoTexture.needsUpdate = true;\r\n }\r\n\r\n renderer.render(scene, camera);\r\n}\r\n```\r\n\r\n## Other Libraries\r\n\r\nAny library published as an ES module works the same way. Examples:\r\n\r\n**Math / utility libraries:**\r\n\r\n```javascript\r\nconst { vec3, mat4 } = await import('https://esm.sh/gl-matrix@3.4.3');\r\nconst SimplexNoise = (await import('https://esm.sh/simplex-noise@4.0.1')).createNoise3D;\r\n```\r\n\r\n**2D rendering libraries:**\r\n\r\n```javascript\r\nconst PIXI = await import('https://esm.sh/pixi.js@7.3.2');\r\n```\r\n\r\n> [!NOTE]\r\n> Some libraries expect a DOM environment and may not work in the worker context. Libraries that accept an external canvas or work with pure data (math, noise, data structures) are the best candidates.\r\n\r\n## WebGL Context\r\n\r\nWhen using a library that creates its own WebGL context (like Three.js or Pixi.js), keep in mind:\r\n\r\n- The library consumes the canvas's WebGL context. You cannot also call `viji.useContext('2d')` or `viji.useContext('webgl')` for the same canvas — pick one rendering approach.\r\n- CV features (face detection, hand tracking, etc.) use additional WebGL contexts internally. Browsers have a limit of 8–16 contexts. Combining a WebGL library with multiple CV features may cause context loss.\r\n\r\nSee [Canvas & Context](/native/canvas-context) for details on `useContext()`.\r\n\r\n## Tips\r\n\r\n- **Pin library versions** in the import URL (e.g., `three@0.160.0`). Unpinned versions may break when the library updates.\r\n- **Import once at the top level.** Never put `await import()` inside `render()` — it would re-fetch the library every frame.\r\n- **Check worker compatibility** before choosing a library. Libraries that require `document`, `window`, or `Image()` will not work. `fetch()` is available.\r\n- **Use `viji.deltaTime`** for animation instead of the library's own timer, to stay frame-rate-independent and synchronized with other Viji features.\r\n\r\n## Related\r\n\r\n- [Canvas & Context](/native/canvas-context) — `useContext('2d')`, `useContext('webgl')`, `useContext('webgl2')`\r\n- [Native Quick Start](/native/quickstart) — building native scenes from scratch\r\n- [Parameters](/native/parameters) — sliders, colors, toggles\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
912
1019
  }
913
1020
  ]
914
1021
  },
915
1022
  "native-parameters-overview": {
916
1023
  "id": "native-parameters-overview",
917
- "title": "native-parameters-overview",
1024
+ "title": "Parameters",
918
1025
  "description": "The Viji parameter system — sliders, colors, toggles, and more for artist-controllable scene inputs.",
919
1026
  "content": [
920
1027
  {
921
1028
  "type": "text",
922
- "markdown": "# Parameters\r\n\r\nParameters are the primary way to give users control over your scene. You define them at the top level of your code, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Read `.value` inside `render()` to get the current state — values update in real-time as users interact with the controls.\r\n\r\n## Parameter Types\r\n\r\n| Type | Function | Value | Use For |\r\n|---|---|---|---|\r\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\r\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\r\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Hex color values (`'#rrggbb'`) |\r\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\r\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\r\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\r\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\r\n\r\n## Basic Pattern\r\n\r\n```javascript\r\n// 1. Define at top level — runs once\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#ff6600', { label: 'Color' });\r\nconst mirror = viji.toggle(false, { label: 'Mirror' });\r\n\r\n// 2. Read .value in render() — updates in real-time\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n ctx.fillStyle = color.value;\r\n // speed.value, mirror.value, etc.\r\n}\r\n```\r\n\r\n> [!WARNING]\r\n> Parameters must be declared at the **top level** of your scene, never inside `render()`. They are registered once during initialization. Declaring them inside `render()` would create duplicate parameters on every frame.\r\n\r\n## Common Config Keys\r\n\r\nAll parameter types share these optional configuration keys:\r\n\r\n| Key | Type | Default | Description |\r\n|---|---|---|---|\r\n| `label` | `string` | **(required)** | Display name shown in the parameter UI |\r\n| `description` | `string` | — | Tooltip or help text |\r\n| `group` | `string` | `'general'` | Group name for organizing parameters — see [Grouping](grouping/) |\r\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on active capabilities — see [Categories](categories/) |\r\n\r\n## Organization\r\n\r\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\r\n\r\n- **[Grouping](grouping/)** — Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\r\n- **[Categories](categories/)** — Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\r\n\r\n## Related\r\n\r\n- [Slider](slider/) — the most common parameter type\r\n- [Grouping](grouping/) — organizing parameters into named groups\r\n- [Categories](categories/) — visibility based on capabilities\r\n- [P5 Parameters](/p5/parameters) — same system in the P5 renderer\r\n- [Shader Parameters](/shader/parameters) — comment-directive syntax for shaders\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1029
+ "markdown": "# Parameters\n\nParameters are the primary way to give users control over your scene. You define them at the top level of your code, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Read `.value` inside `render()` to get the current state — values update in real-time as users interact with the controls.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Hex color values (`'#rrggbb'`) |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger — true for 1 frame (resets, spawns) |\n\n## Basic Pattern\n\n```javascript\n// 1. Define at top level — runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render() — updates in real-time\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = color.value;\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `render()`. They are registered once during initialization. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | **(required)** | Display name shown in the parameter UI |\n| `description` | `string` | — | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters — see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on active capabilities — see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)** — Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)** — Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/) — the most common parameter type\n- [Grouping](grouping/) — organizing parameters into named groups\n- [Categories](categories/) — visibility based on capabilities\n- [P5 Parameters](/p5/parameters) — same system in the P5 renderer\n- [Shader Parameters](/shader/parameters) — comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
923
1030
  }
924
1031
  ]
925
1032
  },
926
1033
  "native-param-slider": {
927
1034
  "id": "native-param-slider",
928
- "title": "native-param-slider",
1035
+ "title": "Slider Parameter",
929
1036
  "description": "Create a numeric slider control with configurable range, step size, and grouping.",
930
1037
  "content": [
931
1038
  {
932
1039
  "type": "text",
933
- "markdown": "# viji.slider()\r\n\r\n```\r\nslider(defaultValue: number, config: SliderConfig): SliderParameter\r\n```\r\n\r\nCreates a numeric slider parameter that the host application renders as a draggable slider control. The artist defines the slider at the top level of the scene, and reads its current value inside `render()`.\r\n\r\n## Parameters\r\n\r\n| Name | Type | Required | Default | Description |\r\n|------|------|----------|---------|-------------|\r\n| `defaultValue` | `number` | Yes | — | Initial value of the slider |\r\n| `config.min` | `number` | No | `0` | Minimum allowed value |\r\n| `config.max` | `number` | No | `100` | Maximum allowed value |\r\n| `config.step` | `number` | No | `1` | Increment between values |\r\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\r\n| `config.description` | `string` | No | — | Tooltip or help text |\r\n| `config.group` | `string` | No | `'general'` | Group name for organizing parameters — see [Grouping](../grouping/) |\r\n| `config.category` | `ParameterCategory` | No | `'general'` | Controls visibility based on capabilities — see [Categories](../categories/) |\r\n\r\n## Return Value\r\n\r\nReturns a `SliderParameter` object:\r\n\r\n| Property | Type | Description |\r\n|----------|------|-------------|\r\n| `value` | `number` | Current slider value. Updates in real-time when the user moves the slider. |\r\n| `min` | `number` | Minimum value |\r\n| `max` | `number` | Maximum value |\r\n| `step` | `number` | Step increment |\r\n| `label` | `string` | Display label |\r\n| `description` | `string \\| undefined` | Description text |\r\n| `group` | `string` | Group name |\r\n| `category` | `ParameterCategory` | Parameter category |\r\n\r\n## Usage\r\n\r\nDefine sliders at the **top level** of your scene code, before the `render()` function. Read `.value` inside `render()` to get the current value.\r\n\r\n```javascript\r\nconst radius = viji.slider(50, {\r\n min: 10,\r\n max: 200,\r\n step: 1,\r\n label: 'Circle Radius',\r\n group: 'shape'\r\n});\r\n\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n ctx.clearRect(0, 0, viji.width, viji.height);\r\n ctx.beginPath();\r\n ctx.arc(viji.width / 2, viji.height / 2, radius.value, 0, Math.PI * 2);\r\n ctx.fill();\r\n}\r\n```\r\n\r\n> [!NOTE]\r\n> Parameters must be defined at the top level, not inside `render()`. They are registered once during scene initialization. Defining them inside `render()` would create duplicate parameters on every frame."
1040
+ "markdown": "# viji.slider()\n\n```\nslider(defaultValue: number, config: SliderConfig): SliderParameter\n```\n\nCreates a numeric slider parameter that the host application renders as a draggable slider control. The artist defines the slider at the top level of the scene, and reads its current value inside `render()`.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `number` | Yes | — | Initial value of the slider |\n| `config.min` | `number` | No | `0` | Minimum allowed value |\n| `config.max` | `number` | No | `100` | Maximum allowed value |\n| `config.step` | `number` | No | `1` | Increment between values |\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 for organizing parameters — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Controls visibility based on capabilities — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `SliderParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `number` | Current slider value. Updates in real-time when the user moves the slider. |\n| `min` | `number` | Minimum value |\n| `max` | `number` | Maximum value |\n| `step` | `number` | Step increment |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\nDefine sliders at the **top level** of your scene code, before the `render()` function. Read `.value` inside `render()` to get the current value.\n\n```javascript\nconst radius = viji.slider(50, {\n min: 10,\n max: 200,\n step: 1,\n label: 'Circle Radius',\n group: 'shape'\n});\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.clearRect(0, 0, viji.width, viji.height);\n ctx.beginPath();\n ctx.arc(viji.width / 2, viji.height / 2, radius.value, 0, Math.PI * 2);\n ctx.fill();\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level, not inside `render()`. They are registered once during scene initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
934
1041
  },
935
1042
  {
936
1043
  "type": "live-example",
@@ -940,18 +1047,155 @@ export const docsApi = {
940
1047
  },
941
1048
  {
942
1049
  "type": "text",
943
- "markdown": "## Resolution-Agnostic Sizing\r\n\r\nWhen using a slider to control sizes or positions, always scale relative to [`viji.width`](../../canvas-context) and [`viji.height`](../../canvas-context) so the scene looks the same at any resolution. A slider value of `0` to `1` works well as a normalized proportion:\r\n\r\n```javascript\r\nconst size = viji.slider(0.15, {\r\n min: 0.02,\r\n max: 0.5,\r\n step: 0.01,\r\n label: 'Size',\r\n group: 'shape'\r\n});\r\n\r\nfunction render(viji) {\r\n const ctx = viji.useContext('2d');\r\n const pixelSize = size.value * Math.min(viji.width, viji.height);\r\n // pixelSize adapts automatically to any resolution\r\n}\r\n```\r\n\r\n## Related\r\n\r\n- [Color](../color/) — color picker parameter\r\n- [Number](../number/) — numeric input without a slider track\r\n- [Select](../select/) — dropdown selection from predefined options\r\n- [Grouping](../grouping/) — organizing parameters into named groups\r\n- [Categories](../categories/) — controlling parameter visibility based on capabilities\r\n- [Shader Slider](/shader/parameters/slider) — equivalent for the Shader renderer"
1050
+ "markdown": "## Resolution-Agnostic Sizing\n\nWhen using a slider to control sizes or positions, always scale relative to [`viji.width`](../../canvas-context) and [`viji.height`](../../canvas-context) so the scene looks the same at any resolution. A slider value of `0` to `1` works well as a normalized proportion:\n\n```javascript\nconst size = viji.slider(0.15, {\n min: 0.02,\n max: 0.5,\n step: 0.01,\n label: 'Size',\n group: 'shape'\n});\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const pixelSize = size.value * Math.min(viji.width, viji.height);\n // pixelSize adapts automatically to any resolution\n}\n```\n\n## Related\n\n- [Color](../color/) — color picker parameter\n- [Number](../number/) — numeric input without a slider track\n- [Select](../select/) — dropdown selection from predefined options\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility based on capabilities\n- [Shader Slider](/shader/parameters/slider) — equivalent for the Shader renderer"
1051
+ }
1052
+ ]
1053
+ },
1054
+ "native-param-color": {
1055
+ "id": "native-param-color",
1056
+ "title": "Color Parameter",
1057
+ "description": "Create a color picker control that returns a hex color string.",
1058
+ "content": [
1059
+ {
1060
+ "type": "text",
1061
+ "markdown": "# viji.color()\n\n```\ncolor(defaultValue: string, config: ColorConfig): ColorParameter\n```\n\nCreates a color picker parameter. The host renders it as a color swatch that opens a full color picker when clicked.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `string` | Yes | — | Initial hex color (e.g., `'#ff6600'`) |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ColorParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Current hex color (e.g., `'#ff6600'`). Updates in real-time. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst bg = viji.color('#1a1a2e', { label: 'Background' });\nconst accent = viji.color('#ff6600', { label: 'Accent', group: 'colors' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n ctx.fillStyle = accent.value;\n ctx.fillRect(viji.width * 0.1, viji.height * 0.1, viji.width * 0.8, viji.height * 0.8);\n}\n```\n\nThe `.value` is always a 6-digit hex string (`#rrggbb`). You can pass it directly to `ctx.fillStyle`, `ctx.strokeStyle`, `ctx.shadowColor`, or any Canvas 2D color property.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
1062
+ },
1063
+ {
1064
+ "type": "live-example",
1065
+ "title": "Color Picker",
1066
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst ring1 = viji.color('#ff4488', { label: 'Ring 1', group: 'colors' });\nconst ring2 = viji.color('#4488ff', { label: 'Ring 2', group: 'colors' });\nconst ring3 = viji.color('#44ff88', { label: 'Ring 3', group: 'colors' });\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 unit = Math.min(w, h);\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n const colors = [ring1.value, ring2.value, ring3.value];\n for (let i = colors.length - 1; i >= 0; i--) {\n const r = unit * (0.15 + i * 0.1);\n const offset = Math.sin(viji.time * 2 + i * 1.2) * unit * 0.03;\n ctx.beginPath();\n ctx.arc(cx + offset, cy, r, 0, Math.PI * 2);\n ctx.fillStyle = colors[i];\n ctx.fill();\n }\n}\n",
1067
+ "sceneFile": "color-basic.scene.js"
1068
+ },
1069
+ {
1070
+ "type": "text",
1071
+ "markdown": "## Parsing Hex Values\n\nIf you need individual RGB components (e.g., for blending or alpha compositing):\n\n```javascript\nconst c = viji.color('#ff6600', { label: 'Color' });\n\nfunction hexToRGB(hex) {\n const n = parseInt(hex.slice(1), 16);\n return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };\n}\n\nfunction render(viji) {\n const { r, g, b } = hexToRGB(c.value);\n const ctx = viji.useContext('2d');\n ctx.fillStyle = `rgba(${r}, ${g}, ${b}, 0.5)`;\n // ...\n}\n```\n\n## Related\n\n- [Slider](../slider/) — numeric slider parameter\n- [Toggle](../toggle/) — boolean on/off parameter\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Shader Color](/shader/parameters/color) — equivalent for the Shader renderer"
1072
+ }
1073
+ ]
1074
+ },
1075
+ "native-param-toggle": {
1076
+ "id": "native-param-toggle",
1077
+ "title": "Toggle Parameter",
1078
+ "description": "Create a boolean on/off switch for enabling or disabling scene features.",
1079
+ "content": [
1080
+ {
1081
+ "type": "text",
1082
+ "markdown": "# viji.toggle()\n\n```\ntoggle(defaultValue: boolean, config: ToggleConfig): ToggleParameter\n```\n\nCreates a boolean toggle parameter. The host renders it as an on/off switch.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `boolean` | Yes | — | Initial state (`true` or `false`) |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ToggleParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `boolean` | Current state. Updates in real-time when the user toggles. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst showGrid = viji.toggle(true, { label: 'Show Grid' });\nconst animate = viji.toggle(true, { label: 'Animate' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.clearRect(0, 0, viji.width, viji.height);\n\n if (showGrid.value) {\n // draw grid\n }\n\n const angle = animate.value ? viji.time : 0;\n // use angle for rotation\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."
1083
+ },
1084
+ {
1085
+ "type": "live-example",
1086
+ "title": "Toggle Features",
1087
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst dotColor = viji.color('#ff6644', { label: 'Dot Color' });\nconst showGrid = viji.toggle(true, { label: 'Show Grid' });\nconst animate = viji.toggle(true, { label: 'Animate' });\nconst count = viji.slider(8, { min: 2, max: 20, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const unit = Math.min(w, h);\n\n if (animate.value) {\n angle += viji.deltaTime * 0.5;\n }\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n if (showGrid.value) {\n ctx.strokeStyle = 'rgba(255,255,255,0.06)';\n ctx.lineWidth = 1;\n const step = unit / 10;\n for (let x = step; x < w; x += step) {\n ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();\n }\n for (let y = step; y < h; y += step) {\n ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();\n }\n }\n\n ctx.fillStyle = dotColor.value;\n const r = unit * 0.02;\n for (let i = 0; i < count.value; i++) {\n const a = angle + (i / count.value) * Math.PI * 2;\n const x = w / 2 + Math.cos(a) * unit * 0.3;\n const y = h / 2 + Math.sin(a) * unit * 0.3;\n ctx.beginPath();\n ctx.arc(x, y, r, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n",
1088
+ "sceneFile": "toggle-basic.scene.js"
1089
+ },
1090
+ {
1091
+ "type": "text",
1092
+ "markdown": "## Common Patterns\n\n### Conditional rendering\n\n```javascript\nconst showParticles = viji.toggle(true, { label: 'Particles' });\n\nfunction render(viji) {\n // ... base scene ...\n if (showParticles.value) {\n // draw particles\n }\n}\n```\n\n### Freezing animation\n\n```javascript\nconst animate = viji.toggle(true, { label: 'Animate' });\nlet angle = 0;\n\nfunction render(viji) {\n if (animate.value) {\n angle += viji.deltaTime;\n }\n // use frozen or live angle\n}\n```\n\n## Related\n\n- [Slider](../slider/) — numeric slider parameter\n- [Color](../color/) — color picker parameter\n- [Select](../select/) — dropdown with multiple options\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Shader Toggle](/shader/parameters/toggle) — equivalent for the Shader renderer"
1093
+ }
1094
+ ]
1095
+ },
1096
+ "native-param-select": {
1097
+ "id": "native-param-select",
1098
+ "title": "Select Parameter",
1099
+ "description": "Create a dropdown selector for choosing between predefined options.",
1100
+ "content": [
1101
+ {
1102
+ "type": "text",
1103
+ "markdown": "# viji.select()\n\n```\nselect(defaultValue: string | number, config: SelectConfig): SelectParameter\n```\n\nCreates a dropdown selection parameter. The host renders it as a dropdown menu or segmented control.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `string \\| number` | Yes | — | Initially selected value (must be in `options`) |\n| `config.options` | `string[] \\| number[]` | Yes | — | Available choices |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `SelectParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string \\| number` | Currently selected option. Updates in real-time. |\n| `options` | `string[] \\| number[]` | The full options list |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst shape = viji.select('circle', {\n options: ['circle', 'square', 'triangle'],\n label: 'Shape'\n});\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n // ...\n if (shape.value === 'circle') {\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n } else if (shape.value === 'square') {\n ctx.rect(cx - r, cy - r, r * 2, r * 2);\n }\n // ...\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."
1104
+ },
1105
+ {
1106
+ "type": "live-example",
1107
+ "title": "Shape Selector",
1108
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst shapeColor = viji.color('#ff6644', { label: 'Shape Color' });\nconst shape = viji.select('circle', {\n options: ['circle', 'square', 'triangle', 'star'],\n label: 'Shape'\n});\nconst size = viji.slider(0.25, { min: 0.05, max: 0.45, step: 0.01, label: 'Size' });\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 r = Math.min(w, h) * size.value;\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n ctx.fillStyle = shapeColor.value;\n ctx.beginPath();\n\n switch (shape.value) {\n case 'circle':\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n break;\n case 'square':\n ctx.rect(cx - r, cy - r, r * 2, r * 2);\n break;\n case 'triangle':\n for (let i = 0; i < 3; i++) {\n const a = (i / 3) * Math.PI * 2 - Math.PI / 2;\n const method = i === 0 ? 'moveTo' : 'lineTo';\n ctx[method](cx + Math.cos(a) * r, cy + Math.sin(a) * r);\n }\n ctx.closePath();\n break;\n case 'star':\n for (let i = 0; i < 10; i++) {\n const a = (i / 10) * Math.PI * 2 - Math.PI / 2;\n const sr = i % 2 === 0 ? r : r * 0.5;\n const method = i === 0 ? 'moveTo' : 'lineTo';\n ctx[method](cx + Math.cos(a) * sr, cy + Math.sin(a) * sr);\n }\n ctx.closePath();\n break;\n }\n\n ctx.fill();\n}\n",
1109
+ "sceneFile": "select-basic.scene.js"
1110
+ },
1111
+ {
1112
+ "type": "text",
1113
+ "markdown": "## Numeric Options\n\nOptions can also be numbers, useful for selecting discrete numeric values:\n\n```javascript\nconst sides = viji.select(6, {\n options: [3, 4, 5, 6, 8],\n label: 'Polygon Sides'\n});\n```\n\n## Related\n\n- [Slider](../slider/) — continuous numeric range\n- [Toggle](../toggle/) — two-state boolean\n- [Number](../number/) — direct numeric input\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Shader Select](/shader/parameters/select) — equivalent for the Shader renderer"
1114
+ }
1115
+ ]
1116
+ },
1117
+ "native-param-number": {
1118
+ "id": "native-param-number",
1119
+ "title": "Number Parameter",
1120
+ "description": "Create a numeric input field with configurable range and step size.",
1121
+ "content": [
1122
+ {
1123
+ "type": "text",
1124
+ "markdown": "# viji.number()\n\n```\nnumber(defaultValue: number, config: NumberConfig): NumberParameter\n```\n\nCreates a numeric input parameter. The host renders it as a direct number input field — use this when you need precise numeric entry rather than a draggable slider.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `number` | Yes | — | Initial value |\n| `config.min` | `number` | No | `0` | Minimum allowed value |\n| `config.max` | `number` | No | `100` | Maximum allowed value |\n| `config.step` | `number` | No | `1` | Increment between values |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `NumberParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `number` | Current value. Updates in real-time. |\n| `min` | `number` | Minimum value |\n| `max` | `number` | Maximum value |\n| `step` | `number` | Step increment |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst count = viji.number(12, {\n min: 1,\n max: 100,\n step: 1,\n label: 'Particle Count'\n});\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n for (let i = 0; i < count.value; i++) {\n // draw particles\n }\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."
1125
+ },
1126
+ {
1127
+ "type": "live-example",
1128
+ "title": "Number Input",
1129
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst dotColor = viji.color('#44ddff', { label: 'Dot Color' });\nconst count = viji.number(16, { min: 1, max: 64, step: 1, label: 'Dot Count' });\nconst layers = viji.number(3, { min: 1, max: 6, step: 1, label: 'Layers' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const unit = Math.min(w, h);\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n ctx.fillStyle = dotColor.value;\n const r = unit * 0.012;\n\n for (let layer = 0; layer < layers.value; layer++) {\n const radius = unit * (0.1 + layer * 0.08);\n const n = count.value;\n for (let i = 0; i < n; i++) {\n const a = (i / n) * Math.PI * 2 + viji.time * (0.5 + layer * 0.2);\n const x = w / 2 + Math.cos(a) * radius;\n const y = h / 2 + Math.sin(a) * radius;\n ctx.beginPath();\n ctx.arc(x, y, r, 0, Math.PI * 2);\n ctx.fill();\n }\n }\n}\n",
1130
+ "sceneFile": "number-basic.scene.js"
1131
+ },
1132
+ {
1133
+ "type": "text",
1134
+ "markdown": "## Number vs Slider\n\nBoth accept the same config options (`min`, `max`, `step`) and return the same shape. The difference is the host UI:\n\n| | Slider | Number |\n|--|--------|--------|\n| UI | Draggable track | Text input field |\n| Best for | Continuous ranges, visual tuning | Precise values, integer counts |\n| Interaction | Drag / click | Type / arrow keys |\n\nUse [`viji.slider()`](../slider/) when the artist needs to \"feel\" the value visually. Use `viji.number()` when exact entry matters.\n\n## Resolution-Agnostic Sizing\n\nWhen a number controls sizes or positions, normalize the value and scale relative to the canvas:\n\n```javascript\nconst gridCols = viji.number(8, { min: 2, max: 30, step: 1, label: 'Columns' });\n\nfunction render(viji) {\n const cellW = viji.width / gridCols.value;\n // ...\n}\n```\n\n## Related\n\n- [Slider](../slider/) — numeric slider with a draggable track\n- [Select](../select/) — dropdown for discrete choices\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Shader Number](/shader/parameters/number) — equivalent for the Shader renderer"
1135
+ }
1136
+ ]
1137
+ },
1138
+ "native-param-text": {
1139
+ "id": "native-param-text",
1140
+ "title": "Text Parameter",
1141
+ "description": "Create a text input field for user-provided strings.",
1142
+ "content": [
1143
+ {
1144
+ "type": "text",
1145
+ "markdown": "# viji.text()\n\n```\ntext(defaultValue: string, config: TextConfig): TextParameter\n```\n\nCreates a text input parameter. The host renders it as a single-line text field.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `string` | Yes | — | Initial text value |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.maxLength` | `number` | No | `1000` | Maximum character count |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `TextParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Current text. Updates in real-time as the user types. |\n| `maxLength` | `number \\| undefined` | Maximum character count |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst message = viji.text('Hello', {\n label: 'Message',\n maxLength: 50\n});\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n ctx.fillStyle = '#fff';\n ctx.font = `${Math.min(viji.width, viji.height) * 0.08}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(message.value, viji.width / 2, viji.height / 2);\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."
1146
+ },
1147
+ {
1148
+ "type": "live-example",
1149
+ "title": "Text Display",
1150
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst textColor = viji.color('#ffffff', { label: 'Text Color' });\nconst message = viji.text('Viji', { label: 'Message', maxLength: 30 });\nconst fontSize = viji.slider(0.12, { min: 0.03, max: 0.3, step: 0.01, label: 'Font Size' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n const unit = Math.min(w, h);\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n const size = unit * fontSize.value;\n ctx.font = `bold ${size}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n\n const wave = Math.sin(viji.time * 2) * unit * 0.02;\n ctx.fillStyle = textColor.value;\n ctx.fillText(message.value, w / 2, h / 2 + wave);\n}\n",
1151
+ "sceneFile": "text-basic.scene.js"
1152
+ },
1153
+ {
1154
+ "type": "text",
1155
+ "markdown": "## Related\n\n- [Slider](../slider/) — numeric slider parameter\n- [Color](../color/) — color picker parameter\n- [Select](../select/) — dropdown for predefined choices\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility"
1156
+ }
1157
+ ]
1158
+ },
1159
+ "native-param-image": {
1160
+ "id": "native-param-image",
1161
+ "title": "Image Parameter",
1162
+ "description": "Accept user-uploaded images for use as textures or source material.",
1163
+ "content": [
1164
+ {
1165
+ "type": "text",
1166
+ "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. The uploaded image becomes available as an `ImageBitmap`.\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` | The uploaded image, or `null` if none provided |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\nAlways check for `null` before drawing — the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n if (tex.value) {\n ctx.drawImage(tex.value, 0, 0, viji.width, viji.height);\n }\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."
1167
+ },
1168
+ {
1169
+ "type": "live-example",
1170
+ "title": "Image Upload",
1171
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst tex = viji.image(null, { label: 'Image' });\nconst tint = viji.color('#ffffff', { label: 'Tint' });\nconst opacity = viji.slider(1, { min: 0, max: 1, step: 0.01, label: 'Opacity' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = bg.value;\n ctx.fillRect(0, 0, w, h);\n\n if (tex.value) {\n ctx.globalAlpha = opacity.value;\n const imgAspect = tex.value.width / tex.value.height;\n const canvasAspect = w / h;\n let dw, dh;\n if (imgAspect > canvasAspect) { dw = w; dh = w / imgAspect; }\n else { dh = h; dw = h * imgAspect; }\n ctx.drawImage(tex.value, (w - dw) / 2, (h - dh) / 2, dw, dh);\n ctx.globalAlpha = 1;\n } else {\n ctx.fillStyle = 'rgba(255,255,255,0.15)';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText('Upload an image above', w / 2, h / 2);\n }\n}\n",
1172
+ "sceneFile": "image-basic.scene.js"
1173
+ },
1174
+ {
1175
+ "type": "text",
1176
+ "markdown": "## Drawing with Aspect Ratio\n\nTo draw an uploaded image without distortion:\n\n```javascript\nif (tex.value) {\n const imgAspect = tex.value.width / tex.value.height;\n const canvasAspect = viji.width / viji.height;\n let drawW, drawH;\n\n if (imgAspect > canvasAspect) {\n drawW = viji.width;\n drawH = viji.width / imgAspect;\n } else {\n drawH = viji.height;\n drawW = viji.height * imgAspect;\n }\n\n const x = (viji.width - drawW) / 2;\n const y = (viji.height - drawH) / 2;\n ctx.drawImage(tex.value, x, y, drawW, drawH);\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- [Shader Image](/shader/parameters/image) — equivalent for the Shader renderer (`sampler2D`)\n- [P5 Image](/p5/parameters/image) — P5 equivalent with `.p5` property"
1177
+ }
1178
+ ]
1179
+ },
1180
+ "native-param-button": {
1181
+ "id": "native-param-button",
1182
+ "title": "Button Parameter",
1183
+ "description": "Create a momentary trigger that is true for exactly one frame when pressed.",
1184
+ "content": [
1185
+ {
1186
+ "type": "text",
1187
+ "markdown": "# viji.button()\n\n```\nbutton(config: ButtonConfig): ButtonParameter\n```\n\nCreates a momentary button parameter. Unlike [`viji.toggle()`](../toggle/) which latches on/off, a button is `true` for exactly **one frame** when the user clicks it, then automatically resets to `false`. The host renders it as a clickable button.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ButtonParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `boolean` | `true` for the single frame after the user clicks, `false` otherwise. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst reset = viji.button({ label: 'Reset' });\n\nlet rotation = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n\n if (reset.value) {\n rotation = 0;\n }\n\n rotation += viji.deltaTime;\n // draw rotated shapes\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## How It Works\n\n1. The user clicks the button in the host UI.\n2. On the **next rendered frame**, `button.value` is `true`.\n3. After that frame completes, the value automatically resets to `false`.\n4. Subsequent frames see `false` until the user clicks again.\n\nThis makes buttons ideal for **one-shot actions** like resetting state, spawning particles, or triggering transitions.\n\n## Common Patterns\n\n### Resetting accumulated state\n\n```javascript\nconst resetPos = viji.button({ label: 'Reset Position' });\n\nlet posX = 0;\nlet posY = 0;\n\nfunction render(viji) {\n if (resetPos.value) {\n posX = 0;\n posY = 0;\n }\n\n posX += viji.deltaTime * 50;\n posY += Math.sin(viji.time) * viji.deltaTime * 30;\n // draw at posX, posY\n}\n```\n\n### Spawning elements\n\n```javascript\nconst spawn = viji.button({ label: 'Spawn Particle' });\nconst particles = [];\n\nfunction render(viji) {\n if (spawn.value) {\n particles.push({ x: viji.width / 2, y: viji.height / 2, age: 0 });\n }\n // update and draw particles\n}\n```\n\n## Button vs Toggle\n\n| | [`viji.button()`](../button/) | [`viji.toggle()`](../toggle/) |\n|---|---|---|\n| Value | `true` for 1 frame, then `false` | Stays `true` or `false` until changed |\n| Default argument | None (always starts `false`) | Required (`true` or `false`) |\n| Use for | One-shot actions, resets, spawns | Persistent on/off switches |\n| Host UI | Clickable button | On/off switch |\n\n## Related\n\n- [Toggle](../toggle/) — persistent boolean switch\n- [Slider](../slider/) — numeric slider parameter\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Shader Button](/shader/parameters/button) — equivalent for the Shader renderer"
944
1188
  }
945
1189
  ]
946
1190
  },
947
1191
  "native-param-grouping": {
948
1192
  "id": "native-param-grouping",
949
- "title": "native-param-grouping",
1193
+ "title": "Grouping Parameters",
950
1194
  "description": "Organize parameters into named groups so related controls appear together in the UI.",
951
1195
  "content": [
952
1196
  {
953
1197
  "type": "text",
954
- "markdown": "# Grouping Parameters\r\n\r\nAs your scene grows, the parameter list can become unwieldy. Groups let you collect related parameters under a shared heading in the host UI. Parameters with the same `group` string appear together, regardless of declaration order.\r\n\r\n## Usage\r\n\r\nSet the `group` config key on any parameter:\r\n\r\n```javascript\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed', group: 'animation' });\r\nconst trail = viji.toggle(true, { label: 'Trail', group: 'animation' });\r\n\r\nconst size = viji.slider(0.05, { min: 0.01, max: 0.15, step: 0.01, label: 'Size', group: 'shape' });\r\nconst color = viji.color('#ff6600', { label: 'Color', group: 'shape' });\r\n```\r\n\r\nThe host application receives the group names and renders them as collapsible sections or visual separators.\r\n\r\n## How It Works\r\n\r\n- The `group` key is a **freeform string** — any name works. There is no fixed list.\r\n- If you omit `group`, the parameter defaults to `'general'`.\r\n- Parameters with the same `group` value are collected together when sent to the host UI.\r\n- Group names are displayed as-is, so use readable names like `'animation'`, `'visual style'`, `'audio settings'`.\r\n- Declaration order within a group is preserved.\r\n\r\n## Live Example\r\n\r\nThis scene uses two groups: \"animation\" (speed and trail toggle) and \"shape\" (size, color, and shape type):"
1198
+ "markdown": "# Grouping Parameters\n\nAs your scene grows, the parameter list can become unwieldy. Groups let you collect related parameters under a shared heading in the host UI. Parameters with the same `group` string appear together, regardless of declaration order.\n\n## Usage\n\nSet the `group` config key on any parameter:\n\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed', group: 'animation' });\nconst trail = viji.toggle(true, { label: 'Trail', group: 'animation' });\n\nconst size = viji.slider(0.05, { min: 0.01, max: 0.15, step: 0.01, label: 'Size', group: 'shape' });\nconst color = viji.color('#ff6600', { label: 'Color', group: 'shape' });\n```\n\nThe host application receives the group names and renders them as collapsible sections or visual separators.\n\n## How It Works\n\n- The `group` key is a **freeform string** — any name works. There is no fixed list.\n- If you omit `group`, the parameter defaults to `'general'`.\n- Parameters with the same `group` value are collected together when sent to the host UI.\n- Group names are displayed as-is, so use readable names like `'animation'`, `'visual style'`, `'audio settings'`.\n- Declaration order within a group is preserved.\n\n## Live Example\n\nThis scene uses two groups: \"animation\" (speed and trail toggle) and \"shape\" (size, color, and shape type):"
955
1199
  },
956
1200
  {
957
1201
  "type": "live-example",
@@ -961,238 +1205,999 @@ export const docsApi = {
961
1205
  },
962
1206
  {
963
1207
  "type": "text",
964
- "markdown": "## Mixing Types\r\n\r\nGroups can contain any mix of parameter types — [`viji.slider()`](../slider/), [`viji.color()`](../color/), [`viji.toggle()`](../toggle/), [`viji.select()`](../select/), and more. There is no restriction on which types can share a group:\r\n\r\n```javascript\r\nconst brightness = viji.slider(1, { min: 0, max: 2, label: 'Brightness', group: 'visual' });\r\nconst tint = viji.color('#ffffff', { label: 'Tint', group: 'visual' });\r\nconst showGrid = viji.toggle(false, { label: 'Grid Overlay', group: 'visual' });\r\nconst blendMode = viji.select('normal', { options: ['normal', 'add', 'multiply'], label: 'Blend', group: 'visual' });\r\n```\r\n\r\n## Groups and Categories\r\n\r\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. You can combine them:\r\n\r\n```javascript\r\nconst volume = viji.slider(1, {\r\n label: 'Volume Scale',\r\n group: 'audio settings',\r\n category: 'audio'\r\n});\r\n```\r\n\r\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\r\n\r\n## Related\r\n\r\n- [Parameters Overview](../) — all parameter types and the basic pattern\r\n- [Categories](../categories/) — visibility based on active capabilities\r\n- [Slider](../slider/) — the most common parameter type\r\n- [P5 Grouping](/p5/parameters/grouping) — same concept in the P5 renderer\r\n- [Shader Grouping](/shader/parameters/grouping) — same concept with `@viji-*` directives"
1208
+ "markdown": "## Mixing Types\n\nGroups can contain any mix of parameter types — [`viji.slider()`](../slider/), [`viji.color()`](../color/), [`viji.toggle()`](../toggle/), [`viji.select()`](../select/), and more. There is no restriction on which types can share a group:\n\n```javascript\nconst brightness = viji.slider(1, { min: 0, max: 2, label: 'Brightness', group: 'visual' });\nconst tint = viji.color('#ffffff', { label: 'Tint', group: 'visual' });\nconst showGrid = viji.toggle(false, { label: 'Grid Overlay', group: 'visual' });\nconst blendMode = viji.select('normal', { options: ['normal', 'add', 'multiply'], label: 'Blend', group: 'visual' });\n```\n\n## Groups and Categories\n\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. You can combine them:\n\n```javascript\nconst volume = viji.slider(1, {\n label: 'Volume Scale',\n group: 'audio settings',\n category: 'audio'\n});\n```\n\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\n\n## Related\n\n- [Parameters Overview](../) — all parameter types and the basic pattern\n- [Categories](../categories/) — visibility based on active capabilities\n- [Slider](../slider/) — the most common parameter type\n- [P5 Grouping](/p5/parameters/grouping) — same concept in the P5 renderer\n- [Shader Grouping](/shader/parameters/grouping) — same concept with `@viji-*` directives"
965
1209
  }
966
1210
  ]
967
1211
  },
968
1212
  "native-param-categories": {
969
1213
  "id": "native-param-categories",
970
- "title": "native-param-categories",
1214
+ "title": "Parameter Categories",
971
1215
  "description": "Control parameter visibility based on active capabilities like audio, video, and interaction.",
972
1216
  "content": [
973
1217
  {
974
1218
  "type": "text",
975
- "markdown": "# Parameter Categories\r\n\r\nCategories let you tag parameters so they're only visible when the corresponding capability is active. An \"Audio Pulse\" [`viji.slider()`](../slider/) is useless if no audio source is connected — with `category: 'audio'`, it automatically appears when audio is available and hides when it's not.\r\n\r\n## The Four Categories\r\n\r\n| Category | Visible When | Use For |\r\n|---|---|---|\r\n| `'general'` | Always | Colors, sizes, speeds, shapes — anything that works without external input |\r\n| `'audio'` | Audio source is connected | Volume scaling, beat reactivity, frequency controls |\r\n| `'video'` | Video/camera source is connected | Video opacity, CV sensitivity, segmentation controls |\r\n| `'interaction'` | User interaction is enabled | Mouse effects, keyboard bindings, touch sensitivity |\r\n\r\n## Usage\r\n\r\nSet the `category` config key on any parameter:\r\n\r\n```javascript\r\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\r\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, label: 'Audio Pulse', category: 'audio' });\r\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\r\n```\r\n\r\n- `baseColor` ([`viji.color()`](../color/)) is always visible — it has no external dependency.\r\n- `pulseAmount` ([`viji.slider()`](../slider/)) only appears when the host connects an audio source.\r\n- `showMouse` ([`viji.toggle()`](../toggle/)) only appears when user interaction is enabled.\r\n\r\nIf you omit `category`, it defaults to `'general'` (always visible).\r\n\r\n## How It Works\r\n\r\n1. The artist sets `category` on each parameter during scene initialization.\r\n2. When the host application requests parameters, Viji filters them based on the current `CoreCapabilities`:\r\n - `hasAudio` — is an audio stream connected?\r\n - `hasVideo` — is a video/camera stream connected?\r\n - `hasInteraction` — is user interaction enabled?\r\n - `hasGeneral` — always `true`.\r\n3. Only parameters matching active capabilities are sent to the UI.\r\n\r\n> [!NOTE]\r\n> Categories filter at both the **group level** and the **individual parameter level**. If a group's category doesn't match, the entire group is hidden. If individual parameters within a visible group have non-matching categories, those parameters are hidden while the group remains visible.\r\n\r\n## Live Example\r\n\r\nThis scene has parameters in three categories — `general` (always visible), `audio` (needs audio), and `interaction` (needs mouse):"
1219
+ "markdown": "# Parameter Categories\n\nCategories let you tag parameters so they're only visible when the corresponding capability is active. An \"Audio Pulse\" [`viji.slider()`](../slider/) is useless if no audio source is connected — with `category: 'audio'`, it automatically appears when audio is available and hides when it's not.\n\n## The Four Categories\n\n| Category | Visible When | Use For |\n|---|---|---|\n| `'general'` | Always | Colors, sizes, speeds, shapes — anything that works without external input |\n| `'audio'` | Audio source is connected | Volume scaling, beat reactivity, frequency controls |\n| `'video'` | Video/camera source is connected | Video opacity, CV sensitivity, segmentation controls |\n| `'interaction'` | User interaction is enabled | Mouse effects, keyboard bindings, touch sensitivity |\n\n## Usage\n\nSet the `category` config key on any parameter:\n\n```javascript\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, label: 'Audio Pulse', category: 'audio' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n```\n\n- `baseColor` ([`viji.color()`](../color/)) is always visible — it has no external dependency.\n- `pulseAmount` ([`viji.slider()`](../slider/)) only appears when the host connects an audio source.\n- `showMouse` ([`viji.toggle()`](../toggle/)) only appears when user interaction is enabled.\n\nIf you omit `category`, it defaults to `'general'` (always visible).\n\n## How It Works\n\n1. The artist sets `category` on each parameter during scene initialization.\n2. When the host application requests parameters, Viji filters them based on the current `CoreCapabilities`:\n - `hasAudio` — is an audio stream connected?\n - `hasVideo` — is a video/camera stream connected?\n - `hasInteraction` — is user interaction enabled?\n - `hasGeneral` — always `true`.\n3. Only parameters matching active capabilities are sent to the UI.\n\n> [!NOTE]\n> Categories filter at both the **group level** and the **individual parameter level**. If a group's category doesn't match, the entire group is hidden. If individual parameters within a visible group have non-matching categories, those parameters are hidden while the group remains visible.\n\n## Live Example\n\nThis scene has parameters in three categories — `general` (always visible), `audio` (needs audio), and `interaction` (needs mouse):"
976
1220
  },
977
1221
  {
978
1222
  "type": "live-example",
979
1223
  "title": "Parameter Categories",
980
1224
  "sceneCode": "const baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\r\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\r\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\r\n\r\nlet angle = 0;\r\nlet mouseTrailX = 0;\r\nlet mouseTrailY = 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 = 'rgba(10, 10, 30, 0.15)';\r\n ctx.fillRect(0, 0, w, h);\r\n\r\n angle += viji.deltaTime;\r\n\r\n const r = parseInt(baseColor.value.slice(1, 3), 16);\r\n const g = parseInt(baseColor.value.slice(3, 5), 16);\r\n const b = parseInt(baseColor.value.slice(5, 7), 16);\r\n\r\n let pulse = 0;\r\n if (viji.audio.isConnected) {\r\n pulse = viji.audio.volume.current * pulseAmount.value;\r\n }\r\n\r\n const baseR = Math.min(w, h) * (0.1 + pulse * 0.15);\r\n const cx = w / 2 + Math.cos(angle) * w * 0.2;\r\n const cy = h / 2 + Math.sin(angle * 0.7) * h * 0.2;\r\n\r\n ctx.beginPath();\r\n ctx.arc(cx, cy, baseR, 0, Math.PI * 2);\r\n ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\r\n ctx.fill();\r\n\r\n if (showMouse.value && viji.mouse.isInCanvas) {\r\n mouseTrailX += (viji.mouse.x - mouseTrailX) * 0.1;\r\n mouseTrailY += (viji.mouse.y - mouseTrailY) * 0.1;\r\n ctx.beginPath();\r\n ctx.arc(mouseTrailX, mouseTrailY, Math.min(w, h) * 0.02, 0, Math.PI * 2);\r\n ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';\r\n ctx.fill();\r\n }\r\n}\r\n",
981
- "sceneFile": "categories-demo.scene.js"
1225
+ "sceneFile": "categories-demo.scene.js",
1226
+ "capabilities": {
1227
+ "audio": true,
1228
+ "interaction": true
1229
+ }
982
1230
  },
983
1231
  {
984
1232
  "type": "text",
985
- "markdown": "## Design Guidelines\r\n\r\n- **Default to `'general'`** unless the parameter genuinely depends on an external input.\r\n- **Use `'audio'` for parameters that only make sense with sound** — beat sensitivity, frequency scaling, audio decay.\r\n- **Use `'video'` for parameters tied to camera/video** — segmentation threshold, face tracking sensitivity.\r\n- **Use `'interaction'` for parameters that need user input** — mouse effect radius, keyboard shortcut configuration.\r\n- **Don't use categories as a replacement for grouping.** Categories control *visibility*; groups control *layout*. Use both when appropriate.\r\n\r\n## Categories and Groups\r\n\r\n`category` and `group` are orthogonal. A parameter in group `'effects'` with category `'audio'` will appear under the \"effects\" group heading, but only when audio is connected:\r\n\r\n```javascript\r\nconst bassReact = viji.slider(0.5, {\r\n label: 'Bass Reactivity',\r\n group: 'effects',\r\n category: 'audio'\r\n});\r\n\r\nconst colorShift = viji.slider(0.2, {\r\n label: 'Color Shift',\r\n group: 'effects',\r\n category: 'general'\r\n});\r\n```\r\n\r\nBoth parameters appear in the \"effects\" group, but `bassReact` only shows when audio is active.\r\n\r\n## Related\r\n\r\n- [Parameters Overview](../) — all parameter types and the basic pattern\r\n- [Grouping](../grouping/) — organizing parameters into named groups\r\n- [P5 Categories](/p5/parameters/categories) — same concept in the P5 renderer\r\n- [Shader Categories](/shader/parameters/categories) — same concept with `@viji-*` directives\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1233
+ "markdown": "## Design Guidelines\n\n- **Default to `'general'`** unless the parameter genuinely depends on an external input.\n- **Use `'audio'` for parameters that only make sense with sound** — beat sensitivity, frequency scaling, audio decay.\n- **Use `'video'` for parameters tied to camera/video** — segmentation threshold, face tracking sensitivity.\n- **Use `'interaction'` for parameters that need user input** — mouse effect radius, keyboard shortcut configuration.\n- **Don't use categories as a replacement for grouping.** Categories control *visibility*; groups control *layout*. Use both when appropriate.\n\n## Categories and Groups\n\n`category` and `group` are orthogonal. A parameter in group `'effects'` with category `'audio'` will appear under the \"effects\" group heading, but only when audio is connected:\n\n```javascript\nconst bassReact = viji.slider(0.5, {\n label: 'Bass Reactivity',\n group: 'effects',\n category: 'audio'\n});\n\nconst colorShift = viji.slider(0.2, {\n label: 'Color Shift',\n group: 'effects',\n category: 'general'\n});\n```\n\nBoth parameters appear in the \"effects\" group, but `bassReact` only shows when audio is active.\n\n## Related\n\n- [Parameters Overview](../) — all parameter types and the basic pattern\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [P5 Categories](/p5/parameters/categories) — same concept in the P5 renderer\n- [Shader Categories](/shader/parameters/categories) — same concept with `@viji-*` directives\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
986
1234
  }
987
1235
  ]
988
1236
  },
989
- "p5-quickstart": {
990
- "id": "p5-quickstart",
991
- "title": "p5-quickstart",
992
- "description": "Build your first Viji scene using the familiar P5.js creative coding API.",
1237
+ "native-pointer": {
1238
+ "id": "native-pointer",
1239
+ "title": "Pointer (Unified)",
1240
+ "description": "A single input abstraction that works identically for mouse and touch — the recommended starting point for position, click, and drag interactions.",
993
1241
  "content": [
994
1242
  {
995
1243
  "type": "text",
996
- "markdown": "# P5.js Quick Start\r\n\r\nThe P5.js renderer gives you the familiar Processing/P5.js drawing API. Viji loads P5.js automatically — no installation needed.\r\n\r\n> [!IMPORTANT]\r\n> P5 and shader scenes must declare their renderer type as the first comment:\r\n> ```\r\n> // @renderer p5\r\n> ```\r\n> Without this directive, the scene defaults to the native renderer.\r\n\r\n## Your First Scene"
1244
+ "markdown": "# Pointer (Unified Input)\n\n`viji.pointer` provides a single, unified input that works the same way whether the user is on a desktop with a mouse or on a mobile device using touch. **For most interactions — click, drag, position tracking — start here.**\n\n## Why Use Pointer?\n\nDrag-to-orbit, click-to-place, and cursor-following effects work identically for mouse and touch. `viji.pointer` gives you one set of coordinates, one pressed state, and one delta — no separate code paths needed.\n\nUse [`viji.mouse`](../mouse/) when you need mouse-specific features like right-click, middle-click, or scroll wheel. Use [`viji.touches`](../touch/) when you need multi-touch, pressure, radius, or per-finger tracking.\n\n## API Reference\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `x` | `number` | Canvas-space X position (pixels) |\n| `y` | `number` | Canvas-space Y position (pixels) |\n| `deltaX` | `number` | Horizontal movement since last frame (pixels) |\n| `deltaY` | `number` | Vertical movement since last frame (pixels) |\n| `isDown` | `boolean` | `true` if left mouse button is held or a touch is active |\n| `wasPressed` | `boolean` | `true` for exactly one frame when input becomes active, then resets |\n| `wasReleased` | `boolean` | `true` for exactly one frame when input is released, then resets |\n| `isInCanvas` | `boolean` | `true` if the input position is within the canvas bounds |\n| `type` | `'mouse' \\| 'touch' \\| 'none'` | Which input device is currently active |\n\n## Coordinate System\n\nPointer coordinates are in **canvas-space pixels**, with `(0, 0)` at the top-left corner. Values range from `0` to [`viji.width`](/native/canvas-context) horizontally and `0` to [`viji.height`](/native/canvas-context) vertically.\n\nThe coordinates always match the canvas dimensions regardless of how the canvas is displayed on screen. Viji handles display scaling automatically, so your code works with canvas-space values directly.\n\n## How It Works\n\nWhen a touch is active, the pointer follows the primary touch point. Otherwise, it follows the mouse. This switching happens automatically each frame.\n\n- **When a touch is active** (`viji.touches.count > 0`): pointer tracks the primary touch. `isDown` is always `true`, `type` is `'touch'`.\n- **When no touch is active**: pointer falls back to the mouse. `isDown` reflects the left mouse button, `type` is `'mouse'` when the cursor is over the canvas, or `'none'` when it's outside.\n\n`wasPressed` and `wasReleased` reflect frame-to-frame transitions of `isDown` — each is `true` for exactly one frame, then automatically resets.\n\n## Basic Example"
997
1245
  },
998
1246
  {
999
1247
  "type": "live-example",
1000
- "title": "P5Rainbow Trail",
1001
- "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",
1002
- "sceneFile": "quickstart-p5.scene.js"
1248
+ "title": "PointerDrag Trail",
1249
+ "sceneCode": "const ctx = viji.useContext('2d');\nconst trail = [];\nconst maxTrail = 80;\n\nfunction render(viji) {\n const w = viji.width, h = viji.height;\n const p = viji.pointer;\n\n ctx.fillStyle = 'rgba(10, 10, 30, 0.15)';\n ctx.fillRect(0, 0, w, h);\n\n if (p.isDown) {\n trail.push({ x: p.x, y: p.y });\n if (trail.length > maxTrail) trail.shift();\n } else if (trail.length > 0) {\n trail.shift();\n }\n\n for (let i = 0; i < trail.length; i++) {\n const t = i / trail.length;\n const radius = 3 + t * 12;\n ctx.beginPath();\n ctx.arc(trail[i].x, trail[i].y, radius, 0, Math.PI * 2);\n ctx.fillStyle = `hsla(${200 + t * 60}, 80%, 65%, ${t * 0.8})`;\n ctx.fill();\n }\n\n const size = Math.min(w, h);\n ctx.fillStyle = p.isDown ? 'rgba(100, 220, 255, 0.9)' : 'rgba(200, 200, 200, 0.5)';\n ctx.beginPath();\n ctx.arc(p.x, p.y, size * 0.015, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.fillStyle = 'rgba(255,255,255,0.5)';\n ctx.font = `${size * 0.025}px monospace`;\n ctx.textAlign = 'left';\n ctx.fillText(`pointer: (${Math.round(p.x)}, ${Math.round(p.y)}) type: ${p.type}`, size * 0.03, h - size * 0.03);\n ctx.fillText(`isDown: ${p.isDown} inCanvas: ${p.isInCanvas}`, size * 0.03, h - size * 0.06);\n}\n",
1250
+ "sceneFile": "pointer-demo.scene.js",
1251
+ "capabilities": {
1252
+ "interaction": true
1253
+ }
1003
1254
  },
1004
1255
  {
1005
1256
  "type": "text",
1006
- "markdown": "### What's Happening\r\n\r\n**Top level — runs once:**\r\n\r\n- `// @renderer p5` tells Viji to use the P5 renderer.\r\n- `viji.slider()` creates UI parameters — declared at the top level, read via `.value` in `render()`.\r\n\r\n**`setup(viji, p5)` — optional, runs once:**\r\n\r\n- Use for one-time configuration like `p5.colorMode()`. If you don't need it, omit it entirely.\r\n\r\n**`render(viji, p5)` — called every frame:**\r\n\r\n- `p5` is a full P5.js instance in **instance mode** — all P5 functions require the `p5.` prefix.\r\n- `viji.width` and `viji.height` give the canvas size — use them for resolution-agnostic positioning.\r\n- `viji.time` is elapsed seconds — use it for animation.\r\n\r\n> [!NOTE]\r\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 create duplicate parameters on every frame.\r\n\r\n## Scene Structure\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// 1. Top level — parameters and state\r\nconst size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\r\n\r\n// 2. setup() — optional one-time config\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB);\r\n}\r\n\r\n// 3. render() — called every frame\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n p5.circle(viji.width / 2, viji.height / 2, size.value);\r\n}\r\n```\r\n\r\n- **`render(viji, p5)` is required.** It replaces P5's `draw()`.\r\n- **`setup(viji, p5)` is optional.** Use it for one-time configuration.\r\n- **No `createCanvas()`.** The canvas is created and managed by Viji.\r\n- **No `preload()`.** Use `viji.image()` parameters or `fetch()` in `setup()`.\r\n\r\n## Instance Mode\r\n\r\n> [!WARNING]\r\n> Viji uses P5 in **instance mode**. All P5 functions require the `p5.` prefix:\r\n> ```javascript\r\n> // Correct\r\n> p5.background(0);\r\n> p5.circle(viji.width / 2, viji.height / 2, 100);\r\n>\r\n> // Wrong — will throw ReferenceError\r\n> background(0);\r\n> circle(width / 2, height / 2, 100);\r\n> ```\r\n\r\n## Input and Interaction\r\n\r\nP5's built-in input globals (`mouseX`, `mouseY`, `keyIsPressed`, etc.) are not updated in the worker environment. Use the Viji API instead:\r\n\r\n```javascript\r\nfunction render(viji, p5) {\r\n if (viji.mouse.isPressed) {\r\n p5.circle(viji.mouse.x, viji.mouse.y, 20);\r\n }\r\n}\r\n```\r\n\r\n## Essential Patterns\r\n\r\n> [!NOTE]\r\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\r\n\r\n> [!WARNING]\r\n> Scenes run in a Web Worker there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\r\n\r\n> [!TIP]\r\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them.\r\n\r\n## Converting Existing Sketches\r\n\r\nIf you have existing P5.js sketches, see [Converting P5 Sketches](/p5/converting-sketches) for a step-by-step migration guide. Key differences: `draw()` `render()`, instance mode, no `createCanvas()`, Viji APIs for input.\r\n\r\n## Next Steps\r\n\r\n- [Scene Structure](/p5/scene-structure) — `setup()`, `render()`, and lifecycle details\r\n- [Drawing with P5](/p5/drawing) — P5 drawing functions in Viji\r\n- [Converting P5 Sketches](/p5/converting-sketches) — migrate existing sketches\r\n- [Parameters](/p5/parameters) — sliders, colors, toggles, and more\r\n- [Audio](/p5/audio) — react to music and sound\r\n- [API Reference](/p5/api-reference) — full list of everything available\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1257
+ "markdown": "## Common Patterns\n\n### Click Detection\n\n```javascript\nfunction render(viji) {\n if (viji.pointer.wasPressed) {\n spawnParticle(viji.pointer.x, viji.pointer.y);\n }\n}\n```\n\n### Drag Interaction\n\n```javascript\nlet offsetX = 0, offsetY = 0;\n\nfunction render(viji) {\n if (viji.pointer.isDown) {\n offsetX += viji.pointer.deltaX;\n offsetY += viji.pointer.deltaY;\n }\n}\n```\n\n### Conditional by Input Type\n\n```javascript\nfunction render(viji) {\n if (viji.pointer.type === 'touch') {\n drawTouchIndicator(viji.pointer.x, viji.pointer.y);\n }\n}\n```\n\n## When to Use Mouse or Touch Instead\n\n| Need | Use |\n|------|-----|\n| Right-click or middle-click | [`viji.mouse`](../mouse/) |\n| Scroll wheel / zoom | [`viji.mouse`](../mouse/) — `wheelDelta`, `wheelX`, `wheelY` |\n| Multi-touch (pinch, two-finger rotation) | [`viji.touches`](../touch/) |\n| Per-touch pressure, radius, or velocity | [`viji.touches`](../touch/) |\n| Individual button states | [`viji.mouse`](../mouse/) `leftButton`, `rightButton`, `middleButton` |\n\n## Related\n\n- [Mouse](../mouse/) — device-specific mouse access with buttons, wheel, and movement deltas\n- [Keyboard](../keyboard/) — key state queries and modifier tracking\n- [Touch](../touch/) — multi-touch with pressure, radius, velocity, and per-finger tracking\n- [P5 Pointer](/p5/pointer) — same API in the P5 renderer\n- [Shader Pointer Uniforms](/shader/pointer) — GLSL uniforms for unified pointer input"
1007
1258
  }
1008
1259
  ]
1009
1260
  },
1010
- "p5-converting": {
1011
- "id": "p5-converting",
1012
- "title": "p5-converting",
1013
- "description": "Step-by-step guide to converting standard P5.js sketches into Viji scenes.",
1261
+ "native-mouse": {
1262
+ "id": "native-mouse",
1263
+ "title": "Mouse",
1264
+ "description": "Full mouse API position, buttons, movement deltas, scroll wheel, and frame-based press/release detection.",
1014
1265
  "content": [
1015
1266
  {
1016
1267
  "type": "text",
1017
- "markdown": "# Converting P5 Sketches\r\n\r\nThis guide shows how to take any standard P5.js sketch and convert it into a Viji scene. The changes are mechanical — once you learn the pattern, converting takes a few minutes.\r\n\r\n> [!TIP]\r\n> Want an AI to do it for you? See [Convert: P5 Sketches](/ai-prompts/convert-p5) for a ready-to-paste prompt that applies all the rules below automatically.\r\n\r\n## Quick Reference\r\n\r\n| Standard P5.js | Viji-P5 |\r\n|---|---|\r\n| `function setup() { ... }` | `function setup(viji, p5) { ... }` |\r\n| `function draw() { ... }` | `function render(viji, p5) { ... }` |\r\n| `createCanvas(800, 600)` | Remove — canvas is provided |\r\n| `background(0)` | `p5.background(0)` |\r\n| `ellipse(x, y, d)` | `p5.ellipse(x, y, d)` |\r\n| `mouseX`, `mouseY` | `viji.mouse.x`, `viji.mouse.y` |\r\n| `keyIsPressed` | `viji.keyboard.isPressed('a')` |\r\n| `width`, `height` | `viji.width`, `viji.height` |\r\n| `frameRate(30)` | Remove host controls frame rate |\r\n| `preload()` | Remove — use `viji.image()` or `fetch()` in `setup()` |\r\n| `save()` / `saveCanvas()` | Remove host-side `captureFrame()` |\r\n| `loadImage('url')` | `viji.image(null, { label: 'Image' })` |\r\n\r\n## Step by Step\r\n\r\n### 1. Add the renderer directive\r\n\r\nAdd `// @renderer p5` as the very first line:\r\n\r\n```javascript\r\n// @renderer p5\r\n```\r\n\r\n> [!IMPORTANT]\r\n> Without `// @renderer p5`, the scene defaults to the native renderer and the `p5` parameter will be `undefined`.\r\n\r\n### 2. Rename `draw()` to `render(viji, p5)`\r\n\r\nStandard P5:\r\n```javascript\r\nfunction draw() {\r\n background(0);\r\n ellipse(width / 2, height / 2, 100);\r\n}\r\n```\r\n\r\nViji-P5:\r\n```javascript\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n p5.ellipse(viji.width / 2, viji.height / 2, 100);\r\n}\r\n```\r\n\r\nBoth `viji` and `p5` are required parameters. `viji` gives access to the Viji API; `p5` is the P5.js instance.\r\n\r\n### 3. Add the `p5.` prefix to all P5 functions\r\n\r\n> [!WARNING]\r\n> Viji uses P5 in **instance mode**. Every P5 function and constant needs the `p5.` prefix. This is the most common source of errors during conversion.\r\n\r\n```javascript\r\n// Standard P5.js (global mode)\r\ncolorMode(HSB);\r\nfill(255, 80, 100);\r\nrect(10, 10, 50, 50);\r\nlet v = createVector(1, 0);\r\n\r\n// Viji-P5 (instance mode)\r\np5.colorMode(p5.HSB);\r\np5.fill(255, 80, 100);\r\np5.rect(10, 10, 50, 50);\r\nlet v = p5.createVector(1, 0);\r\n```\r\n\r\nThis applies to constants too: `PI` `p5.PI`, `TWO_PI` `p5.TWO_PI`, `HALF_PI` `p5.HALF_PI`, `HSB` → `p5.HSB`, `WEBGL` `p5.WEBGL`.\r\n\r\n### 4. Remove `createCanvas()`\r\n\r\nViji creates and manages the canvas for you. Remove any `createCanvas()` call:\r\n\r\n```javascript\r\n// Standard P5.js\r\nfunction setup() {\r\n createCanvas(800, 600);\r\n}\r\n\r\n// Viji-P5 — no createCanvas() needed\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB);\r\n}\r\n```\r\n\r\nFor resolution-agnostic sizing, use `viji.width` and `viji.height` instead of hardcoded values.\r\n\r\n### 5. Replace P5 input globals with Viji APIs\r\n\r\nP5's built-in input variables (`mouseX`, `mouseY`, `keyIsPressed`, etc.) are not available in the worker environment. Use the Viji API instead:\r\n\r\n```javascript\r\n// Standard P5.js\r\nfunction draw() {\r\n if (mouseIsPressed) {\r\n ellipse(mouseX, mouseY, 50);\r\n }\r\n if (keyIsPressed && key === 'r') {\r\n background(255, 0, 0);\r\n }\r\n}\r\n\r\n// Viji-P5\r\nfunction render(viji, p5) {\r\n if (viji.mouse.isPressed) {\r\n p5.ellipse(viji.mouse.x, viji.mouse.y, 50);\r\n }\r\n if (viji.keyboard.isPressed('r')) {\r\n p5.background(255, 0, 0);\r\n }\r\n}\r\n```\r\n\r\n### 6. Remove event callbacks\r\n\r\nP5 event callbacks (`mousePressed()`, `mouseDragged()`, `keyPressed()`, etc.) do not work in the worker environment. Check state in `render()` instead:\r\n\r\n```javascript\r\n// Standard P5.js\r\nfunction mousePressed() {\r\n particles.push(new Particle(mouseX, mouseY));\r\n}\r\n\r\n// Viji-P5 track state manually\r\nlet wasPressed = false;\r\n\r\nfunction render(viji, p5) {\r\n if (viji.mouse.leftButton && !wasPressed) {\r\n particles.push(new Particle(viji.mouse.x, viji.mouse.y));\r\n }\r\n wasPressed = viji.mouse.leftButton;\r\n}\r\n```\r\n\r\n### 7. Replace `preload()` and `loadImage()`\r\n\r\nThere is no `preload()` phase in Viji. For images, use Viji's image parameter or `fetch()` in `setup()`:\r\n\r\n```javascript\r\n// Standard P5.js\r\nlet img;\r\nfunction preload() {\r\n img = loadImage('photo.jpg');\r\n}\r\nfunction draw() {\r\n image(img, 0, 0);\r\n}\r\n\r\n// Viji-P5 use image parameter\r\nconst photo = viji.image(null, { label: 'Photo' });\r\n\r\nfunction render(viji, p5) {\r\n if (photo.value) {\r\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\r\n }\r\n}\r\n```\r\n\r\n> [!NOTE]\r\n> Use `photo.p5` (not `photo.value`) when passing images to P5 drawing functions like `p5.image()`. The `.p5` property provides a P5-compatible wrapper around the raw image data.\r\n\r\nFor JSON or text data, use `fetch()` in an async `setup()`:\r\n\r\n```javascript\r\nlet data = null;\r\n\r\nasync function setup(viji, p5) {\r\n const response = await fetch('https://cdn.example.com/data.json');\r\n data = await response.json();\r\n}\r\n```\r\n\r\n### 8. Replace `save()` and `frameRate()`\r\n\r\nThese host-level concerns are handled outside the scene:\r\n\r\n- **Saving frames**: The host application uses `core.captureFrame()`.\r\n- **Frame rate**: The host controls it via `core.setFrameRate()`.\r\n\r\nSimply remove these calls from your scene code.\r\n\r\n## Complete Conversion Example\r\n\r\nHere is the same scene implemented both ways, followed by the live Viji version:\r\n\r\n**Standard P5.js:**\r\n\r\n```javascript\r\nfunction setup() {\r\n createCanvas(400, 400);\r\n colorMode(HSB, 360, 100, 100, 100);\r\n}\r\n\r\nfunction draw() {\r\n background(0, 0, 10);\r\n let count = 8;\r\n let radius = 120;\r\n for (let i = 0; i < count; i++) {\r\n let a = frameCount * 0.02 + (i / count) * TWO_PI;\r\n let x = width / 2 + cos(a) * radius;\r\n let y = height / 2 + sin(a) * radius;\r\n noStroke();\r\n fill(255, 150, 0);\r\n circle(x, y, 16);\r\n }\r\n}\r\n```\r\n\r\n**Converted Viji-P5:**"
1268
+ "markdown": "# Mouse\n\n`viji.mouse` provides detailed mouse input including individual button states, movement deltas, and scroll wheel data.\n\n> [!TIP]\n> For simple position, click, and drag interactions that should work on both mouse and touch devices, use [`viji.pointer`](../pointer/) instead. The mouse API is for when you need mouse-specific features like right-click, middle-click, or scroll wheel.\n\n## API Reference\n\n### Position\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `x` | `number` | Canvas-space X position (pixels) |\n| `y` | `number` | Canvas-space Y position (pixels) |\n| `isInCanvas` | `boolean` | `true` when the cursor is over the canvas |\n\n### Buttons\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `isPressed` | `boolean` | `true` if any mouse button is currently held |\n| `leftButton` | `boolean` | Left button state |\n| `rightButton` | `boolean` | Right button state |\n| `middleButton` | `boolean` | Middle button state |\n\n### Movement\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `deltaX` | `number` | Horizontal movement this frame (pixels) | Yes `0` |\n| `deltaY` | `number` | Vertical movement this frame (pixels) | Yes `0` |\n| `wasMoved` | `boolean` | `true` if the mouse moved this frame | Yes `false` |\n\n### Scroll Wheel\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `wheelDelta` | `number` | Vertical scroll accumulated this frame | Yes `0` |\n| `wheelX` | `number` | Horizontal scroll accumulated this frame | Yes → `0` |\n| `wheelY` | `number` | Vertical scroll accumulated this frame | Yes `0` |\n\n> [!NOTE]\n> `wheelDelta` and `wheelY` report the same value. `wheelX` is for horizontal scrolling (trackpad gestures, tilt-wheel mice).\n\n### Frame Events\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `wasPressed` | `boolean` | `true` for exactly one frame when any button is first pressed | Yes `false` |\n| `wasReleased` | `boolean` | `true` for exactly one frame when any button is released | Yes `false` |\n\n## Coordinate System\n\nMouse coordinates are in **canvas-space pixels**, with `(0, 0)` at the top-left corner. Values range from `0` to [`viji.width`](/native/canvas-context) horizontally and `0` to [`viji.height`](/native/canvas-context) vertically. The right-click context menu is automatically suppressed on the canvas.\n\n## Frame Lifecycle\n\nPer-frame properties (`deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`) reset to zero/false at the start of each frame. If multiple mouse events occur within a single frame, deltas and wheel values **accumulate**, and `wasPressed`/`wasReleased` are OR'd across all events.\n\nPersistent properties (`x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`) retain their values until the next event changes them.\n\n## Basic Example"
1018
1269
  },
1019
1270
  {
1020
1271
  "type": "live-example",
1021
- "title": "Converted Sketch Orbiting Dots",
1022
- "sceneCode": "// @renderer p5\r\n\r\nconst speed = viji.slider(2, { min: 0.5, max: 8, label: 'Speed' });\r\nconst count = viji.slider(8, { min: 3, max: 20, step: 1, label: 'Count' });\r\nconst dotColor = viji.color('#ff6600', { label: 'Color' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n\r\n p5.background(10);\r\n\r\n const cx = viji.width / 2;\r\n const cy = viji.height / 2;\r\n const radius = Math.min(viji.width, viji.height) * 0.3;\r\n const dotSize = Math.min(viji.width, viji.height) * 0.04;\r\n\r\n const r = parseInt(dotColor.value.slice(1, 3), 16);\r\n const g = parseInt(dotColor.value.slice(3, 5), 16);\r\n const b = parseInt(dotColor.value.slice(5, 7), 16);\r\n\r\n for (let i = 0; i < count.value; i++) {\r\n const a = angle + (i / count.value) * p5.TWO_PI;\r\n const x = cx + p5.cos(a) * radius;\r\n const y = cy + p5.sin(a) * radius;\r\n\r\n p5.noStroke();\r\n p5.fill(r, g, b);\r\n p5.circle(x, y, dotSize);\r\n }\r\n}\r\n",
1023
- "sceneFile": "converted-sketch.scene.js"
1272
+ "title": "MouseButtons & Wheel",
1273
+ "sceneCode": "const ctx = viji.useContext('2d');\nlet hue = 200;\nlet zoom = 1;\nlet prevRight = false;\n\nfunction render(viji) {\n const w = viji.width, h = viji.height;\n const m = viji.mouse;\n const size = Math.min(w, h);\n\n if (m.rightButton && !prevRight) hue = (hue + 50) % 360;\n prevRight = m.rightButton;\n\n zoom -= m.wheelDelta * 0.001;\n zoom = Math.max(0.3, Math.min(5, zoom));\n\n ctx.fillStyle = 'rgba(10, 10, 30, 0.2)';\n ctx.fillRect(0, 0, w, h);\n\n const speed = Math.sqrt(m.deltaX ** 2 + m.deltaY ** 2);\n const radius = (size * 0.02 + speed * 1.5) * zoom;\n\n if (m.isInCanvas) {\n ctx.beginPath();\n ctx.arc(m.x, m.y, radius, 0, Math.PI * 2);\n const alpha = m.isPressed ? 0.9 : 0.4;\n ctx.fillStyle = `hsla(${hue}, 80%, 65%, ${alpha})`;\n ctx.fill();\n\n if (m.leftButton) {\n ctx.strokeStyle = `hsla(${hue}, 80%, 75%, 0.6)`;\n ctx.lineWidth = 2;\n ctx.stroke();\n }\n }\n\n ctx.fillStyle = 'rgba(255,255,255,0.5)';\n ctx.font = `${size * 0.022}px monospace`;\n ctx.textAlign = 'left';\n const y0 = h - size * 0.15;\n ctx.fillText(`pos: (${Math.round(m.x)}, ${Math.round(m.y)}) inCanvas: ${m.isInCanvas}`, size * 0.03, y0);\n ctx.fillText(`buttons: L[${m.leftButton ? '\\u25A0' : '\\u25A1'}] R[${m.rightButton ? '\\u25A0' : '\\u25A1'}] M[${m.middleButton ? '\\u25A0' : '\\u25A1'}]`, size * 0.03, y0 + size * 0.03);\n ctx.fillText(`delta: (${m.deltaX.toFixed(0)}, ${m.deltaY.toFixed(0)}) wheel: ${m.wheelDelta.toFixed(1)}`, size * 0.03, y0 + size * 0.06);\n ctx.fillText(`zoom: ${zoom.toFixed(2)} hue: ${hue}`, size * 0.03, y0 + size * 0.09);\n}\n",
1274
+ "sceneFile": "mouse-demo.scene.js",
1275
+ "capabilities": {
1276
+ "interaction": true
1277
+ }
1024
1278
  },
1025
1279
  {
1026
1280
  "type": "text",
1027
- "markdown": "Key changes made:\r\n\r\n1. Added `// @renderer p5` at the top.\r\n2. Renamed `draw()` → `render(viji, p5)`, added `setup(viji, p5)`.\r\n3. Prefixed all P5 functions with `p5.`.\r\n4. Removed `createCanvas()`.\r\n5. Replaced hardcoded `400` and `120` with `viji.width`, `viji.height`, and proportional math.\r\n6. Replaced `frameCount * 0.02` with a `deltaTime`-based accumulator for frame-rate-independent animation.\r\n7. Extracted the hardcoded color and count into Viji parameters so they become live controls.\r\n\r\n## What Doesn't Work\r\n\r\nThese P5 features are unavailable in the worker environment:\r\n\r\n| Feature | Alternative |\r\n|---|---|\r\n| `p5.dom` (sliders, buttons) | Use Viji parameters (`viji.slider()`, `viji.toggle()`, etc.) |\r\n| `p5.sound` | Use Viji audio API (`viji.audio.*`) |\r\n| `loadImage()`, `loadFont()`, `loadJSON()` | `viji.image()` parameter or `fetch()` in `setup()` |\r\n| `save()`, `saveCanvas()`, `saveFrames()` | Host-side `core.captureFrame()` |\r\n| `createCapture()`, `createVideo()` | Use Viji video API (`viji.video.*`) |\r\n| `cursor()`, `noCursor()` | Not available in workers |\r\n| `fullscreen()` | Host-side concern |\r\n| `frameRate()` | Host-side `core.setFrameRate()` |\r\n| `mousePressed()`, `keyPressed()`, etc. | Check state in `render()` via Viji APIs |\r\n\r\n## Tips\r\n\r\n- **Start with `setup()` and `render()`.** Get the basic structure right first, then fix individual function calls.\r\n- **Search and replace `p5.` prefix.** Most editors support regex replace `\\b(background|fill|stroke|rect|ellipse|circle|...)\\(` with `p5.$1(`.\r\n- **Use `viji.width` / `viji.height`** everywhere instead of hardcoded dimensions. This makes the scene resolution-agnostic.\r\n- **Convert animation timing.** Replace `frameCount`-based animation with `viji.time` or `viji.deltaTime` accumulators for frame-rate independence.\r\n- **Test incrementally.** Convert the structure first, then one feature at a time.\r\n\r\n## Related\r\n\r\n- [P5 Quick Start](/p5/quickstart) — building P5 scenes from scratch in Viji\r\n- [Drawing with P5](/p5/drawing) — P5 drawing functions in the Viji environment\r\n- [Parameters](/p5/parameters) sliders, colors, toggles, images\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1281
+ "markdown": "## Common Patterns\n\n### Right-Click Action\n\n```javascript\nlet prevRight = false;\n\nfunction render(viji) {\n const m = viji.mouse;\n if (m.rightButton && !prevRight) {\n cycleColor();\n }\n prevRight = m.rightButton;\n}\n```\n\n### Scroll Zoom\n\n```javascript\nlet zoom = 1;\n\nfunction render(viji) {\n zoom -= viji.mouse.wheelDelta * 0.001;\n zoom = Math.max(0.1, Math.min(10, zoom));\n}\n```\n\n### Movement Speed\n\n```javascript\nfunction render(viji) {\n const m = viji.mouse;\n const speed = Math.sqrt(m.deltaX ** 2 + m.deltaY ** 2);\n drawParticle(m.x, m.y, speed);\n}\n```\n\n## Related\n\n- [Pointer (Unified)](../pointer/) — recommended starting point for cross-device interactions\n- [Keyboard](../keyboard/) key state queries and modifier tracking\n- [Touch](../touch/) — multi-touch input with pressure, radius, and velocity\n- [P5 Mouse](/p5/mouse) — same API in the P5 renderer\n- [Shader Mouse Uniforms](/shader/mouse) — GLSL uniforms for mouse input"
1028
1282
  }
1029
1283
  ]
1030
1284
  },
1031
- "p5-parameters-overview": {
1032
- "id": "p5-parameters-overview",
1033
- "title": "p5-parameters-overview",
1034
- "description": "The Viji parameter system in P5 scenes — sliders, colors, toggles, and more for artist-controllable inputs.",
1285
+ "native-keyboard": {
1286
+ "id": "native-keyboard",
1287
+ "title": "Keyboard",
1288
+ "description": "Full keyboard API key state queries, modifier tracking, and frame-based press/release detection.",
1035
1289
  "content": [
1036
1290
  {
1037
1291
  "type": "text",
1038
- "markdown": "# Parameters\r\n\r\nParameters give users real-time control over your P5 scene. Define them at the top level, and Viji renders corresponding UI controls in the host application. Read `.value` inside `render()` to get the current state.\r\n\r\n## Parameter Types\r\n\r\n| Type | Function | Value | Use For |\r\n|---|---|---|---|\r\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\r\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\r\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Hex color values (`'#rrggbb'`) |\r\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\r\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\r\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\r\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\r\n\r\n## Basic Pattern\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// 1. Define at top level runs once\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\r\nconst color = viji.color('#ff6600', { label: 'Color' });\r\nconst mirror = viji.toggle(false, { label: 'Mirror' });\r\n\r\n// 2. Read .value in render() updates in real-time\r\nfunction render(viji, p5) {\r\n const r = parseInt(color.value.slice(1, 3), 16);\r\n const g = parseInt(color.value.slice(3, 5), 16);\r\n const b = parseInt(color.value.slice(5, 7), 16);\r\n p5.fill(r, g, b);\r\n // speed.value, mirror.value, etc.\r\n}\r\n```\r\n\r\n> [!WARNING]\r\n> Parameters must be declared at the **top level** of your scene, never inside `render()` or `setup()`. They are registered once during initialization. Declaring them inside `render()` would create duplicate parameters on every frame.\r\n\r\n## Image Parameters in P5\r\n\r\nWhen using [`viji.image()`](image/) with P5 drawing functions, use the `.p5` property instead of `.value`:\r\n\r\n```javascript\r\nconst photo = viji.image(null, { label: 'Photo' });\r\n\r\nfunction render(viji, p5) {\r\n if (photo.value) {\r\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\r\n }\r\n}\r\n```\r\n\r\nThe `.p5` property wraps the raw image data in a P5-compatible object. Use `.value` to check if an image is loaded, and `.p5` when passing to P5 drawing functions.\r\n\r\n## Common Config Keys\r\n\r\nAll parameter types share these optional configuration keys:\r\n\r\n| Key | Type | Default | Description |\r\n|---|---|---|---|\r\n| `label` | `string` | **(required)** | Display name shown in the parameter UI |\r\n| `description` | `string` | | Tooltip or help text |\r\n| `group` | `string` | `'general'` | Group name for organizing parameters — see [Grouping](grouping/) |\r\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on capabilities — see [Categories](categories/) |\r\n\r\n## Organization\r\n\r\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\r\n\r\n- **[Grouping](grouping/)** Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\r\n- **[Categories](categories/)** Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\r\n\r\n## Related\r\n\r\n- [Slider](slider/) the most common parameter type\r\n- [Image](image/) image parameters with the `.p5` property\r\n- [Grouping](grouping/) organizing parameters into named groups\r\n- [Categories](categories/) visibility based on capabilities\r\n- [Native Parameters](/native/parameters) same system in the native renderer\r\n- [Shader Parameters](/shader/parameters) comment-directive syntax for shaders\r\n- [Best Practices](/getting-started/best-practices) essential patterns for all renderers"
1292
+ "markdown": "# Keyboard\n\n`viji.keyboard` provides real-time keyboard state with per-key press detection, modifier tracking, and frame-based event queries.\n\n## API Reference\n\n### Methods\n\n| Method | Returns | Description |\n|--------|---------|-------------|\n| `isPressed(key)` | `boolean` | `true` if the key is currently held down |\n| `wasPressed(key)` | `boolean` | `true` for exactly one frame when the key is first pressed, then resets |\n| `wasReleased(key)` | `boolean` | `true` for exactly one frame when the key is released, then resets |\n\nAll three methods are **case-insensitive** `isPressed('a')` and `isPressed('A')` are equivalent.\n\n### Properties\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `activeKeys` | `Set<string>` | All currently held keys (lowercase) | No |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame (lowercase) | Yes cleared |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame (lowercase) | Yes cleared |\n| `lastKeyPressed` | `string` | Most recently pressed key (original case) | No |\n| `lastKeyReleased` | `string` | Most recently released key (original case) | No |\n\n### Modifier Keys\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `shift` | `boolean` | Shift key state |\n| `ctrl` | `boolean` | Ctrl key state |\n| `alt` | `boolean` | Alt key state |\n| `meta` | `boolean` | Meta/Cmd key state |\n\n## Key Names\n\nKey names follow the browser's `event.key` standard. Common values:\n\n| Key | Name to use |\n|-----|-------------|\n| Letters | `'a'`, `'b'`, `'z'` (case-insensitive) |\n| Digits | `'0'`, `'1'`, `'9'` |\n| Arrows | `'arrowup'`, `'arrowdown'`, `'arrowleft'`, `'arrowright'` |\n| Space | `' '` (a space character) |\n| Enter | `'enter'` |\n| Escape | `'escape'` |\n| Tab | `'tab'` |\n| Backspace | `'backspace'` |\n| Shift | `'shift'` |\n| Control | `'control'` |\n\n> [!NOTE]\n> The `activeKeys`, `pressedThisFrame`, and `releasedThisFrame` sets store keys in lowercase. However, `lastKeyPressed` and `lastKeyReleased` retain the original case as reported by the browser (e.g., `'A'` when Shift is held, `'ArrowUp'` for arrows).\n\n## Frame Lifecycle\n\n- `pressedThisFrame` and `releasedThisFrame` are cleared at the start of each frame.\n- Key repeats are suppressed holding a key down fires `wasPressed()` only on the first frame, not on subsequent repeat events.\n- `activeKeys` persists across frames until a `keyup` event is received.\n\n## Keyboard Event Capture\n\nKeyboard events are captured on the iframe document, not the canvas element itself. This means keys are registered even when the canvas doesn't have direct focus within the iframe. The following keys are allowed through without `preventDefault()`: Tab, F1–F5, F11, F12.\n\n## Basic Example"
1293
+ },
1294
+ {
1295
+ "type": "live-example",
1296
+ "title": "Keyboard — Movement & State",
1297
+ "sceneCode": "const ctx = viji.useContext('2d');\nlet px, py;\n\nfunction render(viji) {\n const w = viji.width, h = viji.height;\n const size = Math.min(w, h);\n const kb = viji.keyboard;\n\n if (px === undefined) { px = w / 2; py = h / 2; }\n\n const speed = size * 0.4 * viji.deltaTime * (kb.shift ? 2.5 : 1);\n if (kb.isPressed('w') || kb.isPressed('arrowup')) py -= speed;\n if (kb.isPressed('s') || kb.isPressed('arrowdown')) py += speed;\n if (kb.isPressed('a') || kb.isPressed('arrowleft')) px -= speed;\n if (kb.isPressed('d') || kb.isPressed('arrowright')) px += speed;\n px = Math.max(0, Math.min(w, px));\n py = Math.max(0, Math.min(h, py));\n\n ctx.fillStyle = 'rgba(10, 10, 30, 0.15)';\n ctx.fillRect(0, 0, w, h);\n\n const r = size * 0.03;\n ctx.beginPath();\n ctx.arc(px, py, r, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${(viji.time * 40) % 360}, 80%, 65%)`;\n ctx.fill();\n\n ctx.fillStyle = 'rgba(255,255,255,0.5)';\n ctx.font = `${size * 0.022}px monospace`;\n ctx.textAlign = 'left';\n const y0 = h - size * 0.12;\n const keys = [...kb.activeKeys];\n ctx.fillText(`active: [${keys.join(', ')}]`, size * 0.03, y0);\n ctx.fillText(`mods: ${kb.shift ? '[Shift] ' : ''}${kb.ctrl ? '[Ctrl] ' : ''}${kb.alt ? '[Alt] ' : ''}${kb.meta ? '[Meta]' : ''}${!kb.shift && !kb.ctrl && !kb.alt && !kb.meta ? 'none' : ''}`, size * 0.03, y0 + size * 0.03);\n ctx.fillText(`last pressed: \"${kb.lastKeyPressed}\" released: \"${kb.lastKeyReleased}\"`, size * 0.03, y0 + size * 0.06);\n\n ctx.fillStyle = 'rgba(255,255,255,0.3)';\n ctx.textAlign = 'center';\n ctx.fillText('WASD / Arrows to move \\u2022 Shift for speed', w / 2, size * 0.04);\n}\n",
1298
+ "sceneFile": "keyboard-demo.scene.js",
1299
+ "capabilities": {
1300
+ "interaction": true
1301
+ }
1302
+ },
1303
+ {
1304
+ "type": "text",
1305
+ "markdown": "## Common Patterns\n\n### WASD Movement\n\n```javascript\nlet x = 0, y = 0;\n\nfunction render(viji) {\n const kb = viji.keyboard;\n const speed = 200 * viji.deltaTime * (kb.shift ? 2.5 : 1);\n\n if (kb.isPressed('w') || kb.isPressed('arrowup')) y -= speed;\n if (kb.isPressed('s') || kb.isPressed('arrowdown')) y += speed;\n if (kb.isPressed('a') || kb.isPressed('arrowleft')) x -= speed;\n if (kb.isPressed('d') || kb.isPressed('arrowright')) x += speed;\n}\n```\n\n### Single-Press Toggle\n\n```javascript\nlet showGrid = false;\n\nfunction render(viji) {\n if (viji.keyboard.wasPressed('g')) {\n showGrid = !showGrid;\n }\n}\n```\n\n### Key Combination\n\n```javascript\nfunction render(viji) {\n const kb = viji.keyboard;\n if (kb.ctrl && kb.wasPressed('z')) {\n undo();\n }\n}\n```\n\n## Related\n\n- [Pointer (Unified)](../pointer/) — recommended starting point for position and click interactions\n- [Mouse](../mouse/) — mouse position, buttons, and scroll wheel\n- [Touch](../touch/) — multi-touch input with pressure and velocity\n- [P5 Keyboard](/p5/keyboard) — same API in the P5 renderer\n- [Shader Keyboard Uniforms](/shader/keyboard) — GLSL uniforms for common keys"
1039
1306
  }
1040
1307
  ]
1041
1308
  },
1042
- "p5-param-grouping": {
1043
- "id": "p5-param-grouping",
1044
- "title": "p5-param-grouping",
1045
- "description": "Organize P5 scene parameters into named groups so related controls appear together in the UI.",
1309
+ "native-touch": {
1310
+ "id": "native-touch",
1311
+ "title": "Touch",
1312
+ "description": "Multi-touch API per-finger tracking with position, pressure, radius, velocity, and lifecycle events.",
1046
1313
  "content": [
1047
1314
  {
1048
1315
  "type": "text",
1049
- "markdown": "# Grouping Parameters\r\n\r\nAs your scene grows, the parameter list can become unwieldy. Groups let you collect related parameters under a shared heading in the host UI. Parameters with the same `group` string appear together, regardless of declaration order.\r\n\r\n## Usage\r\n\r\nSet the `group` config key on any parameter:\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed', group: 'animation' });\r\nconst trail = viji.toggle(true, { label: 'Trail', group: 'animation' });\r\n\r\nconst size = viji.slider(0.05, { min: 0.01, max: 0.15, step: 0.01, label: 'Size', group: 'shape' });\r\nconst color = viji.color('#ff6600', { label: 'Color', group: 'shape' });\r\n```\r\n\r\nThe host application receives the group names and renders them as collapsible sections or visual separators.\r\n\r\n## How It Works\r\n\r\n- `group` is a **freeform string**any name works. There is no fixed list.\r\n- If omitted, parameters default to the `'general'` group.\r\n- Parameters with the same `group` value are collected together when sent to the host UI.\r\n- Group names are displayed as-is, so use readable names like `'animation'`, `'visual style'`, `'audio settings'`.\r\n- Declaration order within a group is preserved.\r\n\r\n## Live Example\r\n\r\nThis scene uses two groups \"animation\" (speed and trail) and \"shape\" (size and color):"
1316
+ "markdown": "# Touch\n\n`viji.touches` provides full multi-touch input with per-finger position, pressure, contact radius, velocity, and lifecycle tracking.\n\n> [!TIP]\n> For single-point interactions (click, drag, follow) that should work on both touch and mouse, use [`viji.pointer`](../pointer/) instead. The touch API is for when you need multi-touch gestures, pressure sensitivity, contact radius, or per-finger velocity.\n\n## API Reference\n\n### TouchAPI (`viji.touches`)\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `points` | `TouchPoint[]` | All currently active touch points |\n| `count` | `number` | Number of active touches |\n| `started` | `TouchPoint[]` | Touches that started this frame |\n| `moved` | `TouchPoint[]` | Touches that moved this frame |\n| `ended` | `TouchPoint[]` | Touches that ended this frame |\n| `primary` | `TouchPoint \\| null` | First active touch (convenience) |\n\n### TouchPoint\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `id` | `number` | Unique touch identifier (stable across frames) |\n| `x` | `number` | Canvas-space X position (pixels) |\n| `y` | `number` | Canvas-space Y position (pixels) |\n| `pressure` | `number` | Touch pressure (01, device-dependent) |\n| `force` | `number` | Same as `pressure` (alias) |\n| `radius` | `number` | Contact radius — `Math.max(radiusX, radiusY)` |\n| `radiusX` | `number` | Horizontal contact radius (pixels) |\n| `radiusY` | `number` | Vertical contact radius (pixels) |\n| `rotationAngle` | `number` | Contact area rotation (radians) |\n| `isInCanvas` | `boolean` | `true` if this touch is within the canvas bounds |\n| `deltaX` | `number` | Horizontal movement since last frame (pixels) |\n| `deltaY` | `number` | Vertical movement since last frame (pixels) |\n| `velocity` | `{ x: number, y: number }` | Movement velocity (pixels/second) |\n| `isNew` | `boolean` | `true` for exactly one frame when this touch starts, then resets |\n| `isActive` | `boolean` | `true` while the touch is ongoing |\n| `isEnding` | `boolean` | `true` for exactly one frame when this touch ends, then resets |\n\n## Coordinate System\n\nTouch coordinates are in **canvas-space pixels**, with `(0, 0)` at the top-left corneridentical to [`viji.mouse`](../mouse/) coordinates. When a touch starts on the canvas and is dragged outside, the browser continues delivering events, and `isInCanvas` correctly reports `false`.\n\n## Frame Lifecycle\n\n- `started`, `moved`, and `ended` arrays are cleared at the start of each frame.\n- `points` and `count` reflect the current state after all events in the frame.\n- A touch appears in `started` on the frame it begins (with `isNew: true`), in `ended` on the frame it lifts (with `isEnding: true`).\n- `primary` is always `points[0]` or `null` when no touches are active.\n\n## Raw Device Values\n\nViji passes through raw device values without injecting defaults. If a device reports `radiusX: 0` or `force: 0`, that is what your code sees. Pressure and radius behavior varies across devices:\n\n| Property | iOS | Android | Desktop |\n|----------|-----|---------|---------|\n| `x`, `y` | Reliable | Reliable | N/A (use mouse) |\n| `radiusX`, `radiusY` | Updates on move | Updates on move | N/A |\n| `pressure` / `force` | Brief value on start, often `0` during move (no 3D Touch on newer iPhones) | Varies by device | N/A |\n| `rotationAngle` | Supported | Supported | N/A |\n| `deltaX/Y`, `velocity` | Computed in Viji — reliable on all devices | Same | N/A |\n\n## Basic Example"
1050
1317
  },
1051
1318
  {
1052
1319
  "type": "live-example",
1053
- "title": "P5 Grouped Parameters",
1054
- "sceneCode": "// @renderer p5\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed', group: 'animation' });\r\nconst trail = viji.toggle(true, { label: 'Trail', group: 'animation' });\r\n\r\nconst size = viji.slider(0.05, { min: 0.01, max: 0.15, step: 0.01, label: 'Size', group: 'shape' });\r\nconst color = viji.color('#ff6600', { label: 'Color', group: 'shape' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n\r\n if (trail.value) {\r\n p5.background(10, 10, 30, 25);\r\n } else {\r\n p5.background(10, 10, 30);\r\n }\r\n\r\n const cx = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\r\n const cy = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\r\n const r = Math.min(viji.width, viji.height) * size.value;\r\n\r\n const cr = parseInt(color.value.slice(1, 3), 16);\r\n const cg = parseInt(color.value.slice(3, 5), 16);\r\n const cb = parseInt(color.value.slice(5, 7), 16);\r\n\r\n p5.noStroke();\r\n p5.fill(cr, cg, cb);\r\n p5.circle(cx, cy, r * 2);\r\n}\r\n",
1055
- "sceneFile": "grouping-demo.scene.js"
1320
+ "title": "Touch Multi-Point Tracker",
1321
+ "sceneCode": "const ctx = viji.useContext('2d');\nconst ripples = [];\n\nfunction render(viji) {\n const w = viji.width, h = viji.height;\n const size = Math.min(w, h);\n const touch = viji.touches;\n const dt = viji.deltaTime;\n\n for (const pt of touch.started) {\n ripples.push({ x: pt.x, y: pt.y, r: 0, alpha: 1 });\n }\n\n ctx.fillStyle = 'rgba(10, 10, 30, 0.2)';\n ctx.fillRect(0, 0, w, h);\n\n for (let i = ripples.length - 1; i >= 0; i--) {\n const rp = ripples[i];\n rp.r += size * 0.3 * dt;\n rp.alpha -= dt * 0.8;\n if (rp.alpha <= 0) { ripples.splice(i, 1); continue; }\n ctx.beginPath();\n ctx.arc(rp.x, rp.y, rp.r, 0, Math.PI * 2);\n ctx.strokeStyle = `hsla(200, 80%, 70%, ${rp.alpha})`;\n ctx.lineWidth = 2;\n ctx.stroke();\n }\n\n for (let i = 0; i < touch.count; i++) {\n const pt = touch.points[i];\n const r = size * 0.02 + pt.pressure * size * 0.04;\n\n ctx.beginPath();\n ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);\n ctx.fillStyle = `hsla(${120 + i * 60}, 80%, 65%, 0.8)`;\n ctx.fill();\n\n const speed = Math.sqrt(pt.velocity.x ** 2 + pt.velocity.y ** 2);\n if (speed > 10) {\n const len = Math.min(speed * 0.05, size * 0.08);\n const angle = Math.atan2(pt.velocity.y, pt.velocity.x);\n ctx.beginPath();\n ctx.moveTo(pt.x, pt.y);\n ctx.lineTo(pt.x + Math.cos(angle) * len, pt.y + Math.sin(angle) * len);\n ctx.strokeStyle = `hsla(${120 + i * 60}, 80%, 75%, 0.5)`;\n ctx.lineWidth = 2;\n ctx.stroke();\n }\n\n ctx.fillStyle = 'rgba(255,255,255,0.6)';\n ctx.font = `${size * 0.02}px monospace`;\n ctx.textAlign = 'center';\n ctx.fillText(`T${pt.id}`, pt.x, pt.y - r - size * 0.01);\n }\n\n ctx.fillStyle = 'rgba(255,255,255,0.4)';\n ctx.font = `${size * 0.022}px monospace`;\n ctx.textAlign = 'left';\n ctx.fillText(`touches: ${touch.count} primary: ${touch.primary ? 'T' + touch.primary.id : '-'}`, size * 0.03, h - size * 0.03);\n}\n",
1322
+ "sceneFile": "touch-demo.scene.js",
1323
+ "capabilities": {
1324
+ "interaction": true
1325
+ }
1056
1326
  },
1057
1327
  {
1058
1328
  "type": "text",
1059
- "markdown": "## Mixing Types\r\n\r\nGroups can contain any mix of parameter types — [`viji.slider()`](../slider/), [`viji.color()`](../color/), [`viji.toggle()`](../toggle/), [`viji.select()`](../select/), and more. There is no restriction on which types can share a group:\r\n\r\n```javascript\r\nconst brightness = viji.slider(1, { min: 0, max: 2, label: 'Brightness', group: 'visual' });\r\nconst tint = viji.color('#ffffff', { label: 'Tint', group: 'visual' });\r\nconst showGrid = viji.toggle(false, { label: 'Grid Overlay', group: 'visual' });\r\nconst blendMode = viji.select('normal', { options: ['normal', 'add', 'multiply'], label: 'Blend', group: 'visual' });\r\n```\r\n\r\n## Groups and Categories\r\n\r\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. You can combine them:\r\n\r\n```javascript\r\nconst volume = viji.slider(1, {\r\n label: 'Volume Scale',\r\n group: 'audio settings',\r\n category: 'audio'\r\n});\r\n```\r\n\r\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\r\n\r\n## Related\r\n\r\n- [Parameters Overview](../) — all parameter types and the basic pattern\r\n- [Categories](../categories/) — visibility based on active capabilities\r\n- [Native Grouping](/native/parameters/grouping) — same concept in the native renderer\r\n- [Shader Grouping](/shader/parameters/grouping) — same concept with `@viji-*` directives\r\n- [Slider](../slider/) — the most common parameter type"
1329
+ "markdown": "## Common Patterns\n\n### Iterate All Touches\n\n```javascript\nfunction render(viji) {\n for (const pt of viji.touches.points) {\n drawCircle(pt.x, pt.y, 10 + pt.pressure * 30);\n }\n}\n```\n\n### Detect New Touches\n\n```javascript\nfunction render(viji) {\n for (const pt of viji.touches.started) {\n spawnRipple(pt.x, pt.y);\n }\n}\n```\n\n### Two-Finger Pinch Distance\n\n```javascript\nfunction render(viji) {\n if (viji.touches.count >= 2) {\n const a = viji.touches.points[0];\n const b = viji.touches.points[1];\n const dist = Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n applyZoom(dist);\n }\n}\n```\n\n### Velocity-Based Effects\n\n```javascript\nfunction render(viji) {\n const p = viji.touches.primary;\n if (p) {\n const speed = Math.sqrt(p.velocity.x ** 2 + p.velocity.y ** 2);\n drawTrail(p.x, p.y, speed);\n }\n}\n```\n\n## Related\n\n- [Pointer (Unified)](../pointer/) — recommended starting point for single-point cross-device interactions\n- [Mouse](../mouse/) — mouse position, buttons, and scroll wheel\n- [Keyboard](../keyboard/) — key state queries and modifier tracking\n- [P5 Touch](/p5/touch) — same API in the P5 renderer\n- [Shader Touch Uniforms](/shader/touch) — GLSL uniforms for touch positions"
1060
1330
  }
1061
1331
  ]
1062
1332
  },
1063
- "p5-param-categories": {
1064
- "id": "p5-param-categories",
1065
- "title": "p5-param-categories",
1066
- "description": "Control P5 scene parameter visibility based on active capabilities like audio, video, and interaction.",
1333
+ "p5-quickstart": {
1334
+ "id": "p5-quickstart",
1335
+ "title": "p5-quickstart",
1336
+ "description": "Build your first Viji scene using the familiar P5.js creative coding API.",
1067
1337
  "content": [
1068
1338
  {
1069
1339
  "type": "text",
1070
- "markdown": "# Parameter Categories\r\n\r\nCategories let you tag parameters so they're only visible when the corresponding capability is active. An \"Audio Pulse\" [`viji.slider()`](../slider/) is useless if no audio source is connected — with `category: 'audio'`, it automatically appears when audio is available and hides when it's not.\r\n\r\n## The Four Categories\r\n\r\n| Category | Visible When | Use For |\r\n|---|---|---|\r\n| `'general'` | Always | Colors, sizes, speeds, shapes — anything that works without external input |\r\n| `'audio'` | Audio source is connected | Volume scaling, beat reactivity, frequency controls |\r\n| `'video'` | Video/camera source is connected | Video opacity, CV sensitivity, segmentation controls |\r\n| `'interaction'` | User interaction is enabled | Mouse effects, keyboard bindings, touch sensitivity |\r\n\r\n## Usage\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\r\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, label: 'Audio Pulse', category: 'audio' });\r\nconst showMouse = viji.toggle(true, { label: 'Mouse Dot', category: 'interaction' });\r\n```\r\n\r\n- `baseColor` ([`viji.color()`](../color/)) is always visible.\r\n- `pulseAmount` ([`viji.slider()`](../slider/)) only appears when audio is connected.\r\n- `showMouse` ([`viji.toggle()`](../toggle/)) only appears when interaction is enabled.\r\n\r\nIf you omit `category`, it defaults to `'general'` (always visible).\r\n\r\n## How It Works\r\n\r\n1. The artist sets `category` on each parameter during scene initialization.\r\n2. When the host application requests parameters, Viji filters them based on the current `CoreCapabilities`:\r\n - `hasAudio` — is an audio stream connected?\r\n - `hasVideo` — is a video/camera stream connected?\r\n - `hasInteraction` — is user interaction enabled?\r\n - `hasGeneral` — always `true`.\r\n3. Only parameters matching active capabilities are sent to the UI.\r\n\r\n> [!NOTE]\r\n> Categories filter at both the **group level** and the **individual parameter level**. If a group's category doesn't match, the entire group is hidden. If individual parameters within a visible group have non-matching categories, those parameters are hidden while the group remains visible.\r\n\r\n## Live Example\r\n\r\nParameters in three categories — `general` (always visible), `audio` (needs audio), and `interaction` (needs mouse):"
1340
+ "markdown": "# P5.js Quick Start\r\n\r\nThe P5.js renderer gives you the familiar Processing/P5.js drawing API. Viji loads P5.js automatically no installation needed.\r\n\r\n> [!IMPORTANT]\r\n> P5 and shader scenes must declare their renderer type as the first comment:\r\n> ```\r\n> // @renderer p5\r\n> ```\r\n> Without this directive, the scene defaults to the native renderer.\r\n\r\n## Your First Scene"
1071
1341
  },
1072
1342
  {
1073
1343
  "type": "live-example",
1074
- "title": "P5 Parameter Categories",
1075
- "sceneCode": "// @renderer p5\r\n\r\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\r\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\r\nconst showMouse = viji.toggle(true, { label: 'Mouse Dot', category: 'interaction' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji, p5) {\r\n p5.background(10, 10, 30, 40);\r\n\r\n angle += viji.deltaTime;\r\n\r\n const r = parseInt(baseColor.value.slice(1, 3), 16);\r\n const g = parseInt(baseColor.value.slice(3, 5), 16);\r\n const b = parseInt(baseColor.value.slice(5, 7), 16);\r\n\r\n let pulse = 0;\r\n if (viji.audio.isConnected) {\r\n pulse = viji.audio.volume.current * pulseAmount.value;\r\n }\r\n\r\n const baseR = Math.min(viji.width, viji.height) * (0.1 + pulse * 0.15);\r\n const cx = viji.width / 2 + p5.cos(angle) * viji.width * 0.2;\r\n const cy = viji.height / 2 + p5.sin(angle * 0.7) * viji.height * 0.2;\r\n\r\n p5.noStroke();\r\n p5.fill(r, g, b);\r\n p5.circle(cx, cy, baseR * 2);\r\n\r\n if (showMouse.value && viji.mouse.isInCanvas) {\r\n p5.fill(255, 255, 255, 200);\r\n p5.circle(viji.mouse.x, viji.mouse.y, Math.min(viji.width, viji.height) * 0.04);\r\n }\r\n}\r\n",
1076
- "sceneFile": "categories-demo.scene.js"
1344
+ "title": "P5 Rainbow Trail",
1345
+ "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",
1346
+ "sceneFile": "quickstart-p5.scene.js"
1077
1347
  },
1078
1348
  {
1079
1349
  "type": "text",
1080
- "markdown": "## Design Guidelines\r\n\r\n- **Default to `'general'`** unless the parameter genuinely depends on an external input.\r\n- **Use `'audio'` for parameters that only make sense with sound** beat sensitivity, frequency scaling, audio decay.\r\n- **Use `'video'` for parameters tied to camera/video** segmentation threshold, face tracking sensitivity.\r\n- **Use `'interaction'` for parameters that need user input** — mouse effect radius, keyboard shortcut configuration.\r\n- **Don't use categories as a replacement for grouping.** Categories control *visibility*; groups control *layout*. Use both when appropriate.\r\n\r\n## Categories and Groups\r\n\r\n`category` and `group` are orthogonal. A parameter in group `'effects'` with category `'audio'` will appear under the \"effects\" group heading, but only when audio is connected:\r\n\r\n```javascript\r\nconst bassReact = viji.slider(0.5, {\r\n label: 'Bass Reactivity',\r\n group: 'effects',\r\n category: 'audio'\r\n});\r\n\r\nconst colorShift = viji.slider(0.2, {\r\n label: 'Color Shift',\r\n group: 'effects',\r\n category: 'general'\r\n});\r\n```\r\n\r\nBoth parameters appear in the \"effects\" group, but `bassReact` only shows when audio is active.\r\n\r\n## Related\r\n\r\n- [Parameters Overview](../) — all parameter types and the basic pattern\r\n- [Grouping](../grouping/) — organizing parameters into named groups\r\n- [Native Categories](/native/parameters/categories) — same concept in the native renderer\r\n- [Shader Categories](/shader/parameters/categories) — same concept with `@viji-*` directives\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1350
+ "markdown": "### What's Happening\r\n\r\n**Top level — runs once:**\r\n\r\n- `// @renderer p5` tells Viji to use the P5 renderer.\r\n- `viji.slider()` creates UI parameters declared at the top level, read via `.value` in `render()`.\r\n\r\n**`setup(viji, p5)` — optional, runs once:**\r\n\r\n- Use for one-time configuration like `p5.colorMode()`. If you don't need it, omit it entirely.\r\n\r\n**`render(viji, p5)` — called every frame:**\r\n\r\n- `p5` is a full P5.js instance in **instance mode** all P5 functions require the `p5.` prefix.\r\n- `viji.width` and `viji.height` give the canvas size — use them for resolution-agnostic positioning.\r\n- `viji.time` is elapsed seconds use it for animation.\r\n\r\n> [!NOTE]\r\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.\r\n\r\n## Scene Structure\r\n\r\n```javascript\r\n// @renderer p5\r\n\r\n// 1. Top level — parameters and state\r\nconst size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\r\n\r\n// 2. setup() — optional one-time config\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB);\r\n}\r\n\r\n// 3. render() — called every frame\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n p5.circle(viji.width / 2, viji.height / 2, size.value);\r\n}\r\n```\r\n\r\n- **`render(viji, p5)` is required.** It replaces P5's `draw()`.\r\n- **`setup(viji, p5)` is optional.** Use it for one-time configuration.\r\n- **No `createCanvas()`.** The canvas is created and managed by Viji.\r\n- **No `preload()`.** Use `viji.image()` parameters or `fetch()` in `setup()`.\r\n\r\n## Instance Mode\r\n\r\n> [!WARNING]\r\n> Viji uses P5 in **instance mode**. All P5 functions require the `p5.` prefix:\r\n> ```javascript\r\n> // Correct\r\n> p5.background(0);\r\n> p5.circle(viji.width / 2, viji.height / 2, 100);\r\n>\r\n> // Wrong — will throw ReferenceError\r\n> background(0);\r\n> circle(width / 2, height / 2, 100);\r\n> ```\r\n\r\n## Input and Interaction\r\n\r\nP5's built-in input globals (`mouseX`, `mouseY`, `keyIsPressed`, etc.) are not updated in the worker environment. Use the Viji API instead. For most interactions, [`viji.pointer`](/p5/pointer) works across both mouse and touch:\r\n\r\n```javascript\r\nfunction render(viji, p5) {\r\n if (viji.pointer.isDown) {\r\n p5.circle(viji.pointer.x, viji.pointer.y, 20);\r\n }\r\n}\r\n```\r\n\r\nFor mouse-specific features (right-click, wheel) use [`viji.mouse`](/p5/mouse). For multi-touch use [`viji.touches`](/p5/touch). For keyboard use [`viji.keyboard`](/p5/keyboard).\r\n\r\n## Essential Patterns\r\n\r\n> [!NOTE]\r\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\r\n\r\n> [!WARNING]\r\n> Scenes run in a Web Worker — there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\r\n\r\n> [!TIP]\r\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them.\r\n\r\n## Converting Existing Sketches\r\n\r\nIf you have existing P5.js sketches, see [Converting P5 Sketches](/p5/converting-sketches) for a step-by-step migration guide. Key differences: `draw()` `render()`, instance mode, no `createCanvas()`, Viji APIs for input.\r\n\r\n## Next Steps\r\n\r\n- [Scene Structure](/p5/scene-structure) — `setup()`, `render()`, and lifecycle details\r\n- [Drawing with P5](/p5/drawing) — P5 drawing functions in Viji\r\n- [Converting P5 Sketches](/p5/converting-sketches) — migrate existing sketches\r\n- [Parameters](/p5/parameters) — sliders, colors, toggles, and more\r\n- [Audio](/p5/audio) — react to music and sound\r\n- [API Reference](/p5/api-reference) — full list of everything available\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1081
1351
  }
1082
1352
  ]
1083
1353
  },
1084
- "shader-quickstart": {
1085
- "id": "shader-quickstart",
1086
- "title": "shader-quickstart",
1087
- "description": "Build your first Viji scene with GLSL fragment shaders running directly on the GPU.",
1354
+ "p5-scene-structure": {
1355
+ "id": "p5-scene-structure",
1356
+ "title": "Scene Structure",
1357
+ "description": "The setup/render lifecycle, instance mode, and how P5 scenes are organized in Viji.",
1088
1358
  "content": [
1089
1359
  {
1090
1360
  "type": "text",
1091
- "markdown": "# Shader Quick Start\r\n\r\nThe shader renderer lets you write GLSL fragment shaders that run on the GPU. Viji handles the fullscreen quad, uniform injection, and parameter system you write only the shader logic.\r\n\r\n> [!IMPORTANT]\r\n> P5 and shader scenes must declare their renderer type as the first comment:\r\n> ```\r\n> // @renderer shader\r\n> ```\r\n> Without this directive, the scene defaults to the native renderer.\r\n\r\n## Your First Shader"
1361
+ "markdown": "# Scene Structure\n\nA P5 scene in Viji follows a specific lifecycle. This page covers the `@renderer p5` directive, the `setup()` and `render()` functions, instance mode, and how P5 scenes differ from standard sketches.\n\n## The `@renderer` Directive\n\n> [!IMPORTANT]\n> P5 and shader scenes must declare their renderer type as the first comment:\n> ```\n> // @renderer p5\n> ```\n> or\n> ```\n> // @renderer shader\n> ```\n> Without this directive, the scene defaults to the native renderer.\n\n## Scene Lifecycle\n\nA P5 scene has three parts: top-level code, an optional `setup()`, and a required `render()`:\n\n```javascript\n// @renderer p5\n\n// 1. Top level — runs once: parameters, constants, state\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\n\n// 2. setup(viji, p5) — optional, runs once after P5 initializes\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB);\n}\n\n// 3. render(viji, p5) — called every frame\nfunction render(viji, p5) {\n p5.background(0);\n angle += speed.value * viji.deltaTime;\n p5.circle(viji.width / 2, viji.height / 2, 100);\n}\n```\n\n### Top Level\n\nTop-level code runs once when the scene is first loaded. Use it for:\n\n- **Parameter declarations** — `viji.slider()`, `viji.color()`, `viji.toggle()`, etc.\n- **Constants** — precomputed values, lookup tables\n- **Mutable state** — variables that accumulate across frames\n- **Dynamic imports** — top-level `await` is supported (e.g., `const lib = await import('https://esm.sh/...')`)\n\n### `setup(viji, p5)` — Optional\n\nRuns once after P5 has initialized. Use it for one-time P5 configuration:\n\n```javascript\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n p5.textFont('monospace');\n p5.noStroke();\n}\n```\n\nIf you don't need any P5 configuration, omit `setup()` entirely. Unlike standard P5, **there is no `createCanvas()` call** — the canvas is already created and sized by Viji.\n\n### `render(viji, p5)` — Required\n\nCalled every frame. This replaces P5's `draw()` function. Both arguments are always provided:\n\n| Argument | Type | Description |\n|----------|------|-------------|\n| `viji` | `VijiAPI` | Full Viji API — timing, audio, video, parameters, input |\n| `p5` | P5 instance | Full P5.js API in instance mode |"
1092
1362
  },
1093
1363
  {
1094
1364
  "type": "live-example",
1095
- "title": "ShaderWave Pattern",
1096
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-slider:scale label:\"Scale\" default:8.0 min:1.0 max:20.0 step:0.5\r\n// @viji-color:tint label:\"Tint\" default:#00ffcc\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n float pattern = sin(uv.x * scale + phase)\r\n * cos(uv.y * scale - phase * 0.7)\r\n * 0.5 + 0.5;\r\n\r\n vec3 color = tint * pattern;\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
1097
- "sceneFile": "quickstart-shader.scene.glsl"
1365
+ "title": "P5 Lifecycle Expanding Rings",
1366
+ "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",
1367
+ "sceneFile": "scene-structure-lifecycle.scene.js"
1098
1368
  },
1099
1369
  {
1100
1370
  "type": "text",
1101
- "markdown": "### What's Happening\r\n\r\n**Comment directives — parsed before compilation:**\r\n\r\n- `// @renderer shader` tells Viji to use the shader renderer.\r\n- `// @viji-slider:speed ...` declares a parameter. Viji generates a `uniform float speed;` automatically.\r\n- `// @viji-color:tint ...` declares a color parameter. Viji generates a `uniform vec3 tint;`.\r\n- `// @viji-accumulator:phase rate:speed` creates a CPU-side accumulator that adds `speed × deltaTime` every frame. The result is a `uniform float phase;` that increases smoothly — no jumps when the slider changes.\r\n\r\n**`void main()` runs for every pixel, every frame:**\r\n\r\n- `gl_FragCoord.xy / u_resolution` gives normalized UV coordinates (0–1).\r\n- `phase` is the accumulator — use it instead of `u_time * speed` for smooth, slider-driven animation.\r\n- `speed`, `scale`, `tint` are your parameter uniforms updated live as the user adjusts controls.\r\n- `gl_FragColor` sets the output color for each pixel.\r\n\r\n> [!NOTE]\r\n> Parameter declarations use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\r\n\r\n## Scene Structure\r\n\r\nA shader scene is a GLSL fragment shader with comment directives:\r\n\r\n```glsl\r\n// @renderer shader\r\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n gl_FragColor = vec4(uv * brightness, 0.5, 1.0);\r\n}\r\n```\r\n\r\n- **No `precision` or `uniform` declarations needed.** Viji auto-injects `precision mediump float;` and all uniform declarations.\r\n- **No vertex shader.** Viji renders a fullscreen quad; your fragment shader defines the color of every pixel.\r\n- **Parameters become uniforms.** `// @viji-slider:name` becomes `uniform float name;` automatically.\r\n\r\n> [!NOTE]\r\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or built-in uniforms — they will conflict. If you use `#version 300 es`, Viji will handle its placement automatically.\r\n\r\n## Parameter Types\r\n\r\nDeclare parameters with `// @viji-TYPE:uniformName key:value` syntax:\r\n\r\n| Type | Uniform | Example |\r\n|------|---------|---------|\r\n| `slider` | `float` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\r\n| `number` | `float` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\r\n| `color` | `vec3` | `// @viji-color:tint label:\"Tint\" default:#ff6600` |\r\n| `toggle` | `bool` | `// @viji-toggle:invert label:\"Invert\" default:false` |\r\n| `select` | `int` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"Wave\",\"Spiral\",\"Grid\"]` |\r\n| `image` | `sampler2D` | `// @viji-image:tex label:\"Texture\"` |\r\n| `accumulator` | `float` | `// @viji-accumulator:phase rate:speed` |\r\n\r\nConfig keys: `label`, `default`, `min`, `max`, `step`, `description`, `group`, `category`.\r\n\r\n### Accumulators\r\n\r\nAccumulators solve the \"jumping animation\" problem. When you write `u_time * speed`, changing the `speed` slider causes a visible jump because the entire phase is recalculated instantly. Accumulators integrate the rate over time on the CPU side:\r\n\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\r\n- `phase` increases by `speed × deltaTime` each frame — changing `speed` only affects future growth, never jumps backward.\r\n- `rate` can reference any declared parameter name or be a numeric constant (e.g., `rate:1.5`).\r\n- Accumulators have no UI control — they are internal uniform values.\r\n- Optional `default` sets the starting value (defaults to 0).\r\n\r\n> [!WARNING]\r\n> Do not use the `u_` prefix for your parameter uniform names — it is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `colorMix`, `intensity` instead.\r\n\r\n## Built-in Uniforms\r\n\r\nThese are always available — no declaration needed:\r\n\r\n| Uniform | Type | Description |\r\n|---------|------|-------------|\r\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\r\n| `u_time` | `float` | Elapsed seconds since scene start |\r\n| `u_deltaTime` | `float` | Seconds since last frame |\r\n| `u_frame` | `int` | Current frame number |\r\n| `u_mouse` | `vec2` | Mouse position in pixels |\r\n| `u_mousePressed` | `bool` | Any mouse button is pressed |\r\n| `u_audioVolume` | `float` | Overall audio volume (0–1) |\r\n| `u_audioLow` | `float` | Low frequency energy (0–1) |\r\n| `u_audioMid` | `float` | Mid frequency energy (0–1) |\r\n| `u_audioHigh` | `float` | High frequency energy (0–1) |\r\n| `u_audioKick` | `float` | Kick beat detection (0–1) |\r\n| `u_video` | `sampler2D` | Current video frame |\r\n\r\nSee [API Reference](/shader/api-reference) for the complete list of 100+ built-in uniforms.\r\n\r\n## Essential Patterns\r\n\r\n**Normalized coordinates:**\r\n\r\n```glsl\r\nvec2 uv = gl_FragCoord.xy / u_resolution; // 0..1\r\nvec2 centered = uv - 0.5; // -0.5..0.5\r\ncentered.x *= u_resolution.x / u_resolution.y; // aspect-corrected\r\n```\r\n\r\n**Distance fields:**\r\n\r\n```glsl\r\nfloat d = length(centered); // distance from center\r\nfloat circle = smoothstep(0.3, 0.29, d); // anti-aliased circle\r\n```\r\n\r\n> [!NOTE]\r\n> Always use `u_resolution` for positioning and sizing and `u_time` / `u_deltaTime` for animation. This keeps your shader resolution-agnostic and frame-rate-independent.\r\n\r\n## GLSL Version\r\n\r\nBy default, shaders use **GLSL ES 1.00** (WebGL 1). If you need WebGL 2 features, add `#version 300 es` as the first line:\r\n\r\n```glsl\r\n#version 300 es\r\n// @renderer shader\r\n\r\n// ES 3.00 requires explicit output declaration\r\nout vec4 fragColor;\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n fragColor = vec4(uv, sin(u_time), 1.0);\r\n}\r\n```\r\n\r\nES 3.00 differences: `gl_FragColor` `out vec4`, `texture2D()` `texture()`. Use ES 1.00 for maximum compatibility.\r\n\r\n## Backbuffer (Previous Frame)\r\n\r\nViji gives you access to the previous frame as a texture — just reference `backbuffer` in your code and it's automatically enabled:\r\n\r\n```glsl\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec4 prev = texture2D(backbuffer, uv); // previous frame\r\n vec3 current = vec3(/* ... your effect ... */);\r\n gl_FragColor = vec4(mix(prev.rgb, current, 0.1), 1.0); // 90% trail\r\n}\r\n```\r\n\r\nThis enables feedback effects, trails, motion blur, and accumulation buffers. No setup needed Viji detects the `backbuffer` reference and creates the ping-pong framebuffers automatically.\r\n\r\nSee [Backbuffer](/shader/backbuffer) for detailed patterns and techniques.\r\n\r\n## Shadertoy Compatibility\r\n\r\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy) for a mapping of Shadertoy uniforms to Viji equivalents.\r\n\r\n## Next Steps\r\n\r\n- [Shader Basics](/shader/basics) — uniforms, coordinate systems, techniques\r\n- [Parameters](/shader/parameters) all parameter types for shaders\r\n- [Audio Uniforms](/shader/audio) — react to music in GLSL\r\n- [Backbuffer](/shader/backbuffer) — feedback effects using the previous frame\r\n- [API Reference](/shader/api-reference) — complete list of built-in uniforms\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1371
+ "markdown": "## 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> // Wrongwill throw ReferenceError\n> background(0);\n> circle(width / 2, height / 2, 100);\n> ```\n\nInstance mode means there are no global P5 functions. Every P5 API call `background()`, `circle()`, `fill()`, `noise()`, `map()`, `constrain()`, `random()`, and all others must use the `p5.` prefix.\n\nConstants are also namespaced: use `p5.PI`, `p5.TWO_PI`, `p5.HSB`, `p5.CENTER`, etc.\n\n## What's Different from Standard P5.js\n\n| Standard P5.js | Viji P5 | Reason |\n|----------------|---------|--------|\n| `function draw() { ... }` | `function render(viji, p5) { ... }` | Viji controls the render loop |\n| `createCanvas(800, 600)` | Not needed | Canvas is managed by Viji |\n| `resizeCanvas(w, h)` | Not needed | Resizing is automatic |\n| `preload()` | Not available | Use `viji.image()` parameters or `fetch()` in `setup()` |\n| `mouseX`, `mouseY` | [`viji.pointer.x`](/p5/pointer), [`viji.pointer.y`](/p5/pointer) (or [`viji.mouse.x`](/p5/mouse), [`viji.mouse.y`](/p5/mouse)) | P5 input globals don't update in workers |\n| `frameRate(30)` | Not available | Viji controls the frame rate |\n| `keyPressed()`, `mouseClicked()` | Check state in `render()` | No event callbacks in worker environment |\n| Global mode (`background(0)`) | Instance mode (`p5.background(0)`) | Worker environment requires explicit namespacing |\n\n## Environment Constraints\n\n> [!WARNING]\n> Scenes run in a Web Worker there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\n\n## Next Steps\n\n- [Canvas & Resolution](/p5/canvas-resolution)[`viji.width`](/p5/canvas-resolution), [`viji.height`](/p5/canvas-resolution), responsive layouts\n- [Timing](/p5/timing) — [`viji.time`](/p5/timing), [`viji.deltaTime`](/p5/timing), frame counting\n- [Drawing with P5](/p5/drawing) — P5 drawing functions in Viji\n- [Parameters](/p5/parameters) — sliders, colors, toggles\n- [Converting P5 Sketches](/p5/converting-sketches) — migrate existing sketches\n- [API Reference](/p5/api-reference) — full list of everything available"
1102
1372
  }
1103
1373
  ]
1104
1374
  },
1105
- "shader-parameters-overview": {
1106
- "id": "shader-parameters-overview",
1107
- "title": "shader-parameters-overview",
1108
- "description": "Shader parameter system declare controls with comment directives that become GLSL uniforms automatically.",
1375
+ "p5-canvas-resolution": {
1376
+ "id": "p5-canvas-resolution",
1377
+ "title": "Canvas & Resolution",
1378
+ "description": "How P5 manages the canvas, and using viji.width and viji.height for resolution-agnostic drawing.",
1379
+ "content": [
1380
+ {
1381
+ "type": "text",
1382
+ "markdown": "# Canvas & Resolution\n\nIn the P5 renderer, the canvas and its rendering context are managed for you. You draw with P5 functions — no need to call `viji.useContext()`. This page covers how resolution works, what `viji.width` and `viji.height` mean, and how to build layouts that adapt to any canvas size.\n\n## Canvas Management\n\nViji creates the canvas and passes it to P5 automatically. Key differences from standard P5.js:\n\n- **No `createCanvas()`.** The canvas already exists. Calling `p5.createCanvas()` is unnecessary and should be avoided.\n- **No `resizeCanvas()`.** When the host resizes the canvas, Viji handles the resize and updates P5 internally. Your `render()` function is always called with the correct dimensions.\n- **P5 owns the rendering context.** You don't call `viji.useContext()` — P5 creates its own 2D context on the provided canvas.\n\n## Resolution Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `p5.width` | `number` | Same value — P5's internal width |\n| `p5.height` | `number` | Same value — P5's internal height |\n| `viji.canvas` | `OffscreenCanvas` | The underlying canvas (rarely needed in P5 scenes) |\n\n`viji.width` and `p5.width` are always in sync — they reflect the same canvas. Use whichever feels natural, but `viji.width` is the canonical source across all renderers.\n\n## Resolution-Agnostic Layouts\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\nThe canvas can be any size — from a small preview to a fullscreen 4K display. Position and scale everything relative to `viji.width` and `viji.height`:\n\n```javascript\n// @renderer p5\n\nfunction render(viji, p5) {\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const r = Math.min(viji.width, viji.height) * 0.3;\n p5.circle(cx, cy, r * 2);\n}\n```"
1383
+ },
1384
+ {
1385
+ "type": "live-example",
1386
+ "title": "Responsive Grid",
1387
+ "sceneCode": "// @renderer p5\n\nconst cols = viji.slider(6, { min: 2, max: 12, step: 1, label: 'Columns' });\nconst padding = viji.slider(0.02, { min: 0, max: 0.05, label: 'Padding' });\nconst cornerRadius = viji.slider(0.3, { min: 0, max: 1, label: 'Corner Roundness' });\nconst bgColor = viji.color('#0f0f1a', { label: 'Background' });\nconst cellColor = viji.color('#3388ff', { label: 'Cell Color' });\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n p5.background(bgColor.value);\n\n const c = cols.value;\n const pad = Math.min(viji.width, viji.height) * padding.value;\n const cellW = (viji.width - pad) / c - pad;\n const rows = Math.floor((viji.height - pad) / (cellW + pad));\n const cellH = (viji.height - pad) / rows - pad;\n\n for (let row = 0; row < rows; row++) {\n for (let col = 0; col < c; col++) {\n const x = pad + col * (cellW + pad);\n const y = pad + row * (cellH + pad);\n const hue = (col / c * 180 + row / rows * 180 + viji.time * 30) % 360;\n\n p5.noStroke();\n p5.fill(hue, 70, 90);\n p5.rect(x, y, cellW, cellH, Math.min(cellW, cellH) * 0.5 * cornerRadius.value);\n }\n }\n}\n",
1388
+ "sceneFile": "canvas-resolution-responsive.scene.js"
1389
+ },
1390
+ {
1391
+ "type": "text",
1392
+ "markdown": "## `viji.canvas` in P5 Scenes\n\n`viji.canvas` is the same `OffscreenCanvas` that P5 draws to. While you _can_ access it directly (e.g., to get raw pixel data), in practice you should use P5 drawing functions for all rendering. The raw canvas is useful in advanced scenarios like reading back pixels with `viji.canvas.getContext('2d').getImageData(...)`.\n\n## Comparison Across Renderers\n\n| Concept | Native | P5 | Shader |\n|---------|--------|-----|--------|\n| Canvas dimensions | `viji.width`, `viji.height` | `viji.width`, `viji.height` (= `p5.width`, `p5.height`) | `u_resolution.x`, `u_resolution.y` |\n| Context creation | `viji.useContext('2d')` | Automatic (P5 manages it) | Automatic (shader adapter manages it) |\n| Resize handling | Use current `viji.width`/`viji.height` each frame | Automatic | Automatic via `u_resolution` |\n\n## Next Steps\n\n- [Scene Structure](/p5/scene-structure) — `setup()`, `render()`, instance mode\n- [Timing](/p5/timing) — [`viji.time`](/p5/timing), [`viji.deltaTime`](/p5/timing), frame counting\n- [Native Canvas & Context](/native/canvas-context) — `viji.useContext()` and manual context management\n- [Shader Resolution](/shader/resolution) — `u_resolution` and coordinate normalization\n- [API Reference](/p5/api-reference) — full list of everything available"
1393
+ }
1394
+ ]
1395
+ },
1396
+ "p5-converting": {
1397
+ "id": "p5-converting",
1398
+ "title": "p5-converting",
1399
+ "description": "Step-by-step guide to converting standard P5.js sketches into Viji scenes.",
1109
1400
  "content": [
1110
1401
  {
1111
1402
  "type": "text",
1112
- "markdown": "# Shader Parameters\r\n\r\nParameters are the primary way to give users control over your scene. You declare them at the top of your shader, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Values update in real-time as users interact with the controls.\r\n\r\nIn the shader renderer, parameters are declared using `// @viji-*` comment directives. Each directive creates a UI control and a corresponding GLSL uniform — no manual `uniform` declarations needed. Read the uniform directly in your shader code to get the current value.\r\n\r\n## Parameter Types\r\n\r\n| Directive | Uniform Type | Value | Use For |\r\n|---|---|---|---|\r\n| [`@viji-slider`](slider/) | `float` | Continuous range | Speed, intensity, size |\r\n| [`@viji-number`](number/) | `float` | Precise numeric | Count, threshold |\r\n| [`@viji-color`](color/) | `vec3` | RGB (0–1) | Tint, palette |\r\n| [`@viji-toggle`](toggle/) | `bool` | On/off | Invert, enable effect |\r\n| [`@viji-select`](select/) | `int` | Index (0-based) | Mode, pattern selection |\r\n| [`@viji-image`](image/) | `sampler2D` | Texture | Overlay, displacement map |\r\n| [`@viji-accumulator`](accumulator/) | `float` | CPU-side integration | Smooth animation phase |\r\n\r\n## Basic Pattern\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-color:tint label:\"Tint\" default:#ff6600\r\n// @viji-toggle:invert label:\"Invert\" default:false\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec3 col = tint * uv.x * speed;\r\n if (invert) col = 1.0 - col;\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n```\r\n\r\nEach `@viji-*` directive auto-generates a `uniform` declaration with the name after the colon. `speed` becomes `uniform float speed;`, `tint` becomes `uniform vec3 tint;`, etc.\r\n\r\n> [!WARNING]\r\n> Do not use the `u_` prefix for parameter names it is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `tint`, `brightness`.\r\n\r\n> [!NOTE]\r\n> Parameter directives use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\r\n\r\n## Directive Syntax\r\n\r\n```\r\n// @viji-TYPE:uniformName key:value key:\"string value\" key:[array]\r\n```\r\n\r\n- **TYPE** one of: [`slider`](slider/), [`number`](number/), [`color`](color/), [`toggle`](toggle/), [`select`](select/), [`image`](image/), [`accumulator`](accumulator/)\r\n- **uniformName** the GLSL uniform name (no `u_` prefix)\r\n- **key:value** pairs configure the parameter\r\n\r\n## Common Config Keys\r\n\r\n| Key | Type | Description |\r\n|---|---|---|\r\n| `label` | `string` | **(required)** Display name in the UI |\r\n| `default` | varies | **(required for most types)** Initial value |\r\n| `min` | `number` | Minimum value ([slider](slider/)/[number](number/)) |\r\n| `max` | `number` | Maximum value ([slider](slider/)/[number](number/)) |\r\n| `step` | `number` | Step increment ([slider](slider/)/[number](number/)) |\r\n| `description` | `string` | Help text |\r\n| `group` | `string` | Group name for organizing parameters see [Grouping](grouping/) |\r\n| `category` | `string` | Visibility category see [Categories](categories/) |\r\n| `options` | `array` | Options list ([select](select/) only) |\r\n\r\n## Uniform Type Mapping\r\n\r\n| Parameter Type | GLSL Uniform | Notes |\r\n|---|---|---|\r\n| [`slider`](slider/) | `uniform float` | |\r\n| [`number`](number/) | `uniform float` | Same as slider |\r\n| [`color`](color/) | `uniform vec3` | RGB components, each 0–1 |\r\n| [`toggle`](toggle/) | `uniform bool` | |\r\n| [`select`](select/) | `uniform int` | 0-based index of selected option |\r\n| [`image`](image/) | `uniform sampler2D` | |\r\n| [`accumulator`](accumulator/) | `uniform float` | CPU-side `value += rate × deltaTime` each frame |\r\n\r\n> [!WARNING]\r\n> All `@viji-*` directives must appear as top-level comments before `void main()`. They are parsed once during shader compilation. Viji's shader auto-injection places the generated `uniform` declarations before your code.\r\n\r\n## Organization\r\n\r\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\r\n\r\n- **[Grouping](grouping/)** Use the `group:` key to collect related parameters under a shared heading (e.g., `group:animation`). Parameters with the same `group` value appear together in the UI.\r\n- **[Categories](categories/)** Use the `category:` key to tag parameters as `general`, `audio`, `video`, or `interaction` to automatically show/hide them based on what inputs are currently active.\r\n\r\n## Related\r\n\r\n- [Slider](slider/) the most common parameter type\r\n- [Accumulator](accumulator/) smooth animation without `u_time * speed` jumps\r\n- [Grouping](grouping/) organizing parameters into named groups\r\n- [Categories](categories/) visibility based on capabilities\r\n- [Native Parameters](/native/parameters) equivalent JavaScript API\r\n- [P5 Parameters](/p5/parameters) same system in the P5 renderer\r\n- [Best Practices](/getting-started/best-practices) essential patterns for all renderers"
1403
+ "markdown": "# Converting P5 Sketches\r\n\r\nThis guide shows how to take any standard P5.js sketch and convert it into a Viji scene. The changes are mechanical once you learn the pattern, converting takes a few minutes.\r\n\r\n> [!TIP]\r\n> Want an AI to do it for you? See [Convert: P5 Sketches](/ai-prompts/convert-p5) for a ready-to-paste prompt that applies all the rules below automatically.\r\n\r\n## Quick Reference\r\n\r\n| Standard P5.js | Viji-P5 |\r\n|---|---|\r\n| `function setup() { ... }` | `function setup(viji, p5) { ... }` |\r\n| `function draw() { ... }` | `function render(viji, p5) { ... }` |\r\n| `createCanvas(800, 600)` | Remove canvas is provided |\r\n| `background(0)` | `p5.background(0)` |\r\n| `ellipse(x, y, d)` | `p5.ellipse(x, y, d)` |\r\n| `mouseX`, `mouseY` | [`viji.pointer.x`](/p5/pointer), [`viji.pointer.y`](/p5/pointer) (or [`viji.mouse.x`](/p5/mouse), [`viji.mouse.y`](/p5/mouse)) |\r\n| `keyIsPressed` | [`viji.keyboard.isPressed('a')`](/p5/keyboard) |\r\n| `width`, `height` | `viji.width`, `viji.height` |\r\n| `frameRate(30)` | Remove host controls frame rate |\r\n| `preload()` | Remove — use `viji.image()` or `fetch()` in `setup()` |\r\n| `save()` / `saveCanvas()` | Remove — host-side `captureFrame()` |\r\n| `loadImage('url')` | `viji.image(null, { label: 'Image' })` |\r\n\r\n## Step by Step\r\n\r\n### 1. Add the renderer directive\r\n\r\nAdd `// @renderer p5` as the very first line:\r\n\r\n```javascript\r\n// @renderer p5\r\n```\r\n\r\n> [!IMPORTANT]\r\n> Without `// @renderer p5`, the scene defaults to the native renderer and the `p5` parameter will be `undefined`.\r\n\r\n### 2. Rename `draw()` to `render(viji, p5)`\r\n\r\nStandard P5:\r\n```javascript\r\nfunction draw() {\r\n background(0);\r\n ellipse(width / 2, height / 2, 100);\r\n}\r\n```\r\n\r\nViji-P5:\r\n```javascript\r\nfunction render(viji, p5) {\r\n p5.background(0);\r\n p5.ellipse(viji.width / 2, viji.height / 2, 100);\r\n}\r\n```\r\n\r\nBoth `viji` and `p5` are required parameters. `viji` gives access to the Viji API; `p5` is the P5.js instance.\r\n\r\n### 3. Add the `p5.` prefix to all P5 functions\r\n\r\n> [!WARNING]\r\n> Viji uses P5 in **instance mode**. Every P5 function and constant needs the `p5.` prefix. This is the most common source of errors during conversion.\r\n\r\n```javascript\r\n// Standard P5.js (global mode)\r\ncolorMode(HSB);\r\nfill(255, 80, 100);\r\nrect(10, 10, 50, 50);\r\nlet v = createVector(1, 0);\r\n\r\n// Viji-P5 (instance mode)\r\np5.colorMode(p5.HSB);\r\np5.fill(255, 80, 100);\r\np5.rect(10, 10, 50, 50);\r\nlet v = p5.createVector(1, 0);\r\n```\r\n\r\nThis applies to constants too: `PI` `p5.PI`, `TWO_PI` `p5.TWO_PI`, `HALF_PI` `p5.HALF_PI`, `HSB` `p5.HSB`, `WEBGL` `p5.WEBGL`.\r\n\r\n### 4. Remove `createCanvas()`\r\n\r\nViji creates and manages the canvas for you. Remove any `createCanvas()` call:\r\n\r\n```javascript\r\n// Standard P5.js\r\nfunction setup() {\r\n createCanvas(800, 600);\r\n}\r\n\r\n// Viji-P5 no createCanvas() needed\r\nfunction setup(viji, p5) {\r\n p5.colorMode(p5.HSB);\r\n}\r\n```\r\n\r\nFor resolution-agnostic sizing, use `viji.width` and `viji.height` instead of hardcoded values.\r\n\r\n### 5. Replace P5 input globals with Viji APIs\r\n\r\nP5's built-in input variables (`mouseX`, `mouseY`, `keyIsPressed`, etc.) are not available in the worker environment. Use the Viji API instead. For most position/click interactions, [`viji.pointer`](/p5/pointer) works across both mouse and touch:\r\n\r\n```javascript\r\n// Standard P5.js\r\nfunction draw() {\r\n if (mouseIsPressed) {\r\n ellipse(mouseX, mouseY, 50);\r\n }\r\n if (keyIsPressed && key === 'r') {\r\n background(255, 0, 0);\r\n }\r\n}\r\n\r\n// Viji-P5\r\nfunction render(viji, p5) {\r\n if (viji.pointer.isDown) {\r\n p5.ellipse(viji.pointer.x, viji.pointer.y, 50);\r\n }\r\n if (viji.keyboard.isPressed('r')) {\r\n p5.background(255, 0, 0);\r\n }\r\n}\r\n```\r\n\r\n### 6. Remove event callbacks\r\n\r\nP5 event callbacks (`mousePressed()`, `mouseDragged()`, `keyPressed()`, etc.) do not work in the worker environment. Check state in `render()` instead:\r\n\r\n```javascript\r\n// Standard P5.js\r\nfunction mousePressed() {\r\n particles.push(new Particle(mouseX, mouseY));\r\n}\r\n\r\n// Viji-P5 track state manually\r\nlet wasPressed = false;\r\n\r\nfunction render(viji, p5) {\r\n if (viji.mouse.leftButton && !wasPressed) {\r\n particles.push(new Particle(viji.mouse.x, viji.mouse.y));\r\n }\r\n wasPressed = viji.mouse.leftButton;\r\n}\r\n```\r\n\r\n### 7. Replace `preload()` and `loadImage()`\r\n\r\nThere is no `preload()` phase in Viji. For images, use Viji's image parameter or `fetch()` in `setup()`:\r\n\r\n```javascript\r\n// Standard P5.js\r\nlet img;\r\nfunction preload() {\r\n img = loadImage('photo.jpg');\r\n}\r\nfunction draw() {\r\n image(img, 0, 0);\r\n}\r\n\r\n// Viji-P5 use image parameter\r\nconst photo = viji.image(null, { label: 'Photo' });\r\n\r\nfunction render(viji, p5) {\r\n if (photo.value) {\r\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\r\n }\r\n}\r\n```\r\n\r\n> [!NOTE]\r\n> Use `photo.p5` (not `photo.value`) when passing images to P5 drawing functions like `p5.image()`. The `.p5` property provides a P5-compatible wrapper around the raw image data.\r\n\r\nFor JSON or text data, use `fetch()` in an async `setup()`:\r\n\r\n```javascript\r\nlet data = null;\r\n\r\nasync function setup(viji, p5) {\r\n const response = await fetch('https://cdn.example.com/data.json');\r\n data = await response.json();\r\n}\r\n```\r\n\r\n### 8. Replace `save()` and `frameRate()`\r\n\r\nThese host-level concerns are handled outside the scene:\r\n\r\n- **Saving frames**: The host application uses `core.captureFrame()`.\r\n- **Frame rate**: The host controls it via `core.setFrameRate()`.\r\n\r\nSimply remove these calls from your scene code.\r\n\r\n## Complete Conversion Example\r\n\r\nHere is the same scene implemented both ways, followed by the live Viji version:\r\n\r\n**Standard P5.js:**\r\n\r\n```javascript\r\nfunction setup() {\r\n createCanvas(400, 400);\r\n colorMode(HSB, 360, 100, 100, 100);\r\n}\r\n\r\nfunction draw() {\r\n background(0, 0, 10);\r\n let count = 8;\r\n let radius = 120;\r\n for (let i = 0; i < count; i++) {\r\n let a = frameCount * 0.02 + (i / count) * TWO_PI;\r\n let x = width / 2 + cos(a) * radius;\r\n let y = height / 2 + sin(a) * radius;\r\n noStroke();\r\n fill(255, 150, 0);\r\n circle(x, y, 16);\r\n }\r\n}\r\n```\r\n\r\n**Converted Viji-P5:**"
1404
+ },
1405
+ {
1406
+ "type": "live-example",
1407
+ "title": "Converted Sketch — Orbiting Dots",
1408
+ "sceneCode": "// @renderer p5\r\n\r\nconst speed = viji.slider(2, { min: 0.5, max: 8, label: 'Speed' });\r\nconst count = viji.slider(8, { min: 3, max: 20, step: 1, label: 'Count' });\r\nconst dotColor = viji.color('#ff6600', { label: 'Color' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n\r\n p5.background(10);\r\n\r\n const cx = viji.width / 2;\r\n const cy = viji.height / 2;\r\n const radius = Math.min(viji.width, viji.height) * 0.3;\r\n const dotSize = Math.min(viji.width, viji.height) * 0.04;\r\n\r\n const r = parseInt(dotColor.value.slice(1, 3), 16);\r\n const g = parseInt(dotColor.value.slice(3, 5), 16);\r\n const b = parseInt(dotColor.value.slice(5, 7), 16);\r\n\r\n for (let i = 0; i < count.value; i++) {\r\n const a = angle + (i / count.value) * p5.TWO_PI;\r\n const x = cx + p5.cos(a) * radius;\r\n const y = cy + p5.sin(a) * radius;\r\n\r\n p5.noStroke();\r\n p5.fill(r, g, b);\r\n p5.circle(x, y, dotSize);\r\n }\r\n}\r\n",
1409
+ "sceneFile": "converted-sketch.scene.js"
1410
+ },
1411
+ {
1412
+ "type": "text",
1413
+ "markdown": "Key changes made:\r\n\r\n1. Added `// @renderer p5` at the top.\r\n2. Renamed `draw()` → `render(viji, p5)`, added `setup(viji, p5)`.\r\n3. Prefixed all P5 functions with `p5.`.\r\n4. Removed `createCanvas()`.\r\n5. Replaced hardcoded `400` and `120` with `viji.width`, `viji.height`, and proportional math.\r\n6. Replaced `frameCount * 0.02` with a `deltaTime`-based accumulator for frame-rate-independent animation.\r\n7. Extracted the hardcoded color and count into Viji parameters so they become live controls.\r\n\r\n## What Doesn't Work\r\n\r\nThese P5 features are unavailable in the worker environment:\r\n\r\n| Feature | Alternative |\r\n|---|---|\r\n| `p5.dom` (sliders, buttons) | Use Viji parameters (`viji.slider()`, `viji.toggle()`, etc.) |\r\n| `p5.sound` | Use Viji audio API (`viji.audio.*`) |\r\n| `loadImage()`, `loadFont()`, `loadJSON()` | `viji.image()` parameter or `fetch()` in `setup()` |\r\n| `save()`, `saveCanvas()`, `saveFrames()` | Host-side `core.captureFrame()` |\r\n| `createCapture()`, `createVideo()` | Use Viji video API (`viji.video.*`) |\r\n| `cursor()`, `noCursor()` | Not available in workers |\r\n| `fullscreen()` | Host-side concern |\r\n| `frameRate()` | Host-side `core.setFrameRate()` |\r\n| `mousePressed()`, `keyPressed()`, etc. | Check state in `render()` via Viji APIs |\r\n\r\n## Tips\r\n\r\n- **Start with `setup()` and `render()`.** Get the basic structure right first, then fix individual function calls.\r\n- **Search and replace `p5.` prefix.** Most editors support regex — replace `\\b(background|fill|stroke|rect|ellipse|circle|...)\\(` with `p5.$1(`.\r\n- **Use `viji.width` / `viji.height`** everywhere instead of hardcoded dimensions. This makes the scene resolution-agnostic.\r\n- **Convert animation timing.** Replace `frameCount`-based animation with `viji.time` or `viji.deltaTime` accumulators for frame-rate independence.\r\n- **Test incrementally.** Convert the structure first, then one feature at a time.\r\n\r\n## Related\r\n\r\n- [P5 Quick Start](/p5/quickstart) — building P5 scenes from scratch in Viji\r\n- [Drawing with P5](/p5/drawing) — P5 drawing functions in the Viji environment\r\n- [Parameters](/p5/parameters) — sliders, colors, toggles, images\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1113
1414
  }
1114
1415
  ]
1115
1416
  },
1116
- "shader-param-accumulator": {
1117
- "id": "shader-param-accumulator",
1118
- "title": "shader-param-accumulator",
1119
- "description": "Smooth, jump-free animation in shaders using CPU-side phase accumulation driven by parameters or constants.",
1417
+ "p5-timing": {
1418
+ "id": "p5-timing",
1419
+ "title": "Timing",
1420
+ "description": "Use viji.time, viji.deltaTime, viji.frameCount, and viji.fps for animation in P5 scenes.",
1120
1421
  "content": [
1121
1422
  {
1122
1423
  "type": "text",
1123
- "markdown": "# @viji-accumulator\r\n\r\n```\r\n// @viji-accumulator:uniformName rate:source [default:value]\r\n```\r\n\r\nAn accumulator is a CPU-side float value that grows by `rate × deltaTime` every frame. It produces a `uniform float` in your shader use it anywhere you need smooth, parameter-driven animation without jumps.\r\n\r\n## The Problem\r\n\r\nWhen you multiply `u_time` by a speed parameter, changing the slider recalculates the entire phase instantly and the animation jumps:\r\n\r\n```glsl\r\n// Bad animation jumps when speed slider changes\r\nfloat wave = sin(u_time * speed);\r\n```\r\n\r\nThe accumulator solves this by integrating the rate over time. Changing the rate only affects future growth it never jumps backward or forward:\r\n\r\n```glsl\r\n// Good smooth at any slider value\r\n// @viji-accumulator:phase rate:speed\r\nfloat wave = sin(phase);\r\n```\r\n\r\n## Basic Usage"
1424
+ "markdown": "# Timing\n\nThe same timing properties available in native scenes work identically in P5. This page covers P5-specific usage patterns and clarifies the relationship between Viji's timing API and P5's own frame utilities.\n\n## Properties\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `viji.time` | `number` | Seconds elapsed since the scene started |\n| `viji.deltaTime` | `number` | Seconds since the previous frame |\n| `viji.frameCount` | `number` | Integer frame counter (monotonically increasing) |\n| `viji.fps` | `number` | Target FPS based on the host's frame rate mode |\n\n## `viji.time` — Oscillations & Cycles\n\nUse `viji.time` for effects that depend on absolute position in time oscillations, rotations, and cycling:\n\n```javascript\n// @renderer p5\n\nfunction render(viji, p5) {\n p5.background(0);\n const x = viji.width / 2 + p5.cos(viji.time * 2) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(viji.time * 3) * viji.height * 0.2;\n p5.circle(x, y, viji.width * 0.05);\n}\n```"
1124
1425
  },
1125
1426
  {
1126
1427
  "type": "live-example",
1127
- "title": "Speed-Driven Accumulator",
1128
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-color:tint label:\"Tint\" default:#00ffcc\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\r\n gl_FragColor = vec4(tint * wave, 1.0);\r\n}\r\n",
1129
- "sceneFile": "acc-basic.scene.glsl"
1428
+ "title": "Time-Based — Lissajous Curve",
1429
+ "sceneCode": "// @renderer p5\n\nconst freqX = viji.slider(2, { min: 1, max: 7, step: 1, label: 'Frequency X' });\nconst freqY = viji.slider(3, { min: 1, max: 7, step: 1, label: 'Frequency Y' });\nconst trailLen = viji.slider(200, { min: 20, max: 600, step: 10, label: 'Trail Length' });\nconst lineColor = viji.color('#ff6644', { label: 'Curve Color' });\n\nfunction setup(viji, p5) {\n p5.noFill();\n}\n\nfunction render(viji, p5) {\n p5.background(10, 10, 26);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const ax = viji.width * 0.38;\n const ay = viji.height * 0.38;\n\n p5.stroke(lineColor.value);\n p5.strokeWeight(Math.max(1, viji.width * 0.003));\n p5.beginShape();\n for (let i = 0; i < trailLen.value; i++) {\n const t = viji.time - i * 0.005;\n const x = cx + p5.sin(t * freqX.value) * ax;\n const y = cy + p5.cos(t * freqY.value) * ay;\n p5.vertex(x, y);\n }\n p5.endShape();\n}\n",
1430
+ "sceneFile": "timing-oscillation.scene.js"
1130
1431
  },
1131
1432
  {
1132
1433
  "type": "text",
1133
- "markdown": "The accumulator `phase` increases by `speed × deltaTime` each frame. Try moving the Speed slider the wave changes pace smoothly, never jumps.\r\n\r\n## Config Keys\r\n\r\n| Key | Type | Required | Default | Description |\r\n|-----|------|----------|---------|-------------|\r\n| `rate` | `string` or `number` | Yes | | Rate source: a declared parameter name or numeric constant |\r\n| `default` | `number` | No | `0` | Initial value of the accumulator |\r\n\r\n## Uniform Type\r\n\r\nAccumulators always produce a `uniform float`:\r\n\r\n```glsl\r\n// This directive:\r\n// @viji-accumulator:phase rate:speed\r\n\r\n// Generates this uniform:\r\nuniform float phase;\r\n```\r\n\r\n## No UI Control\r\n\r\nUnlike sliders, colors, or toggles, accumulators **do not appear** in the host parameter panel. They are internal values managed by the Viji runtime. Artists cannot directly manipulate them — they are driven entirely by their `rate` source.\r\n\r\n## Constant Rate\r\n\r\nThe `rate` can be a numeric constant instead of a parameter name. This is useful for steady background animation that doesn't need user control:\r\n\r\n```glsl\r\n// @viji-accumulator:drift rate:0.5\r\n```\r\n\r\nHere `drift` increases by `0.5` per second, unconditionally."
1434
+ "markdown": "## `viji.deltaTime` — Accumulation\n\nUse `viji.deltaTime` for anything that accumulates frame-to-framemovement, rotation, fading, physics:\n\n```javascript\n// @renderer p5\n\nlet hue = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n hue = (hue + 60 * viji.deltaTime) % 360; // 60 degrees per second\n p5.background(hue, 60, 90);\n}\n```"
1134
1435
  },
1135
1436
  {
1136
1437
  "type": "live-example",
1137
- "title": "Constant-Rate Accumulator",
1138
- "sceneCode": "// @renderer shader\r\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.1 max:2.0 step:0.1\r\n// @viji-accumulator:drift rate:0.5\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n center.x *= u_resolution.x / u_resolution.y;\r\n\r\n float d = length(center);\r\n float ring = smoothstep(0.02, 0.0, abs(d - mod(drift * 0.3, 0.5)));\r\n\r\n vec3 color = vec3(0.1, 0.05, 0.2) + ring * brightness * vec3(0.9, 0.4, 1.0);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
1139
- "sceneFile": "acc-constant.scene.glsl"
1438
+ "title": "DeltaTime — Drifting Particles",
1439
+ "sceneCode": "// @renderer p5\n\nconst particleCount = viji.slider(60, { min: 10, max: 200, step: 1, label: 'Particles' });\nconst driftSpeed = viji.slider(40, { min: 10, max: 150, label: 'Drift Speed' });\nconst dotSize = viji.slider(0.01, { min: 0.003, max: 0.03, label: 'Dot Size' });\nconst dotColor = viji.color('#44ddff', { label: 'Dot Color' });\n\nconst particles = [];\nfor (let i = 0; i < 200; i++) {\n particles.push({ x: Math.random(), y: Math.random(), vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2 });\n}\n\nfunction render(viji, p5) {\n p5.background(10, 10, 26, 40);\n\n const n = Math.min(particleCount.value, particles.length);\n const s = driftSpeed.value / Math.max(viji.width, viji.height);\n const r = Math.min(viji.width, viji.height) * dotSize.value;\n\n p5.noStroke();\n p5.fill(dotColor.value);\n for (let i = 0; i < n; i++) {\n const p = particles[i];\n p.x += p.vx * s * viji.deltaTime;\n p.y += p.vy * s * viji.deltaTime;\n\n if (p.x < 0 || p.x > 1) p.vx = -p.vx;\n if (p.y < 0 || p.y > 1) p.vy = -p.vy;\n p.x = Math.max(0, Math.min(1, p.x));\n p.y = Math.max(0, Math.min(1, p.y));\n\n p5.circle(p.x * viji.width, p.y * viji.height, r * 2);\n }\n}\n",
1440
+ "sceneFile": "timing-delta-p5.scene.js"
1140
1441
  },
1141
1442
  {
1142
1443
  "type": "text",
1143
- "markdown": "## Multiple Accumulators\r\n\r\nYou can declare multiple accumulators with independent rates. This lets you animate different axes or properties at different speeds:\r\n\r\n```glsl\r\n// @viji-slider:speedX label:\"Horizontal Speed\" default:1.0 min:0.0 max:5.0\r\n// @viji-slider:speedY label:\"Vertical Speed\" default:0.7 min:0.0 max:5.0\r\n// @viji-accumulator:phaseX rate:speedX\r\n// @viji-accumulator:phaseY rate:speedY\r\n```\r\n\r\nEach accumulator tracks its own value independently. Changing `speedX` has no effect on `phaseY`."
1444
+ "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"
1445
+ }
1446
+ ]
1447
+ },
1448
+ "p5-parameters-overview": {
1449
+ "id": "p5-parameters-overview",
1450
+ "title": "Parameters",
1451
+ "description": "The Viji parameter system in P5 scenes — sliders, colors, toggles, and more for artist-controllable inputs.",
1452
+ "content": [
1453
+ {
1454
+ "type": "text",
1455
+ "markdown": "# Parameters\n\nParameters give users real-time control over your P5 scene. Define them at the top level, and Viji renders corresponding UI controls in the host application. Read `.value` inside `render()` to get the current state.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Hex color values (`'#rrggbb'`) |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger — true for 1 frame (resets, spawns) |\n\n## Basic Pattern\n\n```javascript\n// @renderer p5\n\n// 1. Define at top level — runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render() — updates in real-time\nfunction render(viji, p5) {\n const r = parseInt(color.value.slice(1, 3), 16);\n const g = parseInt(color.value.slice(3, 5), 16);\n const b = parseInt(color.value.slice(5, 7), 16);\n p5.fill(r, g, b);\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Declaring them inside `setup()` would register the parameter too late — no UI control would appear. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default.\n\n## Image Parameters in P5\n\nWhen using [`viji.image()`](image/) with P5 drawing functions, use the `.p5` property instead of `.value`:\n\n```javascript\nconst photo = viji.image(null, { label: 'Photo' });\n\nfunction render(viji, p5) {\n if (photo.value) {\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nThe `.p5` property wraps the raw image data in a P5-compatible object. Use `.value` to check if an image is loaded, and `.p5` when passing to P5 drawing functions.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | **(required)** | Display name shown in the parameter UI |\n| `description` | `string` | — | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters — see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on capabilities — see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)** — Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)** — Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/) — the most common parameter type\n- [Image](image/) — image parameters with the `.p5` property\n- [Grouping](grouping/) — organizing parameters into named groups\n- [Categories](categories/) — visibility based on capabilities\n- [Native Parameters](/native/parameters) — same system in the native renderer\n- [Shader Parameters](/shader/parameters) — comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1456
+ }
1457
+ ]
1458
+ },
1459
+ "p5-param-slider": {
1460
+ "id": "p5-param-slider",
1461
+ "title": "Slider Parameter",
1462
+ "description": "Create a numeric slider control with configurable range and step size in P5.js scenes.",
1463
+ "content": [
1464
+ {
1465
+ "type": "text",
1466
+ "markdown": "# viji.slider()\n\n```\nslider(defaultValue: number, config: SliderConfig): SliderParameter\n```\n\nCreates a numeric slider parameter. The host renders it as a draggable slider control. Define it at the top level and read `.value` inside `render()`.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `number` | Yes | — | Initial value of the slider |\n| `config.min` | `number` | No | `0` | Minimum allowed value |\n| `config.max` | `number` | No | `100` | Maximum allowed value |\n| `config.step` | `number` | No | `1` | Increment between values |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `SliderParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `number` | Current slider value. Updates in real-time when the user moves the slider. |\n| `min` | `number` | Minimum value |\n| `max` | `number` | Maximum value |\n| `step` | `number` | Step increment |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst radius = viji.slider(0.15, {\n min: 0.02,\n max: 0.5,\n step: 0.01,\n label: 'Radius'\n});\n\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255);\n p5.noStroke();\n const r = radius.value * Math.min(p5.width, p5.height);\n p5.ellipse(p5.width / 2, p5.height / 2, r * 2);\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."
1144
1467
  },
1145
1468
  {
1146
1469
  "type": "live-example",
1147
- "title": "Multi-Axis Accumulator",
1148
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speedX label:\"Horizontal Speed\" default:1.0 min:0.0 max:5.0 step:0.1\r\n// @viji-slider:speedY label:\"Vertical Speed\" default:0.7 min:0.0 max:5.0 step:0.1\r\n// @viji-slider:radius label:\"Radius\" default:0.3 min:0.05 max:0.5 step:0.01\r\n// @viji-color:dotColor label:\"Color\" default:#ff6600\r\n// @viji-accumulator:phaseX rate:speedX\r\n// @viji-accumulator:phaseY rate:speedY\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n center.x *= u_resolution.x / u_resolution.y;\r\n\r\n vec2 pos = vec2(cos(phaseX), sin(phaseY)) * 0.3;\r\n float d = length(center - pos);\r\n float circle = smoothstep(radius + 0.01, radius - 0.01, d);\r\n\r\n vec3 bg = vec3(0.05);\r\n vec3 color = mix(bg, dotColor, circle);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
1149
- "sceneFile": "acc-multi.scene.glsl"
1470
+ "title": "Slider Control",
1471
+ "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",
1472
+ "sceneFile": "slider-p5.scene.js"
1150
1473
  },
1151
1474
  {
1152
1475
  "type": "text",
1153
- "markdown": "## Rules\r\n\r\n- **Only `//` comments.** Like all `@viji-*` directives, accumulators must use single-line `//` comments. Block comments `/* */` are not parsed.\r\n- **No `u_` prefix.** The `u_` prefix is reserved for built-in Viji uniforms. Name your accumulators descriptively: `phase`, `drift`, `rotation`.\r\n- **Rate must exist.** If `rate` references a parameter name that doesn't exist, Viji logs a warning and falls back to rate `0` (the accumulator stays at its default value).\r\n- **`label` is not required.** Since accumulators have no UI, the `label` config key is optional (unlike all other parameter types).\r\n- **Value grows unbounded.** The accumulator value increases indefinitely. If you need a bounded range, use `mod()` or `fract()` in your shader code:\r\n\r\n```glsl\r\n// @viji-accumulator:phase rate:speed\r\nfloat angle = mod(phase, 6.283185); // wrap to 0–2π\r\n```\r\n\r\n## Related\r\n\r\n- [Shader Quick Start](/shader/quickstart) — introduction to shader parameter system\r\n- [Slider](/shader/parameters/slider) — numeric slider parameter (common rate source)\r\n- [Best Practices](/getting-started/best-practices) — animation timing patterns\r\n- [Common Mistakes](/getting-started/common-mistakes) — why `u_time * speed` jumps"
1476
+ "markdown": "## Resolution-Agnostic Sizing\n\nWhen using a slider to control sizes or positions, use normalized values (`0` to `1`) and scale relative to `p5.width` and `p5.height`:\n\n```javascript\nconst size = viji.slider(0.15, {\n min: 0.02,\n max: 0.5,\n step: 0.01,\n label: 'Size'\n});\n\nfunction render(viji, p5) {\n const pixelSize = size.value * Math.min(p5.width, p5.height);\n // pixelSize adapts automatically to any resolution\n}\n```\n\n## Related\n\n- [Color](../color/) color picker parameter\n- [Number](../number/) numeric input without a slider track\n- [Select](../select/) dropdown selection from predefined options\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Slider](/native/parameters/slider) — equivalent for the Native renderer\n- [Shader Slider](/shader/parameters/slider) — equivalent for the Shader renderer"
1154
1477
  }
1155
1478
  ]
1156
1479
  },
1157
- "shader-param-grouping": {
1158
- "id": "shader-param-grouping",
1159
- "title": "shader-param-grouping",
1160
- "description": "Organize shader parameters into named groups using the group config key.",
1480
+ "p5-param-color": {
1481
+ "id": "p5-param-color",
1482
+ "title": "Color Parameter",
1483
+ "description": "Create a color picker control that returns a hex color string in P5.js scenes.",
1161
1484
  "content": [
1162
1485
  {
1163
1486
  "type": "text",
1164
- "markdown": "# Grouping Shader Parameters\r\n\r\nAs your shader grows, the parameter list can become unwieldy. Groups let you collect related parameters under a shared heading in the host UI. In shaders, set the `group:` key in any `@viji-*` directive.\r\n\r\n## Usage\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 group:animation\r\n// @viji-toggle:invert label:\"Invert\" default:false group:animation\r\n// @viji-slider:rings label:\"Ring Count\" default:10.0 min:2.0 max:30.0 step:1.0 group:shape\r\n// @viji-color:tint label:\"Tint\" default:#ff6600 group:shape\r\n```\r\n\r\nParameters with the same `group` value appear together in the UI. The group name is a freeform string any name works.\r\n\r\nThe host application receives the group names and renders them as collapsible sections or visual separators.\r\n\r\n## How It Works\r\n\r\n- The `group` key is a **freeform string** any name works. There is no fixed list.\r\n- If you omit `group`, the parameter defaults to `'general'`.\r\n- Parameters with the same `group` value are collected together when sent to the host UI.\r\n- Group names are displayed as-is, so use readable names like `animation`, `visual style`, `audio settings`.\r\n- Declaration order within a group is preserved.\r\n- The `group:` key in `@viji-*` directives works identically to the `group` config in native/P5 JavaScript parameters.\r\n\r\n## Live Example\r\n\r\nThis shader uses two groups \"animation\" (speed and invert) and \"shape\" (ring count and tint):"
1487
+ "markdown": "# viji.color()\n\n```\ncolor(defaultValue: string, config: ColorConfig): ColorParameter\n```\n\nCreates a color picker parameter. The host renders it as a color swatch that opens a full color picker when clicked.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `string` | Yes | | Initial hex color (e.g., `'#ff6600'`) |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ColorParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Current hex color (e.g., `'#ff6600'`). Updates in real-time. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst bg = viji.color('#1a1a2e', { label: 'Background' });\nconst accent = viji.color('#ff6600', { label: 'Accent' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n p5.fill(accent.value);\n p5.noStroke();\n p5.ellipse(p5.width / 2, p5.height / 2, p5.width * 0.5);\n}\n```\n\nThe `.value` is always a 6-digit hex string (`#rrggbb`). P5.js accepts hex strings directly in `p5.fill()`, `p5.stroke()`, and `p5.background()`.\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."
1165
1488
  },
1166
1489
  {
1167
1490
  "type": "live-example",
1168
- "title": "Shader Grouped Parameters",
1169
- "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 group:animation\r\n// @viji-toggle:invert label:\"Invert\" default:false group:animation\r\n// @viji-slider:rings label:\"Ring Count\" default:10.0 min:2.0 max:30.0 step:1.0 group:shape\r\n// @viji-color:tint label:\"Tint\" default:#ff6600 group:shape\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\r\n float d = length(uv);\r\n\r\n float wave = sin(d * rings - phase * 4.0) * 0.5 + 0.5;\r\n vec3 col = tint * wave;\r\n\r\n if (invert) col = 1.0 - col;\r\n\r\n col *= smoothstep(1.5, 0.5, d);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
1170
- "sceneFile": "grouping-demo.scene.glsl"
1491
+ "title": "Color Picker",
1492
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst color1 = viji.color('#ff4488', { label: 'Color 1', group: 'colors' });\nconst color2 = viji.color('#4488ff', { label: 'Color 2', group: 'colors' });\nconst count = viji.slider(6, { min: 2, max: 16, step: 1, label: 'Count' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n p5.noStroke();\n\n const n = count.value;\n for (let i = 0; i < n; i++) {\n const t = i / (n - 1);\n const col = p5.lerpColor(p5.color(color1.value), p5.color(color2.value), t);\n p5.fill(col);\n const a = (i / n) * p5.TWO_PI + viji.time;\n const r = Math.min(p5.width, p5.height) * 0.3;\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, Math.min(p5.width, p5.height) * 0.08);\n }\n}\n",
1493
+ "sceneFile": "color-p5.scene.js"
1171
1494
  },
1172
1495
  {
1173
1496
  "type": "text",
1174
- "markdown": "## Mixing Types\r\n\r\nGroups can contain any mix of parameter types [`@viji-slider`](../slider/), [`@viji-color`](../color/), [`@viji-toggle`](../toggle/), [`@viji-select`](../select/), and more. There is no restriction on which types can share a group:\r\n\r\n```glsl\r\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0 group:visual\r\n// @viji-color:tint label:\"Tint\" default:#ffffff group:visual\r\n// @viji-toggle:showGrid label:\"Grid Overlay\" default:false group:visual\r\n// @viji-select:blendMode label:\"Blend\" default:0.0 options:normal|add|multiply group:visual\r\n```\r\n\r\n## Groups and Categories\r\n\r\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. Combine them when a grouped parameter depends on an external input:\r\n\r\n```glsl\r\n// @viji-slider:bassReact label:\"Bass React\" default:0.5 min:0.0 max:1.0 group:effects category:audio\r\n// @viji-slider:colorShift label:\"Color Shift\" default:0.2 min:0.0 max:1.0 group:effects category:general\r\n```\r\n\r\nBoth appear in the \"effects\" group, but `bassReact` only shows when audio is connected.\r\n\r\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\r\n\r\n## Related\r\n\r\n- [Parameters Overview](../) — all shader parameter types\r\n- [Categories](../categories/) — visibility based on active capabilities\r\n- [Native Grouping](/native/parameters/grouping) — same concept in JavaScript\r\n- [P5 Grouping](/p5/parameters/grouping) — same concept in P5 renderer\r\n- [Slider](../slider/) — the most common parameter type"
1497
+ "markdown": "## Parsing for P5 Color Functions\n\nIf you need to decompose a hex value for use with `p5.color()` or alpha blending:\n\n```javascript\nconst c = viji.color('#ff6600', { label: 'Color' });\n\nfunction render(viji, p5) {\n const col = p5.color(c.value);\n col.setAlpha(128);\n p5.fill(col);\n // ...\n}\n```\n\n## Related\n\n- [Slider](../slider/) numeric slider parameter\n- [Toggle](../toggle/) — boolean on/off parameter\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Color](/native/parameters/color) — equivalent for the Native renderer\n- [Shader Color](/shader/parameters/color) — equivalent for the Shader renderer"
1175
1498
  }
1176
1499
  ]
1177
1500
  },
1178
- "shader-param-categories": {
1179
- "id": "shader-param-categories",
1180
- "title": "shader-param-categories",
1181
- "description": "Control shader parameter visibility based on active capabilities like audio, video, and interaction.",
1501
+ "p5-param-toggle": {
1502
+ "id": "p5-param-toggle",
1503
+ "title": "Toggle Parameter",
1504
+ "description": "Create a boolean on/off switch for enabling or disabling P5.js scene features.",
1182
1505
  "content": [
1183
1506
  {
1184
1507
  "type": "text",
1185
- "markdown": "# Shader Parameter Categories\r\n\r\nCategories let you tag parameters so they're only visible when the corresponding capability is active. An \"Audio Pulse\" [`@viji-slider`](../slider/) is useless if no audio source is connected with `category:audio`, it automatically appears when audio is available and hides when it's not. In shaders, set the `category:` key in any `@viji-*` directive.\r\n\r\n## The Four Categories\r\n\r\n| Category | Visible When | Use For |\r\n|---|---|---|\r\n| `general` | Always | Colors, sizes, speeds, shapes anything that works without external input |\r\n| `audio` | Audio source is connected | Volume scaling, beat reactivity, frequency controls |\r\n| `video` | Video/camera source is connected | Video opacity, CV sensitivity, segmentation controls |\r\n| `interaction` | User interaction is enabled | Mouse effects, keyboard bindings, touch sensitivity |\r\n\r\n## Usage\r\n\r\n```glsl\r\n// @renderer shader\r\n// @viji-color:tint label:\"Color\" default:#4488ff category:general\r\n// @viji-slider:audioPulse label:\"Audio Pulse\" default:0.3 min:0.0 max:1.0 category:audio\r\n// @viji-slider:mouseSize label:\"Mouse Glow\" default:0.15 min:0.0 max:0.5 category:interaction\r\n```\r\n\r\n- `tint` ([`@viji-color`](../color/)) is always visible.\r\n- `audioPulse` ([`@viji-slider`](../slider/)) only appears when audio is connected.\r\n- `mouseSize` ([`@viji-slider`](../slider/)) only appears when interaction is enabled.\r\n\r\nIf you omit `category`, it defaults to `general` (always visible).\r\n\r\n## How It Works\r\n\r\n1. The artist sets `category:` on each directive during scene declaration.\r\n2. The shader parameter parser extracts `category` along with other config keys.\r\n3. When the host application requests parameters, Viji filters them based on the current `CoreCapabilities`:\r\n - `hasAudio` — is an audio stream connected?\r\n - `hasVideo` — is a video/camera stream connected?\r\n - `hasInteraction` — is user interaction enabled?\r\n - `hasGeneral` — always `true`.\r\n4. Only parameters matching active capabilities are sent to the UI.\r\n\r\n> [!NOTE]\r\n> Categories filter at both the **group level** and the **individual parameter level**. If a group's category doesn't match, the entire group is hidden. If individual parameters within a visible group have non-matching categories, those parameters are hidden while the group remains visible.\r\n\r\n> [!NOTE]\r\n> In shader directives, category values are **unquoted strings**: `category:audio`, not `category:\"audio\"`. Both forms work, but the unquoted form is conventional.\r\n\r\n## Live Example\r\n\r\nParameters in three categories — `general` (always visible), `audio` (needs audio), and `interaction` (needs mouse):"
1508
+ "markdown": "# viji.toggle()\n\n```\ntoggle(defaultValue: boolean, config: ToggleConfig): ToggleParameter\n```\n\nCreates a boolean toggle parameter. The host renders it as an on/off switch.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `boolean` | Yes | | Initial state (`true` or `false`) |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ToggleParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `boolean` | Current state. Updates in real-time when the user toggles. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst showTrail = viji.toggle(true, { label: 'Show Trail' });\nconst animate = viji.toggle(true, { label: 'Animate' });\n\nfunction render(viji, p5) {\n if (!showTrail.value) {\n p5.background(0);\n }\n\n if (animate.value) {\n // draw moving elements\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."
1186
1509
  },
1187
1510
  {
1188
1511
  "type": "live-example",
1189
- "title": "Shader Parameter Categories",
1190
- "sceneCode": "// @renderer shader\r\n// @viji-color:tint label:\"Color\" default:#4488ff category:general\r\n// @viji-slider:audioPulse label:\"Audio Pulse\" default:0.3 min:0.0 max:1.0 category:audio\r\n// @viji-slider:mouseSize label:\"Mouse Glow\" default:0.15 min:0.0 max:0.5 category:interaction\r\n// @viji-accumulator:phase rate:1.0\r\n\r\nvoid main() {\r\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\r\n float d = length(uv);\r\n\r\n float pulse = u_audioVolume * audioPulse;\r\n float wave = sin(d * 15.0 - phase * 3.0) * 0.5 + 0.5;\r\n vec3 col = tint * wave * (1.0 + pulse);\r\n\r\n vec2 mouseUV = (2.0 * u_mouse - u_resolution) / u_resolution.y;\r\n float mouseDist = length(uv - mouseUV);\r\n float glow = mouseSize / (mouseDist + 0.05);\r\n col += vec3(glow * 0.3);\r\n\r\n col *= smoothstep(1.5, 0.3, d);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
1191
- "sceneFile": "categories-demo.scene.glsl"
1512
+ "title": "Toggle Features",
1513
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst dotColor = viji.color('#ff6644', { label: 'Color' });\nconst trail = viji.toggle(true, { label: 'Trail Effect' });\nconst animate = viji.toggle(true, { label: 'Animate' });\n\nlet angle = 0;\n\nfunction render(viji, p5) {\n if (trail.value) {\n p5.background(p5.color(bg.value + '0a'));\n } else {\n p5.background(bg.value);\n }\n\n if (animate.value) {\n angle += viji.deltaTime;\n }\n\n p5.noStroke();\n p5.fill(dotColor.value);\n const r = Math.min(p5.width, p5.height) * 0.3;\n const x = p5.width / 2 + Math.cos(angle) * r;\n const y = p5.height / 2 + Math.sin(angle * 1.3) * r;\n p5.ellipse(x, y, Math.min(p5.width, p5.height) * 0.06);\n}\n",
1514
+ "sceneFile": "toggle-p5.scene.js"
1515
+ },
1516
+ {
1517
+ "type": "text",
1518
+ "markdown": "## Common Patterns\n\n### Trail effect\n\nSkipping `p5.background()` creates a trail effect. Toggle it on or off:\n\n```javascript\nconst trail = viji.toggle(true, { label: 'Trail' });\n\nfunction render(viji, p5) {\n if (!trail.value) {\n p5.background(0);\n } else {\n p5.background(0, 0, 0, 15);\n }\n // draw moving shapes\n}\n```\n\n### Pausing animation\n\n```javascript\nconst animate = viji.toggle(true, { label: 'Animate' });\nlet angle = 0;\n\nfunction render(viji, p5) {\n if (animate.value) {\n angle += viji.deltaTime;\n }\n // use angle\n}\n```\n\n## Related\n\n- [Slider](../slider/) — numeric slider parameter\n- [Color](../color/) — color picker parameter\n- [Select](../select/) — dropdown with multiple options\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Toggle](/native/parameters/toggle) — equivalent for the Native renderer\n- [Shader Toggle](/shader/parameters/toggle) — equivalent for the Shader renderer"
1519
+ }
1520
+ ]
1521
+ },
1522
+ "p5-param-select": {
1523
+ "id": "p5-param-select",
1524
+ "title": "Select Parameter",
1525
+ "description": "Create a dropdown selector for choosing between predefined options in P5.js scenes.",
1526
+ "content": [
1527
+ {
1528
+ "type": "text",
1529
+ "markdown": "# viji.select()\n\n```\nselect(defaultValue: string | number, config: SelectConfig): SelectParameter\n```\n\nCreates a dropdown selection parameter. The host renders it as a dropdown menu or segmented control.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `string \\| number` | Yes | — | Initially selected value (must be in `options`) |\n| `config.options` | `string[] \\| number[]` | Yes | — | Available choices |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `SelectParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string \\| number` | Currently selected option. Updates in real-time. |\n| `options` | `string[] \\| number[]` | The full options list |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst blendMode = viji.select('BLEND', {\n options: ['BLEND', 'ADD', 'MULTIPLY', 'SCREEN'],\n label: 'Blend Mode'\n});\n\nfunction render(viji, p5) {\n p5.background(0);\n p5.blendMode(p5[blendMode.value]);\n // draw shapes\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."
1530
+ },
1531
+ {
1532
+ "type": "live-example",
1533
+ "title": "Shape Selector",
1534
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst fillColor = viji.color('#44ddff', { label: 'Fill' });\nconst shape = viji.select('ellipse', {\n options: ['ellipse', 'rect', 'triangle'],\n label: 'Shape'\n});\nconst size = viji.slider(0.3, { min: 0.05, max: 0.45, step: 0.01, label: 'Size' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n p5.fill(fillColor.value);\n p5.noStroke();\n\n const s = Math.min(p5.width, p5.height) * size.value;\n const cx = p5.width / 2;\n const cy = p5.height / 2;\n\n switch (shape.value) {\n case 'ellipse':\n p5.ellipse(cx, cy, s * 2);\n break;\n case 'rect':\n p5.rectMode(p5.CENTER);\n p5.rect(cx, cy, s * 2, s * 2);\n break;\n case 'triangle':\n p5.triangle(\n cx, cy - s,\n cx - s, cy + s,\n cx + s, cy + s\n );\n break;\n }\n}\n",
1535
+ "sceneFile": "select-p5.scene.js"
1536
+ },
1537
+ {
1538
+ "type": "text",
1539
+ "markdown": "## Numeric Options\n\nOptions can also be numbers, useful for selecting discrete numeric values:\n\n```javascript\nconst sides = viji.select(6, {\n options: [3, 4, 5, 6, 8],\n label: 'Polygon Sides'\n});\n```\n\n## Related\n\n- [Slider](../slider/) — continuous numeric range\n- [Toggle](../toggle/) — two-state boolean\n- [Number](../number/) — direct numeric input\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Select](/native/parameters/select) — equivalent for the Native renderer\n- [Shader Select](/shader/parameters/select) — equivalent for the Shader renderer"
1540
+ }
1541
+ ]
1542
+ },
1543
+ "p5-param-number": {
1544
+ "id": "p5-param-number",
1545
+ "title": "Number Parameter",
1546
+ "description": "Create a numeric input field with configurable range and step size in P5.js scenes.",
1547
+ "content": [
1548
+ {
1549
+ "type": "text",
1550
+ "markdown": "# viji.number()\n\n```\nnumber(defaultValue: number, config: NumberConfig): NumberParameter\n```\n\nCreates a numeric input parameter. The host renders it as a direct number input field — use this when you need precise numeric entry rather than a draggable slider.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `number` | Yes | — | Initial value |\n| `config.min` | `number` | No | `0` | Minimum allowed value |\n| `config.max` | `number` | No | `100` | Maximum allowed value |\n| `config.step` | `number` | No | `1` | Increment between values |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `NumberParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `number` | Current value. Updates in real-time. |\n| `min` | `number` | Minimum value |\n| `max` | `number` | Maximum value |\n| `step` | `number` | Step increment |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst gridCols = viji.number(8, {\n min: 2,\n max: 30,\n step: 1,\n label: 'Columns'\n});\n\nfunction render(viji, p5) {\n p5.background(0);\n const cellW = p5.width / gridCols.value;\n // draw grid\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."
1551
+ },
1552
+ {
1553
+ "type": "live-example",
1554
+ "title": "Number Input",
1555
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst dotColor = viji.color('#ff88cc', { label: 'Color' });\nconst cols = viji.number(6, { min: 2, max: 16, step: 1, label: 'Columns' });\nconst rows = viji.number(6, { min: 2, max: 16, step: 1, label: 'Rows' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n p5.fill(dotColor.value);\n p5.noStroke();\n\n const margin = Math.min(p5.width, p5.height) * 0.1;\n const cellW = (p5.width - margin * 2) / cols.value;\n const cellH = (p5.height - margin * 2) / rows.value;\n const r = Math.min(cellW, cellH) * 0.3;\n\n for (let c = 0; c < cols.value; c++) {\n for (let row = 0; row < rows.value; row++) {\n const x = margin + cellW * (c + 0.5);\n const y = margin + cellH * (row + 0.5);\n const wave = Math.sin(viji.time * 2 + c * 0.5 + row * 0.5) * 0.5 + 0.5;\n p5.ellipse(x, y, r * wave * 2);\n }\n }\n}\n",
1556
+ "sceneFile": "number-p5.scene.js"
1557
+ },
1558
+ {
1559
+ "type": "text",
1560
+ "markdown": "## Number vs Slider\n\nBoth accept the same config options (`min`, `max`, `step`) and return the same shape. The difference is the host UI:\n\n| | Slider | Number |\n|--|--------|--------|\n| UI | Draggable track | Text input field |\n| Best for | Continuous ranges, visual tuning | Precise values, integer counts |\n| Interaction | Drag / click | Type / arrow keys |\n\nUse [`viji.slider()`](../slider/) when the artist needs to \"feel\" the value visually. Use `viji.number()` when exact entry matters.\n\n## Related\n\n- [Slider](../slider/) — numeric slider with a draggable track\n- [Select](../select/) — dropdown for discrete choices\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Number](/native/parameters/number) — equivalent for the Native renderer\n- [Shader Number](/shader/parameters/number) — equivalent for the Shader renderer"
1561
+ }
1562
+ ]
1563
+ },
1564
+ "p5-param-text": {
1565
+ "id": "p5-param-text",
1566
+ "title": "Text Parameter",
1567
+ "description": "Create a text input field for user-provided strings in P5.js scenes.",
1568
+ "content": [
1569
+ {
1570
+ "type": "text",
1571
+ "markdown": "# viji.text()\n\n```\ntext(defaultValue: string, config: TextConfig): TextParameter\n```\n\nCreates a text input parameter. The host renders it as a single-line text field.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `string` | Yes | — | Initial text value |\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.maxLength` | `number` | No | `1000` | Maximum character count |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `TextParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `string` | Current text. Updates in real-time as the user types. |\n| `maxLength` | `number \\| undefined` | Maximum character count |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\nconst label = viji.text('Hello', { label: 'Label', maxLength: 40 });\n\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(p5.width, p5.height) * 0.08);\n p5.text(label.value, p5.width / 2, p5.height / 2);\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."
1572
+ },
1573
+ {
1574
+ "type": "live-example",
1575
+ "title": "Text Display",
1576
+ "sceneCode": "const bg = viji.color('#0f0f1a', { label: 'Background' });\nconst textColor = viji.color('#ffffff', { label: 'Text Color' });\nconst message = viji.text('Viji', { label: 'Message', maxLength: 30 });\nconst fontSize = viji.slider(0.12, { min: 0.03, max: 0.3, step: 0.01, label: 'Font Size' });\n\nfunction render(viji, p5) {\n p5.background(bg.value);\n p5.fill(textColor.value);\n p5.noStroke();\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(p5.width, p5.height) * fontSize.value);\n\n const wave = Math.sin(viji.time * 2) * Math.min(p5.width, p5.height) * 0.02;\n p5.text(message.value, p5.width / 2, p5.height / 2 + wave);\n}\n",
1577
+ "sceneFile": "text-p5.scene.js"
1578
+ },
1579
+ {
1580
+ "type": "text",
1581
+ "markdown": "## Related\n\n- [Slider](../slider/) — numeric slider parameter\n- [Color](../color/) — color picker parameter\n- [Select](../select/) — dropdown for predefined choices\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Text](/native/parameters/text) — equivalent for the Native renderer"
1582
+ }
1583
+ ]
1584
+ },
1585
+ "p5-param-image": {
1586
+ "id": "p5-param-image",
1587
+ "title": "Image Parameter",
1588
+ "description": "Accept user-uploaded images for use as textures in P5.js scenes, including the .p5 property.",
1589
+ "content": [
1590
+ {
1591
+ "type": "text",
1592
+ "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."
1593
+ },
1594
+ {
1595
+ "type": "live-example",
1596
+ "title": "Image Upload",
1597
+ "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",
1598
+ "sceneFile": "image-p5.scene.js"
1599
+ },
1600
+ {
1601
+ "type": "text",
1602
+ "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`)"
1603
+ }
1604
+ ]
1605
+ },
1606
+ "p5-param-button": {
1607
+ "id": "p5-param-button",
1608
+ "title": "Button Parameter",
1609
+ "description": "Create a momentary trigger that is true for exactly one frame when pressed in P5.js scenes.",
1610
+ "content": [
1611
+ {
1612
+ "type": "text",
1613
+ "markdown": "# viji.button()\n\n```\nbutton(config: ButtonConfig): ButtonParameter\n```\n\nCreates a momentary button parameter. Unlike [`viji.toggle()`](../toggle/) which latches on/off, a button is `true` for exactly **one frame** when the user clicks it, then automatically resets to `false`. The host renders it as a clickable button.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `config.label` | `string` | Yes | — | Display name shown in the parameter UI |\n| `config.description` | `string` | No | — | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name — see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category — see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ButtonParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `boolean` | `true` for the single frame after the user clicks, `false` otherwise. |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\n```javascript\n// @renderer p5\nconst reset = viji.button({ label: 'Reset' });\n\nlet angle = 0;\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (reset.value) {\n angle = 0;\n }\n\n angle += viji.deltaTime;\n p5.translate(p5.width / 2, p5.height / 2);\n p5.rotate(angle);\n p5.rect(-50, -50, 100, 100);\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.\n\n## How It Works\n\n1. The user clicks the button in the host UI.\n2. On the **next rendered frame**, `button.value` is `true`.\n3. After that frame completes, the value automatically resets to `false`.\n4. Subsequent frames see `false` until the user clicks again.\n\n## Common Patterns\n\n### Resetting accumulated state\n\n```javascript\nconst resetPos = viji.button({ label: 'Reset Position' });\n\nlet x = 0;\nlet y = 0;\n\nfunction render(viji, p5) {\n p5.background(20);\n\n if (resetPos.value) {\n x = 0;\n y = 0;\n }\n\n x += viji.deltaTime * 50;\n y = p5.sin(viji.time) * 100;\n p5.ellipse(p5.width / 2 + x, p5.height / 2 + y, 30, 30);\n}\n```\n\n### Clearing a canvas\n\n```javascript\nconst clear = viji.button({ label: 'Clear Canvas' });\nlet shouldClear = false;\n\nfunction render(viji, p5) {\n if (clear.value) {\n p5.background(0);\n }\n // draw without clearing — paint mode\n p5.ellipse(viji.mouse.x, viji.mouse.y, 20, 20);\n}\n```\n\n## Button vs Toggle\n\n| | [`viji.button()`](../button/) | [`viji.toggle()`](../toggle/) |\n|---|---|---|\n| Value | `true` for 1 frame, then `false` | Stays `true` or `false` until changed |\n| Default argument | None (always starts `false`) | Required (`true` or `false`) |\n| Use for | One-shot actions, resets, spawns | Persistent on/off switches |\n| Host UI | Clickable button | On/off switch |\n\n## Related\n\n- [Toggle](../toggle/) — persistent boolean switch\n- [Slider](../slider/) — numeric slider parameter\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Categories](../categories/) — controlling parameter visibility\n- [Native Button](/native/parameters/button) — equivalent for the Native renderer\n- [Shader Button](/shader/parameters/button) — equivalent for the Shader renderer"
1614
+ }
1615
+ ]
1616
+ },
1617
+ "p5-param-grouping": {
1618
+ "id": "p5-param-grouping",
1619
+ "title": "Grouping Parameters",
1620
+ "description": "Organize P5 scene parameters into named groups so related controls appear together in the UI.",
1621
+ "content": [
1622
+ {
1623
+ "type": "text",
1624
+ "markdown": "# Grouping Parameters\n\nAs your scene grows, the parameter list can become unwieldy. Groups let you collect related parameters under a shared heading in the host UI. Parameters with the same `group` string appear together, regardless of declaration order.\n\n## Usage\n\nSet the `group` config key on any parameter:\n\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed', group: 'animation' });\nconst trail = viji.toggle(true, { label: 'Trail', group: 'animation' });\n\nconst size = viji.slider(0.05, { min: 0.01, max: 0.15, step: 0.01, label: 'Size', group: 'shape' });\nconst color = viji.color('#ff6600', { label: 'Color', group: 'shape' });\n```\n\nThe host application receives the group names and renders them as collapsible sections or visual separators.\n\n## How It Works\n\n- `group` is a **freeform string** — any name works. There is no fixed list.\n- If omitted, parameters default to the `'general'` group.\n- Parameters with the same `group` value are collected together when sent to the host UI.\n- Group names are displayed as-is, so use readable names like `'animation'`, `'visual style'`, `'audio settings'`.\n- Declaration order within a group is preserved.\n\n## Live Example\n\nThis scene uses two groups — \"animation\" (speed and trail) and \"shape\" (size and color):"
1625
+ },
1626
+ {
1627
+ "type": "live-example",
1628
+ "title": "P5 Grouped Parameters",
1629
+ "sceneCode": "// @renderer p5\r\n\r\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed', group: 'animation' });\r\nconst trail = viji.toggle(true, { label: 'Trail', group: 'animation' });\r\n\r\nconst size = viji.slider(0.05, { min: 0.01, max: 0.15, step: 0.01, label: 'Size', group: 'shape' });\r\nconst color = viji.color('#ff6600', { label: 'Color', group: 'shape' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji, p5) {\r\n angle += speed.value * viji.deltaTime;\r\n\r\n if (trail.value) {\r\n p5.background(10, 10, 30, 25);\r\n } else {\r\n p5.background(10, 10, 30);\r\n }\r\n\r\n const cx = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\r\n const cy = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\r\n const r = Math.min(viji.width, viji.height) * size.value;\r\n\r\n const cr = parseInt(color.value.slice(1, 3), 16);\r\n const cg = parseInt(color.value.slice(3, 5), 16);\r\n const cb = parseInt(color.value.slice(5, 7), 16);\r\n\r\n p5.noStroke();\r\n p5.fill(cr, cg, cb);\r\n p5.circle(cx, cy, r * 2);\r\n}\r\n",
1630
+ "sceneFile": "grouping-demo.scene.js"
1631
+ },
1632
+ {
1633
+ "type": "text",
1634
+ "markdown": "## Mixing Types\n\nGroups can contain any mix of parameter types — [`viji.slider()`](../slider/), [`viji.color()`](../color/), [`viji.toggle()`](../toggle/), [`viji.select()`](../select/), and more. There is no restriction on which types can share a group:\n\n```javascript\nconst brightness = viji.slider(1, { min: 0, max: 2, label: 'Brightness', group: 'visual' });\nconst tint = viji.color('#ffffff', { label: 'Tint', group: 'visual' });\nconst showGrid = viji.toggle(false, { label: 'Grid Overlay', group: 'visual' });\nconst blendMode = viji.select('normal', { options: ['normal', 'add', 'multiply'], label: 'Blend', group: 'visual' });\n```\n\n## Groups and Categories\n\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. You can combine them:\n\n```javascript\nconst volume = viji.slider(1, {\n label: 'Volume Scale',\n group: 'audio settings',\n category: 'audio'\n});\n```\n\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\n\n## Related\n\n- [Parameters Overview](../) — all parameter types and the basic pattern\n- [Categories](../categories/) — visibility based on active capabilities\n- [Native Grouping](/native/parameters/grouping) — same concept in the native renderer\n- [Shader Grouping](/shader/parameters/grouping) — same concept with `@viji-*` directives\n- [Slider](../slider/) — the most common parameter type"
1635
+ }
1636
+ ]
1637
+ },
1638
+ "p5-param-categories": {
1639
+ "id": "p5-param-categories",
1640
+ "title": "Parameter Categories",
1641
+ "description": "Control P5 scene parameter visibility based on active capabilities like audio, video, and interaction.",
1642
+ "content": [
1643
+ {
1644
+ "type": "text",
1645
+ "markdown": "# Parameter Categories\n\nCategories let you tag parameters so they're only visible when the corresponding capability is active. An \"Audio Pulse\" [`viji.slider()`](../slider/) is useless if no audio source is connected — with `category: 'audio'`, it automatically appears when audio is available and hides when it's not.\n\n## The Four Categories\n\n| Category | Visible When | Use For |\n|---|---|---|\n| `'general'` | Always | Colors, sizes, speeds, shapes — anything that works without external input |\n| `'audio'` | Audio source is connected | Volume scaling, beat reactivity, frequency controls |\n| `'video'` | Video/camera source is connected | Video opacity, CV sensitivity, segmentation controls |\n| `'interaction'` | User interaction is enabled | Mouse effects, keyboard bindings, touch sensitivity |\n\n## Usage\n\n```javascript\n// @renderer p5\n\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, label: 'Audio Pulse', category: 'audio' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Dot', category: 'interaction' });\n```\n\n- `baseColor` ([`viji.color()`](../color/)) is always visible.\n- `pulseAmount` ([`viji.slider()`](../slider/)) only appears when audio is connected.\n- `showMouse` ([`viji.toggle()`](../toggle/)) only appears when interaction is enabled.\n\nIf you omit `category`, it defaults to `'general'` (always visible).\n\n## How It Works\n\n1. The artist sets `category` on each parameter during scene initialization.\n2. When the host application requests parameters, Viji filters them based on the current `CoreCapabilities`:\n - `hasAudio` — is an audio stream connected?\n - `hasVideo` — is a video/camera stream connected?\n - `hasInteraction` — is user interaction enabled?\n - `hasGeneral` — always `true`.\n3. Only parameters matching active capabilities are sent to the UI.\n\n> [!NOTE]\n> Categories filter at both the **group level** and the **individual parameter level**. If a group's category doesn't match, the entire group is hidden. If individual parameters within a visible group have non-matching categories, those parameters are hidden while the group remains visible.\n\n## Live Example\n\nParameters in three categories — `general` (always visible), `audio` (needs audio), and `interaction` (needs mouse):"
1646
+ },
1647
+ {
1648
+ "type": "live-example",
1649
+ "title": "P5 Parameter Categories",
1650
+ "sceneCode": "// @renderer p5\r\n\r\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\r\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\r\nconst showMouse = viji.toggle(true, { label: 'Mouse Dot', category: 'interaction' });\r\n\r\nlet angle = 0;\r\n\r\nfunction render(viji, p5) {\r\n p5.background(10, 10, 30, 40);\r\n\r\n angle += viji.deltaTime;\r\n\r\n const r = parseInt(baseColor.value.slice(1, 3), 16);\r\n const g = parseInt(baseColor.value.slice(3, 5), 16);\r\n const b = parseInt(baseColor.value.slice(5, 7), 16);\r\n\r\n let pulse = 0;\r\n if (viji.audio.isConnected) {\r\n pulse = viji.audio.volume.current * pulseAmount.value;\r\n }\r\n\r\n const baseR = Math.min(viji.width, viji.height) * (0.1 + pulse * 0.15);\r\n const cx = viji.width / 2 + p5.cos(angle) * viji.width * 0.2;\r\n const cy = viji.height / 2 + p5.sin(angle * 0.7) * viji.height * 0.2;\r\n\r\n p5.noStroke();\r\n p5.fill(r, g, b);\r\n p5.circle(cx, cy, baseR * 2);\r\n\r\n if (showMouse.value && viji.mouse.isInCanvas) {\r\n p5.fill(255, 255, 255, 200);\r\n p5.circle(viji.mouse.x, viji.mouse.y, Math.min(viji.width, viji.height) * 0.04);\r\n }\r\n}\r\n",
1651
+ "sceneFile": "categories-demo.scene.js",
1652
+ "capabilities": {
1653
+ "audio": true,
1654
+ "interaction": true
1655
+ }
1656
+ },
1657
+ {
1658
+ "type": "text",
1659
+ "markdown": "## Design Guidelines\n\n- **Default to `'general'`** unless the parameter genuinely depends on an external input.\n- **Use `'audio'` for parameters that only make sense with sound** — beat sensitivity, frequency scaling, audio decay.\n- **Use `'video'` for parameters tied to camera/video** — segmentation threshold, face tracking sensitivity.\n- **Use `'interaction'` for parameters that need user input** — mouse effect radius, keyboard shortcut configuration.\n- **Don't use categories as a replacement for grouping.** Categories control *visibility*; groups control *layout*. Use both when appropriate.\n\n## Categories and Groups\n\n`category` and `group` are orthogonal. A parameter in group `'effects'` with category `'audio'` will appear under the \"effects\" group heading, but only when audio is connected:\n\n```javascript\nconst bassReact = viji.slider(0.5, {\n label: 'Bass Reactivity',\n group: 'effects',\n category: 'audio'\n});\n\nconst colorShift = viji.slider(0.2, {\n label: 'Color Shift',\n group: 'effects',\n category: 'general'\n});\n```\n\nBoth parameters appear in the \"effects\" group, but `bassReact` only shows when audio is active.\n\n## Related\n\n- [Parameters Overview](../) — all parameter types and the basic pattern\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Native Categories](/native/parameters/categories) — same concept in the native renderer\n- [Shader Categories](/shader/parameters/categories) — same concept with `@viji-*` directives\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1660
+ }
1661
+ ]
1662
+ },
1663
+ "p5-pointer": {
1664
+ "id": "p5-pointer",
1665
+ "title": "Pointer (Unified)",
1666
+ "description": "A single input abstraction that works identically for mouse and touch — the recommended starting point for position, click, and drag interactions in P5 scenes.",
1667
+ "content": [
1668
+ {
1669
+ "type": "text",
1670
+ "markdown": "# Pointer (Unified Input)\n\n`viji.pointer` provides a single, unified input that works the same way whether the user is on a desktop with a mouse or on a mobile device using touch. **For most interactions — click, drag, position tracking — start here.**\n\n> [!WARNING]\n> P5's built-in input globals (`mouseX`, `mouseY`, `mouseIsPressed`, `touchX`, `touchY`, etc.) are **not updated** in the worker environment. P5 event callbacks (`mousePressed()`, `mouseDragged()`, `touchStarted()`, etc.) are **never called**. Use Viji's interaction APIs instead.\n\n## Why Use Pointer?\n\nDrag-to-orbit, click-to-place, and cursor-following effects work identically for mouse and touch. `viji.pointer` gives you one set of coordinates, one pressed state, and one delta — no separate code paths needed.\n\nUse [`viji.mouse`](../mouse/) when you need mouse-specific features like right-click, middle-click, or scroll wheel. Use [`viji.touches`](../touch/) when you need multi-touch, pressure, radius, or per-finger tracking.\n\n## API Reference\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `x` | `number` | Canvas-space X position (pixels) |\n| `y` | `number` | Canvas-space Y position (pixels) |\n| `deltaX` | `number` | Horizontal movement since last frame (pixels) |\n| `deltaY` | `number` | Vertical movement since last frame (pixels) |\n| `isDown` | `boolean` | `true` if left mouse button is held or a touch is active |\n| `wasPressed` | `boolean` | `true` for exactly one frame when input becomes active, then resets |\n| `wasReleased` | `boolean` | `true` for exactly one frame when input is released, then resets |\n| `isInCanvas` | `boolean` | `true` if the input position is within the canvas bounds |\n| `type` | `'mouse' \\| 'touch' \\| 'none'` | Which input device is currently active |\n\n## Coordinate System\n\nPointer coordinates are in **canvas-space pixels**, with `(0, 0)` at the top-left corner. Values match [`viji.width`](/p5/canvas-resolution) and [`viji.height`](/p5/canvas-resolution) (which are the same as `p5.width` and `p5.height`).\n\n## How It Works\n\nWhen a touch is active, the pointer follows the primary touch point. Otherwise, it follows the mouse. This switching happens automatically each frame.\n\n- **When a touch is active** (`viji.touches.count > 0`): pointer tracks the primary touch. `isDown` is always `true`, `type` is `'touch'`.\n- **When no touch is active**: pointer falls back to the mouse. `isDown` reflects the left mouse button, `type` is `'mouse'` when the cursor is over the canvas, or `'none'` when it's outside.\n\n`wasPressed` and `wasReleased` reflect frame-to-frame transitions of `isDown` — each is `true` for exactly one frame, then automatically resets.\n\n## P5 Input Migration\n\n| P5 Global | Viji Equivalent |\n|-----------|-----------------|\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `touchX` / `touchY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mousePressed()` callback | Check `viji.pointer.wasPressed` in `render()` |\n| `mouseDragged()` callback | Check `viji.pointer.isDown` and `deltaX`/`deltaY` in `render()` |\n| `touchStarted()` callback | Check `viji.pointer.wasPressed` in `render()` |\n\n## Basic Example"
1671
+ },
1672
+ {
1673
+ "type": "live-example",
1674
+ "title": "Pointer — Drag Trail",
1675
+ "sceneCode": "// @renderer p5\n\nconst trail = [];\nconst maxTrail = 80;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n}\n\nfunction render(viji, p5) {\n const ptr = viji.pointer;\n\n p5.background(240, 10, 8, 15);\n\n if (ptr.isDown) {\n trail.push({ x: ptr.x, y: ptr.y });\n if (trail.length > maxTrail) trail.shift();\n } else if (trail.length > 0) {\n trail.shift();\n }\n\n p5.noStroke();\n for (let i = 0; i < trail.length; i++) {\n const t = i / trail.length;\n const r = 3 + t * 12;\n p5.fill(200 + t * 60, 80, 65, t * 80);\n p5.ellipse(trail[i].x, trail[i].y, r * 2);\n }\n\n const cursorSize = Math.min(viji.width, viji.height) * 0.03;\n p5.fill(ptr.isDown ? p5.color(200, 80, 90) : p5.color(0, 0, 80, 50));\n p5.ellipse(ptr.x, ptr.y, cursorSize);\n\n p5.fill(0, 0, 100, 50);\n p5.textSize(Math.min(viji.width, viji.height) * 0.025);\n p5.textAlign(p5.LEFT);\n p5.text(`pointer: (${Math.round(ptr.x)}, ${Math.round(ptr.y)}) type: ${ptr.type}`, viji.width * 0.03, viji.height - viji.height * 0.06);\n p5.text(`isDown: ${ptr.isDown} inCanvas: ${ptr.isInCanvas}`, viji.width * 0.03, viji.height - viji.height * 0.03);\n}\n",
1676
+ "sceneFile": "pointer-p5-demo.scene.js",
1677
+ "capabilities": {
1678
+ "interaction": true
1679
+ }
1680
+ },
1681
+ {
1682
+ "type": "text",
1683
+ "markdown": "## Common Patterns\n\n### Click Detection\n\n```javascript\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) {\n p5.circle(viji.pointer.x, viji.pointer.y, 30);\n }\n}\n```\n\n### Drag Interaction\n\n```javascript\nlet offsetX = 0, offsetY = 0;\n\nfunction render(viji, p5) {\n const ptr = viji.pointer;\n if (ptr.isDown) {\n offsetX += ptr.deltaX;\n offsetY += ptr.deltaY;\n }\n p5.translate(offsetX, offsetY);\n}\n```\n\n## When to Use Mouse or Touch Instead\n\n| Need | Use |\n|------|-----|\n| Right-click or middle-click | [`viji.mouse`](../mouse/) |\n| Scroll wheel / zoom | [`viji.mouse`](../mouse/) — `wheelDelta`, `wheelX`, `wheelY` |\n| Multi-touch (pinch, two-finger rotation) | [`viji.touches`](../touch/) |\n| Per-touch pressure, radius, or velocity | [`viji.touches`](../touch/) |\n| Individual button states | [`viji.mouse`](../mouse/) — `leftButton`, `rightButton`, `middleButton` |\n\n## Related\n\n- [Mouse](../mouse/) — device-specific mouse access with buttons, wheel, and movement deltas\n- [Keyboard](../keyboard/) — key state queries and modifier tracking\n- [Touch](../touch/) — multi-touch with pressure, radius, velocity, and per-finger tracking\n- [Native Pointer](/native/pointer) — same API in the native renderer\n- [Shader Pointer Uniforms](/shader/pointer) — GLSL uniforms for unified pointer input"
1684
+ }
1685
+ ]
1686
+ },
1687
+ "p5-mouse": {
1688
+ "id": "p5-mouse",
1689
+ "title": "Mouse",
1690
+ "description": "Full mouse API for P5 scenes — position, buttons, movement deltas, scroll wheel, and frame-based press/release detection.",
1691
+ "content": [
1692
+ {
1693
+ "type": "text",
1694
+ "markdown": "# Mouse\n\n`viji.mouse` provides detailed mouse input including individual button states, movement deltas, and scroll wheel data.\n\n> [!TIP]\n> For simple position, click, and drag interactions that should work on both mouse and touch devices, use [`viji.pointer`](../pointer/) instead. The mouse API is for when you need mouse-specific features like right-click, middle-click, or scroll wheel.\n\n> [!WARNING]\n> P5's built-in input globals (`mouseX`, `mouseY`, `mouseIsPressed`, `mouseButton`) are **not updated** in the worker environment. P5 event callbacks (`mousePressed()`, `mouseDragged()`, `mouseReleased()`, `mouseClicked()`) are **never called**. Use `viji.mouse` instead.\n\n## API Reference\n\n### Position\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `x` | `number` | Canvas-space X position (pixels) |\n| `y` | `number` | Canvas-space Y position (pixels) |\n| `isInCanvas` | `boolean` | `true` when the cursor is over the canvas |\n\n### Buttons\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `isPressed` | `boolean` | `true` if any mouse button is currently held |\n| `leftButton` | `boolean` | Left button state |\n| `rightButton` | `boolean` | Right button state |\n| `middleButton` | `boolean` | Middle button state |\n\n### Movement\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `deltaX` | `number` | Horizontal movement this frame (pixels) | Yes → `0` |\n| `deltaY` | `number` | Vertical movement this frame (pixels) | Yes → `0` |\n| `wasMoved` | `boolean` | `true` if the mouse moved this frame | Yes → `false` |\n\n### Scroll Wheel\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `wheelDelta` | `number` | Vertical scroll accumulated this frame | Yes → `0` |\n| `wheelX` | `number` | Horizontal scroll accumulated this frame | Yes → `0` |\n| `wheelY` | `number` | Vertical scroll accumulated this frame | Yes → `0` |\n\n### Frame Events\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `wasPressed` | `boolean` | `true` for exactly one frame when any button is first pressed | Yes → `false` |\n| `wasReleased` | `boolean` | `true` for exactly one frame when any button is released | Yes → `false` |\n\n## P5 Input Migration\n\n| P5 Global | Viji Equivalent |\n|-----------|-----------------|\n| `mouseX` | `viji.mouse.x` |\n| `mouseY` | `viji.mouse.y` |\n| `mouseIsPressed` | `viji.mouse.isPressed` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `mouseButton === RIGHT` | `viji.mouse.rightButton` |\n| `mouseButton === CENTER` | `viji.mouse.middleButton` |\n| `mousePressed()` | Check `viji.mouse.wasPressed` in `render()` |\n| `mouseReleased()` | Check `viji.mouse.wasReleased` in `render()` |\n| `mouseMoved()` | Check `viji.mouse.wasMoved` in `render()` |\n| `mouseWheel(event)` | Read `viji.mouse.wheelDelta` in `render()` |\n\n## Coordinate System\n\nMouse coordinates are in **canvas-space pixels**, with `(0, 0)` at the top-left corner. Values range from `0` to `viji.width` horizontally and `0` to `viji.height` vertically (same as `p5.width` and `p5.height`). The right-click context menu is automatically suppressed on the canvas.\n\n## Frame Lifecycle\n\nPer-frame properties (`deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`) reset to zero/false at the start of each frame. If multiple mouse events occur within a single frame, deltas and wheel values **accumulate**, and `wasPressed`/`wasReleased` are OR'd across all events.\n\nPersistent properties (`x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`) retain their values until the next event changes them.\n\n## Basic Example"
1695
+ },
1696
+ {
1697
+ "type": "live-example",
1698
+ "title": "Mouse — Buttons & Wheel",
1699
+ "sceneCode": "// @renderer p5\n\nlet hue = 200;\nlet zoom = 1;\nlet prevRight = false;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n}\n\nfunction render(viji, p5) {\n const m = viji.mouse;\n const size = Math.min(viji.width, viji.height);\n\n if (m.rightButton && !prevRight) hue = (hue + 50) % 360;\n prevRight = m.rightButton;\n\n zoom -= m.wheelDelta * 0.001;\n zoom = Math.max(0.3, Math.min(5, zoom));\n\n p5.background(240, 10, 8, 20);\n\n const speed = Math.sqrt(m.deltaX ** 2 + m.deltaY ** 2);\n const radius = (size * 0.02 + speed * 1.5) * zoom;\n\n if (m.isInCanvas) {\n p5.noStroke();\n p5.fill(hue, 80, 65, m.isPressed ? 90 : 40);\n p5.ellipse(m.x, m.y, radius * 2);\n\n if (m.leftButton) {\n p5.noFill();\n p5.stroke(hue, 80, 75, 60);\n p5.strokeWeight(2);\n p5.ellipse(m.x, m.y, radius * 2);\n }\n }\n\n p5.noStroke();\n p5.fill(0, 0, 100, 50);\n p5.textSize(size * 0.022);\n p5.textFont('monospace');\n p5.textAlign(p5.LEFT);\n const y0 = viji.height - size * 0.12;\n p5.text(`pos: (${Math.round(m.x)}, ${Math.round(m.y)}) inCanvas: ${m.isInCanvas}`, size * 0.03, y0);\n p5.text(`buttons: L[${m.leftButton ? '\\u25A0' : '\\u25A1'}] R[${m.rightButton ? '\\u25A0' : '\\u25A1'}] M[${m.middleButton ? '\\u25A0' : '\\u25A1'}]`, size * 0.03, y0 + size * 0.03);\n p5.text(`wheel: ${m.wheelDelta.toFixed(1)} zoom: ${zoom.toFixed(2)}`, size * 0.03, y0 + size * 0.06);\n}\n",
1700
+ "sceneFile": "mouse-p5-demo.scene.js",
1701
+ "capabilities": {
1702
+ "interaction": true
1703
+ }
1704
+ },
1705
+ {
1706
+ "type": "text",
1707
+ "markdown": "## Common Patterns\n\n### Right-Click Action\n\n```javascript\nlet prevRight = false;\n\nfunction render(viji, p5) {\n if (viji.mouse.rightButton && !prevRight) {\n cycleColor();\n }\n prevRight = viji.mouse.rightButton;\n}\n```\n\n### Scroll Zoom\n\n```javascript\nlet zoom = 1;\n\nfunction render(viji, p5) {\n zoom -= viji.mouse.wheelDelta * 0.001;\n zoom = Math.max(0.1, Math.min(10, zoom));\n p5.scale(zoom);\n}\n```\n\n## Related\n\n- [Pointer (Unified)](../pointer/) — recommended starting point for cross-device interactions\n- [Keyboard](../keyboard/) — key state queries and modifier tracking\n- [Touch](../touch/) — multi-touch input with pressure, radius, and velocity\n- [Native Mouse](/native/mouse) — same API in the native renderer\n- [Shader Mouse Uniforms](/shader/mouse) — GLSL uniforms for mouse input"
1708
+ }
1709
+ ]
1710
+ },
1711
+ "p5-keyboard": {
1712
+ "id": "p5-keyboard",
1713
+ "title": "Keyboard",
1714
+ "description": "Full keyboard API for P5 scenes — key state queries, modifier tracking, and frame-based press/release detection.",
1715
+ "content": [
1716
+ {
1717
+ "type": "text",
1718
+ "markdown": "# Keyboard\n\n`viji.keyboard` provides real-time keyboard state with per-key press detection, modifier tracking, and frame-based event queries.\n\n> [!WARNING]\n> P5's built-in input globals (`keyIsPressed`, `key`, `keyCode`) are **not updated** in the worker environment. P5 event callbacks (`keyPressed()`, `keyReleased()`, `keyTyped()`) are **never called**. Use `viji.keyboard` instead.\n\n## API Reference\n\n### Methods\n\n| Method | Returns | Description |\n|--------|---------|-------------|\n| `isPressed(key)` | `boolean` | `true` if the key is currently held down |\n| `wasPressed(key)` | `boolean` | `true` for exactly one frame when the key is first pressed, then resets |\n| `wasReleased(key)` | `boolean` | `true` for exactly one frame when the key is released, then resets |\n\nAll three methods are **case-insensitive** — `isPressed('a')` and `isPressed('A')` are equivalent.\n\n### Properties\n\n| Property | Type | Description | Resets each frame |\n|----------|------|-------------|-------------------|\n| `activeKeys` | `Set<string>` | All currently held keys (lowercase) | No |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame (lowercase) | Yes → cleared |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame (lowercase) | Yes → cleared |\n| `lastKeyPressed` | `string` | Most recently pressed key (original case) | No |\n| `lastKeyReleased` | `string` | Most recently released key (original case) | No |\n\n### Modifier Keys\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `shift` | `boolean` | Shift key state |\n| `ctrl` | `boolean` | Ctrl key state |\n| `alt` | `boolean` | Alt key state |\n| `meta` | `boolean` | Meta/Cmd key state |\n\n## P5 Input Migration\n\n| P5 Global | Viji Equivalent |\n|-----------|-----------------|\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `keyCode === LEFT_ARROW` | `viji.keyboard.isPressed('arrowleft')` |\n| `keyCode === UP_ARROW` | `viji.keyboard.isPressed('arrowup')` |\n| `keyPressed()` | Check `viji.keyboard.wasPressed('key')` in `render()` |\n| `keyReleased()` | Check `viji.keyboard.wasReleased('key')` in `render()` |\n\n## Key Names\n\nKey names follow the browser's `event.key` standard. Common values:\n\n| Key | Name to use |\n|-----|-------------|\n| Letters | `'a'`, `'b'`, `'z'` (case-insensitive) |\n| Digits | `'0'`, `'1'`, `'9'` |\n| Arrows | `'arrowup'`, `'arrowdown'`, `'arrowleft'`, `'arrowright'` |\n| Space | `' '` (a space character) |\n| Enter | `'enter'` |\n| Escape | `'escape'` |\n\n## Frame Lifecycle\n\n- `pressedThisFrame` and `releasedThisFrame` are cleared at the start of each frame.\n- Key repeats are suppressed — holding a key down fires `wasPressed()` only on the first frame, not on subsequent repeat events.\n- `activeKeys` persists across frames until a `keyup` event is received.\n\n## Basic Example"
1719
+ },
1720
+ {
1721
+ "type": "live-example",
1722
+ "title": "Keyboard — Movement & State",
1723
+ "sceneCode": "// @renderer p5\n\nlet px, py;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n}\n\nfunction render(viji, p5) {\n const w = viji.width, h = viji.height;\n const size = Math.min(w, h);\n const kb = viji.keyboard;\n\n if (px === undefined) { px = w / 2; py = h / 2; }\n\n const speed = size * 0.4 * viji.deltaTime * (kb.shift ? 2.5 : 1);\n if (kb.isPressed('w') || kb.isPressed('arrowup')) py -= speed;\n if (kb.isPressed('s') || kb.isPressed('arrowdown')) py += speed;\n if (kb.isPressed('a') || kb.isPressed('arrowleft')) px -= speed;\n if (kb.isPressed('d') || kb.isPressed('arrowright')) px += speed;\n px = Math.max(0, Math.min(w, px));\n py = Math.max(0, Math.min(h, py));\n\n p5.background(240, 10, 8, 15);\n\n const r = size * 0.03;\n p5.noStroke();\n p5.fill((viji.time * 40) % 360, 80, 65);\n p5.ellipse(px, py, r * 2);\n\n p5.fill(0, 0, 100, 50);\n p5.textSize(size * 0.022);\n p5.textFont('monospace');\n p5.textAlign(p5.LEFT);\n const y0 = h - size * 0.09;\n p5.text(`active: [${[...kb.activeKeys].join(', ')}]`, size * 0.03, y0);\n p5.text(`last: \"${kb.lastKeyPressed}\" mods: ${kb.shift ? 'Shift ' : ''}${kb.ctrl ? 'Ctrl ' : ''}${kb.alt ? 'Alt ' : ''}${!kb.shift && !kb.ctrl && !kb.alt ? 'none' : ''}`, size * 0.03, y0 + size * 0.03);\n\n p5.fill(0, 0, 100, 30);\n p5.textAlign(p5.CENTER);\n p5.text('WASD / Arrows to move \\u2022 Shift for speed', w / 2, size * 0.04);\n}\n",
1724
+ "sceneFile": "keyboard-p5-demo.scene.js",
1725
+ "capabilities": {
1726
+ "interaction": true
1727
+ }
1728
+ },
1729
+ {
1730
+ "type": "text",
1731
+ "markdown": "## Common Patterns\n\n### WASD Movement\n\n```javascript\nlet x = 0, y = 0;\n\nfunction render(viji, p5) {\n const kb = viji.keyboard;\n const speed = 200 * viji.deltaTime * (kb.shift ? 2.5 : 1);\n\n if (kb.isPressed('w') || kb.isPressed('arrowup')) y -= speed;\n if (kb.isPressed('s') || kb.isPressed('arrowdown')) y += speed;\n if (kb.isPressed('a') || kb.isPressed('arrowleft')) x -= speed;\n if (kb.isPressed('d') || kb.isPressed('arrowright')) x += speed;\n}\n```\n\n### Single-Press Toggle\n\n```javascript\nlet showGrid = false;\n\nfunction render(viji, p5) {\n if (viji.keyboard.wasPressed('g')) {\n showGrid = !showGrid;\n }\n}\n```\n\n## Related\n\n- [Pointer (Unified)](../pointer/) — recommended starting point for position and click interactions\n- [Mouse](../mouse/) — mouse position, buttons, and scroll wheel\n- [Touch](../touch/) — multi-touch input with pressure and velocity\n- [Native Keyboard](/native/keyboard) — same API in the native renderer\n- [Shader Keyboard Uniforms](/shader/keyboard) — GLSL uniforms for common keys"
1732
+ }
1733
+ ]
1734
+ },
1735
+ "p5-touch": {
1736
+ "id": "p5-touch",
1737
+ "title": "Touch",
1738
+ "description": "Multi-touch API for P5 scenes — per-finger tracking with position, pressure, radius, velocity, and lifecycle events.",
1739
+ "content": [
1740
+ {
1741
+ "type": "text",
1742
+ "markdown": "# Touch\n\n`viji.touches` provides full multi-touch input with per-finger position, pressure, contact radius, velocity, and lifecycle tracking.\n\n> [!TIP]\n> For single-point interactions (click, drag, follow) that should work on both touch and mouse, use [`viji.pointer`](../pointer/) instead. The touch API is for when you need multi-touch gestures, pressure sensitivity, contact radius, or per-finger velocity.\n\n> [!WARNING]\n> P5's built-in touch globals (`touchX`, `touchY`, `touches`) are **not updated** in the worker environment. P5 event callbacks (`touchStarted()`, `touchMoved()`, `touchEnded()`) are **never called**. Use `viji.touches` instead.\n\n## API Reference\n\n### TouchAPI (`viji.touches`)\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `points` | `TouchPoint[]` | All currently active touch points |\n| `count` | `number` | Number of active touches |\n| `started` | `TouchPoint[]` | Touches that started this frame |\n| `moved` | `TouchPoint[]` | Touches that moved this frame |\n| `ended` | `TouchPoint[]` | Touches that ended this frame |\n| `primary` | `TouchPoint \\| null` | First active touch (convenience) |\n\n### TouchPoint\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `id` | `number` | Unique touch identifier (stable across frames) |\n| `x` | `number` | Canvas-space X position (pixels) |\n| `y` | `number` | Canvas-space Y position (pixels) |\n| `pressure` | `number` | Touch pressure (0–1, device-dependent) |\n| `force` | `number` | Same as `pressure` (alias) |\n| `radius` | `number` | Contact radius — `Math.max(radiusX, radiusY)` |\n| `radiusX` | `number` | Horizontal contact radius (pixels) |\n| `radiusY` | `number` | Vertical contact radius (pixels) |\n| `rotationAngle` | `number` | Contact area rotation (radians) |\n| `isInCanvas` | `boolean` | `true` if this touch is within the canvas bounds |\n| `deltaX` | `number` | Horizontal movement since last frame (pixels) |\n| `deltaY` | `number` | Vertical movement since last frame (pixels) |\n| `velocity` | `{ x: number, y: number }` | Movement velocity (pixels/second) |\n| `isNew` | `boolean` | `true` for exactly one frame when this touch starts, then resets |\n| `isActive` | `boolean` | `true` while the touch is ongoing |\n| `isEnding` | `boolean` | `true` for exactly one frame when this touch ends, then resets |\n\n## P5 Input Migration\n\n| P5 Global | Viji Equivalent |\n|-----------|-----------------|\n| `touchX` / `touchY` | `viji.touches.primary.x` / `.y` (check for `null` first) |\n| `touches` array | `viji.touches.points` |\n| `touchStarted()` | Check `viji.touches.started` in `render()` |\n| `touchMoved()` | Check `viji.touches.moved` in `render()` |\n| `touchEnded()` | Check `viji.touches.ended` in `render()` |\n\n## Coordinate System\n\nTouch coordinates are in **canvas-space pixels**, with `(0, 0)` at the top-left corner — identical to [`viji.mouse`](../mouse/) coordinates (same as `p5.width` / `p5.height`). When a touch starts on the canvas and is dragged outside, the browser continues delivering events, and `isInCanvas` correctly reports `false`.\n\n## Frame Lifecycle\n\n- `started`, `moved`, and `ended` arrays are cleared at the start of each frame.\n- `points` and `count` reflect the current state after all events in the frame.\n- A touch appears in `started` on the frame it begins (with `isNew: true`), in `ended` on the frame it lifts (with `isEnding: true`).\n- `primary` is always `points[0]` or `null` when no touches are active.\n\n## Basic Example"
1743
+ },
1744
+ {
1745
+ "type": "live-example",
1746
+ "title": "Touch — Multi-Point Tracker",
1747
+ "sceneCode": "// @renderer p5\n\nconst ripples = [];\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100, 100);\n}\n\nfunction render(viji, p5) {\n const size = Math.min(viji.width, viji.height);\n const touch = viji.touches;\n const dt = viji.deltaTime;\n\n for (const pt of touch.started) {\n ripples.push({ x: pt.x, y: pt.y, r: 0, alpha: 100 });\n }\n\n p5.background(240, 10, 8, 20);\n\n p5.noFill();\n for (let i = ripples.length - 1; i >= 0; i--) {\n const rp = ripples[i];\n rp.r += size * 0.3 * dt;\n rp.alpha -= dt * 80;\n if (rp.alpha <= 0) { ripples.splice(i, 1); continue; }\n p5.stroke(200, 80, 70, rp.alpha);\n p5.strokeWeight(2);\n p5.ellipse(rp.x, rp.y, rp.r * 2);\n }\n\n p5.noStroke();\n for (let i = 0; i < touch.count; i++) {\n const pt = touch.points[i];\n const r = size * 0.02 + pt.pressure * size * 0.04;\n\n p5.fill(120 + i * 60, 80, 65, 80);\n p5.ellipse(pt.x, pt.y, r * 2);\n\n const speed = Math.sqrt(pt.velocity.x ** 2 + pt.velocity.y ** 2);\n if (speed > 10) {\n const len = Math.min(speed * 0.05, size * 0.08);\n const angle = Math.atan2(pt.velocity.y, pt.velocity.x);\n p5.stroke(120 + i * 60, 80, 75, 50);\n p5.strokeWeight(2);\n p5.line(pt.x, pt.y, pt.x + Math.cos(angle) * len, pt.y + Math.sin(angle) * len);\n p5.noStroke();\n }\n\n p5.fill(0, 0, 100, 60);\n p5.textSize(size * 0.02);\n p5.textAlign(p5.CENTER);\n p5.text('T' + pt.id, pt.x, pt.y - r - size * 0.01);\n }\n\n p5.noStroke();\n p5.fill(0, 0, 100, 40);\n p5.textSize(size * 0.022);\n p5.textFont('monospace');\n p5.textAlign(p5.LEFT);\n p5.text(`touches: ${touch.count} primary: ${touch.primary ? 'T' + touch.primary.id : '-'}`, size * 0.03, viji.height - size * 0.03);\n}\n",
1748
+ "sceneFile": "touch-p5-demo.scene.js",
1749
+ "capabilities": {
1750
+ "interaction": true
1751
+ }
1752
+ },
1753
+ {
1754
+ "type": "text",
1755
+ "markdown": "## Common Patterns\n\n### Draw at Each Touch Point\n\n```javascript\nfunction render(viji, p5) {\n for (const pt of viji.touches.points) {\n p5.circle(pt.x, pt.y, 20 + pt.pressure * 60);\n }\n}\n```\n\n### Detect New Touches\n\n```javascript\nfunction render(viji, p5) {\n for (const pt of viji.touches.started) {\n spawnRipple(pt.x, pt.y);\n }\n}\n```\n\n### Two-Finger Distance\n\n```javascript\nfunction render(viji, p5) {\n if (viji.touches.count >= 2) {\n const a = viji.touches.points[0];\n const b = viji.touches.points[1];\n const dist = p5.dist(a.x, a.y, b.x, b.y);\n applyZoom(dist);\n }\n}\n```\n\n## Related\n\n- [Pointer (Unified)](../pointer/) — recommended starting point for single-point cross-device interactions\n- [Mouse](../mouse/) — mouse position, buttons, and scroll wheel\n- [Keyboard](../keyboard/) — key state queries and modifier tracking\n- [Native Touch](/native/touch) — same API in the native renderer\n- [Shader Touch Uniforms](/shader/touch) — GLSL uniforms for touch positions"
1756
+ }
1757
+ ]
1758
+ },
1759
+ "shader-quickstart": {
1760
+ "id": "shader-quickstart",
1761
+ "title": "shader-quickstart",
1762
+ "description": "Build your first Viji scene with GLSL fragment shaders running directly on the GPU.",
1763
+ "content": [
1764
+ {
1765
+ "type": "text",
1766
+ "markdown": "# Shader Quick Start\r\n\r\nThe shader renderer lets you write GLSL fragment shaders that run on the GPU. Viji handles the fullscreen quad, uniform injection, and parameter system — you write only the shader logic.\r\n\r\n> [!IMPORTANT]\r\n> P5 and shader scenes must declare their renderer type as the first comment:\r\n> ```\r\n> // @renderer shader\r\n> ```\r\n> Without this directive, the scene defaults to the native renderer.\r\n\r\n## Your First Shader"
1767
+ },
1768
+ {
1769
+ "type": "live-example",
1770
+ "title": "Shader — Wave Pattern",
1771
+ "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-slider:scale label:\"Scale\" default:8.0 min:1.0 max:20.0 step:0.5\r\n// @viji-color:tint label:\"Tint\" default:#00ffcc\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n\r\n float pattern = sin(uv.x * scale + phase)\r\n * cos(uv.y * scale - phase * 0.7)\r\n * 0.5 + 0.5;\r\n\r\n vec3 color = tint * pattern;\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
1772
+ "sceneFile": "quickstart-shader.scene.glsl"
1773
+ },
1774
+ {
1775
+ "type": "text",
1776
+ "markdown": "### What's Happening\r\n\r\n**Comment directives — parsed before compilation:**\r\n\r\n- `// @renderer shader` tells Viji to use the shader renderer.\r\n- `// @viji-slider:speed ...` declares a parameter. Viji generates a `uniform float speed;` automatically.\r\n- `// @viji-color:tint ...` declares a color parameter. Viji generates a `uniform vec3 tint;`.\r\n- `// @viji-accumulator:phase rate:speed` creates a CPU-side accumulator that adds `speed × deltaTime` every frame. The result is a `uniform float phase;` that increases smoothly — no jumps when the slider changes.\r\n\r\n**`void main()` — runs for every pixel, every frame:**\r\n\r\n- `gl_FragCoord.xy / u_resolution` gives normalized UV coordinates (0–1).\r\n- `phase` is the accumulator — use it instead of `u_time * speed` for smooth, slider-driven animation.\r\n- `speed`, `scale`, `tint` are your parameter uniforms — updated live as the user adjusts controls.\r\n- `gl_FragColor` sets the output color for each pixel.\r\n\r\n> [!NOTE]\r\n> Parameter declarations use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\r\n\r\n## Scene Structure\r\n\r\nA shader scene is a GLSL fragment shader with comment directives:\r\n\r\n```glsl\r\n// @renderer shader\r\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n gl_FragColor = vec4(uv * brightness, 0.5, 1.0);\r\n}\r\n```\r\n\r\n- **No `precision` or `uniform` declarations needed.** Viji auto-injects `precision mediump float;` and all uniform declarations.\r\n- **No vertex shader.** Viji renders a fullscreen quad; your fragment shader defines the color of every pixel.\r\n- **Parameters become uniforms.** `// @viji-slider:name` becomes `uniform float name;` automatically.\r\n\r\n> [!NOTE]\r\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations — both built-in uniforms (`u_resolution`, `u_time`, etc.) and parameter uniforms from `@viji-*` directives. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or any uniforms — they will conflict. If you use `#version 300 es`, Viji will handle its placement automatically.\r\n\r\n## Parameter Types\r\n\r\nDeclare parameters with `// @viji-TYPE:uniformName key:value` syntax:\r\n\r\n| Type | Uniform | Example |\r\n|------|---------|---------|\r\n| `slider` | `float` | `// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0` |\r\n| `number` | `float` | `// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0` |\r\n| `color` | `vec3` | `// @viji-color:tint label:\"Tint\" default:#ff6600` |\r\n| `toggle` | `bool` | `// @viji-toggle:invert label:\"Invert\" default:false` |\r\n| `select` | `int` | `// @viji-select:mode label:\"Mode\" default:0 options:[\"Wave\",\"Spiral\",\"Grid\"]` |\r\n| `image` | `sampler2D` | `// @viji-image:tex label:\"Texture\"` |\r\n| `button` | `bool` | `// @viji-button:trigger label:\"Trigger\"` |\r\n| `accumulator` | `float` | `// @viji-accumulator:phase rate:speed` |\r\n\r\nConfig keys: `label`, `default`, `min`, `max`, `step`, `description`, `group`, `category`.\r\n\r\n### Accumulators\r\n\r\nAccumulators solve the \"jumping animation\" problem. When you write `u_time * speed`, changing the `speed` slider causes a visible jump because the entire phase is recalculated instantly. Accumulators integrate the rate over time on the CPU side:\r\n\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\r\n- `phase` increases by `speed × deltaTime` each frame — changing `speed` only affects future growth, never jumps backward.\r\n- `rate` can reference any declared parameter name or be a numeric constant (e.g., `rate:1.5`).\r\n- Accumulators have no UI control — they are internal uniform values.\r\n- Optional `default` sets the starting value (defaults to 0).\r\n\r\n> [!WARNING]\r\n> Do not use the `u_` prefix for your parameter uniform names — it is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `colorMix`, `intensity` instead.\r\n\r\n## Built-in Uniforms\r\n\r\nThese are always available — no declaration needed:\r\n\r\n| Uniform | Type | Description |\r\n|---------|------|-------------|\r\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\r\n| `u_time` | `float` | Elapsed seconds since scene start |\r\n| `u_deltaTime` | `float` | Seconds since last frame |\r\n| `u_frame` | `int` | Current frame number |\r\n| `u_pointer` | `vec2` | Unified input position — mouse or touch (pixels) |\r\n| `u_pointerDown` | `bool` | Unified input active (left-click or touch) |\r\n| `u_mouse` | `vec2` | Mouse position in pixels |\r\n| `u_mousePressed` | `bool` | Any mouse button is pressed |\r\n| `u_audioVolume` | `float` | Overall audio volume (0–1) |\r\n| `u_audioLow` | `float` | Low frequency energy (0–1) |\r\n| `u_audioMid` | `float` | Mid frequency energy (0–1) |\r\n| `u_audioHigh` | `float` | High frequency energy (0–1) |\r\n| `u_audioKick` | `float` | Kick beat detection (0–1) |\r\n| `u_video` | `sampler2D` | Current video frame |\r\n\r\nSee [API Reference](/shader/api-reference) for the complete list of 100+ built-in uniforms.\r\n\r\n## Essential Patterns\r\n\r\n**Normalized coordinates:**\r\n\r\n```glsl\r\nvec2 uv = gl_FragCoord.xy / u_resolution; // 0..1\r\nvec2 centered = uv - 0.5; // -0.5..0.5\r\ncentered.x *= u_resolution.x / u_resolution.y; // aspect-corrected\r\n```\r\n\r\n**Distance fields:**\r\n\r\n```glsl\r\nfloat d = length(centered); // distance from center\r\nfloat circle = smoothstep(0.3, 0.29, d); // anti-aliased circle\r\n```\r\n\r\n> [!NOTE]\r\n> Always use `u_resolution` for positioning and sizing and `u_time` / `u_deltaTime` for animation. This keeps your shader resolution-agnostic and frame-rate-independent.\r\n\r\n## GLSL Version\r\n\r\nBy default, shaders use **GLSL ES 1.00** (WebGL 1). If you need WebGL 2 features, add `#version 300 es` as the first line:\r\n\r\n```glsl\r\n#version 300 es\r\n// @renderer shader\r\n\r\n// ES 3.00 requires explicit output declaration\r\nout vec4 fragColor;\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n fragColor = vec4(uv, sin(u_time), 1.0);\r\n}\r\n```\r\n\r\nES 3.00 differences: `gl_FragColor` → `out vec4`, `texture2D()` → `texture()`. Use ES 1.00 for maximum compatibility.\r\n\r\n## Backbuffer (Previous Frame)\r\n\r\nViji gives you access to the previous frame as a texture — just reference `backbuffer` in your code and it's automatically enabled:\r\n\r\n```glsl\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec4 prev = texture2D(backbuffer, uv); // previous frame\r\n vec3 current = vec3(/* ... your effect ... */);\r\n gl_FragColor = vec4(mix(prev.rgb, current, 0.1), 1.0); // 90% trail\r\n}\r\n```\r\n\r\nThis enables feedback effects, trails, motion blur, and accumulation buffers. No setup needed — Viji detects the `backbuffer` reference and creates the ping-pong framebuffers automatically.\r\n\r\nSee [Backbuffer](/shader/backbuffer) for detailed patterns and techniques.\r\n\r\n## Shadertoy Compatibility\r\n\r\nIf you have existing Shadertoy shaders, see [Shadertoy Compatibility](/shader/shadertoy) for a mapping of Shadertoy uniforms to Viji equivalents.\r\n\r\n## Next Steps\r\n\r\n- [Shader Basics](/shader/basics) — uniforms, coordinate systems, techniques\r\n- [Parameters](/shader/parameters) — all parameter types for shaders\r\n- [Audio Uniforms](/shader/audio) — react to music in GLSL\r\n- [Backbuffer](/shader/backbuffer) — feedback effects using the previous frame\r\n- [API Reference](/shader/api-reference) — complete list of built-in uniforms\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1777
+ }
1778
+ ]
1779
+ },
1780
+ "shader-basics": {
1781
+ "id": "shader-basics",
1782
+ "title": "Shader Basics",
1783
+ "description": "GLSL fragment shader fundamentals — auto-injection, GLSL versions, shader structure, and the @renderer directive.",
1784
+ "content": [
1785
+ {
1786
+ "type": "text",
1787
+ "markdown": "# Shader Basics\n\nThe shader renderer lets you write GLSL fragment shaders that run on the GPU. Viji handles the fullscreen quad, uniform injection, and compilation pipeline. This page covers the fundamentals: how a shader scene is structured, what Viji auto-injects, and how GLSL versions work.\n\n## The `@renderer` Directive\n\n> [!IMPORTANT]\n> P5 and shader scenes must declare their renderer type as the first comment:\n> ```\n> // @renderer p5\n> ```\n> or\n> ```\n> // @renderer shader\n> ```\n> Without this directive, the scene defaults to the native renderer.\n\n## Fragment Shader Structure\n\nA Viji shader scene is a GLSL fragment shader. You write only `void main()` (and optional helper functions) — Viji provides everything else:\n\n```glsl\n// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, 0.5, 1.0);\n}\n```\n\nThere is no vertex shader to write. Viji renders a fullscreen quad that covers the entire canvas — your fragment shader determines the color of every pixel."
1788
+ },
1789
+ {
1790
+ "type": "live-example",
1791
+ "title": "Minimal Shader",
1792
+ "sceneCode": "// @renderer shader\n// @viji-color:color1 label:\"Color A\" default:#ff4488\n// @viji-color:color2 label:\"Color B\" default:#4488ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec3 color = mix(color1, color2, uv.x);\n float wave = sin(uv.y * 20.0 + u_time * 3.0) * 0.05;\n color += wave;\n gl_FragColor = vec4(color, 1.0);\n}\n",
1793
+ "sceneFile": "basics-minimal.scene.glsl"
1794
+ },
1795
+ {
1796
+ "type": "text",
1797
+ "markdown": "## Auto-Injection\n\n> [!NOTE]\n> The Viji shader renderer automatically injects `precision mediump float;` and all `uniform` declarations — both built-in uniforms (`u_resolution`, `u_time`, etc.) and parameter uniforms from `@viji-*` directives. Write only your helper functions and `void main() { ... }`. Do NOT redeclare `precision` or any uniforms — they will conflict. If you use `#version 300 es`, Viji will handle its placement automatically.\n\nWhen your shader is compiled, Viji prepends the following before your code:\n\n1. `#extension GL_OES_standard_derivatives : enable` — only if your code uses `fwidth` (GLSL ES 1.00 only)\n2. `precision mediump float;`\n3. All built-in uniform declarations (`u_resolution`, `u_time`, `u_deltaTime`, etc.)\n4. All `@viji-*` parameter uniform declarations\n\n**You must not redeclare any of these.** Writing `precision mediump float;` or `uniform vec2 u_resolution;` in your code will cause a compilation error.\n\n### What You Write vs What Viji Adds\n\n```glsl\n// What you write:\n// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n\n// What Viji compiles (conceptual):\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform float u_deltaTime;\nuniform int u_frame;\nuniform float u_fps;\n// ... (100+ more built-in uniforms)\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n## Key Built-in Uniforms\n\nThese are always available — a brief overview (each has a dedicated page):\n\n| Uniform | Type | Description | Details |\n|---------|------|-------------|---------|\n| [`u_resolution`](/shader/resolution) | `vec2` | Canvas width and height in pixels | [Resolution & Coordinates](/shader/resolution) |\n| [`u_time`](/shader/timing) | `float` | Elapsed seconds since scene start | [Timing & Animation](/shader/timing) |\n| [`u_deltaTime`](/shader/timing) | `float` | Seconds since last frame | [Timing & Animation](/shader/timing) |\n| [`u_frame`](/shader/timing) | `int` | Current frame number | [Timing & Animation](/shader/timing) |\n| [`u_fps`](/shader/timing) | `float` | Target FPS | [Timing & Animation](/shader/timing) |\n| `u_mouse` | `vec2` | Mouse position in pixels | [Mouse Uniforms](/shader/mouse) |\n\nSee [API Reference](/shader/api-reference) for the complete list of 100+ built-in uniforms.\n\n## GLSL Versions\n\n### GLSL ES 1.00 (Default)\n\nThe default. No `#version` declaration needed. Maximum browser compatibility. Uses `gl_FragColor` for output and `texture2D()` for texture sampling:\n\n```glsl\n// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, 0.5, 1.0);\n}\n```\n\n### GLSL ES 3.00 (WebGL 2)\n\nFor WebGL 2 features, add `#version 300 es` as the very first line (before the `@renderer` directive). Viji extracts it, places it at the top of the compiled output, and requests a WebGL 2 context:"
1798
+ },
1799
+ {
1800
+ "type": "live-example",
1801
+ "title": "ES 3.00 — Output Variable",
1802
+ "sceneCode": "#version 300 es\n// @renderer shader\n// @viji-slider:zoom label:\"Zoom\" default:4.0 min:1.0 max:12.0 step:0.5\n// @viji-color:gridColor label:\"Grid Color\" default:#44ffaa\n\nout vec4 fragColor;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 grid = fract(uv * zoom);\n float line = step(0.95, max(grid.x, grid.y));\n vec3 bg = vec3(0.04, 0.04, 0.1);\n vec3 color = mix(bg, gridColor, line);\n fragColor = vec4(color, 1.0);\n}\n",
1803
+ "sceneFile": "basics-es300.scene.glsl"
1804
+ },
1805
+ {
1806
+ "type": "text",
1807
+ "markdown": "### ES 3.00 Differences\n\n| ES 1.00 | ES 3.00 | Notes |\n|---------|---------|-------|\n| `gl_FragColor = vec4(...)` | `out vec4 fragColor; ... fragColor = vec4(...)` | You must declare the output variable |\n| `texture2D(sampler, uv)` | `texture(sampler, uv)` | Function renamed |\n| `attribute` / `varying` | `in` / `out` | Keyword changes (vertex shader) |\n\n**Recommendation:** Use GLSL ES 1.00 unless you specifically need WebGL 2 features. ES 1.00 has the widest browser support and is simpler to write.\n\n> [!WARNING]\n> If `#version 300 es` is declared but the browser doesn't support WebGL 2, the shader will fail with a \"WebGL 2 not supported\" error.\n\n## Parameter Directives\n\nShader parameters are declared with `// @viji-TYPE:uniformName` comments. Viji parses them, generates uniform declarations, and creates UI controls on the host:\n\n```glsl\n// @renderer shader\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0\n// @viji-color:tint label:\"Tint\" default:#ff6600\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv * brightness * tint, 1.0);\n}\n```\n\n> [!NOTE]\n> Parameter declarations use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\n\nSee [Shader Parameters](/shader/parameters) for the full reference on all parameter types, config keys, and the accumulator directive.\n\n## Environment Constraints\n\n> [!WARNING]\n> Scenes run in a Web Worker — there is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. Note: `fetch()` IS available and can be used to load external data (JSON, etc.) from CDNs.\n\nThis applies to the host environment, not the shader itself — GLSL runs on the GPU and has no concept of DOM APIs. The constraint matters if you're using JavaScript alongside your shader (not applicable to pure shader scenes).\n\n## Next Steps\n\n- [Resolution & Coordinates](/shader/resolution) — [`u_resolution`](/shader/resolution), normalization, aspect ratio\n- [Timing & Animation](/shader/timing) — [`u_time`](/shader/timing), [`u_deltaTime`](/shader/timing), animation patterns\n- [Parameters](/shader/parameters) — sliders, colors, toggles, accumulators\n- [Backbuffer](/shader/backbuffer) — feedback effects using the previous frame\n- [API Reference](/shader/api-reference) — complete list of built-in uniforms\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1808
+ }
1809
+ ]
1810
+ },
1811
+ "shader-resolution": {
1812
+ "id": "shader-resolution",
1813
+ "title": "Resolution & Coordinates",
1814
+ "description": "Use u_resolution for coordinate normalization, centering, and aspect-ratio-correct drawing in shaders.",
1815
+ "content": [
1816
+ {
1817
+ "type": "text",
1818
+ "markdown": "# Resolution & Coordinates\n\nIn shader scenes, `u_resolution` is how you know the canvas size. This page covers coordinate normalization, centering, aspect ratio correction, and patterns for resolution-agnostic shader code.\n\n## `u_resolution`\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width (`x`) and height (`y`) in pixels |\n\nThis uniform is auto-injected — don't declare it yourself. It updates automatically whenever the host resizes the canvas.\n\n## Normalized Coordinates (0 to 1)\n\nThe most common pattern. Divide `gl_FragCoord.xy` by `u_resolution` to get UV coordinates ranging from `(0, 0)` at the bottom-left to `(1, 1)` at the top-right:\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution;\n```\n\nThis is the foundation for most shader effects. Shapes defined in UV space automatically adapt to any canvas size."
1819
+ },
1820
+ {
1821
+ "type": "live-example",
1822
+ "title": "Normalized UV — Color Rings",
1823
+ "sceneCode": "// @renderer shader\n// @viji-slider:rings label:\"Ring Count\" default:8.0 min:2.0 max:20.0 step:1.0\n// @viji-color:color1 label:\"Inner Color\" default:#ff3366\n// @viji-color:color2 label:\"Outer Color\" default:#3366ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 centered = uv - 0.5;\n float dist = length(centered) * 2.0;\n float ring = sin(dist * rings * 3.14159 - u_time * 2.0) * 0.5 + 0.5;\n vec3 color = mix(color1, color2, ring);\n gl_FragColor = vec4(color, 1.0);\n}\n",
1824
+ "sceneFile": "resolution-normalize.scene.glsl"
1825
+ },
1826
+ {
1827
+ "type": "text",
1828
+ "markdown": "## Centered Coordinates (-0.5 to 0.5)\n\nShift the origin to the center of the canvas:\n\n```glsl\nvec2 centered = gl_FragCoord.xy / u_resolution - 0.5;\n```\n\nNow `(0, 0)` is the center, with values ranging from `-0.5` to `0.5`. This is useful for radial effects, distance fields, and anything centered on the canvas.\n\n## Aspect Ratio Correction\n\nOn non-square canvases, a circle defined with `centered` coordinates appears as an ellipse because the X and Y ranges cover different physical distances. Fix this by dividing by the Y component:\n\n```glsl\nvec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\n```\n\nNow:\n- Y ranges from `-0.5` to `0.5`\n- X ranges from `-0.5 * aspect` to `0.5 * aspect`\n- A `length(uv) < 0.3` circle is truly circular regardless of aspect ratio"
1829
+ },
1830
+ {
1831
+ "type": "live-example",
1832
+ "title": "Aspect-Correct — Circle & Grid",
1833
+ "sceneCode": "// @renderer shader\n// @viji-slider:gridScale label:\"Grid Scale\" default:6.0 min:2.0 max:16.0 step:1.0\n// @viji-slider:radius label:\"Circle Radius\" default:0.3 min:0.05 max:0.45 step:0.01\n// @viji-color:circleColor label:\"Circle Color\" default:#44ffaa\n// @viji-color:gridColor label:\"Grid Color\" default:#334466\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\n\n float d = length(uv);\n float circle = smoothstep(radius, radius - 2.0 / u_resolution.y, d);\n\n vec2 grid = abs(fract(uv * gridScale) - 0.5);\n float line = 1.0 - smoothstep(0.0, 2.0 / u_resolution.y * gridScale, min(grid.x, grid.y));\n\n vec3 bg = vec3(0.04, 0.04, 0.1);\n vec3 color = bg;\n color = mix(color, gridColor * 0.5, line * 0.4);\n color = mix(color, circleColor, circle);\n\n gl_FragColor = vec4(color, 1.0);\n}\n",
1834
+ "sceneFile": "resolution-aspect.scene.glsl"
1835
+ },
1836
+ {
1837
+ "type": "text",
1838
+ "markdown": "### The Aspect Ratio Value\n\nWhen you need the aspect ratio as a number:\n\n```glsl\nfloat aspect = u_resolution.x / u_resolution.y;\n```\n\nUse it to scale one axis:\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution - 0.5;\nuv.x *= aspect; // correct for non-square canvas\n```\n\n## Common Patterns\n\n### Distance from Center\n\n```glsl\nvec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\nfloat d = length(uv);\n```\n\n### Anti-Aliased Circle\n\n```glsl\nfloat circle = smoothstep(0.3, 0.3 - 2.0 / u_resolution.y, d);\n```\n\nThe `2.0 / u_resolution.y` term creates a 2-pixel-wide anti-aliased edge that stays sharp at any resolution.\n\n### Tiled Coordinates\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution;\nvec2 tiled = fract(uv * 10.0); // 10x10 grid of tiles\n```\n\n## Resolution Agnosticism\n\n> [!NOTE]\n> Always use `u_resolution` for coordinate normalization. This keeps your shader resolution-agnostic — it will look correct at any canvas size from small thumbnails to fullscreen 4K.\n\nNever hardcode pixel values in a shader:\n\n```glsl\n// Bad — assumes specific resolution\nvec2 uv = gl_FragCoord.xy / vec2(1920.0, 1080.0);\n\n// Good — adapts to any resolution\nvec2 uv = gl_FragCoord.xy / u_resolution;\n```\n\n## Comparison Across Renderers\n\n| Concept | Native | P5 | Shader |\n|---------|--------|-----|--------|\n| Canvas width | [`viji.width`](/native/canvas-context) | [`viji.width`](/p5/canvas-resolution) / `p5.width` | `u_resolution.x` |\n| Canvas height | [`viji.height`](/native/canvas-context) | [`viji.height`](/p5/canvas-resolution) / `p5.height` | `u_resolution.y` |\n| Aspect ratio | `viji.width / viji.height` | `viji.width / viji.height` | `u_resolution.x / u_resolution.y` |\n\n## Next Steps\n\n- [Timing & Animation](/shader/timing) — [`u_time`](/shader/timing), [`u_deltaTime`](/shader/timing), animation patterns\n- [Shader Basics](/shader/basics) — auto-injection, GLSL versions, `@renderer shader`\n- [Parameters](/shader/parameters) — sliders, colors, toggles, accumulators\n- [Native Canvas & Context](/native/canvas-context) — `viji.width`, `viji.height` in native scenes\n- [P5 Canvas & Resolution](/p5/canvas-resolution) — resolution in P5 scenes\n- [API Reference](/shader/api-reference) — complete list of built-in uniforms"
1839
+ }
1840
+ ]
1841
+ },
1842
+ "shader-timing": {
1843
+ "id": "shader-timing",
1844
+ "title": "Timing & Animation",
1845
+ "description": "Use u_time, u_deltaTime, u_frame, and u_fps for animation patterns in shaders.",
1846
+ "content": [
1847
+ {
1848
+ "type": "text",
1849
+ "markdown": "# Timing & Animation\n\nViji injects four timing uniforms into every shader scene. This page covers each one, common animation patterns, and when to use the `@viji-accumulator` directive for smooth speed control.\n\n## Timing Uniforms\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_time` | `float` | Seconds elapsed since the scene started |\n| `u_deltaTime` | `float` | Seconds since the previous frame |\n| `u_frame` | `int` | Frame counter (monotonically increasing) |\n| `u_fps` | `float` | Target FPS based on the host's frame rate mode |\n\nAll four are auto-injected — do not redeclare them.\n\n## `u_time` — Absolute Time\n\n`u_time` is a monotonically increasing float. Use it for oscillations, rotations, and any effect driven by elapsed time:\n\n```glsl\nfloat wave = sin(u_time * 3.0); // oscillate\nfloat rotation = u_time * 0.5; // continuous rotation\nvec2 offset = vec2(cos(u_time), sin(u_time)) * 0.2; // circular motion\n```"
1850
+ },
1851
+ {
1852
+ "type": "live-example",
1853
+ "title": "u_time — Plasma Effect",
1854
+ "sceneCode": "// @renderer shader\n// @viji-slider:scale label:\"Scale\" default:4.0 min:1.0 max:10.0 step:0.5\n// @viji-slider:speed label:\"Speed\" default:1.5 min:0.2 max:5.0\n// @viji-color:colorA label:\"Color A\" default:#ff2266\n// @viji-color:colorB label:\"Color B\" default:#2266ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float t = u_time * speed;\n\n float v1 = sin(uv.x * scale * 3.14159 + t);\n float v2 = cos(uv.y * scale * 3.14159 - t * 0.7);\n float v3 = sin((uv.x + uv.y) * scale * 2.0 + t * 1.3);\n float plasma = (v1 + v2 + v3) / 3.0 * 0.5 + 0.5;\n\n vec3 color = mix(colorA, colorB, plasma);\n gl_FragColor = vec4(color, 1.0);\n}\n",
1855
+ "sceneFile": "timing-patterns.scene.glsl"
1856
+ },
1857
+ {
1858
+ "type": "text",
1859
+ "markdown": "## `u_deltaTime` — Frame Delta\n\n`u_deltaTime` is the time between the current and previous frame. In shaders, it's primarily useful for:\n\n- Display purposes (showing timing info)\n- Custom accumulation in combination with the `backbuffer`\n- Understanding frame rate behavior\n\nFor most shader animation, `u_time` is the right choice because GLSL is stateless — each pixel is computed independently every frame with no per-pixel accumulation.\n\n## `u_frame` — Frame Counter\n\nAn integer that increments by 1 each frame. Useful for:\n\n- Alternating patterns: `mod(float(u_frame), 2.0)`\n- Noise seeding that changes per frame\n- Periodic events: `mod(float(u_frame), 60.0) < 1.0`\n\n## `u_fps` — Target Frame Rate\n\n`u_fps` is the **target** frame rate based on the host configuration, not a measured value:\n\n- `frameRateMode: 'full'` → screen refresh rate (typically 60.0 or 120.0)\n- `frameRateMode: 'half'` → half the screen refresh rate (typically 30.0 or 60.0)\n\nThis value is stable and does not fluctuate. Use it for informational display, not for animation timing.\n\n## Animation Patterns\n\n### Oscillation\n\n```glsl\nfloat pulse = sin(u_time * 3.14159) * 0.5 + 0.5; // 0..1 pulse, period ~2s\nfloat blink = step(0.5, fract(u_time * 2.0)); // on/off blink, 2 Hz\n```\n\n### Smooth Cycling\n\n```glsl\nfloat t = fract(u_time * 0.25); // 0..1 over 4 seconds, then repeats\nvec3 color = mix(colorA, colorB, t);\n```\n\n### Rotation\n\n```glsl\nfloat angle = u_time * 1.5;\nmat2 rot = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));\nvec2 rotated = rot * uv;\n```\n\n### Speed-Controlled Animation: The Accumulator\n\nWhen you write `u_time * speed`, changing the `speed` slider causes a visible jump because the entire phase is recalculated. The [`@viji-accumulator`](/shader/parameters/accumulator) directive solves this:\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n // Use `phase` instead of `u_time * speed`\n float wave = sin(phase * 6.28);\n // ...\n}\n```\n\nThe accumulator adds `speed × deltaTime` to `phase` each frame on the CPU side. Changing `speed` only affects future growth — no backward jumps."
1860
+ },
1861
+ {
1862
+ "type": "live-example",
1863
+ "title": "Accumulator — Smooth Spiral",
1864
+ "sceneCode": "// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// @viji-accumulator:phase rate:speed\n// @viji-color:color1 label:\"Color A\" default:#ff6633\n// @viji-color:color2 label:\"Color B\" default:#3366ff\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;\n float angle = atan(uv.y, uv.x);\n float dist = length(uv);\n\n float spiral = sin(angle * 4.0 + dist * 20.0 - phase * 6.28);\n float pattern = smoothstep(-0.2, 0.2, spiral);\n\n vec3 color = mix(color1, color2, pattern);\n float vignette = 1.0 - dist * 0.8;\n color *= vignette;\n\n gl_FragColor = vec4(color, 1.0);\n}\n",
1865
+ "sceneFile": "timing-accumulator.scene.glsl"
1866
+ },
1867
+ {
1868
+ "type": "text",
1869
+ "markdown": "### When to Use `u_time` vs `@viji-accumulator`\n\n| Scenario | Use | Why |\n|----------|-----|-----|\n| Fixed-speed animation | `u_time` | No slider controls speed, so no jump risk |\n| Slider-controlled speed | `@viji-accumulator` | Prevents jumps when user changes the slider |\n| Phase offset per element | `u_time + offset` | Offset is constant, no jump risk |\n| Speed × slider in one expression | `@viji-accumulator` | `u_time * speed` jumps on slider change |\n\n## Frame-Rate Independence\n\n> [!NOTE]\n> Always use `u_resolution` for coordinate normalization. Use `u_time` or `u_deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\nShaders are inherently frame-rate-independent when using `u_time` because the time value is absolute. The only case where frame rate matters is backbuffer-based feedback effects — there, scale the feedback strength by `u_deltaTime`.\n\n## Comparison Across Renderers\n\n| Concept | Native | P5 | Shader |\n|---------|--------|-----|--------|\n| Elapsed time | [`viji.time`](/native/timing) | [`viji.time`](/p5/timing) | `u_time` |\n| Frame delta | [`viji.deltaTime`](/native/timing) | [`viji.deltaTime`](/p5/timing) | `u_deltaTime` |\n| Frame count | [`viji.frameCount`](/native/timing) | [`viji.frameCount`](/p5/timing) | `u_frame` |\n| Target FPS | [`viji.fps`](/native/timing) | [`viji.fps`](/p5/timing) | `u_fps` |\n\n## Next Steps\n\n- [Resolution & Coordinates](/shader/resolution) — [`u_resolution`](/shader/resolution), normalization, aspect ratio\n- [Shader Basics](/shader/basics) — auto-injection, GLSL versions, `@renderer shader`\n- [Parameters](/shader/parameters) — sliders, colors, toggles\n- [Accumulator](/shader/parameters/accumulator) — full accumulator reference\n- [Backbuffer](/shader/backbuffer) — feedback effects using the previous frame\n- [Native Timing](/native/timing) — timing in the native renderer\n- [P5 Timing](/p5/timing) — timing in the P5 renderer\n- [API Reference](/shader/api-reference) — complete list of built-in uniforms"
1870
+ }
1871
+ ]
1872
+ },
1873
+ "shader-parameters-overview": {
1874
+ "id": "shader-parameters-overview",
1875
+ "title": "Parameters",
1876
+ "description": "Shader parameter system — declare controls with comment directives that become GLSL uniforms automatically.",
1877
+ "content": [
1878
+ {
1879
+ "type": "text",
1880
+ "markdown": "# Shader Parameters\n\nParameters are the primary way to give users control over your scene. You declare them at the top of your shader, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Values update in real-time as users interact with the controls.\n\nIn the shader renderer, parameters are declared using `// @viji-*` comment directives. Each directive creates a UI control and a corresponding GLSL uniform — no manual `uniform` declarations needed. Read the uniform directly in your shader code to get the current value.\n\n## Parameter Types\n\n| Directive | Uniform Type | Value | Use For |\n|---|---|---|---|\n| [`@viji-slider`](slider/) | `float` | Continuous range | Speed, intensity, size |\n| [`@viji-number`](number/) | `float` | Precise numeric | Count, threshold |\n| [`@viji-color`](color/) | `vec3` | RGB (0–1) | Tint, palette |\n| [`@viji-toggle`](toggle/) | `bool` | On/off | Invert, enable effect |\n| [`@viji-select`](select/) | `int` | Index (0-based) | Mode, pattern selection |\n| [`@viji-image`](image/) | `sampler2D` | Texture | Overlay, displacement map |\n| [`@viji-button`](button/) | `bool` | Momentary trigger | Reset, spawn, one-shot action |\n| [`@viji-accumulator`](accumulator/) | `float` | CPU-side integration | Smooth animation phase |\n\n## Basic Pattern\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// @viji-toggle:invert label:\"Invert\" default:false\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec3 col = tint * uv.x * speed;\n if (invert) col = 1.0 - col;\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nEach `@viji-*` directive auto-generates a `uniform` declaration with the name after the colon. `speed` becomes `uniform float speed;`, `tint` becomes `uniform vec3 tint;`, etc.\n\n> [!WARNING]\n> Do not use the `u_` prefix for parameter names — it is reserved for built-in Viji uniforms. Use descriptive names like `speed`, `tint`, `brightness`.\n\n> [!NOTE]\n> Parameter directives use **single-line `//` comments only**. Block comments `/* */` are not parsed for `@viji-*` directives.\n\n## Directive Syntax\n\n```\n// @viji-TYPE:uniformName key:value key:\"string value\" key:[array]\n```\n\n- **TYPE** — one of: [`slider`](slider/), [`number`](number/), [`color`](color/), [`toggle`](toggle/), [`select`](select/), [`image`](image/), [`button`](button/), [`accumulator`](accumulator/)\n- **uniformName** — the GLSL uniform name (no `u_` prefix)\n- **key:value** pairs — configure the parameter\n\n## Common Config Keys\n\n| Key | Type | Description |\n|---|---|---|\n| `label` | `string` | **(required)** Display name in the UI |\n| `default` | varies | **(required for most types)** Initial value |\n| `min` | `number` | Minimum value ([slider](slider/)/[number](number/)) |\n| `max` | `number` | Maximum value ([slider](slider/)/[number](number/)) |\n| `step` | `number` | Step increment ([slider](slider/)/[number](number/)) |\n| `description` | `string` | Help text |\n| `group` | `string` | Group name for organizing parameters — see [Grouping](grouping/) |\n| `category` | `string` | Visibility category — see [Categories](categories/) |\n| `options` | `array` | Options list ([select](select/) only) |\n\n## Uniform Type Mapping\n\n| Parameter Type | GLSL Uniform | Notes |\n|---|---|---|\n| [`slider`](slider/) | `uniform float` | |\n| [`number`](number/) | `uniform float` | Same as slider |\n| [`color`](color/) | `uniform vec3` | RGB components, each 0–1 |\n| [`toggle`](toggle/) | `uniform bool` | |\n| [`select`](select/) | `uniform int` | 0-based index of selected option |\n| [`image`](image/) | `uniform sampler2D` | |\n| [`button`](button/) | `uniform bool` | `true` for 1 frame after click, then `false` |\n| [`accumulator`](accumulator/) | `uniform float` | CPU-side `value += rate × deltaTime` each frame |\n\n> [!WARNING]\n> All `@viji-*` directives must appear as top-level comments before `void main()`. They are parsed once during shader compilation. Viji's shader auto-injection places the generated `uniform` declarations before your code.\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)** — Use the `group:` key to collect related parameters under a shared heading (e.g., `group:animation`). Parameters with the same `group` value appear together in the UI.\n- **[Categories](categories/)** — Use the `category:` key to tag parameters as `general`, `audio`, `video`, or `interaction` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/) — the most common parameter type\n- [Accumulator](accumulator/) — smooth animation without `u_time * speed` jumps\n- [Grouping](grouping/) — organizing parameters into named groups\n- [Categories](categories/) — visibility based on capabilities\n- [Native Parameters](/native/parameters) — equivalent JavaScript API\n- [P5 Parameters](/p5/parameters) — same system in the P5 renderer\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1881
+ }
1882
+ ]
1883
+ },
1884
+ "shader-param-slider": {
1885
+ "id": "shader-param-slider",
1886
+ "title": "Slider Parameter",
1887
+ "description": "Declare a draggable slider that maps to a uniform float in GLSL shaders.",
1888
+ "content": [
1889
+ {
1890
+ "type": "text",
1891
+ "markdown": "# @viji-slider\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1 min:0 max:5 step:0.1\nuniform float speed;\n```\n\nDeclares a numeric slider parameter. The host renders it as a draggable slider control. The value is injected as a `uniform float`.\n\n## Directive Syntax\n\n```\n// @viji-slider:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `default` | Yes | — | Initial value |\n| `min` | No | `0` | Minimum allowed value |\n| `max` | No | `100` | Maximum allowed value |\n| `step` | No | `1` | Increment between values |\n| `description` | No | — | Tooltip text (use quotes) |\n| `group` | No | `general` | Group name (use quotes) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe slider value is always injected as a `float`:\n\n```glsl\n// @viji-slider:count label:\"Count\" default:8 min:1 max:20 step:1\nuniform float count; // always float, use int(count) if needed\n```\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-slider:zoom label:\"Zoom\" default:1 min:0.1 max:5 step:0.1\n// @viji-slider:rotation label:\"Rotation\" default:0 min:0 max:6.2832 step:0.01\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy / u_resolution - 0.5) * zoom;\n float c = cos(rotation), s = sin(rotation);\n uv = mat2(c, -s, s, c) * uv;\n float d = length(uv);\n float ring = sin(d * 20.0 - u_time * 3.0) * 0.5 + 0.5;\n gl_FragColor = vec4(vec3(ring), 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations — both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
1892
+ },
1893
+ {
1894
+ "type": "live-example",
1895
+ "title": "Slider Controls",
1896
+ "sceneCode": "// @renderer shader\n// @viji-slider:zoom label:\"Zoom\" default:1 min:0.1 max:5 step:0.1\n// @viji-slider:speed label:\"Speed\" default:3 min:0 max:10 step:0.5\n// @viji-color:ringColor label:\"Color\" default:#44ddff\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy / u_resolution - 0.5) * zoom;\n float aspect = u_resolution.x / u_resolution.y;\n uv.x *= aspect;\n\n float d = length(uv);\n float ring = sin(d * 20.0 - u_time * speed) * 0.5 + 0.5;\n vec3 col = ringColor * ring;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
1897
+ "sceneFile": "slider-shader.scene.glsl"
1898
+ },
1899
+ {
1900
+ "type": "text",
1901
+ "markdown": "## Slider vs Number in Shaders\n\nBoth `@viji-slider` and `@viji-number` produce a `uniform float` and accept the same config keys (`min`, `max`, `step`, `default`). The only difference is the host UI:\n\n| | @viji-slider | @viji-number |\n|--|--------------|--------------|\n| UI | Draggable track | Text input field |\n| Best for | Continuous ranges, visual tuning | Precise values, integer counts |\n\n## Smooth Animation with Accumulators\n\nA common pattern is using a slider to control animation speed:\n\n```glsl\nfloat phase = u_time * speed; // jumps when speed changes mid-animation\n```\n\nThe problem: if the user changes `speed` from `1.0` to `3.0` at `u_time = 10`, the phase jumps from `10` to `30` instantly. The [`@viji-accumulator`](../accumulator/) solves this by integrating the rate over time — changing the rate only affects future growth, never jumps:\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1 min:0 max:5 step:0.1\n// @viji-accumulator:phase rate:speed\n// Generates: uniform float speed; and uniform float phase;\n// phase grows by speed × deltaTime each frame, no jumps\n```\n\nSee [Accumulator](../accumulator/) for full details and examples.\n\n## Rules\n\n- Numeric values have no quotes: `default:1`, `min:0`, `max:5`\n- String values use quotes: `label:\"Speed\"`\n- The `label` and `default` keys are required\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Number](../number/) — numeric input `uniform float`\n- [Color](../color/) — color picker `uniform vec3`\n- [Toggle](../toggle/) — boolean `uniform bool`\n- [Select](../select/) — dropdown `uniform int`\n- [Accumulator](../accumulator/) — frame-persistent state driven by slider values\n- [Native Slider](/native/parameters/slider) — equivalent for the Native renderer\n- [P5 Slider](/p5/parameters/slider) — equivalent for the P5 renderer"
1902
+ }
1903
+ ]
1904
+ },
1905
+ "shader-param-color": {
1906
+ "id": "shader-param-color",
1907
+ "title": "Color Parameter",
1908
+ "description": "Declare a color picker that maps to a vec3 uniform in GLSL shaders.",
1909
+ "content": [
1910
+ {
1911
+ "type": "text",
1912
+ "markdown": "# @viji-color\n\n```glsl\n// @viji-color:myColor label:\"Color\" default:#ff6600\nuniform vec3 myColor;\n```\n\nDeclares a color picker parameter. The host renders a color swatch that opens a full picker when clicked. The hex value is converted to a `vec3` uniform with RGB components normalized to `0.0–1.0`.\n\n## Directive Syntax\n\n```\n// @viji-color:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `default` | Yes | — | Initial hex color (e.g., `#ff6600`) — **no quotes** |\n| `description` | No | — | Tooltip text (use quotes: `description:\"Help text\"`) |\n| `group` | No | `general` | Group name (use quotes: `group:\"colors\"`) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe color is injected as a `vec3`:\n\n| Component | Value | Range |\n|-----------|-------|-------|\n| `.r` | Red channel | 0.0 – 1.0 |\n| `.g` | Green channel | 0.0 – 1.0 |\n| `.b` | Blue channel | 0.0 – 1.0 |\n\nFor hex `#ff8040`: `.r = 1.0`, `.g ≈ 0.502`, `.b ≈ 0.251`.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-color:bgColor label:\"Background\" default:#0f0f1a\n// @viji-color:accent label:\"Accent\" default:#ff4488\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float d = distance(uv, vec2(0.5));\n vec3 col = mix(accent, bgColor, smoothstep(0.1, 0.4, d));\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations — both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
1913
+ },
1914
+ {
1915
+ "type": "live-example",
1916
+ "title": "Color Blending",
1917
+ "sceneCode": "// @renderer shader\n// @viji-color:bgColor label:\"Background\" default:#0f0f1a\n// @viji-color:color1 label:\"Color 1\" default:#ff4488 group:\"colors\"\n// @viji-color:color2 label:\"Color 2\" default:#4488ff group:\"colors\"\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float t = sin(uv.x * 6.2832 + u_time) * 0.5 + 0.5;\n vec3 gradient = mix(color1, color2, t);\n float d = distance(uv, vec2(0.5));\n vec3 col = mix(gradient, bgColor, smoothstep(0.2, 0.5, d));\n gl_FragColor = vec4(col, 1.0);\n}\n",
1918
+ "sceneFile": "color-shader.scene.glsl"
1919
+ },
1920
+ {
1921
+ "type": "text",
1922
+ "markdown": "## Rules\n\n- Hex values do not use quotes: `default:#ff6600` (not `default:\"#ff6600\"`)\n- String values use quotes: `label:\"My Color\"`, `description:\"Pick a color\"`\n- The `label` key is required for all parameter directives\n- The `default` key is required for color parameters\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Toggle](../toggle/) — boolean `uniform bool`\n- [Select](../select/) — dropdown `uniform int`\n- [Native Color](/native/parameters/color) — equivalent for the Native renderer\n- [P5 Color](/p5/parameters/color) — equivalent for the P5 renderer"
1923
+ }
1924
+ ]
1925
+ },
1926
+ "shader-param-toggle": {
1927
+ "id": "shader-param-toggle",
1928
+ "title": "Toggle Parameter",
1929
+ "description": "Declare a boolean switch that maps to a uniform bool in GLSL shaders.",
1930
+ "content": [
1931
+ {
1932
+ "type": "text",
1933
+ "markdown": "# @viji-toggle\n\n```glsl\n// @viji-toggle:showGrid label:\"Show Grid\" default:true\nuniform bool showGrid;\n```\n\nDeclares a boolean toggle parameter. The host renders it as an on/off switch. The value is injected as a `uniform bool`.\n\n## Directive Syntax\n\n```\n// @viji-toggle:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `default` | Yes | — | Initial state: `true` or `false` |\n| `description` | No | — | Tooltip text (use quotes: `description:\"Help text\"`) |\n| `group` | No | `general` | Group name (use quotes: `group:\"effects\"`) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe toggle is injected as a `bool`:\n\n| Value | GLSL |\n|-------|------|\n| On | `true` |\n| Off | `false` |\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-toggle:invert label:\"Invert Colors\" default:false\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec3 col = vec3(uv.x, uv.y, sin(u_time) * 0.5 + 0.5);\n if (invert) {\n col = 1.0 - col;\n }\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations — both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
1934
+ },
1935
+ {
1936
+ "type": "live-example",
1937
+ "title": "Toggle Inversion",
1938
+ "sceneCode": "// @renderer shader\n// @viji-toggle:invert label:\"Invert\" default:false\n// @viji-toggle:animate label:\"Animate\" default:true\n// @viji-color:baseColor label:\"Base Color\" default:#4488ff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float t = animate ? u_time : 0.0;\n float pattern = sin(uv.x * 10.0 + t * 2.0) * sin(uv.y * 10.0 + t * 1.5);\n vec3 col = baseColor * (pattern * 0.5 + 0.5);\n if (invert) {\n col = 1.0 - col;\n }\n gl_FragColor = vec4(col, 1.0);\n}\n",
1939
+ "sceneFile": "toggle-shader.scene.glsl"
1940
+ },
1941
+ {
1942
+ "type": "text",
1943
+ "markdown": "## Rules\n\n- Boolean values have no quotes: `default:true` or `default:false`\n- String values use quotes: `label:\"Show Grid\"`\n- The `label` key is required\n- The `default` key is required\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Color](../color/) — color picker `uniform vec3`\n- [Select](../select/) — dropdown `uniform int`\n- [Number](../number/) — numeric `uniform float`\n- [Native Toggle](/native/parameters/toggle) — equivalent for the Native renderer\n- [P5 Toggle](/p5/parameters/toggle) — equivalent for the P5 renderer"
1944
+ }
1945
+ ]
1946
+ },
1947
+ "shader-param-select": {
1948
+ "id": "shader-param-select",
1949
+ "title": "Select Parameter",
1950
+ "description": "Declare a dropdown selector that maps to a uniform int in GLSL shaders.",
1951
+ "content": [
1952
+ {
1953
+ "type": "text",
1954
+ "markdown": "# @viji-select\n\n```glsl\n// @viji-select:mode label:\"Mode\" options:[\"Wave\",\"Circles\",\"Grid\"] default:0\nuniform int mode;\n```\n\nDeclares a dropdown selection parameter. The host renders it as a dropdown menu. The selected option's **index** is injected as a `uniform int`.\n\n## Directive Syntax\n\n```\n// @viji-select:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `options` | Yes | — | Array of string choices: `options:[\"A\",\"B\",\"C\"]` |\n| `default` | Yes | — | Initially selected index (0-based integer) |\n| `description` | No | — | Tooltip text (use quotes) |\n| `group` | No | `general` | Group name (use quotes) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe select value is injected as an `int` — the **zero-based index** of the selected option:\n\n| Selected Option | GLSL Value |\n|-----------------|------------|\n| First option | `0` |\n| Second option | `1` |\n| Third option | `2` |\n\nThis differs from Native and P5 renderers, where `.value` is the option string itself.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-select:pattern label:\"Pattern\" options:[\"Stripes\",\"Dots\",\"Checker\"] default:0\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float v = 0.0;\n\n if (pattern == 0) {\n v = step(0.5, fract(uv.x * 10.0 + u_time));\n } else if (pattern == 1) {\n v = 1.0 - step(0.3, length(fract(uv * 5.0) - 0.5));\n } else if (pattern == 2) {\n v = mod(floor(uv.x * 8.0) + floor(uv.y * 8.0), 2.0);\n }\n\n gl_FragColor = vec4(vec3(v), 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations — both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
1955
+ },
1956
+ {
1957
+ "type": "live-example",
1958
+ "title": "Pattern Selector",
1959
+ "sceneCode": "// @renderer shader\n// @viji-select:pattern label:\"Pattern\" options:[\"Stripes\",\"Dots\",\"Checker\"] default:0\n// @viji-color:color1 label:\"Color\" default:#ff4488\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float v = 0.0;\n\n if (pattern == 0) {\n v = step(0.5, fract(uv.x * 10.0 + u_time));\n } else if (pattern == 1) {\n v = 1.0 - step(0.3, length(fract(uv * 5.0) - 0.5));\n } else {\n v = mod(floor(uv.x * 8.0) + floor(uv.y * 8.0), 2.0);\n }\n\n vec3 col = mix(vec3(0.06), color1, v);\n gl_FragColor = vec4(col, 1.0);\n}\n",
1960
+ "sceneFile": "select-shader.scene.glsl"
1961
+ },
1962
+ {
1963
+ "type": "text",
1964
+ "markdown": "## Rules\n\n- Options must be a JSON-style array with quoted strings: `options:[\"A\",\"B\",\"C\"]`\n- The `default` value is a 0-based index (integer), not a string\n- String values use quotes: `label:\"Pattern\"`\n- The `label`, `options`, and `default` keys are all required\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Toggle](../toggle/) — two-state `uniform bool`\n- [Number](../number/) — continuous `uniform float`\n- [Color](../color/) — color picker `uniform vec3`\n- [Native Select](/native/parameters/select) — equivalent for the Native renderer\n- [P5 Select](/p5/parameters/select) — equivalent for the P5 renderer"
1965
+ }
1966
+ ]
1967
+ },
1968
+ "shader-param-number": {
1969
+ "id": "shader-param-number",
1970
+ "title": "Number Parameter",
1971
+ "description": "Declare a numeric input that maps to a uniform float in GLSL shaders.",
1972
+ "content": [
1973
+ {
1974
+ "type": "text",
1975
+ "markdown": "# @viji-number\n\n```glsl\n// @viji-number:density label:\"Density\" default:5 min:1 max:20 step:1\nuniform float density;\n```\n\nDeclares a numeric input parameter. The host renders it as a direct number input field. The value is injected as a `uniform float`.\n\n## Directive Syntax\n\n```\n// @viji-number:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `default` | Yes | — | Initial value |\n| `min` | No | `0` | Minimum allowed value |\n| `max` | No | `100` | Maximum allowed value |\n| `step` | No | `1` | Increment between values |\n| `description` | No | — | Tooltip text (use quotes) |\n| `group` | No | `general` | Group name (use quotes) |\n| `category` | No | `general` | Visibility category |\n\n## Uniform Type\n\nThe number is always injected as a `float`, even when configured with integer steps:\n\n```glsl\n// @viji-number:count label:\"Count\" default:8 min:1 max:20 step:1\nuniform float count; // always float, use int(count) if needed\n```\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-number:rings label:\"Rings\" default:5 min:1 max:15 step:1\n// @viji-color:ringColor label:\"Color\" default:#44ddff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 center = uv - 0.5;\n float aspect = u_resolution.x / u_resolution.y;\n center.x *= aspect;\n\n float d = length(center);\n float wave = sin(d * rings * 6.2832 - u_time * 3.0) * 0.5 + 0.5;\n vec3 col = ringColor * wave;\n\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations — both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
1976
+ },
1977
+ {
1978
+ "type": "live-example",
1979
+ "title": "Ring Count",
1980
+ "sceneCode": "// @renderer shader\n// @viji-number:rings label:\"Rings\" default:5 min:1 max:15 step:1\n// @viji-number:speed label:\"Speed\" default:3 min:0 max:10 step:0.5\n// @viji-color:ringColor label:\"Color\" default:#44ddff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 center = uv - 0.5;\n float aspect = u_resolution.x / u_resolution.y;\n center.x *= aspect;\n\n float d = length(center);\n float wave = sin(d * rings * 6.2832 - u_time * speed) * 0.5 + 0.5;\n vec3 col = ringColor * wave;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
1981
+ "sceneFile": "number-shader.scene.glsl"
1982
+ },
1983
+ {
1984
+ "type": "text",
1985
+ "markdown": "## Number vs Slider in Shaders\n\nBoth `@viji-number` and `@viji-slider` produce a `uniform float` and accept the same config keys (`min`, `max`, `step`, `default`). The only difference is the host UI:\n\n| | @viji-slider | @viji-number |\n|--|--------------|--------------|\n| UI | Draggable track | Text input field |\n| Best for | Continuous ranges, visual tuning | Precise values, integer counts |\n\n## Smooth Animation with Accumulators\n\nWhen a number parameter controls animation speed, multiplying by `u_time` directly causes jumps when the value changes. The [`@viji-accumulator`](../accumulator/) integrates the rate over time for smooth transitions:\n\n```glsl\n// @viji-number:bpm label:\"BPM\" default:120 min:30 max:300 step:1\n// @viji-accumulator:beat rate:bpm\n// Generates: uniform float bpm; and uniform float beat;\n// beat grows by bpm × deltaTime each frame, no jumps\n```\n\nSee [Accumulator](../accumulator/) for full details and examples.\n\n## Rules\n\n- Numeric values have no quotes: `default:5`, `min:1`, `max:20`\n- String values use quotes: `label:\"Density\"`\n- The `label` and `default` keys are required\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Slider](/shader/parameters/slider) — numeric slider `uniform float`\n- [Toggle](../toggle/) — boolean `uniform bool`\n- [Select](../select/) — dropdown `uniform int`\n- [Accumulator](../accumulator/) — frame-persistent state driven by numeric values\n- [Native Number](/native/parameters/number) — equivalent for the Native renderer\n- [P5 Number](/p5/parameters/number) — equivalent for the P5 renderer"
1986
+ }
1987
+ ]
1988
+ },
1989
+ "shader-param-image": {
1990
+ "id": "shader-param-image",
1991
+ "title": "Image Parameter",
1992
+ "description": "Declare an image upload that maps to a sampler2D uniform in GLSL shaders.",
1993
+ "content": [
1994
+ {
1995
+ "type": "text",
1996
+ "markdown": "# @viji-image\n\n```glsl\n// @viji-image:tex label:\"Texture\"\nuniform sampler2D tex;\n```\n\nDeclares an image upload parameter. The host renders a file picker or drag-and-drop area. The uploaded image is bound as a WebGL texture and accessed via a `uniform sampler2D`.\n\n## Directive Syntax\n\n```\n// @viji-image:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `description` | No | — | Tooltip text (use quotes) |\n| `group` | No | `general` | Group name (use quotes) |\n| `category` | No | `general` | Visibility category |\n\nImage parameters do not have a `default` key — the default is always \"no image\" until the user uploads one.\n\n## Uniform Type\n\nThe image is injected as a `sampler2D`. When no image is uploaded, the texture contains a single transparent black pixel (`vec4(0.0)`).\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-image:tex label:\"Texture\"\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec4 img = texture2D(tex, uv);\n gl_FragColor = img;\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Viji auto-injects all `uniform` declarations — both built-in uniforms and parameter uniforms from directives. Do **not** redeclare them in your shader code; duplicate declarations cause compilation errors."
1997
+ },
1998
+ {
1999
+ "type": "live-example",
2000
+ "title": "Image Texture",
2001
+ "sceneCode": "// @renderer shader\n// @viji-image:tex label:\"Texture\"\n// @viji-slider:distort label:\"Distortion\" default:0.02 min:0 max:0.1 step:0.005\n// @viji-color:tint label:\"Tint\" default:#ffffff\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 d = vec2(\n sin(uv.y * 10.0 + u_time * 2.0) * distort,\n cos(uv.x * 10.0 + u_time * 2.0) * distort\n );\n vec4 img = texture2D(tex, uv + d);\n\n float hasImage = step(0.001, img.a);\n vec3 fallback = vec3(uv, sin(u_time) * 0.5 + 0.5);\n vec3 col = mix(fallback, img.rgb * tint, hasImage);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
2002
+ "sceneFile": "image-shader.scene.glsl"
2003
+ },
2004
+ {
2005
+ "type": "text",
2006
+ "markdown": "## Handling Missing Images\n\nWhen no image is uploaded, `texture2D(tex, uv)` returns `vec4(0.0)` (transparent black). You can provide a fallback:\n\n```glsl\nvec4 img = texture2D(tex, uv);\nfloat hasImage = step(0.001, img.a);\nvec3 fallback = vec3(uv, 0.5);\nvec3 col = mix(fallback, img.rgb, hasImage);\ngl_FragColor = vec4(col, 1.0);\n```\n\n## Effects on Textures\n\nCombine the image with time-based effects:\n\n```glsl\nvec2 distorted = uv + vec2(sin(uv.y * 10.0 + u_time) * 0.02);\nvec4 img = texture2D(tex, distorted);\n```\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Color](../color/) — color picker `uniform vec3`\n- [Slider](/shader/parameters/slider) — numeric slider `uniform float`\n- [Native Image](/native/parameters/image) — Native renderer equivalent (`ImageBitmap`)\n- [P5 Image](/p5/parameters/image) — P5 renderer equivalent (`.p5` property)"
2007
+ }
2008
+ ]
2009
+ },
2010
+ "shader-param-button": {
2011
+ "id": "shader-param-button",
2012
+ "title": "Button Parameter",
2013
+ "description": "Declare a momentary trigger that maps to a uniform bool — true for exactly one frame when pressed.",
2014
+ "content": [
2015
+ {
2016
+ "type": "text",
2017
+ "markdown": "# @viji-button\n\n```glsl\n// @viji-button:resetPhase label:\"Reset Phase\"\n```\n\nDeclares a momentary button parameter. Unlike [`@viji-toggle`](../toggle/) which latches on/off, a button is `true` for exactly **one frame** when the user clicks it, then automatically resets to `false`. The value is injected as a `uniform bool`.\n\n## Directive Syntax\n\n```\n// @viji-button:uniformName key:value key:value ...\n```\n\n| Key | Required | Default | Description |\n|-----|----------|---------|-------------|\n| `label` | Yes | — | Display name in the parameter UI |\n| `description` | No | — | Tooltip text (use quotes: `description:\"Help text\"`) |\n| `group` | No | `general` | Group name (use quotes: `group:\"controls\"`) |\n| `category` | No | `general` | Visibility category |\n\nThe `default` key is **not used** for buttons — they always start as `false`.\n\n## Uniform Type\n\nThe button is injected as a `bool`:\n\n| State | GLSL Value |\n|-------|------------|\n| Not pressed | `false` |\n| Frame after click | `true` (1 frame only) |\n\n> [!WARNING]\n> Do not use the `u_` prefix for your parameter uniform names — it is reserved for built-in Viji uniforms. Use descriptive names like `resetPhase`, `spawnBurst`, `clearTrail` instead.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-button:resetPhase label:\"Reset Phase\"\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float p = resetPhase ? 0.0 : phase;\n float wave = sin(uv.x * 10.0 + p);\n vec3 col = mix(vec3(0.1, 0.1, 0.3), vec3(0.3, 0.8, 1.0), wave * 0.5 + 0.5);\n\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\n> [!WARNING]\n> The directive must use `//` comments only. Block comments (`/* */`) are not parsed.\n\n> [!NOTE]\n> Because the button is `true` for only one frame, it is most useful for triggering one-shot state changes. In the example above, the reset only affects one frame — combine with a backbuffer or accumulator pattern for persistent resets.\n\n## Rules\n\n- The `label` key is required\n- The `default` key is not used and will be ignored with a warning\n- String values use quotes: `label:\"Reset Phase\"`\n- The uniform name in the directive must match the `uniform bool` declaration\n\n## Button vs Toggle\n\n| | `@viji-button` | `@viji-toggle` |\n|---|---|---|\n| Uniform type | `bool` | `bool` |\n| Value | `true` for 1 frame, then `false` | Stays `true` or `false` until changed |\n| `default` key | Not used | Required |\n| Use for | One-shot triggers, resets | Persistent on/off switches |\n\n## Related\n\n- [Shader Basics](/shader/basics) — shader file structure and directives\n- [Toggle](../toggle/) — persistent boolean `uniform bool`\n- [Accumulator](../accumulator/) — smooth parameter-driven animation\n- [Slider](../slider/) — numeric `uniform float`\n- [Native Button](/native/parameters/button) — equivalent for the Native renderer\n- [P5 Button](/p5/parameters/button) — equivalent for the P5 renderer"
2018
+ }
2019
+ ]
2020
+ },
2021
+ "shader-param-accumulator": {
2022
+ "id": "shader-param-accumulator",
2023
+ "title": "shader-param-accumulator",
2024
+ "description": "Smooth, jump-free animation in shaders using CPU-side phase accumulation driven by parameters or constants.",
2025
+ "content": [
2026
+ {
2027
+ "type": "text",
2028
+ "markdown": "# @viji-accumulator\r\n\r\n```\r\n// @viji-accumulator:uniformName rate:source [default:value]\r\n```\r\n\r\nAn accumulator is a CPU-side float value that grows by `rate × deltaTime` every frame. It produces a `uniform float` in your shader — use it anywhere you need smooth, parameter-driven animation without jumps.\r\n\r\n## The Problem\r\n\r\nWhen you multiply `u_time` by a speed parameter, changing the slider recalculates the entire phase instantly and the animation jumps:\r\n\r\n```glsl\r\n// Bad — animation jumps when speed slider changes\r\nfloat wave = sin(u_time * speed);\r\n```\r\n\r\nThe accumulator solves this by integrating the rate over time. Changing the rate only affects future growth — it never jumps backward or forward:\r\n\r\n```glsl\r\n// Good — smooth at any slider value\r\n// @viji-accumulator:phase rate:speed\r\nfloat wave = sin(phase);\r\n```\r\n\r\n## Basic Usage"
2029
+ },
2030
+ {
2031
+ "type": "live-example",
2032
+ "title": "Speed-Driven Accumulator",
2033
+ "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\r\n// @viji-color:tint label:\"Tint\" default:#00ffcc\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\r\n gl_FragColor = vec4(tint * wave, 1.0);\r\n}\r\n",
2034
+ "sceneFile": "acc-basic.scene.glsl"
2035
+ },
2036
+ {
2037
+ "type": "text",
2038
+ "markdown": "The accumulator `phase` increases by `speed × deltaTime` each frame. Try moving the Speed slider — the wave changes pace smoothly, never jumps.\r\n\r\n## Config Keys\r\n\r\n| Key | Type | Required | Default | Description |\r\n|-----|------|----------|---------|-------------|\r\n| `rate` | `string` or `number` | Yes | — | Rate source: a declared parameter name or numeric constant |\r\n| `default` | `number` | No | `0` | Initial value of the accumulator |\r\n\r\n## Uniform Type\r\n\r\nAccumulators always produce a `uniform float`:\r\n\r\n```glsl\r\n// This directive:\r\n// @viji-accumulator:phase rate:speed\r\n\r\n// Generates this uniform:\r\nuniform float phase;\r\n```\r\n\r\n## No UI Control\r\n\r\nUnlike sliders, colors, or toggles, accumulators **do not appear** in the host parameter panel. They are internal values managed by the Viji runtime. Artists cannot directly manipulate them — they are driven entirely by their `rate` source.\r\n\r\n## Constant Rate\r\n\r\nThe `rate` can be a numeric constant instead of a parameter name. This is useful for steady background animation that doesn't need user control:\r\n\r\n```glsl\r\n// @viji-accumulator:drift rate:0.5\r\n```\r\n\r\nHere `drift` increases by `0.5` per second, unconditionally."
2039
+ },
2040
+ {
2041
+ "type": "live-example",
2042
+ "title": "Constant-Rate Accumulator",
2043
+ "sceneCode": "// @renderer shader\r\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.1 max:2.0 step:0.1\r\n// @viji-accumulator:drift rate:0.5\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n center.x *= u_resolution.x / u_resolution.y;\r\n\r\n float d = length(center);\r\n float ring = smoothstep(0.02, 0.0, abs(d - mod(drift * 0.3, 0.5)));\r\n\r\n vec3 color = vec3(0.1, 0.05, 0.2) + ring * brightness * vec3(0.9, 0.4, 1.0);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
2044
+ "sceneFile": "acc-constant.scene.glsl"
2045
+ },
2046
+ {
2047
+ "type": "text",
2048
+ "markdown": "## Multiple Accumulators\r\n\r\nYou can declare multiple accumulators with independent rates. This lets you animate different axes or properties at different speeds:\r\n\r\n```glsl\r\n// @viji-slider:speedX label:\"Horizontal Speed\" default:1.0 min:0.0 max:5.0\r\n// @viji-slider:speedY label:\"Vertical Speed\" default:0.7 min:0.0 max:5.0\r\n// @viji-accumulator:phaseX rate:speedX\r\n// @viji-accumulator:phaseY rate:speedY\r\n```\r\n\r\nEach accumulator tracks its own value independently. Changing `speedX` has no effect on `phaseY`."
2049
+ },
2050
+ {
2051
+ "type": "live-example",
2052
+ "title": "Multi-Axis Accumulator",
2053
+ "sceneCode": "// @renderer shader\r\n// @viji-slider:speedX label:\"Horizontal Speed\" default:1.0 min:0.0 max:5.0 step:0.1\r\n// @viji-slider:speedY label:\"Vertical Speed\" default:0.7 min:0.0 max:5.0 step:0.1\r\n// @viji-slider:radius label:\"Radius\" default:0.3 min:0.05 max:0.5 step:0.01\r\n// @viji-color:dotColor label:\"Color\" default:#ff6600\r\n// @viji-accumulator:phaseX rate:speedX\r\n// @viji-accumulator:phaseY rate:speedY\r\n\r\nvoid main() {\r\n vec2 uv = gl_FragCoord.xy / u_resolution;\r\n vec2 center = uv - 0.5;\r\n center.x *= u_resolution.x / u_resolution.y;\r\n\r\n vec2 pos = vec2(cos(phaseX), sin(phaseY)) * 0.3;\r\n float d = length(center - pos);\r\n float circle = smoothstep(radius + 0.01, radius - 0.01, d);\r\n\r\n vec3 bg = vec3(0.05);\r\n vec3 color = mix(bg, dotColor, circle);\r\n gl_FragColor = vec4(color, 1.0);\r\n}\r\n",
2054
+ "sceneFile": "acc-multi.scene.glsl"
2055
+ },
2056
+ {
2057
+ "type": "text",
2058
+ "markdown": "## Rules\r\n\r\n- **Only `//` comments.** Like all `@viji-*` directives, accumulators must use single-line `//` comments. Block comments `/* */` are not parsed.\r\n- **No `u_` prefix.** The `u_` prefix is reserved for built-in Viji uniforms. Name your accumulators descriptively: `phase`, `drift`, `rotation`.\r\n- **Rate must exist.** If `rate` references a parameter name that doesn't exist, Viji logs a warning and falls back to rate `0` (the accumulator stays at its default value).\r\n- **`label` is not required.** Since accumulators have no UI, the `label` config key is optional (unlike all other parameter types).\r\n- **Value grows unbounded.** The accumulator value increases indefinitely. If you need a bounded range, use `mod()` or `fract()` in your shader code:\r\n\r\n```glsl\r\n// @viji-accumulator:phase rate:speed\r\nfloat angle = mod(phase, 6.283185); // wrap to 0–2π\r\n```\r\n\r\n## Related\r\n\r\n- [Shader Quick Start](/shader/quickstart) — introduction to shader parameter system\r\n- [Slider](/shader/parameters/slider) — numeric slider parameter (common rate source)\r\n- [Best Practices](/getting-started/best-practices) — animation timing patterns\r\n- [Common Mistakes](/getting-started/common-mistakes) — why `u_time * speed` jumps"
2059
+ }
2060
+ ]
2061
+ },
2062
+ "shader-param-grouping": {
2063
+ "id": "shader-param-grouping",
2064
+ "title": "Grouping Parameters",
2065
+ "description": "Organize shader parameters into named groups using the group config key.",
2066
+ "content": [
2067
+ {
2068
+ "type": "text",
2069
+ "markdown": "# Grouping Shader Parameters\n\nAs your shader grows, the parameter list can become unwieldy. Groups let you collect related parameters under a shared heading in the host UI. In shaders, set the `group:` key in any `@viji-*` directive.\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 group:animation\n// @viji-toggle:invert label:\"Invert\" default:false group:animation\n// @viji-slider:rings label:\"Ring Count\" default:10.0 min:2.0 max:30.0 step:1.0 group:shape\n// @viji-color:tint label:\"Tint\" default:#ff6600 group:shape\n```\n\nParameters with the same `group` value appear together in the UI. The group name is a freeform string — any name works.\n\nThe host application receives the group names and renders them as collapsible sections or visual separators.\n\n## How It Works\n\n- The `group` key is a **freeform string** — any name works. There is no fixed list.\n- If you omit `group`, the parameter defaults to `'general'`.\n- Parameters with the same `group` value are collected together when sent to the host UI.\n- Group names are displayed as-is, so use readable names like `animation`, `visual style`, `audio settings`.\n- Declaration order within a group is preserved.\n- The `group:` key in `@viji-*` directives works identically to the `group` config in native/P5 JavaScript parameters.\n\n## Live Example\n\nThis shader uses two groups — \"animation\" (speed and invert) and \"shape\" (ring count and tint):"
2070
+ },
2071
+ {
2072
+ "type": "live-example",
2073
+ "title": "Shader Grouped Parameters",
2074
+ "sceneCode": "// @renderer shader\r\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 group:animation\r\n// @viji-toggle:invert label:\"Invert\" default:false group:animation\r\n// @viji-slider:rings label:\"Ring Count\" default:10.0 min:2.0 max:30.0 step:1.0 group:shape\r\n// @viji-color:tint label:\"Tint\" default:#ff6600 group:shape\r\n// @viji-accumulator:phase rate:speed\r\n\r\nvoid main() {\r\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\r\n float d = length(uv);\r\n\r\n float wave = sin(d * rings - phase * 4.0) * 0.5 + 0.5;\r\n vec3 col = tint * wave;\r\n\r\n if (invert) col = 1.0 - col;\r\n\r\n col *= smoothstep(1.5, 0.5, d);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
2075
+ "sceneFile": "grouping-demo.scene.glsl"
2076
+ },
2077
+ {
2078
+ "type": "text",
2079
+ "markdown": "## Mixing Types\n\nGroups can contain any mix of parameter types — [`@viji-slider`](../slider/), [`@viji-color`](../color/), [`@viji-toggle`](../toggle/), [`@viji-select`](../select/), and more. There is no restriction on which types can share a group:\n\n```glsl\n// @viji-slider:brightness label:\"Brightness\" default:1.0 min:0.0 max:2.0 group:visual\n// @viji-color:tint label:\"Tint\" default:#ffffff group:visual\n// @viji-toggle:showGrid label:\"Grid Overlay\" default:false group:visual\n// @viji-select:blendMode label:\"Blend\" default:0.0 options:normal|add|multiply group:visual\n```\n\n## Groups and Categories\n\n`group` and `category` are independent. A group organizes the visual layout; a category controls visibility based on active capabilities. Combine them when a grouped parameter depends on an external input:\n\n```glsl\n// @viji-slider:bassReact label:\"Bass React\" default:0.5 min:0.0 max:1.0 group:effects category:audio\n// @viji-slider:colorShift label:\"Color Shift\" default:0.2 min:0.0 max:1.0 group:effects category:general\n```\n\nBoth appear in the \"effects\" group, but `bassReact` only shows when audio is connected.\n\nSee [Categories](../categories/) for details on how `category` controls parameter visibility.\n\n## Related\n\n- [Parameters Overview](../) — all shader parameter types\n- [Categories](../categories/) — visibility based on active capabilities\n- [Native Grouping](/native/parameters/grouping) — same concept in JavaScript\n- [P5 Grouping](/p5/parameters/grouping) — same concept in P5 renderer\n- [Slider](../slider/) — the most common parameter type"
2080
+ }
2081
+ ]
2082
+ },
2083
+ "shader-param-categories": {
2084
+ "id": "shader-param-categories",
2085
+ "title": "Parameter Categories",
2086
+ "description": "Control shader parameter visibility based on active capabilities like audio, video, and interaction.",
2087
+ "content": [
2088
+ {
2089
+ "type": "text",
2090
+ "markdown": "# Shader Parameter Categories\n\nCategories let you tag parameters so they're only visible when the corresponding capability is active. An \"Audio Pulse\" [`@viji-slider`](../slider/) is useless if no audio source is connected — with `category:audio`, it automatically appears when audio is available and hides when it's not. In shaders, set the `category:` key in any `@viji-*` directive.\n\n## The Four Categories\n\n| Category | Visible When | Use For |\n|---|---|---|\n| `general` | Always | Colors, sizes, speeds, shapes — anything that works without external input |\n| `audio` | Audio source is connected | Volume scaling, beat reactivity, frequency controls |\n| `video` | Video/camera source is connected | Video opacity, CV sensitivity, segmentation controls |\n| `interaction` | User interaction is enabled | Mouse effects, keyboard bindings, touch sensitivity |\n\n## Usage\n\n```glsl\n// @renderer shader\n// @viji-color:tint label:\"Color\" default:#4488ff category:general\n// @viji-slider:audioPulse label:\"Audio Pulse\" default:0.3 min:0.0 max:1.0 category:audio\n// @viji-slider:mouseSize label:\"Mouse Glow\" default:0.15 min:0.0 max:0.5 category:interaction\n```\n\n- `tint` ([`@viji-color`](../color/)) is always visible.\n- `audioPulse` ([`@viji-slider`](../slider/)) only appears when audio is connected.\n- `mouseSize` ([`@viji-slider`](../slider/)) only appears when interaction is enabled.\n\nIf you omit `category`, it defaults to `general` (always visible).\n\n## How It Works\n\n1. The artist sets `category:` on each directive during scene declaration.\n2. The shader parameter parser extracts `category` along with other config keys.\n3. When the host application requests parameters, Viji filters them based on the current `CoreCapabilities`:\n - `hasAudio` — is an audio stream connected?\n - `hasVideo` — is a video/camera stream connected?\n - `hasInteraction` — is user interaction enabled?\n - `hasGeneral` — always `true`.\n4. Only parameters matching active capabilities are sent to the UI.\n\n> [!NOTE]\n> Categories filter at both the **group level** and the **individual parameter level**. If a group's category doesn't match, the entire group is hidden. If individual parameters within a visible group have non-matching categories, those parameters are hidden while the group remains visible.\n\n> [!NOTE]\n> In shader directives, category values are **unquoted strings**: `category:audio`, not `category:\"audio\"`. Both forms work, but the unquoted form is conventional.\n\n## Live Example\n\nParameters in three categories — `general` (always visible), `audio` (needs audio), and `interaction` (needs mouse):"
2091
+ },
2092
+ {
2093
+ "type": "live-example",
2094
+ "title": "Shader Parameter Categories",
2095
+ "sceneCode": "// @renderer shader\r\n// @viji-color:tint label:\"Color\" default:#4488ff category:general\r\n// @viji-slider:audioPulse label:\"Audio Pulse\" default:0.3 min:0.0 max:1.0 category:audio\r\n// @viji-slider:mouseSize label:\"Mouse Glow\" default:0.15 min:0.0 max:0.5 category:interaction\r\n// @viji-accumulator:phase rate:1.0\r\n\r\nvoid main() {\r\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\r\n float d = length(uv);\r\n\r\n float pulse = u_audioVolume * audioPulse;\r\n float wave = sin(d * 15.0 - phase * 3.0) * 0.5 + 0.5;\r\n vec3 col = tint * wave * (1.0 + pulse);\r\n\r\n vec2 mouseUV = (2.0 * u_mouse - u_resolution) / u_resolution.y;\r\n float mouseDist = length(uv - mouseUV);\r\n float glow = mouseSize / (mouseDist + 0.05);\r\n col += vec3(glow * 0.3);\r\n\r\n col *= smoothstep(1.5, 0.3, d);\r\n gl_FragColor = vec4(col, 1.0);\r\n}\r\n",
2096
+ "sceneFile": "categories-demo.scene.glsl",
2097
+ "capabilities": {
2098
+ "audio": true,
2099
+ "interaction": true
2100
+ }
2101
+ },
2102
+ {
2103
+ "type": "text",
2104
+ "markdown": "## Design Guidelines\n\n- **Default to `general`** unless the parameter genuinely depends on an external input.\n- **Use `audio` for parameters that only make sense with sound** — beat sensitivity, frequency multipliers, audio decay.\n- **Use `video` for parameters tied to camera/video** — segmentation threshold, face tracking sensitivity.\n- **Use `interaction` for parameters that need user input** — mouse effect radius, touch sensitivity.\n- **Don't use categories as a replacement for grouping.** Categories control *visibility*; groups control *layout*. Use both when appropriate.\n\n## Categories and Groups\n\n`category` and `group` are orthogonal. A parameter in group `effects` with category `audio` will appear under the \"effects\" group heading, but only when audio is connected:\n\n```glsl\n// @viji-slider:bassReact label:\"Bass Reactivity\" default:0.5 min:0.0 max:1.0 group:effects category:audio\n// @viji-slider:colorShift label:\"Color Shift\" default:0.2 min:0.0 max:1.0 group:effects category:general\n```\n\nBoth parameters appear in the \"effects\" group, but `bassReact` only shows when audio is active.\n\n## Related\n\n- [Parameters Overview](../) — all shader parameter types\n- [Grouping](../grouping/) — organizing parameters into named groups\n- [Native Categories](/native/parameters/categories) — same concept in JavaScript\n- [P5 Categories](/p5/parameters/categories) — same concept in P5 renderer\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
2105
+ }
2106
+ ]
2107
+ },
2108
+ "shader-pointer": {
2109
+ "id": "shader-pointer",
2110
+ "title": "Pointer Uniforms",
2111
+ "description": "GLSL uniforms for unified pointer input — a single set of uniforms that work identically for mouse and touch.",
2112
+ "content": [
2113
+ {
2114
+ "type": "text",
2115
+ "markdown": "# Pointer Uniforms\n\nThe pointer uniforms provide a unified input that works the same way whether the user is using a mouse or touch. **For most shader interactions — click effects, drag-based rotation, cursor tracking — start here.**\n\n## Uniform Reference\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords — bottom-left origin) |\n| `u_pointerDelta` | `vec2` | Movement delta in pixels per frame (WebGL coords) |\n| `u_pointerDown` | `bool` | `true` if the primary input is active (left-click or touch) |\n| `u_pointerWasPressed` | `bool` | `true` for exactly one frame when input becomes active, then resets |\n| `u_pointerWasReleased` | `bool` | `true` for exactly one frame when input is released, then resets |\n| `u_pointerInCanvas` | `bool` | `true` if the input position is inside the canvas |\n\n> [!IMPORTANT]\n> All position uniforms use **WebGL coordinates** — the Y axis is flipped compared to DOM coordinates. `(0, 0)` is at the **bottom-left** corner. `u_pointer.y` increases upward, matching `gl_FragCoord.y`.\n\n## How It Works\n\nWhen a touch is active, the pointer tracks the primary touch point (`u_pointerDown` is `true`). Otherwise, it follows the mouse (`u_pointerDown` reflects the left mouse button). This switching happens automatically each frame.\n\nUse [Mouse Uniforms](../mouse/) when you need right-click, scroll wheel, or individual button states. Use [Touch Uniforms](../touch/) when you need multi-touch positions.\n\n## Using Pointer Delta as an Accumulator Rate\n\n`u_pointerDelta` is a `vec2`, so its components can drive accumulators for smooth drag-based rotation or panning:\n\n```glsl\n// @renderer shader\n\n// Drag-based orbit via accumulators driven by pointer delta\n// @viji-accumulator:orbitY rate:u_pointerDelta.x\n// @viji-accumulator:orbitX rate:u_pointerDelta.y\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n\n float rotY = orbitY * 0.01;\n float rotX = orbitX * 0.01;\n\n // Use rotY and rotX for camera or object rotation...\n gl_FragColor = vec4(uv + 0.5, 0.5 + 0.5 * sin(rotY), 1.0);\n}\n```\n\nThe accumulator value grows smoothly as the user drags, and stops when they release — no jumps, no resets.\n\n## Basic Example"
2116
+ },
2117
+ {
2118
+ "type": "live-example",
2119
+ "title": "Pointer — Click Flash & Glow",
2120
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 pNorm = u_pointer / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n float dist = length(uv - pNorm);\n float glow = 0.015 / (dist + 0.01);\n vec3 glowColor = u_pointerDown\n ? vec3(0.3, 0.7, 1.0)\n : vec3(0.5, 0.5, 0.6);\n col += glow * glowColor;\n\n float flash = u_pointerWasPressed ? 1.0 : 0.0;\n col += vec3(0.2, 0.5, 0.8) * flash * smoothstep(0.3, 0.0, dist);\n\n float release = u_pointerWasReleased ? 1.0 : 0.0;\n float ring = smoothstep(0.002, 0.0, abs(dist - 0.15)) * release;\n col += vec3(0.8, 0.4, 0.2) * ring;\n\n if (!u_pointerInCanvas) {\n col *= 0.5;\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
2121
+ "sceneFile": "pointer-shader-demo.scene.glsl",
2122
+ "capabilities": {
2123
+ "interaction": true
2124
+ }
2125
+ },
2126
+ {
2127
+ "type": "text",
2128
+ "markdown": "## Common Patterns\n\n### Normalized Pointer Position\n\n```glsl\nvec2 pNorm = u_pointer / u_resolution;\n```\n\n### Distance from Pointer\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution;\nvec2 pNorm = u_pointer / u_resolution;\nfloat dist = length(uv - pNorm);\nfloat glow = 0.02 / dist;\n```\n\n### Flash on Press/Release\n\n```glsl\nfloat flash = u_pointerWasPressed ? 1.0 : 0.0;\ncol += vec3(flash * 0.3);\n```\n\n## Related\n\n- [Mouse Uniforms](../mouse/) — individual button states, scroll wheel, and movement deltas\n- [Keyboard Uniforms](../keyboard/) — common key state uniforms\n- [Touch Uniforms](../touch/) — multi-touch position uniforms\n- [Native Pointer](/native/pointer) — JavaScript API equivalent\n- [Accumulator](/shader/parameters/accumulator) — frame-persistent state for smooth drag interactions"
2129
+ }
2130
+ ]
2131
+ },
2132
+ "shader-mouse": {
2133
+ "id": "shader-mouse",
2134
+ "title": "Mouse Uniforms",
2135
+ "description": "GLSL uniforms for mouse input — position, buttons, movement deltas, scroll wheel, and press/release events.",
2136
+ "content": [
2137
+ {
2138
+ "type": "text",
2139
+ "markdown": "# Mouse Uniforms\n\nThe mouse uniforms give you detailed mouse state in GLSL — individual button states, movement deltas, scroll wheel, and frame-based events.\n\n> [!TIP]\n> For simple cursor tracking, click effects, and drag interactions that should work for both mouse and touch, use the [Pointer Uniforms](../pointer/) instead. Mouse uniforms are for when you need mouse-specific features like right-click detection, scroll wheel, or individual button states.\n\n## Uniform Reference\n\n### Position\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords — bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | `true` when the cursor is over the canvas |\n\n### Buttons\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mousePressed` | `bool` | `true` if any mouse button is held |\n| `u_mouseLeft` | `bool` | Left button state |\n| `u_mouseRight` | `bool` | Right button state |\n| `u_mouseMiddle` | `bool` | Middle button state |\n\n### Movement & Wheel\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouseDelta` | `vec2` | Movement delta in pixels per frame (WebGL coords) |\n| `u_mouseWheel` | `float` | Vertical scroll delta this frame |\n\n### Frame Events\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouseWasPressed` | `bool` | `true` for exactly one frame when any button is first pressed, then resets |\n| `u_mouseWasReleased` | `bool` | `true` for exactly one frame when any button is released, then resets |\n\n> [!IMPORTANT]\n> All position uniforms use **WebGL coordinates** — `(0, 0)` is at the **bottom-left** corner. `u_mouse.y` increases upward. `u_mouseDelta.y` is positive when the mouse moves **up** on screen.\n\n## Using Mouse Delta and Wheel as Accumulator Rates\n\n`u_mouseDelta` is a `vec2`, so its components can drive accumulators. `u_mouseWheel` is a `float` accumulator rate for zoom effects:\n\n```glsl\n// @renderer shader\n\n// @viji-accumulator:orbitY rate:u_mouseDelta.x\n// @viji-accumulator:orbitX rate:u_mouseDelta.y\n// @viji-accumulator:zoomAcc rate:u_mouseWheel\n\nvoid main() {\n float rotY = orbitY * 0.01;\n float rotX = orbitX * 0.01;\n float zoom = 3.0 - clamp(zoomAcc * 0.01, -2.0, 2.0);\n // ...\n}\n```\n\n## Basic Example"
2140
+ },
2141
+ {
2142
+ "type": "live-example",
2143
+ "title": "Mouse — Glow & Button States",
2144
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 mNorm = u_mouse / u_resolution;\n\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n float dist = length(uv - mNorm);\n float glow = 0.012 / (dist + 0.01);\n\n vec3 btnColor = vec3(0.5);\n if (u_mouseLeft) btnColor = vec3(0.3, 0.8, 1.0);\n if (u_mouseRight) btnColor = vec3(1.0, 0.4, 0.3);\n if (u_mouseMiddle) btnColor = vec3(0.3, 1.0, 0.5);\n col += glow * btnColor;\n\n float flash = u_mouseWasPressed ? 1.0 : 0.0;\n col += vec3(0.3) * flash * smoothstep(0.2, 0.0, dist);\n\n float speed = length(u_mouseDelta) / max(u_resolution.x, 1.0);\n col += vec3(0.0, 0.2, 0.5) * speed * 5.0 * smoothstep(0.3, 0.0, dist);\n\n float wheelVis = clamp(u_mouseWheel * 0.001, -1.0, 1.0);\n col.r += abs(wheelVis) * 0.15 * smoothstep(0.5, 0.0, dist);\n\n if (!u_mouseInCanvas) {\n col *= 0.4;\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
2145
+ "sceneFile": "mouse-shader-demo.scene.glsl",
2146
+ "capabilities": {
2147
+ "interaction": true
2148
+ }
2149
+ },
2150
+ {
2151
+ "type": "text",
2152
+ "markdown": "## Common Patterns\n\n### Normalized Mouse Position\n\n```glsl\nvec2 mNorm = u_mouse / u_resolution;\n```\n\n### Mouse Distance Field\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution;\nfloat dist = length(uv - u_mouse / u_resolution);\nfloat glow = 0.02 / dist;\n```\n\n### Button-Dependent Color\n\n```glsl\nvec3 col = vec3(0.1);\nif (u_mouseLeft) col.r += 0.5;\nif (u_mouseRight) col.b += 0.5;\nif (u_mouseMiddle) col.g += 0.5;\n```\n\n### Wheel-Based Zoom\n\n```glsl\n// @viji-accumulator:zoomAcc rate:u_mouseWheel\n\nvoid main() {\n float zoom = 1.0 + zoomAcc * 0.001;\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / (min(u_resolution.x, u_resolution.y) * zoom);\n // ...\n}\n```\n\n## Related\n\n- [Pointer Uniforms](../pointer/) — recommended starting point for cross-device interactions\n- [Keyboard Uniforms](../keyboard/) — common key state uniforms\n- [Touch Uniforms](../touch/) — multi-touch position uniforms\n- [Native Mouse](/native/mouse) — JavaScript API equivalent\n- [Accumulator](/shader/parameters/accumulator) — frame-persistent state for smooth interactions"
2153
+ }
2154
+ ]
2155
+ },
2156
+ "shader-keyboard": {
2157
+ "id": "shader-keyboard",
2158
+ "title": "Keyboard Uniforms",
2159
+ "description": "GLSL uniforms for keyboard input — boolean key uniforms, a Shadertoy-compatible keyboard texture, and accumulator-driven animation.",
2160
+ "content": [
2161
+ {
2162
+ "type": "text",
2163
+ "markdown": "# Keyboard Uniforms\n\nViji provides two ways to read keyboard state in shaders:\n\n1. **Boolean uniforms** — 12 predefined `bool` uniforms for common keys (WASD, arrows, space, modifiers). Simple and direct.\n2. **Keyboard texture** — `u_keyboard`, a `sampler2D` (256x3) that exposes the held/pressed/toggle state of every key by its JavaScript `keyCode`.\n\n## Boolean Uniforms\n\n### Movement Keys\n\n| Uniform | Type | Key |\n|---------|------|-----|\n| `u_keyW` | `bool` | W key |\n| `u_keyA` | `bool` | A key |\n| `u_keyS` | `bool` | S key |\n| `u_keyD` | `bool` | D key |\n| `u_keyUp` | `bool` | Up arrow |\n| `u_keyDown` | `bool` | Down arrow |\n| `u_keyLeft` | `bool` | Left arrow |\n| `u_keyRight` | `bool` | Right arrow |\n\n### Action & Modifiers\n\n| Uniform | Type | Key |\n|---------|------|-----|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift |\n| `u_keyCtrl` | `bool` | Ctrl / Cmd |\n| `u_keyAlt` | `bool` | Alt / Option |\n\n## Keyboard Texture (`u_keyboard`)\n\nFor access to any key beyond the 12 boolean uniforms, use the `u_keyboard` texture. It is a 256x3 `sampler2D` that stores the state of every key indexed by its JavaScript `keyCode` value.\n\n### Texture Layout\n\n| Row | Y coordinate | Meaning |\n|-----|-------------|---------|\n| 0 | `0.5/3.0` | **Held** — 1.0 while the key is pressed, 0.0 when released |\n| 1 | `1.5/3.0` | **Pressed this frame** — 1.0 only on the frame the key was first pressed |\n| 2 | `2.5/3.0` | **Toggle** — flips between 0.0 and 1.0 on each press |\n\n### Sampling\n\n```glsl\n// WebGL 2 — integer coordinates (preferred)\nfloat held = texelFetch(u_keyboard, ivec2(KEY_CODE, 0), 0).x;\nfloat pressed = texelFetch(u_keyboard, ivec2(KEY_CODE, 1), 0).x;\nfloat toggle = texelFetch(u_keyboard, ivec2(KEY_CODE, 2), 0).x;\n\n// WebGL 1 — normalized coordinates (center-of-pixel)\nfloat held = texture2D(u_keyboard, vec2((float(KEY_CODE) + 0.5) / 256.0, 0.5 / 3.0)).x;\n```\n\n### Common Key Codes\n\n| Key | Code | Key | Code | Key | Code |\n|-----|------|-----|------|-----|------|\n| A–Z | 65–90 | 0–9 | 48–57 | Space | 32 |\n| Left | 37 | Up | 38 | Right | 39 |\n| Down | 40 | Enter | 13 | Shift | 16 |\n| Ctrl | 17 | Alt | 18 | Tab | 9 |\n\n### Shadertoy Compatibility\n\nThe `u_keyboard` texture is layout-compatible with Shadertoy's keyboard channel. When converting a Shadertoy shader that reads keyboard state from `iChannel0` (or any channel set to \"Keyboard\"), simply replace the channel name:\n\n```glsl\n// Shadertoy\nfloat held = texelFetch(iChannel0, ivec2(65, 0), 0).x; // 'A' held?\n\n// Viji\nfloat held = texelFetch(u_keyboard, ivec2(65, 0), 0).x; // 'A' held?\n```\n\n## Using Key Uniforms as Accumulator Rates\n\nKey booleans can drive accumulators for smooth continuous movement. When the key is held, the accumulator grows at a steady rate. When released, it stops — no jumps.\n\n```glsl\n// @renderer shader\n\n// Hold D to rotate right, A to rotate left\n// @viji-accumulator:rotRight rate:u_keyD\n// @viji-accumulator:rotLeft rate:u_keyA\n\nvoid main() {\n float angle = (rotRight - rotLeft) * 2.0;\n\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n float c = cos(angle), s = sin(angle);\n uv = mat2(c, -s, s, c) * uv;\n\n gl_FragColor = vec4(uv + 0.5, 0.5, 1.0);\n}\n```\n\n## Basic Example"
2164
+ },
2165
+ {
2166
+ "type": "live-example",
2167
+ "title": "Keyboard — WASD Movement",
2168
+ "sceneCode": "// @renderer shader\n\n// Smooth WASD/arrow movement via accumulators\n// @viji-accumulator:moveRight rate:u_keyD\n// @viji-accumulator:moveLeft rate:u_keyA\n// @viji-accumulator:moveUp rate:u_keyW\n// @viji-accumulator:moveDown rate:u_keyS\n// @viji-accumulator:arRight rate:u_keyRight\n// @viji-accumulator:arLeft rate:u_keyLeft\n// @viji-accumulator:arUp rate:u_keyUp\n// @viji-accumulator:arDown rate:u_keyDown\n\nvoid main() {\n vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);\n\n float speed = u_keyShift ? 0.5 : 0.2;\n vec2 offset = vec2(\n (moveRight + arRight - moveLeft - arLeft) * speed,\n (moveUp + arUp - moveDown - arDown) * speed\n );\n\n vec2 p = uv - offset;\n\n float d = length(p) - 0.1;\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n vec3 dotColor = u_keySpace ? vec3(1.0, 0.5, 0.2) : vec3(0.3, 0.7, 1.0);\n col += dotColor * smoothstep(0.01, 0.0, d);\n\n float glow = 0.005 / (abs(d) + 0.005);\n col += dotColor * glow * 0.3;\n\n float grid = step(0.98, fract(uv.x * 10.0)) + step(0.98, fract(uv.y * 10.0));\n col += grid * 0.03;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
2169
+ "sceneFile": "keyboard-shader-demo.scene.glsl",
2170
+ "capabilities": {
2171
+ "interaction": true
2172
+ }
2173
+ },
2174
+ {
2175
+ "type": "text",
2176
+ "markdown": "## Common Patterns\n\n### Movement Direction\n\n```glsl\nvec2 dir = vec2(0.0);\nif (u_keyD || u_keyRight) dir.x += 1.0;\nif (u_keyA || u_keyLeft) dir.x -= 1.0;\nif (u_keyW || u_keyUp) dir.y += 1.0;\nif (u_keyS || u_keyDown) dir.y -= 1.0;\n```\n\n### Speed Modifier\n\n```glsl\nfloat speed = u_keyShift ? 3.0 : 1.0;\n```\n\n### Toggle Effect with Accumulator\n\n```glsl\n// @viji-accumulator:spaceAcc rate:u_keySpace\n\nvoid main() {\n float effect = fract(spaceAcc * 0.5);\n // effect ramps while space is held\n}\n```\n\n### Arbitrary Key via Texture\n\n```glsl\n// Check if 'E' (keyCode 69) is held\nfloat eHeld = texelFetch(u_keyboard, ivec2(69, 0), 0).x;\n\n// Toggle state of 'T' (keyCode 84)\nfloat tToggle = texelFetch(u_keyboard, ivec2(84, 2), 0).x;\n```\n\n## Related\n\n- [Pointer Uniforms](../pointer/) — unified position and click input\n- [Mouse Uniforms](../mouse/) — mouse position, buttons, and scroll wheel\n- [Touch Uniforms](../touch/) — multi-touch positions\n- [Native Keyboard](/native/keyboard) — JavaScript API with arbitrary key access\n- [Accumulator](/shader/parameters/accumulator) — frame-persistent state for smooth key-driven animation\n- [Shadertoy Compatibility](/shader/shadertoy) — converting Shadertoy shaders with keyboard input"
2177
+ }
2178
+ ]
2179
+ },
2180
+ "shader-touch": {
2181
+ "id": "shader-touch",
2182
+ "title": "Touch Uniforms",
2183
+ "description": "GLSL uniforms for touch input — up to 5 touch point positions and a touch count.",
2184
+ "content": [
2185
+ {
2186
+ "type": "text",
2187
+ "markdown": "# Touch Uniforms\n\nThe touch uniforms expose up to 5 simultaneous touch positions and a count of active touches.\n\n> [!TIP]\n> For single-point interactions that should work for both touch and mouse, use the [Pointer Uniforms](../pointer/) instead. Touch uniforms are for when you need multiple simultaneous touch positions in GLSL.\n\n> [!NOTE]\n> Only touch **positions** are exposed as uniforms. Per-touch pressure, radius, velocity, and lifecycle state are available in the JavaScript API ([Native Touch](/native/touch), [P5 Touch](/p5/touch)) but not as GLSL uniforms.\n\n## Uniform Reference\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touch points (0–5) |\n| `u_touch0` | `vec2` | First touch position in pixels (WebGL coords) |\n| `u_touch1` | `vec2` | Second touch position in pixels (WebGL coords) |\n| `u_touch2` | `vec2` | Third touch position in pixels (WebGL coords) |\n| `u_touch3` | `vec2` | Fourth touch position in pixels (WebGL coords) |\n| `u_touch4` | `vec2` | Fifth touch position in pixels (WebGL coords) |\n\n> [!IMPORTANT]\n> All position uniforms use **WebGL coordinates** — `(0, 0)` is at the **bottom-left** corner. When no touch is active at a given index, the uniform is `vec2(0.0, 0.0)`.\n\n## Basic Example"
2188
+ },
2189
+ {
2190
+ "type": "live-example",
2191
+ "title": "Touch — Multi-Point Glow",
2192
+ "sceneCode": "// @renderer shader\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy;\n\n vec3 col = vec3(0.04, 0.04, 0.08);\n\n vec3 colors[5];\n colors[0] = vec3(0.3, 0.7, 1.0);\n colors[1] = vec3(0.3, 1.0, 0.5);\n colors[2] = vec3(1.0, 0.5, 0.3);\n colors[3] = vec3(1.0, 0.8, 0.2);\n colors[4] = vec3(0.8, 0.3, 1.0);\n\n for (int i = 0; i < 5; i++) {\n if (i >= u_touchCount) break;\n vec2 tp;\n if (i == 0) tp = u_touch0;\n else if (i == 1) tp = u_touch1;\n else if (i == 2) tp = u_touch2;\n else if (i == 3) tp = u_touch3;\n else tp = u_touch4;\n\n float d = length(uv - tp);\n col += colors[i] * 20.0 / (d + 8.0);\n\n float ring = smoothstep(2.0, 0.0, abs(d - 50.0));\n col += colors[i] * ring * 0.5;\n }\n\n if (u_touchCount == 0) {\n vec2 center = u_resolution * 0.5;\n float pulse = 0.5 + 0.5 * sin(u_time * 2.0);\n float d = length(uv - center);\n col += vec3(0.2, 0.3, 0.5) * pulse * 10.0 / (d + 20.0);\n }\n\n if (u_touchCount >= 2) {\n float dist = length(u_touch0 - u_touch1);\n vec2 mid = (u_touch0 + u_touch1) * 0.5;\n float dMid = length(uv - mid);\n float ring = smoothstep(2.0, 0.0, abs(dMid - dist * 0.5));\n col += vec3(0.5, 0.5, 0.8) * ring * 0.4;\n }\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
2193
+ "sceneFile": "touch-shader-demo.scene.glsl",
2194
+ "capabilities": {
2195
+ "interaction": true
2196
+ }
1192
2197
  },
1193
2198
  {
1194
2199
  "type": "text",
1195
- "markdown": "## Design Guidelines\r\n\r\n- **Default to `general`** unless the parameter genuinely depends on an external input.\r\n- **Use `audio` for parameters that only make sense with sound** beat sensitivity, frequency multipliers, audio decay.\r\n- **Use `video` for parameters tied to camera/video** segmentation threshold, face tracking sensitivity.\r\n- **Use `interaction` for parameters that need user input** mouse effect radius, touch sensitivity.\r\n- **Don't use categories as a replacement for grouping.** Categories control *visibility*; groups control *layout*. Use both when appropriate.\r\n\r\n## Categories and Groups\r\n\r\n`category` and `group` are orthogonal. A parameter in group `effects` with category `audio` will appear under the \"effects\" group heading, but only when audio is connected:\r\n\r\n```glsl\r\n// @viji-slider:bassReact label:\"Bass Reactivity\" default:0.5 min:0.0 max:1.0 group:effects category:audio\r\n// @viji-slider:colorShift label:\"Color Shift\" default:0.2 min:0.0 max:1.0 group:effects category:general\r\n```\r\n\r\nBoth parameters appear in the \"effects\" group, but `bassReact` only shows when audio is active.\r\n\r\n## Related\r\n\r\n- [Parameters Overview](../) — all shader parameter types\r\n- [Grouping](../grouping/) — organizing parameters into named groups\r\n- [Native Categories](/native/parameters/categories) — same concept in JavaScript\r\n- [P5 Categories](/p5/parameters/categories) — same concept in P5 renderer\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
2200
+ "markdown": "## Common Patterns\n\n### Glow at Each Touch Point\n\n```glsl\nvec3 col = vec3(0.0);\nvec2 uv = gl_FragCoord.xy;\n\nfor (int i = 0; i < 5; i++) {\n if (i >= u_touchCount) break;\n vec2 tp;\n if (i == 0) tp = u_touch0;\n else if (i == 1) tp = u_touch1;\n else if (i == 2) tp = u_touch2;\n else if (i == 3) tp = u_touch3;\n else tp = u_touch4;\n\n float d = length(uv - tp);\n col += vec3(0.3, 0.6, 1.0) * 15.0 / (d + 5.0);\n}\n```\n\n### Two-Finger Distance\n\n```glsl\nif (u_touchCount >= 2) {\n float dist = length(u_touch0 - u_touch1);\n float zoom = dist / u_resolution.x;\n}\n```\n\n### Single Touch with Fallback to Pointer\n\nFor single-touch effects, prefer `u_pointer` since it also handles mouse input automatically. Use `u_touch0`–`u_touch4` only when you need individual finger positions for multi-touch.\n\n## Related\n\n- [Pointer Uniforms](../pointer/) — recommended starting point for single-point interactions\n- [Mouse Uniforms](../mouse/) — mouse position, buttons, and scroll wheel\n- [Keyboard Uniforms](../keyboard/) — common key state uniforms\n- [Native Touch](/native/touch) — JavaScript API with full per-touch data"
1196
2201
  }
1197
2202
  ]
1198
2203
  },
@@ -1223,7 +2228,7 @@ export const docsApi = {
1223
2228
  },
1224
2229
  {
1225
2230
  "type": "text",
1226
- "markdown": "The native version is simpler: no forward declarations, no `main()` wrapper, and direct use of `u_resolution`, `phase`, and `gl_FragCoord`.\r\n\r\n## GLSL Sandbox Compatibility\r\n\r\nGLSL Sandbox shaders use different variable names. The compatibility layer is simpler:\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n#define time u_time\r\n#define mouse (u_mouse / u_resolution)\r\n#define resolution u_resolution\r\n```\r\n\r\nGLSL Sandbox shaders use `void main()` and `gl_FragColor` directly, so no function wrapper is needed — just paste the shader below the defines.\r\n\r\n## Conversion Checklist\r\n\r\n1. Add `// @renderer shader` at the very top.\r\n2. Paste the compatibility defines (or convert to native Viji uniforms).\r\n3. If the shader uses `mainImage`, add the `main()` wrapper.\r\n4. Replace `iChannel0`–`iChannel3` with `@viji-image` parameters.\r\n5. Remove any `precision` statements — Viji auto-injects them.\r\n6. If using GLSL ES 3.00, add `#version 300 es` as the first line and replace `gl_FragColor` with `out vec4`, `texture2D` with `texture`.\r\n7. Test and adjust — most shaders work immediately; complex ones may need minor tweaks.\r\n\r\n## Related\r\n\r\n- [Shader Quick Start](/shader/quickstart) — build shaders from scratch in Viji\r\n- [Accumulator](/shader/parameters/accumulator) — smooth speed control for converted shaders\r\n- [Backbuffer](/shader/backbuffer) — feedback effects (replaces Shadertoy's Buffer A–D for simple cases)\r\n- [Built-in Uniforms](/shader/api-reference) — complete list of Viji uniforms\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
2231
+ "markdown": "The native version is simpler: no forward declarations, no `main()` wrapper, and direct use of `u_resolution`, `phase`, and `gl_FragCoord`.\r\n\r\n## GLSL Sandbox Compatibility\r\n\r\nGLSL Sandbox shaders use different variable names. The compatibility layer is simpler:\r\n\r\n```glsl\r\n// @renderer shader\r\n\r\n#define time u_time\r\n#define mouse (u_mouse / u_resolution)\r\n#define resolution u_resolution\r\n```\r\n\r\nGLSL Sandbox shaders use `void main()` and `gl_FragColor` directly, so no function wrapper is needed — just paste the shader below the defines.\r\n\r\n## Keyboard Texture\r\n\r\nShadertoy provides keyboard input as a 256x3 texture. Viji supports this with `u_keyboard`:\r\n\r\n```glsl\r\n// Shadertoy pattern:\r\nfloat held = texelFetch(iChannel0, ivec2(KEY, 0), 0).x;\r\nfloat pressed = texelFetch(iChannel0, ivec2(KEY, 1), 0).x;\r\nfloat toggle = texelFetch(iChannel0, ivec2(KEY, 2), 0).x;\r\n\r\n// Viji equivalent (no iChannel — use u_keyboard directly):\r\nfloat held = texelFetch(u_keyboard, ivec2(KEY, 0), 0).x;\r\nfloat pressed = texelFetch(u_keyboard, ivec2(KEY, 1), 0).x;\r\nfloat toggle = texelFetch(u_keyboard, ivec2(KEY, 2), 0).x;\r\n```\r\n\r\nThe texture layout matches Shadertoy exactly:\r\n\r\n| Row | Y | Meaning |\r\n|---|---|---|\r\n| 0 | `0.5/3.0` | 1.0 if key is currently held, 0.0 otherwise |\r\n| 1 | `1.5/3.0` | 1.0 on the frame the key was pressed, 0.0 otherwise |\r\n| 2 | `2.5/3.0` | Toggles between 0.0 and 1.0 on each press |\r\n\r\nThe X coordinate is the JavaScript `keyCode` value (e.g., 65 for A, 32 for Space, 37 for Left arrow). When converting a shader that uses keyboard via `iChannel`, replace `iChannelN` with `u_keyboard` — no other changes are needed.\r\n\r\n> [!NOTE]\r\n> In addition to the texture, Viji also provides 12 boolean uniforms (`u_keyW`, `u_keyA`, `u_keySpace`, etc.) for the most common keys. See [Keyboard Uniforms](/shader/keyboard) for details.\r\n\r\n## Known Limitations\r\n\r\nNot all Shadertoy features are available in Viji. Shaders that rely on the following will require manual adaptation or cannot be converted:\r\n\r\n### Multipass Rendering (Buffer A–D)\r\n\r\nShadertoy supports up to 4 intermediate buffers (Buffer A through D) for multi-pass rendering pipelines. Viji provides a single **backbuffer** that stores the previous frame's output — sufficient for feedback effects, trails, and simple accumulation, but not for pipelines where separate passes produce distinct intermediate textures.\r\n\r\n**What works:** Shaders that read their own previous output via a single buffer (e.g., `iChannel0` set to \"Buffer A\" in Buffer A itself).\r\n\r\n**What doesn't work:** Shaders where Buffer A feeds into Buffer B, or where the Image pass reads from multiple buffers simultaneously.\r\n\r\n### CubeMap Buffer\r\n\r\nShadertoy's CubeMap buffer renders six cube faces as a `samplerCube`. Viji only supports `sampler2D` textures — there is no cube map rendering or sampling.\r\n\r\n### 3D / Volume Textures\r\n\r\nShadertoy provides 3D noise textures accessible via `sampler3D`. Viji does not expose 3D textures. Workaround: use 2D noise with layered sampling to approximate 3D noise.\r\n\r\n### `iChannelTime`\r\n\r\nShadertoy provides `iChannelTime[4]` for the current playback position of video or audio textures bound to each channel. Viji does not have an equivalent — use `u_time` for elapsed time instead.\r\n\r\n### `iChannelResolution`\r\n\r\nShadertoy provides `iChannelResolution[4]` with the dimensions of textures bound to each channel. Viji does not auto-inject these. If needed, track image dimensions manually or hardcode expected sizes.\r\n\r\n### Texture Filtering and Wrapping\r\n\r\nShadertoy allows per-channel configuration of texture filtering (nearest, linear, mipmap) and wrapping (clamp, repeat). Viji image parameter textures use fixed settings: `CLAMP_TO_EDGE` wrapping and `LINEAR` filtering. Shaders that depend on `repeat` wrapping should use `fract(uv)` to simulate the behavior.\r\n\r\n### Sound Output Buffer\r\n\r\nShadertoy's \"Sound\" tab generates audio procedurally from a shader. Viji does not support shader-based sound generation.\r\n\r\n### VR Mode\r\n\r\nShadertoy's `mainVR()` entry point for stereoscopic rendering is not supported.\r\n\r\n## Conversion Checklist\r\n\r\n1. Add `// @renderer shader` at the very top.\r\n2. Paste the compatibility defines (or convert to native Viji uniforms).\r\n3. If the shader uses `mainImage`, add the `main()` wrapper.\r\n4. Replace `iChannel0`–`iChannel3` with `@viji-image` parameters.\r\n5. If the shader uses a keyboard channel, replace `iChannelN` keyboard reads with `u_keyboard`.\r\n6. Remove any `precision` statements — Viji auto-injects them.\r\n7. If using GLSL ES 3.00, add `#version 300 es` as the first line and replace `gl_FragColor` with `out vec4`, `texture2D` with `texture`.\r\n8. Check for [known limitations](#known-limitations) — multipass, cubemap, 3D textures, and sound output are not supported.\r\n9. Test and adjust — most shaders work immediately; complex ones may need minor tweaks.\r\n\r\n## Related\r\n\r\n- [Shader Quick Start](/shader/quickstart) — build shaders from scratch in Viji\r\n- [Keyboard Uniforms](/shader/keyboard) — keyboard texture and boolean uniforms\r\n- [Accumulator](/shader/parameters/accumulator) — smooth speed control for converted shaders\r\n- [Backbuffer](/shader/backbuffer) — feedback effects (replaces Shadertoy's Buffer A–D for simple cases)\r\n- [Built-in Uniforms](/shader/api-reference) — complete list of Viji uniforms\r\n- [Best Practices](/getting-started/best-practices) — essential patterns for all renderers"
1227
2232
  }
1228
2233
  ]
1229
2234
  }