@viji-dev/core 0.5.1 → 0.5.3
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/artist-dts-p5.js +1 -1
- package/dist/artist-dts.js +1 -1
- package/dist/artist-global-p5.d.ts +1 -1
- package/dist/artist-global.d.ts +1 -1
- package/dist/docs-api.js +47 -37
- package/dist/{essentia-wasm.web-x6zu4Vib.js → essentia-wasm.web-CPrFAj59.js} +2 -2
- package/dist/{essentia-wasm.web-x6zu4Vib.js.map → essentia-wasm.web-CPrFAj59.js.map} +1 -1
- package/dist/{index-Cqh1k_49.js → index-Bhq4eJe_.js} +101 -33
- package/dist/index-Bhq4eJe_.js.map +1 -0
- package/dist/index.d.ts +51 -10
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/index-Cqh1k_49.js.map +0 -1
package/dist/docs-api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const docsApi = {
|
|
2
2
|
"version": "1.1.0",
|
|
3
|
-
"coreVersion": "0.5.
|
|
4
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"coreVersion": "0.5.3",
|
|
4
|
+
"generatedAt": "2026-05-07T10:18:06.592Z",
|
|
5
5
|
"navigation": [
|
|
6
6
|
{
|
|
7
7
|
"id": "getting-started",
|
|
@@ -1067,6 +1067,11 @@ export const docsApi = {
|
|
|
1067
1067
|
"level": 2,
|
|
1068
1068
|
"text": "Parameter Categories"
|
|
1069
1069
|
},
|
|
1070
|
+
{
|
|
1071
|
+
"id": "no-redundant-input-toggles",
|
|
1072
|
+
"level": 2,
|
|
1073
|
+
"text": "No Redundant Input Toggles"
|
|
1074
|
+
},
|
|
1070
1075
|
{
|
|
1071
1076
|
"id": "canvas-context-selection",
|
|
1072
1077
|
"level": 2,
|
|
@@ -1081,7 +1086,7 @@ export const docsApi = {
|
|
|
1081
1086
|
"content": [
|
|
1082
1087
|
{
|
|
1083
1088
|
"type": "text",
|
|
1084
|
-
"markdown": "# Best Practices\n\nThe rules below apply to every Viji scene, no matter which renderer you use. They cover the few topics where the same constraint has to hold across Native, P5, and Shader at once: animation timing, resolution, audio/video guards, and a handful of safety rules the runtime expects. Get them right and your scenes look correct at any resolution, run smoothly at any frame rate, and stay reliable across devices.\n\nLooking for a specific bug? [Common Mistakes](../common-mistakes/) has wrong/right pairs you can scan.\n\n---\n\n## Animation Timing Across Renderers\n\nViji exposes two timing values that are equivalent across all three renderers:\n\n- **`viji.time`** / **`u_time`**: seconds since the scene started. Use it for animation at a **constant** speed (oscillations, hue cycling, periodic events).\n- **`viji.deltaTime`** / **`u_deltaTime`**: seconds since the last frame. Use it when you accumulate values smoothly regardless of frame rate (movement, physics, fading), or when **animation speed is controlled by a parameter**.\n\n```javascript\n// viji.time: animation at a constant speed\nconst angle = viji.time * 2.0; // fixed 2 rad/s, no parameter involved\nconst x = Math.cos(angle) * radius;\n\n// viji.deltaTime: accumulation that stays smooth at any FPS\nposition += velocity * viji.deltaTime;\nopacity -= fadeRate * viji.deltaTime;\n```\n\n### When to use which\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| Oscillation at **constant** speed | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Depends on absolute position in time |\n| Hue cycling at constant rate | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Needs a continuous, monotonic input |\n| Oscillation at **parameter-driven** speed | accumulator with [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) / [`@viji-accumulator`](/shader/parameters/accumulator) | Avoids phase jumps when the slider changes |\n| Position accumulation | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Must advance the same amount per second, regardless of FPS |\n| Physics / velocity | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Distance = speed × time elapsed |\n| Fading / easing | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Progress should be per-second, not per-frame |\n| Periodic events | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Trigger at fixed time intervals |\n\n**Rule of thumb:** if the speed multiplier is a **constant**, `viji.time * constant` is fine. If the speed comes from a **parameter** (slider, etc.), accumulate with `viji.deltaTime` to prevent jumps.\n\n### Accumulator Pattern: Parameter-Driven Speed\n\nWhen animation speed is controlled by a user parameter, **never** multiply `viji.time` (or `u_time`) by the parameter directly. Changing the slider recalculates the entire phase history with the new value and causes a visible jump. Instead, accumulate `speed × deltaTime` incrementally so the slider only affects future frames.\n\nThe pattern is the same across all three renderers:\n\n```javascript\n// Native\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const wave = Math.sin(phase);\n}\n```\n\n```javascript\n// @renderer p5\nconst spin = viji.slider(1, { min: 0.1, max: 5, label: 'Spin' });\nlet phase = 0;\nfunction render(viji, p5) {\n phase += spin.value * viji.deltaTime;\n p5.rotate(phase);\n}\n```\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n // Use `phase` instead of `u_time * speed`\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\nFor shaders, the [`@viji-accumulator`](/shader/parameters/accumulator) directive integrates `speed × deltaTime` into the uniform automatically: there is no manual loop variable to maintain.\n\n### Nested Multiplication: The Same Trap After Accumulating\n\nAfter adopting the accumulator pattern, a common follow-up mistake is multiplying the accumulated value by another parameter. This causes the same jump because the full accumulated history is rescaled instantly:\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nphase += speed.value * viji.deltaTime; // accumulates correctly\nconst rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase; // smooth at any slider value\n}\n```\n\n**Rule of thumb:** any time a growing value (accumulated phase, elapsed time) is multiplied by a user parameter, the result will jump when that parameter changes. Each such product needs its own `deltaTime`-based accumulator (or its own `@viji-accumulator` in shaders).\n\n> [!NOTE]\n> When animation speed is driven by a parameter, always use the accumulator pattern (`deltaTime` in JS/P5, `@viji-accumulator` in shaders) to prevent phase jumps. The same rule extends to nested multiplications: never multiply an accumulated value by a parameter.\n\n**See also:** [`native/timing`](/native/timing#when-to-use-vijitime-vs-vijideltatime), [`shader/timing`](/shader/timing#speed-controlled-animation-the-accumulator), [`shader/parameters/accumulator`](/shader/parameters/accumulator#basic-usage), [`p5/timing`](/p5/timing#when-to-use-vijitime-vs-vijideltatime).\n\n---\n\n## Resolution Agnosticism Across Renderers\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 or assume a specific canvas size.\n\nThe dimensions are exposed differently per renderer but the rule is identical:\n\n- **Native + P5**: `viji.width` and `viji.height` (also `p5.width` / `p5.height`, which Viji keeps in sync)\n- **Shader**: `u_resolution` (a `vec2`)\n\n### Native and P5\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\n### Shader\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0-1 coordinates\n float aspect = u_resolution.x / u_resolution.y;\n // ...\n}\n```\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**See also:** [`native/canvas-context`](/native/canvas-context), [`p5/canvas-resolution`](/p5/canvas-resolution), [`shader/resolution`](/shader/resolution).\n\n---\n\n## Audio and Video `isConnected` Guards\n\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` before reading audio bands, drawing video frames, or running CV inference. Without the guard your scene reads zeros or undefined values silently, with no indication that the input is missing.\n\nThe rule applies in three places:\n\n1. **Default audio source** (`viji.audio.isConnected`)\n2. **Default video source** (`viji.video.isConnected`)\n3. **Each external device entry** in `viji.devices[]` (`device.audio.isConnected`, `device.video.isConnected`)\n\n```javascript\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n // ... react to audio\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 for (const device of viji.devices) {\n if (device.audio?.isConnected) {\n const level = device.audio.volume.current;\n // ... per-device audio reaction\n }\n if (device.video?.isConnected && device.video.currentFrame) {\n // ... per-device video draw\n }\n }\n}\n```\n\nThe same checks work in P5 (replace canvas calls with `p5.image(...)`). In shaders, default audio and video uniforms safely fall back to `0.0` (or black for the `u_video` texture) when nothing is connected, so a typical scene can simply use the values without an explicit guard. When you read from a specific stream or device, gate on the corresponding boolean uniform: `u_audioStream{N}Connected`, `u_videoStream{N}Connected`, or `u_device{N}Connected`.\n\n**See also:** [`native/audio`](/native/audio), [`native/video`](/native/video), [`p5/audio`](/p5/audio), [`p5/video`](/p5/video), [`shader/audio`](/shader/audio), [`shader/video`](/shader/video).\n\n---\n\n## Worker Environment\n\nScenes run inside a Web Worker. There is no DOM. Use `viji.image()` for images, the `viji.audio` / `viji.video` APIs for media, and `fetch()` for external data.\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n**See also:** [`native/canvas-context`](/native/canvas-context) for the worker context model and the canonical `useContext()` pattern.\n\n---\n\n## Parameters at Top Level\n\nParameter helpers (`viji.slider()`, `viji.color()`, etc.) register UI controls with the host. They must run **once** during initialization, never inside `render()`.\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\nFor P5, the same rule applies, with one extra constraint: parameters must also not be declared inside `setup()`, since they need to be registered before `setup()` runs.\n\n**See also:** [`native/parameters`](/native/parameters), [`p5/parameters`](/p5/parameters), [`shader/parameters`](/shader/parameters).\n\n---\n\n## Memory in the Render Loop\n\nAllocating objects, arrays, or strings inside `render()` creates garbage-collection pressure and frame drops. Pre-allocate at the top level and mutate in place.\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**See also:** [`native/quickstart`](/native/quickstart), [`p5/quickstart`](/p5/quickstart).\n\n---\n\n## 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. Understand the relative cost before enabling more than one.\n\n| Feature | Relative Cost | WebGL Contexts | Notes |\n|---------|--------------|----------------|-------|\n| Face Detection | Low | 1 | Bounding box + basic landmarks only |\n| Face Mesh | Medium-High | 1 | 468 facial landmarks, requires more processing |\n| Emotion Detection | High | 1 | 7 expressions + 52 blendshape coefficients |\n| Hand Tracking | Medium | 1 | Up to 2 hands, 21 landmarks each |\n| Pose Detection | Medium | 1 | 33 body landmarks |\n| Body Segmentation | High | 1 | 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> [!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\nThe cost rule applies in all three renderers: CV is most often used in Native scenes, but it is reachable from P5 and Shader through the same `viji.video.cv.enableX()` / `viji.video.cv.disableX()` API.\n\n**See also:** [`native/video`](/native/video), [`p5/video`](/p5/video), [`shader/video`](/shader/video).\n\n## `currentFrame` vs `analysedFrame`: Pick the Right Trade-off\n\nCV results are computed asynchronously from the video stream. By the time `viji.video.cv.faces` (or `hands` / `pose` / `segmentation`) is available, the camera has moved on. With CV enabled you always have two frames to choose between, and they fail in different ways:\n\n- **`viji.video.currentFrame`** is the just-arrived video frame. It refreshes every frame your scene runs.\n- **`viji.video.cv.analysedFrame`** is the exact frame MediaPipe ran on to produce the current CV results. It refreshes only when an inference completes, which is not every frame.\n\nEach pairing has a different failure mode:\n\n| Choice | Mismatch | What you see |\n|---|---|---|\n| `currentFrame` + CV results | Spatial: landmarks and mask lag the displayed pixels by one inference | Overlay trails the face/body during fast motion |\n| `analysedFrame` + CV results | Temporal: the displayed image freezes briefly between inferences | The video itself stutters or holds, then jumps |\n\nYou cannot avoid both. Pick the mismatch that matters less for your scene.\n\n### How to choose\n\nAsk: *does my effect read pixels from the displayed frame at CV-derived positions?*\n\n- **No.** Drawing landmark dots, particles attached to fingertips, audio-reactive visuals driven by CV positions, generative geometry, abstract scenes that do not display the camera at all: use `currentFrame`. Spatial drift on a small overlay is much easier to live with than the displayed video stuttering.\n- **Yes.** Compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters: use `analysedFrame`. Spatial alignment is the whole point of the effect, and the visible stutter is the cost.\n\nDrawing landmark dots over `currentFrame` is **not** a mistake. It is a deliberate choice to keep the live-camera feel.\n\n```javascript\n// Live camera with landmark dots: currentFrame is the right default\nctx.drawImage(viji.video.currentFrame, 0, 0, w, h);\nviji.video.cv.faces.forEach(face => { /* dots */ });\n\n// Mask compositing or pixel sampling under landmarks: analysedFrame is required\nconst frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\nctx.drawImage(frame, 0, 0, w, h);\n// composite mask against the same frame, or sample skin tone at landmark positions...\n```\n\nFor shaders, `u_video` and `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) follow the same trade-off. Sample `u_videoAnalysed` only when the effect reads pixels at CV-derived positions; otherwise `u_video` is freshest and simplest.\n\n`analysedFrame` is `null` until the first CV inference completes after a feature is enabled (typically under one second). The `?? currentFrame` fallback covers that startup window, but be aware the source switches when the first inference lands; a brief visual hitch is normal.\n\n`analysedFrame` is available only on `viji.video` (the main stream); `viji.videoStreams[i]` and `viji.devices[i].video` always expose `null`.\n\n---\n\n## Drawing Video at the Right Aspect Ratio\n\nCamera frames almost never match the canvas aspect ratio. Drawing the video to `(0, 0, viji.width, viji.height)` stretches it visibly: faces get squashed or elongated, circles become ovals, and CV bounding boxes drift off the actual face.\n\nThe fix is a small helper that returns the destination rectangle for `drawImage` (or `p5.image`) and lets you map CV coordinates into the same rectangle. The recipe and full code live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use those helpers in every video / CV scene.\n\n### Cover vs Contain\n\n| Mode | What it does | Use when |\n|---|---|---|\n| `cover` (default) | Video fills the canvas, edges cropped when aspects mismatch | Live camera feeds, filter-style scenes, anywhere stretching would look wrong but black bars would too |\n| `contain` | Full video shown, letterboxed when aspects mismatch | CV demos where bounding boxes near frame edges must stay visible, body segmentation where the user might be at the edge of the camera view, severe aspect mismatches |\n\n**Default to `cover`.** It matches the convention from CSS `object-fit: cover` and most camera apps: the camera fills the visible area cleanly, and the small edge crop is normal. Switch to `contain` only when losing the edges of the frame would lose meaning, which is mostly CV-overlay scenes.\n\n**Stretching `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional** (an artistic effect that wants the squash). On every other scene it is a quality bug.\n\n---\n\n## Parameter Categories\n\nParameters that depend on an external input (audio, video / camera / CV, or user interaction) must include the matching `category` so the host UI can show or hide them based on whether that input is active.\n\n| Input | Required `category` |\n|-------|---------------------|\n| Audio-related parameters (volume sensitivity, bass reactivity, beat response) | `'audio'` |\n| Video/camera/CV-related parameters (video opacity, CV sensitivity, segmentation toggles) | `'video'` |\n| Interaction-related parameters (mouse attraction, keyboard speed, touch sensitivity) | `'interaction'` |\n| Everything else (colors, sizes, speeds, shapes) | `'general'` (default: can be omitted) |\n\n**Native / P5:**\n\n```javascript\n// Wrong\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio' });\n\n// Right\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\n```\n\n**Shader:** use the `category:` key in `@viji-*` directives:\n\n```glsl\n// @viji-toggle:audioReactive label:\"Audio Reactive\" default:true group:audio category:audio\n```\n\n> [!NOTE]\n> `group` controls layout (which section the parameter appears in). `category` controls visibility (whether the parameter is shown at all). They are orthogonal: use both when appropriate.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [`p5/parameters/categories`](/p5/parameters/categories), [`shader/parameters/categories`](/shader/parameters/categories).\n\n---\n\n## Canvas Context Selection\n\nUse `viji.useContext()` to obtain a 2D or WebGL context for the scene canvas, not `viji.canvas.getContext()`. A canvas only supports one context type for its entire lifetime: pick one and stay with it.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call `useContext('2d')` and later call `useContext('webgl')` (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n**See also:** [`native/canvas-context`](/native/canvas-context).\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/): symptom catalog with wrong/right pairs.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer."
|
|
1089
|
+
"markdown": "# Best Practices\n\nThe rules below apply to every Viji scene, no matter which renderer you use. They cover the few topics where the same constraint has to hold across Native, P5, and Shader at once: animation timing, resolution, audio/video guards, and a handful of safety rules the runtime expects. Get them right and your scenes look correct at any resolution, run smoothly at any frame rate, and stay reliable across devices.\n\nLooking for a specific bug? [Common Mistakes](../common-mistakes/) has wrong/right pairs you can scan.\n\n---\n\n## Animation Timing Across Renderers\n\nViji exposes two timing values that are equivalent across all three renderers:\n\n- **`viji.time`** / **`u_time`**: seconds since the scene started. Use it for animation at a **constant** speed (oscillations, hue cycling, periodic events).\n- **`viji.deltaTime`** / **`u_deltaTime`**: seconds since the last frame. Use it when you accumulate values smoothly regardless of frame rate (movement, physics, fading), or when **animation speed is controlled by a parameter**.\n\n```javascript\n// viji.time: animation at a constant speed\nconst angle = viji.time * 2.0; // fixed 2 rad/s, no parameter involved\nconst x = Math.cos(angle) * radius;\n\n// viji.deltaTime: accumulation that stays smooth at any FPS\nposition += velocity * viji.deltaTime;\nopacity -= fadeRate * viji.deltaTime;\n```\n\n### When to use which\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| Oscillation at **constant** speed | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Depends on absolute position in time |\n| Hue cycling at constant rate | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Needs a continuous, monotonic input |\n| Oscillation at **parameter-driven** speed | accumulator with [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) / [`@viji-accumulator`](/shader/parameters/accumulator) | Avoids phase jumps when the slider changes |\n| Position accumulation | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Must advance the same amount per second, regardless of FPS |\n| Physics / velocity | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Distance = speed × time elapsed |\n| Fading / easing | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Progress should be per-second, not per-frame |\n| Periodic events | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Trigger at fixed time intervals |\n\n**Rule of thumb:** if the speed multiplier is a **constant**, `viji.time * constant` is fine. If the speed comes from a **parameter** (slider, etc.), accumulate with `viji.deltaTime` to prevent jumps.\n\n### Accumulator Pattern: Parameter-Driven Speed\n\nWhen animation speed is controlled by a user parameter, **never** multiply `viji.time` (or `u_time`) by the parameter directly. Changing the slider recalculates the entire phase history with the new value and causes a visible jump. Instead, accumulate `speed × deltaTime` incrementally so the slider only affects future frames.\n\nThe pattern is the same across all three renderers:\n\n```javascript\n// Native\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const wave = Math.sin(phase);\n}\n```\n\n```javascript\n// @renderer p5\nconst spin = viji.slider(1, { min: 0.1, max: 5, label: 'Spin' });\nlet phase = 0;\nfunction render(viji, p5) {\n phase += spin.value * viji.deltaTime;\n p5.rotate(phase);\n}\n```\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n // Use `phase` instead of `u_time * speed`\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\nFor shaders, the [`@viji-accumulator`](/shader/parameters/accumulator) directive integrates `speed × deltaTime` into the uniform automatically: there is no manual loop variable to maintain.\n\n### Nested Multiplication: The Same Trap After Accumulating\n\nAfter adopting the accumulator pattern, a common follow-up mistake is multiplying the accumulated value by another parameter. This causes the same jump because the full accumulated history is rescaled instantly:\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nphase += speed.value * viji.deltaTime; // accumulates correctly\nconst rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase; // smooth at any slider value\n}\n```\n\n**Rule of thumb:** any time a growing value (accumulated phase, elapsed time) is multiplied by a user parameter, the result will jump when that parameter changes. Each such product needs its own `deltaTime`-based accumulator (or its own `@viji-accumulator` in shaders).\n\n> [!NOTE]\n> When animation speed is driven by a parameter, always use the accumulator pattern (`deltaTime` in JS/P5, `@viji-accumulator` in shaders) to prevent phase jumps. The same rule extends to nested multiplications: never multiply an accumulated value by a parameter.\n\n**See also:** [`native/timing`](/native/timing#when-to-use-vijitime-vs-vijideltatime), [`shader/timing`](/shader/timing#speed-controlled-animation-the-accumulator), [`shader/parameters/accumulator`](/shader/parameters/accumulator#basic-usage), [`p5/timing`](/p5/timing#when-to-use-vijitime-vs-vijideltatime).\n\n---\n\n## Resolution Agnosticism Across Renderers\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 or assume a specific canvas size.\n\nThe dimensions are exposed differently per renderer but the rule is identical:\n\n- **Native + P5**: `viji.width` and `viji.height` (also `p5.width` / `p5.height`, which Viji keeps in sync)\n- **Shader**: `u_resolution` (a `vec2`)\n\n### Native and P5\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\n### Shader\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0-1 coordinates\n float aspect = u_resolution.x / u_resolution.y;\n // ...\n}\n```\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**See also:** [`native/canvas-context`](/native/canvas-context), [`p5/canvas-resolution`](/p5/canvas-resolution), [`shader/resolution`](/shader/resolution).\n\n---\n\n## Audio and Video `isConnected` Guards\n\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` before reading audio bands, drawing video frames, or running CV inference. Without the guard your scene reads zeros or undefined values silently, with no indication that the input is missing.\n\nThe rule applies in three places:\n\n1. **Default audio source** (`viji.audio.isConnected`)\n2. **Default video source** (`viji.video.isConnected`)\n3. **Each external device entry** in `viji.devices[]` (`device.audio.isConnected`, `device.video.isConnected`)\n\n```javascript\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n // ... react to audio\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 for (const device of viji.devices) {\n if (device.audio?.isConnected) {\n const level = device.audio.volume.current;\n // ... per-device audio reaction\n }\n if (device.video?.isConnected && device.video.currentFrame) {\n // ... per-device video draw\n }\n }\n}\n```\n\nThe same checks work in P5 (replace canvas calls with `p5.image(...)`). In shaders, default audio and video uniforms safely fall back to `0.0` (or black for the `u_video` texture) when nothing is connected, so a typical scene can simply use the values without an explicit guard. When you read from a specific stream or device, gate on the corresponding boolean uniform: `u_audioStream{N}Connected`, `u_videoStream{N}Connected`, or `u_device{N}Connected`.\n\n**See also:** [`native/audio`](/native/audio), [`native/video`](/native/video), [`p5/audio`](/p5/audio), [`p5/video`](/p5/video), [`shader/audio`](/shader/audio), [`shader/video`](/shader/video).\n\n---\n\n## Worker Environment\n\nScenes run inside a Web Worker. There is no DOM. Use `viji.image()` for images, the `viji.audio` / `viji.video` APIs for media, and `fetch()` for external data.\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n**See also:** [`native/canvas-context`](/native/canvas-context) for the worker context model and the canonical `useContext()` pattern.\n\n---\n\n## Parameters at Top Level\n\nParameter helpers (`viji.slider()`, `viji.color()`, etc.) register UI controls with the host. They must run **once** during initialization, never inside `render()`.\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\nFor P5, the same rule applies, with one extra constraint: parameters must also not be declared inside `setup()`, since they need to be registered before `setup()` runs.\n\n**See also:** [`native/parameters`](/native/parameters), [`p5/parameters`](/p5/parameters), [`shader/parameters`](/shader/parameters).\n\n---\n\n## Memory in the Render Loop\n\nAllocating objects, arrays, or strings inside `render()` creates garbage-collection pressure and frame drops. Pre-allocate at the top level and mutate in place.\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**See also:** [`native/quickstart`](/native/quickstart), [`p5/quickstart`](/p5/quickstart).\n\n---\n\n## 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. Understand the relative cost before enabling more than one.\n\n| Feature | Relative Cost | WebGL Contexts | Notes |\n|---------|--------------|----------------|-------|\n| Face Detection | Low | 1 | Bounding box + basic landmarks only |\n| Face Mesh | Medium-High | 1 | 468 facial landmarks, requires more processing |\n| Emotion Detection | High | 1 | 7 expressions + 52 blendshape coefficients |\n| Hand Tracking | Medium | 1 | Up to 2 hands, 21 landmarks each |\n| Pose Detection | Medium | 1 | 33 body landmarks |\n| Body Segmentation | High | 1 | 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> [!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\nThe cost rule applies in all three renderers: CV is most often used in Native scenes, but it is reachable from P5 and Shader through the same `viji.video.cv.enableX()` / `viji.video.cv.disableX()` API.\n\n**See also:** [`native/video`](/native/video), [`p5/video`](/p5/video), [`shader/video`](/shader/video).\n\n## `currentFrame` vs `analysedFrame`: Pick the Right Trade-off\n\nCV results are computed asynchronously from the video stream. By the time `viji.video.cv.faces` (or `hands` / `pose` / `segmentation`) is available, the camera has moved on. With CV enabled you always have two frames to choose between, and they fail in different ways:\n\n- **`viji.video.currentFrame`** is the just-arrived video frame. It refreshes every frame your scene runs.\n- **`viji.video.cv.analysedFrame`** is the exact frame MediaPipe ran on to produce the current CV results. It refreshes only when an inference completes, which is not every frame.\n\nEach pairing has a different failure mode:\n\n| Choice | Mismatch | What you see |\n|---|---|---|\n| `currentFrame` + CV results | Spatial: landmarks and mask lag the displayed pixels by one inference | Overlay trails the face/body during fast motion |\n| `analysedFrame` + CV results | Temporal: the displayed image freezes briefly between inferences | The video itself stutters or holds, then jumps |\n\nYou cannot avoid both. Pick the mismatch that matters less for your scene.\n\n### How to choose\n\nAsk: *does my effect read pixels from the displayed frame at CV-derived positions?*\n\n- **No.** Drawing landmark dots, particles attached to fingertips, audio-reactive visuals driven by CV positions, generative geometry, abstract scenes that do not display the camera at all: use `currentFrame`. Spatial drift on a small overlay is much easier to live with than the displayed video stuttering.\n- **Yes.** Compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters: use `analysedFrame`. Spatial alignment is the whole point of the effect, and the visible stutter is the cost.\n\nDrawing landmark dots over `currentFrame` is **not** a mistake. It is a deliberate choice to keep the live-camera feel.\n\n```javascript\n// Live camera with landmark dots: currentFrame is the right default\nctx.drawImage(viji.video.currentFrame, 0, 0, w, h);\nviji.video.cv.faces.forEach(face => { /* dots */ });\n\n// Mask compositing or pixel sampling under landmarks: analysedFrame is required\nconst frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\nctx.drawImage(frame, 0, 0, w, h);\n// composite mask against the same frame, or sample skin tone at landmark positions...\n```\n\nFor shaders, `u_video` and `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) follow the same trade-off. Sample `u_videoAnalysed` only when the effect reads pixels at CV-derived positions; otherwise `u_video` is freshest and simplest.\n\n`analysedFrame` is `null` until the first CV inference completes after a feature is enabled (typically under one second). The `?? currentFrame` fallback covers that startup window, but be aware the source switches when the first inference lands; a brief visual hitch is normal.\n\n`analysedFrame` is available only on `viji.video` (the main stream); `viji.videoStreams[i]` and `viji.devices[i].video` always expose `null`.\n\n---\n\n## Drawing Video at the Right Aspect Ratio\n\nCamera frames almost never match the canvas aspect ratio. Drawing the video to `(0, 0, viji.width, viji.height)` stretches it visibly: faces get squashed or elongated, circles become ovals, and CV bounding boxes drift off the actual face.\n\nThe fix is a small helper that returns the destination rectangle for `drawImage` (or `p5.image`) and lets you map CV coordinates into the same rectangle. The recipe and full code live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use those helpers in every video / CV scene.\n\n### Cover vs Contain\n\n| Mode | What it does | Use when |\n|---|---|---|\n| `cover` (default) | Video fills the canvas, edges cropped when aspects mismatch | Live camera feeds, filter-style scenes, anywhere stretching would look wrong but black bars would too |\n| `contain` | Full video shown, letterboxed when aspects mismatch | CV demos where bounding boxes near frame edges must stay visible, body segmentation where the user might be at the edge of the camera view, severe aspect mismatches |\n\n**Default to `cover`.** It matches the convention from CSS `object-fit: cover` and most camera apps: the camera fills the visible area cleanly, and the small edge crop is normal. Switch to `contain` only when losing the edges of the frame would lose meaning, which is mostly CV-overlay scenes.\n\n**Stretching `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional** (an artistic effect that wants the squash). On every other scene it is a quality bug.\n\n---\n\n## Parameter Categories\n\nParameters that depend on an external input (audio, video / camera / CV, or user interaction) must include the matching `category` so the host UI can show or hide them based on whether that input is active.\n\n| Input | Required `category` |\n|-------|---------------------|\n| Audio-related parameters (volume sensitivity, bass reactivity, beat response) | `'audio'` |\n| Video/camera/CV-related parameters (video opacity, CV sensitivity, segmentation toggles) | `'video'` |\n| Interaction-related parameters (mouse attraction, keyboard speed, touch sensitivity) | `'interaction'` |\n| Everything else (colors, sizes, speeds, shapes) | `'general'` (default: can be omitted) |\n\n**Native / P5:**\n\n```javascript\n// Wrong\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\n\n// Right\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n```\n\n**Shader:** use the `category:` key in `@viji-*` directives:\n\n```glsl\n// @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n```\n\n> [!NOTE]\n> `group` controls layout (which section the parameter appears in). `category` controls visibility (whether the parameter is shown at all). They are orthogonal: use both when appropriate.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [`p5/parameters/categories`](/p5/parameters/categories), [`shader/parameters/categories`](/shader/parameters/categories).\n\n---\n\n## No Redundant Input Toggles\n\nThe host UI controls whether audio, video, and interaction inputs are available to the scene. A scene-level on/off toggle for whether to *react to* an already-available input is redundant: muting host audio achieves the same thing as flipping an \"Audio Reactive\" toggle off, and any creative-strength slider at zero is also already an off-switch.\n\n**The rule.** Don't add `viji.toggle` parameters whose only effect is gating whether the scene reads from `viji.audio`, `viji.video`, `viji.mouse`, `viji.keyboard`, or `viji.touch`. Use `isConnected` / `isInCanvas` guards plus a creative-strength parameter (slider, color, etc.) tagged with the matching `category` instead.\n\n```javascript\n// Wrong: redundant on/off toggles\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\nconst useCamera = viji.toggle(true, { label: 'Use Camera', category: 'video' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n\n// Right: creative-strength parameters with the matching category\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\n**Why.** The host already exposes input switches in its UI. A scene-level \"Audio Reactive (on/off)\" toggle just duplicates that switch one layer down, and any creative-strength slider going to zero (`bassSensitivity = 0`) achieves the off state without an extra control. Two controls for the same thing is friction for the user and a quality bug for our example scenes.\n\n**The exception: CV feature toggles.** `viji.video.cv.enableFaceDetection`, `enableFaceMesh`, `enableHandTracking`, etc. are *not* input toggles; they activate paid features that consume WebGL contexts and ML inference cycles. Those have to stay opt-in. The line: if the toggle's effect is \"should the scene read an already-available input?\" → remove. If it's \"should the runtime spend resources to enable a feature?\" → keep.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [Common Mistakes: Redundant Input Toggles](../common-mistakes/#redundant-input-onoff-toggles).\n\n---\n\n## Canvas Context Selection\n\nUse `viji.useContext()` to obtain a 2D or WebGL context for the scene canvas, not `viji.canvas.getContext()`. A canvas only supports one context type for its entire lifetime: pick one and stay with it.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call `useContext('2d')` and later call `useContext('webgl')` (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n**See also:** [`native/canvas-context`](/native/canvas-context).\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/): symptom catalog with wrong/right pairs.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer."
|
|
1085
1090
|
}
|
|
1086
1091
|
]
|
|
1087
1092
|
},
|
|
@@ -1155,6 +1160,11 @@ export const docsApi = {
|
|
|
1155
1160
|
"level": 3,
|
|
1156
1161
|
"text": "Missing category on Input-Related Parameters"
|
|
1157
1162
|
},
|
|
1163
|
+
{
|
|
1164
|
+
"id": "redundant-input-onoff-toggles",
|
|
1165
|
+
"level": 3,
|
|
1166
|
+
"text": "Redundant Input On/Off Toggles"
|
|
1167
|
+
},
|
|
1158
1168
|
{
|
|
1159
1169
|
"id": "animation-timing-mistakes",
|
|
1160
1170
|
"level": 2,
|
|
@@ -1244,7 +1254,7 @@ export const docsApi = {
|
|
|
1244
1254
|
"content": [
|
|
1245
1255
|
{
|
|
1246
1256
|
"type": "text",
|
|
1247
|
-
"markdown": "# Common Mistakes\n\nWhen something looks wrong in your scene, scan this page for a matching symptom. Each entry shows the broken version, the working replacement, and a link to where the rule is explained in full.\n\nFor the why behind these patterns, see [Best Practices](../best-practices/).\n\n## How to read this page\n\nEvery entry has the same shape: a one-line symptom, the wrong code, the right code, and a see-also link. Symptoms are grouped by where they typically come up: general code, animation timing, P5-specific quirks, and shader-specific quirks.\n\n---\n\n## General Mistakes\n\n### Using DOM APIs\n\nScenes run in a Web Worker. There is no DOM.\n\n```javascript\n// Wrong: DOM APIs don't exist in workers\nconst img = new Image();\nimg.src = 'photo.jpg';\n\ndocument.createElement('canvas');\nwindow.innerWidth;\nlocalStorage.setItem('key', 'value');\n```\n\n```javascript\n// Right: use Viji's API for inputs, fetch() for external data\nconst photo = viji.image(null, { label: 'Photo' });\nconst data = await fetch('https://cdn.example.com/data.json').then(r => r.json());\n// Use viji.canvas, viji.width, viji.height instead of window/document.\n```\n\n**See also:** [Best Practices: Worker Environment](../best-practices/#worker-environment), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Declaring Parameters Inside `render()`\n\nParameter functions register UI controls with the host. Calling them in `render()` re-registers the parameter every frame, resetting its value to the default.\n\n```javascript\n// Wrong: re-registers the slider every frame, resetting its value\nfunction render(viji) {\n const speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n}\n```\n\n```javascript\n// Right: declare once at top level, read .value in render()\nconst speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n\nfunction render(viji) {\n const s = speed.value;\n}\n```\n\n**See also:** [Best Practices: Parameters at Top Level](../best-practices/#parameters-at-top-level), [`native/parameters`](/native/parameters).\n\n---\n\n### Forgetting `.value` on Parameters\n\nParameter objects are not raw values. Read `.value` to get the current number, color, or other primitive.\n\n```javascript\n// Wrong: uses the parameter object, not its value\nconst radius = viji.slider(50, { min: 10, max: 200, label: 'Radius' });\n\nfunction render(viji) {\n ctx.arc(x, y, radius, 0, Math.PI * 2); // radius is an object, not a number\n}\n```\n\n```javascript\n// Right: access .value\nfunction render(viji) {\n ctx.arc(x, y, radius.value, 0, Math.PI * 2);\n}\n```\n\n**See also:** [`native/parameters`](/native/parameters).\n\n---\n\n### Hardcoding Pixel Values\n\nThe host controls your scene's resolution. Hardcoded values break at different sizes.\n\n```javascript\n// Wrong: only looks right at one specific resolution\nfunction render(viji) {\n ctx.arc(960, 540, 50, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: adapts to any resolution\nfunction render(viji) {\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const r = Math.min(viji.width, viji.height) * 0.05;\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n}\n```\n\n**See also:** [Best Practices: Resolution Agnosticism Across Renderers](../best-practices/#resolution-agnosticism-across-renderers), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Allocating Objects in `render()`\n\nCreating new objects every frame causes garbage collection pauses.\n\n```javascript\n// Wrong: new array and objects every frame\nfunction render(viji) {\n const particles = [];\n for (let i = 0; i < 100; i++) {\n particles.push({ x: Math.random() * viji.width, y: Math.random() * viji.height });\n }\n}\n```\n\n```javascript\n// Right: pre-allocate and mutate in place\nconst particles = Array.from({ length: 100 }, () => ({ x: 0, y: 0 }));\n\nfunction render(viji) {\n for (const p of particles) {\n p.x = Math.random() * viji.width;\n p.y = Math.random() * viji.height;\n }\n}\n```\n\n**See also:** [Best Practices: Memory in the Render Loop](../best-practices/#memory-in-the-render-loop).\n\n---\n\n### Using `viji.canvas.getContext()` Instead of `viji.useContext()`\n\nCalling `viji.canvas.getContext()` directly bypasses the Viji runtime's internal context tracking and may miss future runtime setup such as WebGL viewport initialization.\n\n```javascript\n// Wrong: bypasses Viji's context management\nconst ctx = viji.canvas.getContext('2d');\n```\n\n```javascript\n// Right: use the Viji API, which tracks the context and sets up internals\nconst ctx = viji.useContext('2d');\n```\n\nThe same applies to canvas dimensions: prefer `viji.width` / `viji.height` over `viji.canvas.width` / `viji.canvas.height`.\n\n**See also:** [Best Practices: Canvas Context Selection](../best-practices/#canvas-context-selection), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Not Checking `isConnected` for Audio/Video\n\nAudio and video streams may not be available. Without a guard, your scene reads zero values silently.\n\n```javascript\n// Wrong: no guard, silently uses zero values\nfunction render(viji) {\n const bass = viji.audio.bands.low;\n ctx.drawImage(viji.video.currentFrame, 0, 0);\n}\n```\n\n```javascript\n// Right: check connection state first\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // helper from native/video/basics\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n}\n```\n\n**See also:** [Best Practices: Audio and Video isConnected Guards](../best-practices/#audio-and-video-isconnected-guards).\n\n---\n\n### Enabling All CV Features by Default\n\nEnabling CV features without user consent wastes resources on devices that can't handle it and risks WebGL context loss.\n\n```javascript\n// Wrong: activates expensive CV on every device, regardless of capability\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\n\n```javascript\n// Right: let the user opt in, then enable from inside render()\nconst useFace = viji.toggle(false, { label: 'Enable Face Tracking', category: 'video' });\nconst useHands = viji.toggle(false, { label: 'Enable Hand Tracking', category: 'video' });\n\nfunction render(viji) {\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n}\n```\n\nThe `enableX(true)` and `enableX(false)` methods are idempotent: calling them every frame is safe, the runtime returns immediately when the requested state already matches.\n\n**See also:** [Best Practices: Computer Vision Costs](../best-practices/#computer-vision-costs).\n\n---\n\n### Compositing CV Masks or Sampling Pixels Over `currentFrame`\n\nThis entry is specifically about effects that **read pixels from the displayed frame at CV-derived positions**: compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters. For these, `currentFrame` is wrong: the CV results come from a frame MediaPipe analysed ~1-3 frames earlier, so mask edges drift off the body and pixel samples land on the wrong skin.\n\n```javascript\n// Wrong: mask edges drift off the body during motion\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against currentFrame...\n}\n```\n\n```javascript\n// Right: composite the mask against the frame it was computed from\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\n ctx.drawImage(frame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against the same frame...\n}\n```\n\nThe `analysedFrame ?? currentFrame` fallback covers the brief startup window before the first CV result lands. For shaders, the equivalent is sampling `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) instead of `u_video`.\n\n**This is *not* a blanket rule against drawing CV-derived visuals over `currentFrame`.** Drawing landmark dots, particles, or other geometry on top of the live camera is a different case: there are no displayed pixels being read at CV-derived positions, just CV positions being used as drawing coordinates. For those, `currentFrame` is usually the better default; `analysedFrame` would freeze the displayed video between inferences. See the trade-off discussion in Best Practices.\n\n**See also:** [Best Practices: `currentFrame` vs `analysedFrame`](../best-practices/#currentframe-vs-analysedframe-pick-the-right-trade-off), [`native/video/face-mesh`](/native/video/face-mesh), [`native/video/body-segmentation`](/native/video/body-segmentation).\n\n---\n\n### Stretching the Video to Fill the Canvas\n\nDrawing video to `(0, 0, viji.width, viji.height)` stretches it whenever the canvas aspect does not match the camera aspect (which is almost always). Faces get squashed, circles become ovals, and CV bounding boxes drift off the actual face.\n\n```javascript\n// Wrong: stretches the video to fill the canvas\nctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n```\n\n```javascript\n// Right: preserve the source aspect ratio\nconst v = videoFit(viji); // see recipe below\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n```\n\nThe `videoFit` helper and the matching CV-coordinate remapping live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use them in every video / CV scene. Stretching is allowed only when distortion is intentional (an artistic squash effect).\n\n**See also:** [Best Practices: Drawing Video at the Right Aspect Ratio](../best-practices/#drawing-video-at-the-right-aspect-ratio).\n\n---\n\n### Missing `category` on Input-Related Parameters\n\nParameters tied to audio, video, or interaction inputs need `category` so the host UI hides them when the input is inactive. Without it, users see controls that do nothing.\n\n```javascript\n// Wrong: audio parameters are always visible, even when no audio is connected\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio' });\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\n```\n\n```javascript\n// Right: parameters are hidden until audio is available\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n```\n\n**See also:** [Best Practices: Parameter Categories](../best-practices/#parameter-categories), [`native/parameters/categories`](/native/parameters/categories).\n\n---\n\n## Animation Timing Mistakes\n\nThe four entries below share the same root cause: when an animation value is computed from absolute time multiplied by a user-controlled parameter, changing the parameter rescales the entire phase history at once and the animation jumps. The fix is always the same shape (accumulate `speed × deltaTime` incrementally), so once you understand one you can spot the others. For the full rationale and cross-renderer narrative, see [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### Frame-Rate-Dependent Animation\n\nCounting frames or using fixed increments makes animation speed depend on the device's frame rate.\n\n```javascript\n// Wrong: faster on 120Hz displays, slower on 30Hz\nlet angle = 0;\nfunction render(viji) {\n angle += 0.02;\n}\n```\n\n```javascript\n// Right: use viji.time for constant-speed animation\nfunction render(viji) {\n const angle = viji.time * 2.0; // fixed 2 rad/s, independent of FPS\n}\n\n// Or use viji.deltaTime for accumulation\nlet position = 0;\nfunction render(viji) {\n position += velocity * viji.deltaTime;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `viji.time * speed.value` (JS / P5)\n\nMultiplying `viji.time` by a parameter causes the entire phase to jump when the slider moves, because the full time history is recalculated instantly with the new value.\n\n```javascript\n// Wrong: animation jumps when speed slider changes\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nfunction render(viji) {\n const angle = viji.time * speed.value;\n ctx.arc(x, y, Math.sin(angle) * r, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: accumulate with deltaTime, slider only affects future rate\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n ctx.arc(x, y, Math.sin(phase) * r, 0, Math.PI * 2);\n}\n```\n\nThe same pattern applies in P5: replace the canvas calls with `p5.*` equivalents and the accumulator stays the same.\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `u_time * speed` (Shader)\n\nThe same trap in shaders: multiplying `u_time` by a parameter uniform causes a phase jump on slider changes.\n\n```glsl\n// Wrong: animation jumps when speed slider changes\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\nvoid main() {\n float wave = sin(u_time * speed);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n```glsl\n// Right: @viji-accumulator integrates speed × deltaTime smoothly\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers), [`shader/parameters/accumulator`](/shader/parameters/accumulator).\n\n---\n\n### Nested Multiplication: Accumulated Value Multiplied by Parameter\n\nAfter adopting the accumulator pattern, multiplying the accumulated value by another parameter causes the same jump because the full accumulated history is rescaled.\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime; // accumulates correctly\n const rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n}\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n## P5-Specific\n\n### Missing the `p5.` Prefix\n\nViji runs P5 in **instance mode**. All P5 functions must be called on the `p5` object.\n\n```javascript\n// @renderer p5\n\n// Wrong: global P5 functions don't exist in instance mode\nfunction render(viji, p5) {\n background(0); // ReferenceError\n fill(255, 0, 0); // ReferenceError\n circle(width / 2, height / 2, 100); // ReferenceError\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: use p5. prefix for P5 functions, viji.* for dimensions\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255, 0, 0);\n p5.circle(viji.width / 2, viji.height / 2, 100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using `draw()` Instead of `render()`\n\nP5's built-in draw loop is disabled in Viji. Your function must be named `render`, not `draw`.\n\n```javascript\n// @renderer p5\n\n// Wrong: Viji never calls draw()\nfunction draw(viji, p5) {\n p5.background(0);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: Viji calls render() every frame\nfunction render(viji, p5) {\n p5.background(0);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Calling `createCanvas()`\n\nThe canvas is created and managed by Viji. Calling `createCanvas()` creates a second, invisible canvas. WEBGL mode is selected via the `// @renderer p5 webgl` directive on the first line, never by passing `p5.WEBGL` to `createCanvas`.\n\n```javascript\n// @renderer p5\n\n// Wrong: creates a separate, invisible canvas\nfunction setup(viji, p5) {\n p5.createCanvas(800, 600);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Wrong: main canvas already exists; this does not switch it to WEBGL\nfunction setup(viji, p5) {\n p5.createCanvas(p5.width, p5.height, p5.WEBGL);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: canvas is already provided, just configure settings\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: WEBGL is selected by the first-line directive; never call createCanvas\nfunction setup(viji, p5) {\n p5.angleMode(p5.RADIANS);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Expecting WEBGL Without `// @renderer p5 webgl`\n\n`// @renderer p5` alone gives a 2D main canvas. For 3D / WEBGL, the first comment must include `webgl` after `p5`.\n\n```javascript\n// @renderer p5\n\n// Wrong: this scene is 2D only; box() and WEBGL lighting won't work as intended\nfunction render(viji, p5) {\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: Viji creates the main canvas in WEBGL mode\nfunction render(viji, p5) {\n p5.background(32);\n p5.ambientLight(100);\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using P5 Event Callbacks\n\nP5 event callbacks like `mousePressed()`, `keyPressed()`, `touchStarted()` do not work in Viji's worker environment. Use Viji's interaction APIs instead.\n\n```javascript\n// @renderer p5\n\n// Wrong: these callbacks are never called\nfunction mousePressed() {\n console.log('clicked');\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: check Viji's interaction state in render()\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) {\n console.log('clicked');\n }\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart), [`native/pointer`](/native/pointer).\n\n---\n\n## Shader-Specific\n\n### Redeclaring Auto-Injected Code\n\nViji auto-injects `precision`, all built-in uniform declarations, and all parameter uniforms from `@viji-*` directives. Redeclaring any of them causes compilation errors.\n\n```glsl\n// @renderer shader\n\n// Wrong: these are already injected by Viji\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\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```glsl\n// @renderer shader\n\n// Right: just write your code, uniforms are available automatically\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n**See also:** [`shader/basics`](/shader/basics).\n\n---\n\n### Using `u_` Prefix for Custom Parameters\n\nThe `u_` prefix is reserved for Viji's built-in uniforms. Using it for your parameters risks naming collisions.\n\n```glsl\n// Wrong: u_ prefix is reserved\n// @viji-slider:u_speed label:\"Speed\" default:1.0\n```\n\n```glsl\n// Right: use descriptive names without u_ prefix\n// @viji-slider:speed label:\"Speed\" default:1.0\n```\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n### Missing `@renderer shader` Directive\n\nWithout the directive, your GLSL code is treated as JavaScript and throws syntax errors.\n\n```glsl\n// Wrong: no directive, treated as JavaScript\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: directive tells Viji to use the shader renderer\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n**See also:** [`shader/quickstart`](/shader/quickstart).\n\n---\n\n### Block Comments for `@viji-*` Parameters\n\nThe `@viji-*` parameter declarations only work with single-line `//` comments. Block comments `/* */` are silently ignored.\n\n```glsl\n// @renderer shader\n\n// Wrong: block comments are not parsed for parameters\n/* @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0 */\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0); // speed is undefined\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: use single-line comments for parameter declarations\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0);\n}\n```\n\n> [!NOTE]\n> The `@renderer` directive supports both `//` and `/* */` styles, but `@viji-*` parameter declarations require `//`.\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n## Related\n\n- [Best Practices](../best-practices/): cross-renderer authoring patterns and rationale.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer.\n- [Audio](/native/audio), [Video & CV](/native/video): full audio and video API."
|
|
1257
|
+
"markdown": "# Common Mistakes\n\nWhen something looks wrong in your scene, scan this page for a matching symptom. Each entry shows the broken version, the working replacement, and a link to where the rule is explained in full.\n\nFor the why behind these patterns, see [Best Practices](../best-practices/).\n\n## How to read this page\n\nEvery entry has the same shape: a one-line symptom, the wrong code, the right code, and a see-also link. Symptoms are grouped by where they typically come up: general code, animation timing, P5-specific quirks, and shader-specific quirks.\n\n---\n\n## General Mistakes\n\n### Using DOM APIs\n\nScenes run in a Web Worker. There is no DOM.\n\n```javascript\n// Wrong: DOM APIs don't exist in workers\nconst img = new Image();\nimg.src = 'photo.jpg';\n\ndocument.createElement('canvas');\nwindow.innerWidth;\nlocalStorage.setItem('key', 'value');\n```\n\n```javascript\n// Right: use Viji's API for inputs, fetch() for external data\nconst photo = viji.image(null, { label: 'Photo' });\nconst data = await fetch('https://cdn.example.com/data.json').then(r => r.json());\n// Use viji.canvas, viji.width, viji.height instead of window/document.\n```\n\n**See also:** [Best Practices: Worker Environment](../best-practices/#worker-environment), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Declaring Parameters Inside `render()`\n\nParameter functions register UI controls with the host. Calling them in `render()` re-registers the parameter every frame, resetting its value to the default.\n\n```javascript\n// Wrong: re-registers the slider every frame, resetting its value\nfunction render(viji) {\n const speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n}\n```\n\n```javascript\n// Right: declare once at top level, read .value in render()\nconst speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n\nfunction render(viji) {\n const s = speed.value;\n}\n```\n\n**See also:** [Best Practices: Parameters at Top Level](../best-practices/#parameters-at-top-level), [`native/parameters`](/native/parameters).\n\n---\n\n### Forgetting `.value` on Parameters\n\nParameter objects are not raw values. Read `.value` to get the current number, color, or other primitive.\n\n```javascript\n// Wrong: uses the parameter object, not its value\nconst radius = viji.slider(50, { min: 10, max: 200, label: 'Radius' });\n\nfunction render(viji) {\n ctx.arc(x, y, radius, 0, Math.PI * 2); // radius is an object, not a number\n}\n```\n\n```javascript\n// Right: access .value\nfunction render(viji) {\n ctx.arc(x, y, radius.value, 0, Math.PI * 2);\n}\n```\n\n**See also:** [`native/parameters`](/native/parameters).\n\n---\n\n### Hardcoding Pixel Values\n\nThe host controls your scene's resolution. Hardcoded values break at different sizes.\n\n```javascript\n// Wrong: only looks right at one specific resolution\nfunction render(viji) {\n ctx.arc(960, 540, 50, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: adapts to any resolution\nfunction render(viji) {\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const r = Math.min(viji.width, viji.height) * 0.05;\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n}\n```\n\n**See also:** [Best Practices: Resolution Agnosticism Across Renderers](../best-practices/#resolution-agnosticism-across-renderers), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Allocating Objects in `render()`\n\nCreating new objects every frame causes garbage collection pauses.\n\n```javascript\n// Wrong: new array and objects every frame\nfunction render(viji) {\n const particles = [];\n for (let i = 0; i < 100; i++) {\n particles.push({ x: Math.random() * viji.width, y: Math.random() * viji.height });\n }\n}\n```\n\n```javascript\n// Right: pre-allocate and mutate in place\nconst particles = Array.from({ length: 100 }, () => ({ x: 0, y: 0 }));\n\nfunction render(viji) {\n for (const p of particles) {\n p.x = Math.random() * viji.width;\n p.y = Math.random() * viji.height;\n }\n}\n```\n\n**See also:** [Best Practices: Memory in the Render Loop](../best-practices/#memory-in-the-render-loop).\n\n---\n\n### Using `viji.canvas.getContext()` Instead of `viji.useContext()`\n\nCalling `viji.canvas.getContext()` directly bypasses the Viji runtime's internal context tracking and may miss future runtime setup such as WebGL viewport initialization.\n\n```javascript\n// Wrong: bypasses Viji's context management\nconst ctx = viji.canvas.getContext('2d');\n```\n\n```javascript\n// Right: use the Viji API, which tracks the context and sets up internals\nconst ctx = viji.useContext('2d');\n```\n\nThe same applies to canvas dimensions: prefer `viji.width` / `viji.height` over `viji.canvas.width` / `viji.canvas.height`.\n\n**See also:** [Best Practices: Canvas Context Selection](../best-practices/#canvas-context-selection), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Not Checking `isConnected` for Audio/Video\n\nAudio and video streams may not be available. Without a guard, your scene reads zero values silently.\n\n```javascript\n// Wrong: no guard, silently uses zero values\nfunction render(viji) {\n const bass = viji.audio.bands.low;\n ctx.drawImage(viji.video.currentFrame, 0, 0);\n}\n```\n\n```javascript\n// Right: check connection state first\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // helper from native/video/basics\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n}\n```\n\n**See also:** [Best Practices: Audio and Video isConnected Guards](../best-practices/#audio-and-video-isconnected-guards).\n\n---\n\n### Enabling All CV Features by Default\n\nEnabling CV features without user consent wastes resources on devices that can't handle it and risks WebGL context loss.\n\n```javascript\n// Wrong: activates expensive CV on every device, regardless of capability\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\n\n```javascript\n// Right: let the user opt in, then enable from inside render()\nconst useFace = viji.toggle(false, { label: 'Enable Face Tracking', category: 'video' });\nconst useHands = viji.toggle(false, { label: 'Enable Hand Tracking', category: 'video' });\n\nfunction render(viji) {\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n}\n```\n\nThe `enableX(true)` and `enableX(false)` methods are idempotent: calling them every frame is safe, the runtime returns immediately when the requested state already matches.\n\n**See also:** [Best Practices: Computer Vision Costs](../best-practices/#computer-vision-costs).\n\n---\n\n### Compositing CV Masks or Sampling Pixels Over `currentFrame`\n\nThis entry is specifically about effects that **read pixels from the displayed frame at CV-derived positions**: compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters. For these, `currentFrame` is wrong: the CV results come from a frame MediaPipe analysed ~1-3 frames earlier, so mask edges drift off the body and pixel samples land on the wrong skin.\n\n```javascript\n// Wrong: mask edges drift off the body during motion\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against currentFrame...\n}\n```\n\n```javascript\n// Right: composite the mask against the frame it was computed from\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\n ctx.drawImage(frame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against the same frame...\n}\n```\n\nThe `analysedFrame ?? currentFrame` fallback covers the brief startup window before the first CV result lands. For shaders, the equivalent is sampling `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) instead of `u_video`.\n\n**This is *not* a blanket rule against drawing CV-derived visuals over `currentFrame`.** Drawing landmark dots, particles, or other geometry on top of the live camera is a different case: there are no displayed pixels being read at CV-derived positions, just CV positions being used as drawing coordinates. For those, `currentFrame` is usually the better default; `analysedFrame` would freeze the displayed video between inferences. See the trade-off discussion in Best Practices.\n\n**See also:** [Best Practices: `currentFrame` vs `analysedFrame`](../best-practices/#currentframe-vs-analysedframe-pick-the-right-trade-off), [`native/video/face-mesh`](/native/video/face-mesh), [`native/video/body-segmentation`](/native/video/body-segmentation).\n\n---\n\n### Stretching the Video to Fill the Canvas\n\nDrawing video to `(0, 0, viji.width, viji.height)` stretches it whenever the canvas aspect does not match the camera aspect (which is almost always). Faces get squashed, circles become ovals, and CV bounding boxes drift off the actual face.\n\n```javascript\n// Wrong: stretches the video to fill the canvas\nctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n```\n\n```javascript\n// Right: preserve the source aspect ratio\nconst v = videoFit(viji); // see recipe below\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n```\n\nThe `videoFit` helper and the matching CV-coordinate remapping live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use them in every video / CV scene. Stretching is allowed only when distortion is intentional (an artistic squash effect).\n\n**See also:** [Best Practices: Drawing Video at the Right Aspect Ratio](../best-practices/#drawing-video-at-the-right-aspect-ratio).\n\n---\n\n### Missing `category` on Input-Related Parameters\n\nParameters tied to audio, video, or interaction inputs need `category` so the host UI hides them when the input is inactive. Without it, users see controls that do nothing.\n\n```javascript\n// Wrong: audio parameters are always visible, even when no audio is connected\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\nconst decay = viji.slider(0.8, { min: 0, max: 1, label: 'Audio Decay', group: 'audio' });\n```\n\n```javascript\n// Right: parameters are hidden until audio is available\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\nconst decay = viji.slider(0.8, { min: 0, max: 1, label: 'Audio Decay', group: 'audio', category: 'audio' });\n```\n\n**See also:** [Best Practices: Parameter Categories](../best-practices/#parameter-categories), [`native/parameters/categories`](/native/parameters/categories).\n\n---\n\n### Redundant Input On/Off Toggles\n\nThe host UI controls whether audio, video, and interaction inputs are wired up. A scene-level on/off toggle for whether to *react to* an already-available input is redundant: muting host audio achieves the same thing as flipping an \"Audio Reactive\" toggle off.\n\n```javascript\n// Wrong: scene-level toggles duplicate host input switches\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\nconst useCamera = viji.toggle(true, { label: 'Use Camera', category: 'video' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n```\n\n```javascript\n// Right: creative-strength parameters with the matching category;\n// `isConnected` / `isInCanvas` guards handle the \"input not available\" case\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\nThe exception is **CV feature toggles** (`enableFaceDetection`, `enableHandTracking`, etc.), which are not input toggles: they activate paid features that consume WebGL contexts and ML inference. Those stay opt-in.\n\n**See also:** [Best Practices: No Redundant Input Toggles](../best-practices/#no-redundant-input-toggles).\n\n---\n\n## Animation Timing Mistakes\n\nThe four entries below share the same root cause: when an animation value is computed from absolute time multiplied by a user-controlled parameter, changing the parameter rescales the entire phase history at once and the animation jumps. The fix is always the same shape (accumulate `speed × deltaTime` incrementally), so once you understand one you can spot the others. For the full rationale and cross-renderer narrative, see [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### Frame-Rate-Dependent Animation\n\nCounting frames or using fixed increments makes animation speed depend on the device's frame rate.\n\n```javascript\n// Wrong: faster on 120Hz displays, slower on 30Hz\nlet angle = 0;\nfunction render(viji) {\n angle += 0.02;\n}\n```\n\n```javascript\n// Right: use viji.time for constant-speed animation\nfunction render(viji) {\n const angle = viji.time * 2.0; // fixed 2 rad/s, independent of FPS\n}\n\n// Or use viji.deltaTime for accumulation\nlet position = 0;\nfunction render(viji) {\n position += velocity * viji.deltaTime;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `viji.time * speed.value` (JS / P5)\n\nMultiplying `viji.time` by a parameter causes the entire phase to jump when the slider moves, because the full time history is recalculated instantly with the new value.\n\n```javascript\n// Wrong: animation jumps when speed slider changes\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nfunction render(viji) {\n const angle = viji.time * speed.value;\n ctx.arc(x, y, Math.sin(angle) * r, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: accumulate with deltaTime, slider only affects future rate\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n ctx.arc(x, y, Math.sin(phase) * r, 0, Math.PI * 2);\n}\n```\n\nThe same pattern applies in P5: replace the canvas calls with `p5.*` equivalents and the accumulator stays the same.\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `u_time * speed` (Shader)\n\nThe same trap in shaders: multiplying `u_time` by a parameter uniform causes a phase jump on slider changes.\n\n```glsl\n// Wrong: animation jumps when speed slider changes\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\nvoid main() {\n float wave = sin(u_time * speed);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n```glsl\n// Right: @viji-accumulator integrates speed × deltaTime smoothly\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers), [`shader/parameters/accumulator`](/shader/parameters/accumulator).\n\n---\n\n### Nested Multiplication: Accumulated Value Multiplied by Parameter\n\nAfter adopting the accumulator pattern, multiplying the accumulated value by another parameter causes the same jump because the full accumulated history is rescaled.\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime; // accumulates correctly\n const rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n}\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n## P5-Specific\n\n### Missing the `p5.` Prefix\n\nViji runs P5 in **instance mode**. All P5 functions must be called on the `p5` object.\n\n```javascript\n// @renderer p5\n\n// Wrong: global P5 functions don't exist in instance mode\nfunction render(viji, p5) {\n background(0); // ReferenceError\n fill(255, 0, 0); // ReferenceError\n circle(width / 2, height / 2, 100); // ReferenceError\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: use p5. prefix for P5 functions, viji.* for dimensions\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255, 0, 0);\n p5.circle(viji.width / 2, viji.height / 2, 100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using `draw()` Instead of `render()`\n\nP5's built-in draw loop is disabled in Viji. Your function must be named `render`, not `draw`.\n\n```javascript\n// @renderer p5\n\n// Wrong: Viji never calls draw()\nfunction draw(viji, p5) {\n p5.background(0);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: Viji calls render() every frame\nfunction render(viji, p5) {\n p5.background(0);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Calling `createCanvas()`\n\nThe canvas is created and managed by Viji. Calling `createCanvas()` creates a second, invisible canvas. WEBGL mode is selected via the `// @renderer p5 webgl` directive on the first line, never by passing `p5.WEBGL` to `createCanvas`.\n\n```javascript\n// @renderer p5\n\n// Wrong: creates a separate, invisible canvas\nfunction setup(viji, p5) {\n p5.createCanvas(800, 600);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Wrong: main canvas already exists; this does not switch it to WEBGL\nfunction setup(viji, p5) {\n p5.createCanvas(p5.width, p5.height, p5.WEBGL);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: canvas is already provided, just configure settings\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: WEBGL is selected by the first-line directive; never call createCanvas\nfunction setup(viji, p5) {\n p5.angleMode(p5.RADIANS);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Expecting WEBGL Without `// @renderer p5 webgl`\n\n`// @renderer p5` alone gives a 2D main canvas. For 3D / WEBGL, the first comment must include `webgl` after `p5`.\n\n```javascript\n// @renderer p5\n\n// Wrong: this scene is 2D only; box() and WEBGL lighting won't work as intended\nfunction render(viji, p5) {\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: Viji creates the main canvas in WEBGL mode\nfunction render(viji, p5) {\n p5.background(32);\n p5.ambientLight(100);\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using P5 Event Callbacks\n\nP5 event callbacks like `mousePressed()`, `keyPressed()`, `touchStarted()` do not work in Viji's worker environment. Use Viji's interaction APIs instead.\n\n```javascript\n// @renderer p5\n\n// Wrong: these callbacks are never called\nfunction mousePressed() {\n console.log('clicked');\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: check Viji's interaction state in render()\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) {\n console.log('clicked');\n }\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart), [`native/pointer`](/native/pointer).\n\n---\n\n## Shader-Specific\n\n### Redeclaring Auto-Injected Code\n\nViji auto-injects `precision`, all built-in uniform declarations, and all parameter uniforms from `@viji-*` directives. Redeclaring any of them causes compilation errors.\n\n```glsl\n// @renderer shader\n\n// Wrong: these are already injected by Viji\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\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```glsl\n// @renderer shader\n\n// Right: just write your code, uniforms are available automatically\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n**See also:** [`shader/basics`](/shader/basics).\n\n---\n\n### Using `u_` Prefix for Custom Parameters\n\nThe `u_` prefix is reserved for Viji's built-in uniforms. Using it for your parameters risks naming collisions.\n\n```glsl\n// Wrong: u_ prefix is reserved\n// @viji-slider:u_speed label:\"Speed\" default:1.0\n```\n\n```glsl\n// Right: use descriptive names without u_ prefix\n// @viji-slider:speed label:\"Speed\" default:1.0\n```\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n### Missing `@renderer shader` Directive\n\nWithout the directive, your GLSL code is treated as JavaScript and throws syntax errors.\n\n```glsl\n// Wrong: no directive, treated as JavaScript\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: directive tells Viji to use the shader renderer\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n**See also:** [`shader/quickstart`](/shader/quickstart).\n\n---\n\n### Block Comments for `@viji-*` Parameters\n\nThe `@viji-*` parameter declarations only work with single-line `//` comments. Block comments `/* */` are silently ignored.\n\n```glsl\n// @renderer shader\n\n// Wrong: block comments are not parsed for parameters\n/* @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0 */\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0); // speed is undefined\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: use single-line comments for parameter declarations\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0);\n}\n```\n\n> [!NOTE]\n> The `@renderer` directive supports both `//` and `/* */` styles, but `@viji-*` parameter declarations require `//`.\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n## Related\n\n- [Best Practices](../best-practices/): cross-renderer authoring patterns and rationale.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer.\n- [Audio](/native/audio), [Video & CV](/native/video): full audio and video API."
|
|
1248
1258
|
}
|
|
1249
1259
|
]
|
|
1250
1260
|
},
|
|
@@ -1315,7 +1325,7 @@ export const docsApi = {
|
|
|
1315
1325
|
"content": [
|
|
1316
1326
|
{
|
|
1317
1327
|
"type": "text",
|
|
1318
|
-
"markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene: a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples of useful questions: \"Should this react to audio or stay purely visual?\", \"Should it use the camera or only mouse input?\", \"Roughly how dense / how minimal do you want it?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust (speed, density, colors, mode toggles).\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"make it smoother\", \"add a trail\", \"make it audio-reactive on the kick\"). Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact API names and types.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, behavior nuances, full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Use them instead of parsing hex by hand. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time`: elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\n - `viji.deltaTime`: seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER multiply `viji.time` by a parameter for animation speed: it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\n ```javascript\n // WRONG: jumps when speed changes:\n const t = viji.time * speed.value;\n // RIGHT: smooth:\n let phase = 0; // top level\n phase += speed.value * viji.deltaTime; // in render()\n ```\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\n ```javascript\n // WRONG: jumps when rotSpeed changes:\n const rot = phase * rotSpeed.value;\n // RIGHT: give it its own accumulator:\n let rotPhase = 0; // top level\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\n ```\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')`: Canvas 2D\n - `viji.useContext('webgl')`: WebGL 1\n - `viji.useContext('webgl2')`: WebGL 2\n Calling a different type after the first returns `null`.\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n12. Be mindful of WebGL context limits: each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive.\n ```javascript\n const audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\n const followMouse = viji.toggle(true, { label: 'Follow Mouse', group: 'interaction', category: 'interaction' });\n ```\n14. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0-1 |\n| `beat.snare` | `number` | Snare energy 0-1 |\n| `beat.hat` | `number` | Hi-hat energy 0-1 |\n| `beat.any` | `number` | Any beat energy 0-1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60-240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `analysedFrame` | `OffscreenCanvas\\|null` | Frame paired with the current `faces`/`hands`/`pose`/`segmentation` results. `null` until first CV result. Read-only. |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `getAnalysedFrameData()` | `ImageData\\|null` | Pixel data of `analysedFrame`, paired with CV results |\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds. Define this `videoFit` helper at module scope and use it for every video / CV scene:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (default) or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0-1 to the source video frame, not the canvas.\n// Map them through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live camera feeds (fills the canvas, edges cropped). Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible (full frame shown with letterbox bars). Stretching directly to `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n\n**`currentFrame` vs `analysedFrame`.** Default to `currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut: all 0-1).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow help the artist build a Viji native scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want: be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Native Quick Start](/native/quickstart): your first Viji native scene\n- [Native API Reference](/native/api-reference): full API reference\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
|
|
1328
|
+
"markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene: a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples of useful questions: \"Should this react to audio or stay purely visual?\", \"Should it use the camera or only mouse input?\", \"Roughly how dense / how minimal do you want it?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust (speed, density, colors, mode toggles).\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"make it smoother\", \"add a trail\", \"make it audio-reactive on the kick\"). Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact API names and types.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, behavior nuances, full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Use them instead of parsing hex by hand. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time`: elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\n - `viji.deltaTime`: seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER multiply `viji.time` by a parameter for animation speed: it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\n ```javascript\n // WRONG: jumps when speed changes:\n const t = viji.time * speed.value;\n // RIGHT: smooth:\n let phase = 0; // top level\n phase += speed.value * viji.deltaTime; // in render()\n ```\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\n ```javascript\n // WRONG: jumps when rotSpeed changes:\n const rot = phase * rotSpeed.value;\n // RIGHT: give it its own accumulator:\n let rotPhase = 0; // top level\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\n ```\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')`: Canvas 2D\n - `viji.useContext('webgl')`: WebGL 1\n - `viji.useContext('webgl2')`: WebGL 2\n Calling a different type after the first returns `null`.\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n12. Be mindful of WebGL context limits: each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n14. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0-1 |\n| `beat.snare` | `number` | Snare energy 0-1 |\n| `beat.hat` | `number` | Hi-hat energy 0-1 |\n| `beat.any` | `number` | Any beat energy 0-1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60-240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `analysedFrame` | `OffscreenCanvas\\|null` | Frame paired with the current `faces`/`hands`/`pose`/`segmentation` results. `null` until first CV result. Read-only. |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `getAnalysedFrameData()` | `ImageData\\|null` | Pixel data of `analysedFrame`, paired with CV results |\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds. Define this `videoFit` helper at module scope and use it for every video / CV scene:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (default) or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0-1 to the source video frame, not the canvas.\n// Map them through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live camera feeds (fills the canvas, edges cropped). Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible (full frame shown with letterbox bars). Stretching directly to `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n\n**`currentFrame` vs `analysedFrame`.** Default to `currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut: all 0-1).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow help the artist build a Viji native scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want: be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Native Quick Start](/native/quickstart): your first Viji native scene\n- [Native API Reference](/native/api-reference): full API reference\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
|
|
1319
1329
|
}
|
|
1320
1330
|
]
|
|
1321
1331
|
},
|
|
@@ -1343,7 +1353,7 @@ export const docsApi = {
|
|
|
1343
1353
|
"content": [
|
|
1344
1354
|
{
|
|
1345
1355
|
"type": "text",
|
|
1346
|
-
"markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene: a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js v1.9.4.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D canvas or WEBGL / 3D mode?\", \"Should it use the camera or only mouse input?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes. Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or P5 function names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete docs (every Viji page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene's needs.\n2. ALWAYS use `render(viji, p5)`: not `draw()`. ALWAYS use `setup(viji, p5)`: not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255; matches `colorMode(RGB, 255)`) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100; matches `colorMode(HSB, 360, 100, 100)`); prefer those over parsing hex. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed, it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications, never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG: jumps: const t = viji.time * speed.value;\n // WRONG: nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only: `p5.drawingContext` is WebGL, not Canvas 2D. **Always preserve the source aspect ratio with the `videoFit` helper.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n ```javascript\n function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // 'cover' (default) or 'contain'\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n // CV coords (face.bounds, hand.landmarks, etc.) are normalized 0-1 to the\n // SOURCE video frame, not the canvas. Map through v: x = v.x + pt.x * v.width.\n ```\n Default to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. Stretching with `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)`: Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context: never use Canvas 2D-only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default: use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive.\n ```javascript\n const audioReact = viji.toggle(true, { label: 'Audio Reactive', group: 'audio', category: 'audio' });\n const followMouse = viji.toggle(true, { label: 'Follow Mouse', group: 'interaction', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes: the canvas is managed by P5.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0-1 |\n| `beat.snare` | `number` | Snare energy 0-1 |\n| `beat.hat` | `number` | Hi-hat energy 0-1 |\n| `beat.any` | `number` | Any beat energy 0-1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60-240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5 using the `videoFit` helper (see Drawing & Canvas section above) to preserve source aspect: `const v = videoFit(viji); p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);`. Never `(0, 0, viji.width, viji.height)` unless distortion is intentional.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut: all 0-1).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` | Remove: canvas is provided |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow help the artist build a Viji P5 scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5): convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart): your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference): full API reference\n- [Drawing with P5](/p5/drawing): Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/): full P5.js documentation"
|
|
1356
|
+
"markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene: a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js v1.9.4.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D canvas or WEBGL / 3D mode?\", \"Should it use the camera or only mouse input?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes. Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or P5 function names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete docs (every Viji page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene's needs.\n2. ALWAYS use `render(viji, p5)`: not `draw()`. ALWAYS use `setup(viji, p5)`: not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255; matches `colorMode(RGB, 255)`) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100; matches `colorMode(HSB, 360, 100, 100)`); prefer those over parsing hex. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed, it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications, never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG: jumps: const t = viji.time * speed.value;\n // WRONG: nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only: `p5.drawingContext` is WebGL, not Canvas 2D. **Always preserve the source aspect ratio with the `videoFit` helper.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n ```javascript\n function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // 'cover' (default) or 'contain'\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n // CV coords (face.bounds, hand.landmarks, etc.) are normalized 0-1 to the\n // SOURCE video frame, not the canvas. Map through v: x = v.x + pt.x * v.width.\n ```\n Default to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. Stretching with `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)`: Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context: never use Canvas 2D-only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default: use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes: the canvas is managed by P5.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category? }) // { value: '#rrggbb', rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 } }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy 0-1 |\n| `beat.snare` | `number` | Snare energy 0-1 |\n| `beat.hat` | `number` | Hi-hat energy 0-1 |\n| `beat.any` | `number` | Any beat energy 0-1 |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoothed beat values |\n| `beat.triggers.kick` | `boolean` | True on kick frame |\n| `beat.triggers.snare` | `boolean` | True on snare frame |\n| `beat.triggers.hat` | `boolean` | True on hat frame |\n| `beat.triggers.any` | `boolean` | True on any beat frame |\n| `beat.events` | `Array<{type,time,strength}>` | Recent beat events |\n| `beat.bpm` | `number` | Estimated BPM (60-240) |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True when BPM is locked |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5 using the `videoFit` helper (see Drawing & Canvas section above) to preserve source aspect: `const v = videoFit(viji); p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);`. Never `(0, 0, viji.width, viji.height)` unless distortion is intentional.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false);\nawait viji.video.cv.enableEmotionDetection(true/false);\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll}), `blendshapes` (52 ARKit coefficients: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut: all 0-1).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array, 0=background 255=person), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` | Remove: canvas is provided |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow help the artist build a Viji P5 scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5): convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart): your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference): full API reference\n- [Drawing with P5](/p5/drawing): Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/): full P5.js documentation"
|
|
1347
1357
|
}
|
|
1348
1358
|
]
|
|
1349
1359
|
},
|
|
@@ -1371,7 +1381,7 @@ export const docsApi = {
|
|
|
1371
1381
|
"content": [
|
|
1372
1382
|
{
|
|
1373
1383
|
"type": "text",
|
|
1374
|
-
"markdown": "# Prompt: Shader Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the shader effect you want. The prompt gives the AI everything it needs about Viji's shader renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji GLSL shader scene: a fragment shader that runs on a fullscreen quad inside a Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working GLSL code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D pattern or raymarched 3D?\", \"Hard geometric or soft organic feel?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable shader that follows every rule in this prompt. Include `@viji-*` parameter directives for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the shader works, which uniforms and parameters it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"more chaotic\", \"warmer palette\", \"make the kick punch harder\"). Treat each follow-up as a refinement: keep the working shader as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV uniforms, behavior nuances, full shader examples).\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt.\n- Never invent uniform names or directive names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Viji renders a **fullscreen quad**. Your shader defines the color of every pixel.\n- Viji **auto-injects** `precision mediump float;` and ALL uniform declarations: both built-in uniforms and parameter uniforms from `@viji-*` directives.\n- You write only helper functions and `void main() { ... }`.\n- **GLSL ES 1.00** by default. Add `#version 300 es` as the very first line for ES 3.00.\n- ES 3.00 requires `out vec4 fragColor;` (before `main`) and `fragColor = ...` instead of `gl_FragColor`.\n- ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader uses `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the first line (or after `#version 300 es` if using ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. NEVER use the `u_` prefix for your own parameter names: it is reserved for built-in uniforms. Name parameters descriptively: `speed`, `colorMix`, `intensity`.\n6. `@viji-*` parameter directives ONLY work with `//` comments. NEVER use `/* */` for directives.\n7. ALWAYS use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation: this prevents jumps when sliders change:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase); // smooth, no jumps\n ```\n The same applies to **nested** multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators:\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame), just reference it in code: Viji auto-detects and enables it.\n9. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n10. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls. This lets the host UI hide irrelevant controls when that input is inactive:\n ```glsl\n // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true group:audio category:audio\n // @viji-toggle:showVideo label:\"Show Video\" default:true group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 group:interaction category:interaction\n ```\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are always available: do NOT declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy 0-1 |\n| `u_audioSnare` | `float` | Snare energy 0-1 |\n| `u_audioHat` | `float` | Hi-hat energy 0-1 |\n| `u_audioAny` | `float` | Any beat energy 0-1 |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoothed beat values |\n| `u_audioKickTrigger` | `bool` | True on kick beat frame |\n| `u_audioSnareTrigger` | `bool` | True on snare beat frame |\n| `u_audioHatTrigger` | `bool` | True on hat beat frame |\n| `u_audioAnyTrigger` | `bool` | True on any beat frame |\n| `u_audioBPM` | `float` | Estimated BPM (60-240) |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0-1 |\n| `u_audioIsLocked` | `bool` | True when BPM is locked |\n| `u_audioBrightness` | `float` | Spectral brightness 0-1 |\n| `u_audioFlatness` | `float` | Spectral flatness 0-1 |\n\n### Audio: Textures\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioFFT` | `sampler2D` | FFT frequency spectrum (1024 bins, 0-255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (−1 to 1) |\n\n**Note:** `u_audioFFT` / `u_audioWaveform` apply only to the main audio source. Additional streams (host `audioStreams` and device audio) use `u_audioStream{i}*` float/bool uniforms only: see \"Streams (Compositor)\" → \"Audio streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Just-arrived video frame texture |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with the CV uniforms (`u_face*`, `u_hand*`, `u_pose*`, `u_segmentationMask`) |\n| `u_videoAnalysedAvailable` | `bool` | True after the first CV result lands |\n| `u_videoResolution` | `vec2` | Video frame size in pixels (shared between `u_video` and `u_videoAnalysed`) |\n| `u_videoFrameRate` | `float` | Video frame rate |\n| `u_videoConnected` | `bool` | True if video source is active |\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with the canvas's normalized UV stretches the video and misaligns CV uniforms. Define this `vijiVideoUV` helper at the top of the shader and use it for every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, crops video edges)\n// 0 = contain (fits video, letterboxes canvas)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // 1 = cover (default), 0 = contain\n vec4 video = texture2D(u_video, videoUV);\n // CV uniforms (u_face0Center, etc.) are normalized to the source video,\n // so compare them against videoUV (not uv):\n float faceDist = length(videoUV - u_face0Center);\n // ...\n}\n```\n\nDefault to mode `1` (cover) for live camera shaders. Use mode `0` (contain) for CV-overlay shaders where features near frame edges must stay visible. Sampling `texture2D(u_video, uv)` directly is allowed only when distortion is intentional.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when the shader reads pixels from the displayed frame at CV-derived positions (compositing `u_segmentationMask` onto the body, sampling skin under face landmarks, warping the face along its mesh, texture-mapped face filters). For shaders that consume CV uniforms without sampling at CV positions (head-pose-driven color shifts, generative geometry from `u_face*` / `u_hand*` / `u_pose*`), keep `u_video`: `u_videoAnalysed` only advances when MediaPipe completes an inference, so the displayed video stutters or holds between inferences.\n\nWhen `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` to fall back during the brief startup window before the first CV result lands:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration with gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | Orientation (alpha, beta, gamma) degrees |\n| `u_deviceOrientationAbsolute` | `bool` | True if using magnetometer |\n\n### External Devices\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceCount` | `int` | Number of device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | Number of external devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Device camera resolutions |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Device connection status |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration w/ gravity |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n**Note:** device audio (when an external device provides an audio source) is exposed as `u_audioStream{i}*` scalar uniforms: same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0-7). There are NO per-device or per-stream FFT/waveform textures; only the main audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0-1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0-1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0-1 |\n\n(`i` = 0…7.) **Lightweight scalars only**: **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0-1)\n// `default:` accepts: #rrggbb, #rgb, vec3(r,g,b) in 0..1, rgb(r,g,b) in 0..255,\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:invert label:\"Invert\" default:false\n// → uniform bool invert;\n\n// @viji-select:mode label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index)\n\n// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// → uniform float count;\n\n// @viji-image:tex label:\"Texture\"\n// → uniform sampler2D tex;\n\n// @viji-button:reset label:\"Reset\"\n// → uniform bool reset; (true for one frame on press)\n\n// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]\n// → uniform vec2 origin; (default uses [x,y] array; both components -1 to 1)\n\n// @viji-accumulator:phase rate:speed\n// → uniform float phase; (CPU-side: += speed × deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow help the artist build a Viji shader scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the shader:\n- Follow every rule in this prompt.\n- Use `// @renderer shader` as the first line. Do NOT declare precision or uniforms. Use `@viji-accumulator` for parameter-driven animation. Use `@viji-slider/color/toggle` for artist controls.\n- Output the GLSL code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the shader works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the shader effect you want.\n4. The AI will return a complete Viji shader scene.\n\n> [!TIP]\n> For better results, describe the visual effect you want (patterns, colors, motion), mention data sources (audio, video, mouse), and what controls the user should have. If you have existing Shadertoy shaders to convert, use the [Convert: Shadertoy](/ai-prompts/convert-shadertoy) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: Shadertoy](/ai-prompts/convert-shadertoy): convert existing Shadertoy shaders to Viji\n- [Shader Quick Start](/shader/quickstart): your first Viji shader\n- [Shader API Reference](/shader/api-reference): full uniform reference\n- [Backbuffer & Feedback](/shader/backbuffer): previous-frame feedback effects\n- [Shadertoy Compatibility](/shader/shadertoy): compatibility layer for Shadertoy code"
|
|
1384
|
+
"markdown": "# Prompt: Shader Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the shader effect you want. The prompt gives the AI everything it needs about Viji's shader renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji GLSL shader scene: a fragment shader that runs on a fullscreen quad inside a Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working GLSL code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D pattern or raymarched 3D?\", \"Hard geometric or soft organic feel?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable shader that follows every rule in this prompt. Include `@viji-*` parameter directives for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the shader works, which uniforms and parameters it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"more chaotic\", \"warmer palette\", \"make the kick punch harder\"). Treat each follow-up as a refinement: keep the working shader as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV uniforms, behavior nuances, full shader examples).\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt.\n- Never invent uniform names or directive names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Viji renders a **fullscreen quad**. Your shader defines the color of every pixel.\n- Viji **auto-injects** `precision mediump float;` and ALL uniform declarations: both built-in uniforms and parameter uniforms from `@viji-*` directives.\n- You write only helper functions and `void main() { ... }`.\n- **GLSL ES 1.00** by default. Add `#version 300 es` as the very first line for ES 3.00.\n- ES 3.00 requires `out vec4 fragColor;` (before `main`) and `fragColor = ...` instead of `gl_FragColor`.\n- ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader uses `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the first line (or after `#version 300 es` if using ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. NEVER use the `u_` prefix for your own parameter names: it is reserved for built-in uniforms. Name parameters descriptively: `speed`, `colorMix`, `intensity`.\n6. `@viji-*` parameter directives ONLY work with `//` comments. NEVER use `/* */` for directives.\n7. ALWAYS use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation: this prevents jumps when sliders change:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase); // smooth, no jumps\n ```\n The same applies to **nested** multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators:\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame), just reference it in code: Viji auto-detects and enables it.\n9. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n10. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls. This lets the host UI hide irrelevant controls when that input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level \"Audio Reactive\" / \"Show Video\" toggle just duplicates the host switch.\n ```glsl\n // Right: creative-strength sliders with the matching category.\n // @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n // @viji-slider:videoMix label:\"Video Mix\" default:0.6 min:0 max:1 group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 min:0 max:1 group:interaction category:interaction\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true category:audio\n // // @viji-toggle:showVideo label:\"Show Video\" default:true category:video\n ```\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are always available: do NOT declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy 0-1 |\n| `u_audioSnare` | `float` | Snare energy 0-1 |\n| `u_audioHat` | `float` | Hi-hat energy 0-1 |\n| `u_audioAny` | `float` | Any beat energy 0-1 |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoothed beat values |\n| `u_audioKickTrigger` | `bool` | True on kick beat frame |\n| `u_audioSnareTrigger` | `bool` | True on snare beat frame |\n| `u_audioHatTrigger` | `bool` | True on hat beat frame |\n| `u_audioAnyTrigger` | `bool` | True on any beat frame |\n| `u_audioBPM` | `float` | Estimated BPM (60-240) |\n| `u_audioConfidence` | `float` | Beat tracking confidence 0-1 |\n| `u_audioIsLocked` | `bool` | True when BPM is locked |\n| `u_audioBrightness` | `float` | Spectral brightness 0-1 |\n| `u_audioFlatness` | `float` | Spectral flatness 0-1 |\n\n### Audio: Textures\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioFFT` | `sampler2D` | FFT frequency spectrum (1024 bins, 0-255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (−1 to 1) |\n\n**Note:** `u_audioFFT` / `u_audioWaveform` apply only to the main audio source. Additional streams (host `audioStreams` and device audio) use `u_audioStream{i}*` float/bool uniforms only: see \"Streams (Compositor)\" → \"Audio streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Just-arrived video frame texture |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with the CV uniforms (`u_face*`, `u_hand*`, `u_pose*`, `u_segmentationMask`) |\n| `u_videoAnalysedAvailable` | `bool` | True after the first CV result lands |\n| `u_videoResolution` | `vec2` | Video frame size in pixels (shared between `u_video` and `u_videoAnalysed`) |\n| `u_videoFrameRate` | `float` | Video frame rate |\n| `u_videoConnected` | `bool` | True if video source is active |\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with the canvas's normalized UV stretches the video and misaligns CV uniforms. Define this `vijiVideoUV` helper at the top of the shader and use it for every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, crops video edges)\n// 0 = contain (fits video, letterboxes canvas)\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // 1 = cover (default), 0 = contain\n vec4 video = texture2D(u_video, videoUV);\n // CV uniforms (u_face0Center, etc.) are normalized to the source video,\n // so compare them against videoUV (not uv):\n float faceDist = length(videoUV - u_face0Center);\n // ...\n}\n```\n\nDefault to mode `1` (cover) for live camera shaders. Use mode `0` (contain) for CV-overlay shaders where features near frame edges must stay visible. Sampling `texture2D(u_video, uv)` directly is allowed only when distortion is intentional.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when the shader reads pixels from the displayed frame at CV-derived positions (compositing `u_segmentationMask` onto the body, sampling skin under face landmarks, warping the face along its mesh, texture-mapped face filters). For shaders that consume CV uniforms without sampling at CV positions (head-pose-driven color shifts, generative geometry from `u_face*` / `u_hand*` / `u_pose*`), keep `u_video`: `u_videoAnalysed` only advances when MediaPipe completes an inference, so the displayed video stutters or holds between inferences.\n\nWhen `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` to fall back during the brief startup window before the first CV result lands:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration with gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | Orientation (alpha, beta, gamma) degrees |\n| `u_deviceOrientationAbsolute` | `bool` | True if using magnetometer |\n\n### External Devices\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceCount` | `int` | Number of device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | Number of external devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Device camera resolutions |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Device connection status |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration w/ gravity |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n**Note:** device audio (when an external device provides an audio source) is exposed as `u_audioStream{i}*` scalar uniforms: same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0-7). There are NO per-device or per-stream FFT/waveform textures; only the main audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0-1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0-1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0-1 |\n\n(`i` = 0…7.) **Lightweight scalars only**: **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0-1)\n// `default:` accepts: #rrggbb, #rgb, vec3(r,g,b) in 0..1, rgb(r,g,b) in 0..255,\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:invert label:\"Invert\" default:false\n// → uniform bool invert;\n\n// @viji-select:mode label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index)\n\n// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// → uniform float count;\n\n// @viji-image:tex label:\"Texture\"\n// → uniform sampler2D tex;\n\n// @viji-button:reset label:\"Reset\"\n// → uniform bool reset; (true for one frame on press)\n\n// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]\n// → uniform vec2 origin; (default uses [x,y] array; both components -1 to 1)\n\n// @viji-accumulator:phase rate:speed\n// → uniform float phase; (CPU-side: += speed × deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow help the artist build a Viji shader scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the shader:\n- Follow every rule in this prompt.\n- Use `// @renderer shader` as the first line. Do NOT declare precision or uniforms. Use `@viji-accumulator` for parameter-driven animation. Use `@viji-slider/color/toggle` for artist controls.\n- Output the GLSL code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the shader works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the shader effect you want.\n4. The AI will return a complete Viji shader scene.\n\n> [!TIP]\n> For better results, describe the visual effect you want (patterns, colors, motion), mention data sources (audio, video, mouse), and what controls the user should have. If you have existing Shadertoy shaders to convert, use the [Convert: Shadertoy](/ai-prompts/convert-shadertoy) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: Shadertoy](/ai-prompts/convert-shadertoy): convert existing Shadertoy shaders to Viji\n- [Shader Quick Start](/shader/quickstart): your first Viji shader\n- [Shader API Reference](/shader/api-reference): full uniform reference\n- [Backbuffer & Feedback](/shader/backbuffer): previous-frame feedback effects\n- [Shadertoy Compatibility](/shader/shadertoy): compatibility layer for Shadertoy code"
|
|
1375
1385
|
}
|
|
1376
1386
|
]
|
|
1377
1387
|
},
|
|
@@ -2549,12 +2559,12 @@ export const docsApi = {
|
|
|
2549
2559
|
"content": [
|
|
2550
2560
|
{
|
|
2551
2561
|
"type": "text",
|
|
2552
|
-
"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 videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst
|
|
2562
|
+
"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 videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', 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- `videoOpacity` ([`viji.slider()`](../slider/)) only appears when a video/camera source is connected.\n- `mouseAttraction` ([`viji.slider()`](../slider/)) only appears when user interaction is enabled.\n\n> [!NOTE]\n> The `interaction` parameter is a creative-strength slider, not an on/off toggle. The host UI already controls whether interaction is wired up at all; a scene-level \"Mouse Trail (on/off)\" toggle would only duplicate that. See [Best Practices: No Redundant Input Toggles](/getting-started/best-practices/#no-redundant-input-toggles).\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 all four categories: `general` (always visible), `audio` (needs audio), `video` (needs camera), and `interaction` (needs mouse). Each control reveals itself as you connect the corresponding capability:"
|
|
2553
2563
|
},
|
|
2554
2564
|
{
|
|
2555
2565
|
"type": "live-example",
|
|
2556
2566
|
"title": "Parameter Categories",
|
|
2557
|
-
"sceneCode": "const baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, step: 0.01, label: 'Video Opacity', category: 'video' });\nconst
|
|
2567
|
+
"sceneCode": "const baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, step: 0.01, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, step: 0.01, label: 'Mouse Attraction', category: 'interaction' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nlet angle = 0;\nlet mouseTrailX = 0;\nlet mouseTrailY = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n if (viji.video.isConnected && viji.video.currentFrame && videoOpacity.value > 0) {\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, w, h);\n const v = videoFit(viji);\n ctx.globalAlpha = videoOpacity.value;\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n ctx.globalAlpha = 1;\n } else {\n ctx.fillStyle = 'rgba(10, 10, 30, 0.15)';\n ctx.fillRect(0, 0, w, h);\n }\n\n angle += viji.deltaTime;\n\n const r = parseInt(baseColor.value.slice(1, 3), 16);\n const g = parseInt(baseColor.value.slice(3, 5), 16);\n const b = parseInt(baseColor.value.slice(5, 7), 16);\n\n let pulse = 0;\n if (viji.audio.isConnected) {\n pulse = viji.audio.volume.current * pulseAmount.value;\n }\n\n const baseR = Math.min(w, h) * (0.1 + pulse * 0.15);\n\n const orbitX = w / 2 + Math.cos(angle) * w * 0.2;\n const orbitY = h / 2 + Math.sin(angle * 0.7) * h * 0.2;\n\n let cx = orbitX;\n let cy = orbitY;\n if (viji.mouse.isInCanvas) {\n const k = mouseAttraction.value;\n cx = orbitX * (1 - k) + viji.mouse.x * k;\n cy = orbitY * (1 - k) + viji.mouse.y * k;\n\n mouseTrailX += (viji.mouse.x - mouseTrailX) * 0.1;\n mouseTrailY += (viji.mouse.y - mouseTrailY) * 0.1;\n ctx.beginPath();\n ctx.arc(mouseTrailX, mouseTrailY, Math.min(w, h) * 0.02, 0, Math.PI * 2);\n ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';\n ctx.fill();\n }\n\n ctx.beginPath();\n ctx.arc(cx, cy, baseR, 0, Math.PI * 2);\n ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\n ctx.fill();\n}\n",
|
|
2558
2568
|
"sceneFile": "categories-demo.scene.js",
|
|
2559
2569
|
"capabilities": {
|
|
2560
2570
|
"audio": true,
|
|
@@ -3015,7 +3025,7 @@ export const docsApi = {
|
|
|
3015
3025
|
{
|
|
3016
3026
|
"type": "live-example",
|
|
3017
3027
|
"title": "Video with Face Detection",
|
|
3018
|
-
"sceneCode": "const useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n
|
|
3028
|
+
"sceneCode": "const useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n const v = videoFit(viji, 'contain');\n ctx.globalAlpha = 0.6;\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n ctx.globalAlpha = 1.0;\n\n if (!useFace.value) {\n cvHint(ctx, viji, 'Face Detection');\n return;\n }\n\n viji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n\n ctx.strokeStyle = '#4ecdc4';\n ctx.lineWidth = 2;\n ctx.strokeRect(bx, by, bw, bh);\n\n ctx.fillStyle = '#4ecdc4';\n ctx.font = `${Math.min(w, h) * 0.03}px sans-serif`;\n ctx.textAlign = 'left';\n ctx.fillText('Face #' + face.id + ' (' + (face.confidence * 100).toFixed(0) + '%)', bx, by - 6);\n });\n}\n",
|
|
3019
3029
|
"sceneFile": "video-overview.scene.js",
|
|
3020
3030
|
"capabilities": {
|
|
3021
3031
|
"video": true
|
|
@@ -3071,7 +3081,7 @@ export const docsApi = {
|
|
|
3071
3081
|
{
|
|
3072
3082
|
"type": "live-example",
|
|
3073
3083
|
"title": "Connection State",
|
|
3074
|
-
"sceneCode": "function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n const fontSize = Math.min(w, h) * 0.035;\n ctx.font = `${fontSize}px sans-serif`;\n ctx.textAlign = 'center';\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n const pulse = 0.4 + Math.sin(viji.time * 2) * 0.15;\n ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;\n ctx.fillText('Waiting for video stream...', w / 2, h / 2 - fontSize);\n ctx.fillStyle = '#444';\n ctx.fillText('Connect a camera or video source', w / 2, h / 2 + fontSize);\n return;\n }\n\n
|
|
3084
|
+
"sceneCode": "function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n const fontSize = Math.min(w, h) * 0.035;\n ctx.font = `${fontSize}px sans-serif`;\n ctx.textAlign = 'center';\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n const pulse = 0.4 + Math.sin(viji.time * 2) * 0.15;\n ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;\n ctx.fillText('Waiting for video stream...', w / 2, h / 2 - fontSize);\n ctx.fillStyle = '#444';\n ctx.fillText('Connect a camera or video source', w / 2, h / 2 + fontSize);\n return;\n }\n\n const v = videoFit(viji);\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n const infoY = h - fontSize * 4;\n ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';\n ctx.fillRect(0, infoY, w, fontSize * 4);\n\n ctx.fillStyle = '#4CAF50';\n ctx.textAlign = 'left';\n const pad = w * 0.04;\n ctx.fillText('Video connected', pad, infoY + fontSize * 1.2);\n ctx.fillStyle = '#aaa';\n ctx.fillText(\n viji.video.frameWidth + ' x ' + viji.video.frameHeight + ' @ ' + viji.video.frameRate.toFixed(0) + ' fps',\n pad, infoY + fontSize * 2.6\n );\n}\n",
|
|
3075
3085
|
"sceneFile": "connection-demo.scene.js",
|
|
3076
3086
|
"capabilities": {
|
|
3077
3087
|
"video": true
|
|
@@ -3188,7 +3198,7 @@ export const docsApi = {
|
|
|
3188
3198
|
{
|
|
3189
3199
|
"type": "live-example",
|
|
3190
3200
|
"title": "Face Detection",
|
|
3191
|
-
"sceneCode": "const useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\
|
|
3201
|
+
"sceneCode": "const useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n if (!useFace.value) {\n cvHint(ctx, viji, 'Face Detection');\n return;\n }\n\n viji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n\n ctx.strokeStyle = '#4ecdc4';\n ctx.lineWidth = 2;\n ctx.strokeRect(bx, by, bw, bh);\n\n ctx.fillStyle = '#4ecdc4';\n ctx.beginPath();\n ctx.arc(v.x + face.center.x * v.width, v.y + face.center.y * v.height, 4, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.font = `${Math.min(w, h) * 0.025}px sans-serif`;\n ctx.textAlign = 'left';\n ctx.fillText('Face #' + face.id + ' (' + (face.confidence * 100).toFixed(0) + '%)', bx, by - 6);\n });\n}\n",
|
|
3192
3202
|
"sceneFile": "face-detection-demo.scene.js",
|
|
3193
3203
|
"capabilities": {
|
|
3194
3204
|
"video": true
|
|
@@ -3244,7 +3254,7 @@ export const docsApi = {
|
|
|
3244
3254
|
{
|
|
3245
3255
|
"type": "live-example",
|
|
3246
3256
|
"title": "Face Mesh",
|
|
3247
|
-
"sceneCode": "const useMesh = viji.toggle(false, { label: 'Face Mesh', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useMesh.value) viji.video.cv.enableFaceMesh(true);\n else viji.video.cv.enableFaceMesh(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n
|
|
3257
|
+
"sceneCode": "const useMesh = viji.toggle(false, { label: 'Face Mesh', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useMesh.value) viji.video.cv.enableFaceMesh(true);\n else viji.video.cv.enableFaceMesh(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n ctx.globalAlpha = 0.4;\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n ctx.globalAlpha = 1.0;\n\n if (!useMesh.value) {\n cvHint(ctx, viji, 'Face Mesh');\n return;\n }\n\n viji.video.cv.faces.forEach(face => {\n if (face.landmarks.length === 0) return;\n\n ctx.fillStyle = 'rgba(69, 183, 209, 0.7)';\n face.landmarks.forEach(pt => {\n ctx.beginPath();\n ctx.arc(v.x + pt.x * v.width, v.y + pt.y * v.height, 1, 0, Math.PI * 2);\n ctx.fill();\n });\n\n const fontSize = Math.min(w, h) * 0.025;\n ctx.fillStyle = '#fff';\n ctx.font = `${fontSize}px sans-serif`;\n ctx.textAlign = 'left';\n const hp = face.headPose;\n ctx.fillText(\n face.landmarks.length + ' landmarks | Pitch: ' + hp.pitch.toFixed(1) +\n ' Yaw: ' + hp.yaw.toFixed(1) + ' Roll: ' + hp.roll.toFixed(1),\n w * 0.03, h - fontSize * 0.8\n );\n });\n}\n",
|
|
3248
3258
|
"sceneFile": "face-mesh-demo.scene.js",
|
|
3249
3259
|
"capabilities": {
|
|
3250
3260
|
"video": true
|
|
@@ -3295,7 +3305,7 @@ export const docsApi = {
|
|
|
3295
3305
|
{
|
|
3296
3306
|
"type": "live-example",
|
|
3297
3307
|
"title": "Emotion Detection",
|
|
3298
|
-
"sceneCode": "const useEmotion = viji.toggle(false, { label: 'Emotion Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useEmotion.value) {\n viji.video.cv.enableEmotionDetection(true);\n } else {\n viji.video.cv.enableEmotionDetection(false);\n }\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n
|
|
3308
|
+
"sceneCode": "const useEmotion = viji.toggle(false, { label: 'Emotion Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useEmotion.value) {\n viji.video.cv.enableEmotionDetection(true);\n } else {\n viji.video.cv.enableEmotionDetection(false);\n }\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n ctx.globalAlpha = 0.4;\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n ctx.globalAlpha = 1.0;\n\n if (!useEmotion.value) {\n cvHint(ctx, viji, 'Emotion Detection');\n return;\n }\n\n const face = viji.video.cv.faces[0];\n if (!face) return;\n\n const expr = face.expressions;\n const labels = ['neutral', 'happy', 'sad', 'angry', 'surprised', 'disgusted', 'fearful'];\n const values = [expr.neutral, expr.happy, expr.sad, expr.angry, expr.surprised, expr.disgusted, expr.fearful];\n const colors = ['#888', '#4CAF50', '#2196F3', '#f44336', '#FF9800', '#9C27B0', '#607D8B'];\n\n const barH = h * 0.04;\n const barW = w * 0.3;\n const x = w * 0.65;\n let y = h * 0.12;\n const fontSize = barH * 0.7;\n\n ctx.font = `${fontSize}px sans-serif`;\n labels.forEach((label, i) => {\n ctx.fillStyle = '#aaa';\n ctx.textAlign = 'right';\n ctx.fillText(label, x - 8, y + barH * 0.75);\n\n ctx.fillStyle = '#222';\n ctx.fillRect(x, y, barW, barH);\n\n ctx.fillStyle = colors[i];\n ctx.fillRect(x, y, barW * values[i], barH);\n\n ctx.fillStyle = '#ddd';\n ctx.textAlign = 'left';\n ctx.fillText((values[i] * 100).toFixed(0) + '%', x + barW + 6, y + barH * 0.75);\n\n y += barH * 1.8;\n });\n}\n",
|
|
3299
3309
|
"sceneFile": "emotion-detection-demo.scene.js",
|
|
3300
3310
|
"capabilities": {
|
|
3301
3311
|
"video": true
|
|
@@ -3341,7 +3351,7 @@ export const docsApi = {
|
|
|
3341
3351
|
{
|
|
3342
3352
|
"type": "live-example",
|
|
3343
3353
|
"title": "Hand Tracking",
|
|
3344
|
-
"sceneCode": "const useHands = viji.toggle(false, { label: 'Hand Tracking', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n
|
|
3354
|
+
"sceneCode": "const useHands = viji.toggle(false, { label: 'Hand Tracking', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n ctx.globalAlpha = 0.4;\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n ctx.globalAlpha = 1.0;\n\n if (!useHands.value) {\n cvHint(ctx, viji, 'Hand Tracking');\n return;\n }\n\n viji.video.cv.hands.forEach(hand => {\n const color = hand.handedness === 'left' ? '#ff9ff3' : '#54a0ff';\n\n ctx.fillStyle = color;\n hand.landmarks.forEach(pt => {\n ctx.beginPath();\n ctx.arc(v.x + pt.x * v.width, v.y + pt.y * v.height, 3, 0, Math.PI * 2);\n ctx.fill();\n });\n\n ctx.beginPath();\n ctx.arc(v.x + hand.palm.x * v.width, v.y + hand.palm.y * v.height, 8, 0, Math.PI * 2);\n ctx.strokeStyle = color;\n ctx.lineWidth = 2;\n ctx.stroke();\n\n const g = hand.gestures;\n const names = ['fist', 'openPalm', 'peace', 'thumbsUp', 'thumbsDown', 'pointing', 'iLoveYou'];\n const vals = [g.fist, g.openPalm, g.peace, g.thumbsUp, g.thumbsDown, g.pointing, g.iLoveYou];\n\n const barW = w * 0.12;\n const barH = h * 0.02;\n let bx = v.x + hand.bounds.x * v.width;\n let by = v.y + (hand.bounds.y + hand.bounds.height) * v.height + 8;\n const fontSize = barH * 0.9;\n ctx.font = `${fontSize}px sans-serif`;\n\n names.forEach((name, i) => {\n ctx.fillStyle = '#aaa';\n ctx.textAlign = 'right';\n ctx.fillText(name, bx + barW * 0.6 - 4, by + barH * 0.85);\n\n ctx.fillStyle = '#333';\n ctx.fillRect(bx + barW * 0.6, by, barW * 0.4, barH);\n ctx.fillStyle = color;\n ctx.fillRect(bx + barW * 0.6, by, barW * 0.4 * vals[i], barH);\n\n by += barH * 1.5;\n });\n });\n}\n",
|
|
3345
3355
|
"sceneFile": "hand-tracking-demo.scene.js",
|
|
3346
3356
|
"capabilities": {
|
|
3347
3357
|
"video": true
|
|
@@ -3387,7 +3397,7 @@ export const docsApi = {
|
|
|
3387
3397
|
{
|
|
3388
3398
|
"type": "live-example",
|
|
3389
3399
|
"title": "Pose Detection",
|
|
3390
|
-
"sceneCode": "const usePose = viji.toggle(false, { label: 'Pose Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (usePose.value) viji.video.cv.enablePoseDetection(true);\n else viji.video.cv.enablePoseDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n
|
|
3400
|
+
"sceneCode": "const usePose = viji.toggle(false, { label: 'Pose Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (usePose.value) viji.video.cv.enablePoseDetection(true);\n else viji.video.cv.enablePoseDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n ctx.globalAlpha = 0.4;\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n ctx.globalAlpha = 1.0;\n\n if (!usePose.value) {\n cvHint(ctx, viji, 'Pose Detection');\n return;\n }\n\n const pose = viji.video.cv.pose;\n if (!pose) return;\n\n ctx.fillStyle = '#ff6b6b';\n pose.landmarks.forEach(pt => {\n if (pt.visibility > 0.5) {\n ctx.beginPath();\n ctx.arc(v.x + pt.x * v.width, v.y + pt.y * v.height, 4, 0, Math.PI * 2);\n ctx.fill();\n }\n });\n\n ctx.strokeStyle = '#ff6b6b';\n ctx.lineWidth = 2;\n const drawGroup = (group) => {\n if (group.length < 2) return;\n ctx.beginPath();\n ctx.moveTo(v.x + group[0].x * v.width, v.y + group[0].y * v.height);\n for (let i = 1; i < group.length; i++) {\n ctx.lineTo(v.x + group[i].x * v.width, v.y + group[i].y * v.height);\n }\n ctx.stroke();\n };\n\n ctx.strokeStyle = '#ff9ff3';\n drawGroup(pose.leftArm);\n ctx.strokeStyle = '#54a0ff';\n drawGroup(pose.rightArm);\n ctx.strokeStyle = '#ff9ff3';\n drawGroup(pose.leftLeg);\n ctx.strokeStyle = '#54a0ff';\n drawGroup(pose.rightLeg);\n ctx.strokeStyle = '#feca57';\n drawGroup(pose.torso);\n\n ctx.fillStyle = '#fff';\n ctx.font = `${Math.min(w, h) * 0.025}px sans-serif`;\n ctx.textAlign = 'left';\n ctx.fillText('Confidence: ' + (pose.confidence * 100).toFixed(0) + '%', w * 0.03, h - Math.min(w, h) * 0.03);\n}\n",
|
|
3391
3401
|
"sceneFile": "pose-detection-demo.scene.js",
|
|
3392
3402
|
"capabilities": {
|
|
3393
3403
|
"video": true
|
|
@@ -3438,7 +3448,7 @@ export const docsApi = {
|
|
|
3438
3448
|
{
|
|
3439
3449
|
"type": "live-example",
|
|
3440
3450
|
"title": "Body Segmentation",
|
|
3441
|
-
"sceneCode": "const useSeg = viji.toggle(false, { label: 'Body Segmentation', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useSeg.value) viji.video.cv.enableBodySegmentation(true);\n else viji.video.cv.enableBodySegmentation(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n
|
|
3451
|
+
"sceneCode": "const useSeg = viji.toggle(false, { label: 'Body Segmentation', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(ctx, viji, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (Math.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n ctx.fillStyle = `rgba(255, 200, 0, ${0.85 + t * 0.15})`;\n ctx.fillRect(0, bandY, viji.width, bandH);\n ctx.fillStyle = '#111';\n ctx.font = `bold ${s * 0.045}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n ctx.textBaseline = 'alphabetic';\n}\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const w = viji.width;\n const h = viji.height;\n\n ctx.fillStyle = '#111';\n ctx.fillRect(0, 0, w, h);\n\n if (useSeg.value) viji.video.cv.enableBodySegmentation(true);\n else viji.video.cv.enableBodySegmentation(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n ctx.fillStyle = '#555';\n ctx.font = `${Math.min(w, h) * 0.04}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText('Waiting for video...', w / 2, h / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n const frameToShow = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\n ctx.drawImage(frameToShow, v.x, v.y, v.width, v.height);\n\n if (!useSeg.value) {\n cvHint(ctx, viji, 'Body Segmentation');\n return;\n }\n\n const seg = viji.video.cv.segmentation;\n if (!seg) return;\n\n let personPixels = 0;\n for (let i = 0; i < seg.mask.length; i++) {\n if (seg.mask[i] > 0) personPixels++;\n }\n const personRatio = personPixels / seg.mask.length;\n\n if (personRatio > 0.05) {\n ctx.shadowBlur = 30 * personRatio;\n ctx.shadowColor = `hsl(${170 + personRatio * 60}, 80%, 60%)`;\n ctx.strokeStyle = `hsla(${170 + personRatio * 60}, 80%, 60%, 0.6)`;\n ctx.lineWidth = 4;\n ctx.strokeRect(v.x, v.y, v.width, v.height);\n ctx.shadowBlur = 0;\n }\n\n const fontSize = Math.min(w, h) * 0.03;\n ctx.fillStyle = 'rgba(0,0,0,0.5)';\n ctx.fillRect(0, h - fontSize * 2, w, fontSize * 2);\n ctx.fillStyle = '#fff';\n ctx.font = `${fontSize}px sans-serif`;\n ctx.textAlign = 'center';\n ctx.fillText(\n 'Person: ' + (personRatio * 100).toFixed(0) + '% | Mask: ' + seg.width + 'x' + seg.height,\n w / 2, h - fontSize * 0.5\n );\n}\n",
|
|
3442
3452
|
"sceneFile": "body-segmentation-demo.scene.js",
|
|
3443
3453
|
"capabilities": {
|
|
3444
3454
|
"video": true
|
|
@@ -5299,12 +5309,12 @@ export const docsApi = {
|
|
|
5299
5309
|
"content": [
|
|
5300
5310
|
{
|
|
5301
5311
|
"type": "text",
|
|
5302
|
-
"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 videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst
|
|
5312
|
+
"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 videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\n- `baseColor` ([`viji.color()`](../color/)) is always visible.\n- `pulseAmount` ([`viji.slider()`](../slider/)) only appears when audio is connected.\n- `videoOpacity` ([`viji.slider()`](../slider/)) only appears when a video/camera source is connected.\n- `mouseAttraction` ([`viji.slider()`](../slider/)) only appears when interaction is enabled.\n\n> [!NOTE]\n> The `interaction` parameter is a creative-strength slider, not an on/off toggle. The host UI already controls whether interaction is wired up at all; a scene-level \"Mouse Dot (on/off)\" toggle would only duplicate that. See [Best Practices: No Redundant Input Toggles](/getting-started/best-practices/#no-redundant-input-toggles).\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 all four categories: `general` (always visible), `audio` (needs audio), `video` (needs camera), and `interaction` (needs mouse). Each control reveals itself as you connect the corresponding capability:"
|
|
5303
5313
|
},
|
|
5304
5314
|
{
|
|
5305
5315
|
"type": "live-example",
|
|
5306
5316
|
"title": "P5 Parameter Categories",
|
|
5307
|
-
"sceneCode": "// @renderer p5\n\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, step: 0.01, label: 'Video Opacity', category: 'video' });\nconst
|
|
5317
|
+
"sceneCode": "// @renderer p5\n\nconst baseColor = viji.color('#4488ff', { label: 'Base Color', category: 'general' });\nconst pulseAmount = viji.slider(0.3, { min: 0, max: 1, step: 0.01, label: 'Audio Pulse', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, step: 0.01, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, step: 0.01, label: 'Mouse Attraction', category: 'interaction' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nlet angle = 0;\n\nfunction render(viji, p5) {\n if (viji.video.isConnected && viji.video.currentFrame && videoOpacity.value > 0) {\n p5.background(0);\n const v = videoFit(viji);\n p5.tint(255, videoOpacity.value * 255);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n p5.noTint();\n } else {\n p5.background(10, 10, 30, 40);\n }\n\n angle += viji.deltaTime;\n\n const r = parseInt(baseColor.value.slice(1, 3), 16);\n const g = parseInt(baseColor.value.slice(3, 5), 16);\n const b = parseInt(baseColor.value.slice(5, 7), 16);\n\n let pulse = 0;\n if (viji.audio.isConnected) {\n pulse = viji.audio.volume.current * pulseAmount.value;\n }\n\n const baseR = Math.min(viji.width, viji.height) * (0.1 + pulse * 0.15);\n\n const orbitX = viji.width / 2 + p5.cos(angle) * viji.width * 0.2;\n const orbitY = viji.height / 2 + p5.sin(angle * 0.7) * viji.height * 0.2;\n\n let cx = orbitX;\n let cy = orbitY;\n if (viji.mouse.isInCanvas) {\n const k = mouseAttraction.value;\n cx = orbitX * (1 - k) + viji.mouse.x * k;\n cy = orbitY * (1 - k) + viji.mouse.y * k;\n\n p5.noStroke();\n p5.fill(255, 255, 255, 200);\n p5.circle(viji.mouse.x, viji.mouse.y, Math.min(viji.width, viji.height) * 0.04);\n }\n\n p5.noStroke();\n p5.fill(r, g, b);\n p5.circle(cx, cy, baseR * 2);\n}\n",
|
|
5308
5318
|
"sceneFile": "categories-demo.scene.js",
|
|
5309
5319
|
"capabilities": {
|
|
5310
5320
|
"audio": true,
|
|
@@ -5765,7 +5775,7 @@ export const docsApi = {
|
|
|
5765
5775
|
{
|
|
5766
5776
|
"type": "live-example",
|
|
5767
5777
|
"title": "Video with Face Detection",
|
|
5768
|
-
"sceneCode": "// @renderer p5\n\nconst useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n
|
|
5778
|
+
"sceneCode": "// @renderer p5\n\nconst useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n const v = videoFit(viji, 'contain');\n p5.tint(255, 150);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n p5.noTint();\n\n if (!useFace.value) {\n cvHint(viji, p5, 'Face Detection');\n return;\n }\n\n viji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n\n p5.noFill();\n p5.stroke(78, 205, 196);\n p5.strokeWeight(2);\n p5.rect(bx, by, bw, bh);\n\n p5.noStroke();\n p5.fill(78, 205, 196);\n p5.textSize(Math.min(viji.width, viji.height) * 0.025);\n p5.textAlign(p5.LEFT, p5.BOTTOM);\n p5.text('Face #' + face.id + ' (' + (face.confidence * 100).toFixed(0) + '%)', bx, by - 4);\n });\n}\n",
|
|
5769
5779
|
"sceneFile": "video-overview.scene.js",
|
|
5770
5780
|
"capabilities": {
|
|
5771
5781
|
"video": true
|
|
@@ -5816,7 +5826,7 @@ export const docsApi = {
|
|
|
5816
5826
|
{
|
|
5817
5827
|
"type": "live-example",
|
|
5818
5828
|
"title": "Connection State",
|
|
5819
|
-
"sceneCode": "// @renderer p5\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n const fontSize = Math.min(viji.width, viji.height) * 0.035;\n p5.textSize(fontSize);\n p5.textAlign(p5.CENTER, p5.CENTER);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n const pulse = 0.4 + Math.sin(viji.time * 2) * 0.15;\n p5.fill(255, pulse * 255);\n p5.text('Waiting for video stream...', viji.width / 2, viji.height / 2 - fontSize);\n p5.fill(80);\n p5.text('Connect a camera or video source', viji.width / 2, viji.height / 2 + fontSize);\n return;\n }\n\n
|
|
5829
|
+
"sceneCode": "// @renderer p5\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n const fontSize = Math.min(viji.width, viji.height) * 0.035;\n p5.textSize(fontSize);\n p5.textAlign(p5.CENTER, p5.CENTER);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n const pulse = 0.4 + Math.sin(viji.time * 2) * 0.15;\n p5.fill(255, pulse * 255);\n p5.text('Waiting for video stream...', viji.width / 2, viji.height / 2 - fontSize);\n p5.fill(80);\n p5.text('Connect a camera or video source', viji.width / 2, viji.height / 2 + fontSize);\n return;\n }\n\n const v = videoFit(viji);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n const pad = viji.width * 0.04;\n const barY = viji.height - fontSize * 3;\n p5.fill(0, 150);\n p5.noStroke();\n p5.rect(0, barY, viji.width, fontSize * 3);\n\n p5.fill(76, 175, 80);\n p5.textAlign(p5.LEFT, p5.TOP);\n p5.text('Video connected', pad, barY + fontSize * 0.3);\n p5.fill(170);\n p5.text(\n viji.video.frameWidth + ' x ' + viji.video.frameHeight + ' @ ' + viji.video.frameRate.toFixed(0) + ' fps',\n pad, barY + fontSize * 1.5\n );\n}\n",
|
|
5820
5830
|
"sceneFile": "connection-demo.scene.js",
|
|
5821
5831
|
"capabilities": {
|
|
5822
5832
|
"video": true
|
|
@@ -5923,7 +5933,7 @@ export const docsApi = {
|
|
|
5923
5933
|
{
|
|
5924
5934
|
"type": "live-example",
|
|
5925
5935
|
"title": "Face Detection",
|
|
5926
|
-
"sceneCode": "// @renderer p5\n\nconst useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n
|
|
5936
|
+
"sceneCode": "// @renderer p5\n\nconst useFace = viji.toggle(false, { label: 'Face Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n if (!useFace.value) {\n cvHint(viji, p5, 'Face Detection');\n return;\n }\n\n viji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n\n p5.noFill();\n p5.stroke(78, 205, 196);\n p5.strokeWeight(2);\n p5.rect(bx, by, bw, bh);\n\n p5.noStroke();\n p5.fill(78, 205, 196);\n p5.circle(v.x + face.center.x * v.width, v.y + face.center.y * v.height, 8);\n\n p5.textSize(Math.min(viji.width, viji.height) * 0.025);\n p5.textAlign(p5.LEFT, p5.BOTTOM);\n p5.text('Face #' + face.id + ' (' + (face.confidence * 100).toFixed(0) + '%)', bx, by - 4);\n });\n}\n",
|
|
5927
5937
|
"sceneFile": "face-detection-demo.scene.js",
|
|
5928
5938
|
"capabilities": {
|
|
5929
5939
|
"video": true
|
|
@@ -5969,7 +5979,7 @@ export const docsApi = {
|
|
|
5969
5979
|
{
|
|
5970
5980
|
"type": "live-example",
|
|
5971
5981
|
"title": "Face Mesh",
|
|
5972
|
-
"sceneCode": "// @renderer p5\n\nconst useMesh = viji.toggle(false, { label: 'Face Mesh', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useMesh.value) viji.video.cv.enableFaceMesh(true);\n else viji.video.cv.enableFaceMesh(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n
|
|
5982
|
+
"sceneCode": "// @renderer p5\n\nconst useMesh = viji.toggle(false, { label: 'Face Mesh', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useMesh.value) viji.video.cv.enableFaceMesh(true);\n else viji.video.cv.enableFaceMesh(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n p5.tint(255, 100);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n p5.noTint();\n\n if (!useMesh.value) {\n cvHint(viji, p5, 'Face Mesh');\n return;\n }\n\n viji.video.cv.faces.forEach(face => {\n if (face.landmarks.length === 0) return;\n\n p5.noStroke();\n p5.fill(69, 183, 209, 180);\n face.landmarks.forEach(pt => {\n p5.circle(v.x + pt.x * v.width, v.y + pt.y * v.height, 2);\n });\n\n const hp = face.headPose;\n p5.fill(255);\n p5.textSize(Math.min(viji.width, viji.height) * 0.025);\n p5.textAlign(p5.LEFT, p5.BOTTOM);\n p5.text(\n face.landmarks.length + ' landmarks | Pitch: ' + hp.pitch.toFixed(1) +\n ' Yaw: ' + hp.yaw.toFixed(1) + ' Roll: ' + hp.roll.toFixed(1),\n viji.width * 0.03, viji.height - 10\n );\n });\n}\n",
|
|
5973
5983
|
"sceneFile": "face-mesh-demo.scene.js",
|
|
5974
5984
|
"capabilities": {
|
|
5975
5985
|
"video": true
|
|
@@ -6020,7 +6030,7 @@ export const docsApi = {
|
|
|
6020
6030
|
{
|
|
6021
6031
|
"type": "live-example",
|
|
6022
6032
|
"title": "Emotion Detection",
|
|
6023
|
-
"sceneCode": "// @renderer p5\n\nconst useEmotion = viji.toggle(false, { label: 'Emotion Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useEmotion.value) viji.video.cv.enableEmotionDetection(true);\n else viji.video.cv.enableEmotionDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n
|
|
6033
|
+
"sceneCode": "// @renderer p5\n\nconst useEmotion = viji.toggle(false, { label: 'Emotion Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useEmotion.value) viji.video.cv.enableEmotionDetection(true);\n else viji.video.cv.enableEmotionDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n p5.tint(255, 100);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n p5.noTint();\n\n if (!useEmotion.value) {\n cvHint(viji, p5, 'Emotion Detection');\n return;\n }\n\n const face = viji.video.cv.faces[0];\n if (!face) return;\n\n const expr = face.expressions;\n const labels = ['neutral', 'happy', 'sad', 'angry', 'surprised', 'disgusted', 'fearful'];\n const values = [expr.neutral, expr.happy, expr.sad, expr.angry, expr.surprised, expr.disgusted, expr.fearful];\n const colors = [[136,136,136], [76,175,80], [33,150,243], [244,67,54], [255,152,0], [156,39,176], [96,125,139]];\n\n const barH = viji.height * 0.04;\n const barW = viji.width * 0.3;\n const x = viji.width * 0.65;\n let y = viji.height * 0.12;\n const fontSize = barH * 0.7;\n\n p5.textSize(fontSize);\n p5.noStroke();\n\n labels.forEach((label, i) => {\n p5.fill(170);\n p5.textAlign(p5.RIGHT, p5.CENTER);\n p5.text(label, x - 8, y + barH / 2);\n\n p5.fill(34);\n p5.rect(x, y, barW, barH);\n p5.fill(colors[i][0], colors[i][1], colors[i][2]);\n p5.rect(x, y, barW * values[i], barH);\n\n p5.fill(220);\n p5.textAlign(p5.LEFT, p5.CENTER);\n p5.text((values[i] * 100).toFixed(0) + '%', x + barW + 6, y + barH / 2);\n\n y += barH * 1.8;\n });\n}\n",
|
|
6024
6034
|
"sceneFile": "emotion-detection-demo.scene.js",
|
|
6025
6035
|
"capabilities": {
|
|
6026
6036
|
"video": true
|
|
@@ -6066,7 +6076,7 @@ export const docsApi = {
|
|
|
6066
6076
|
{
|
|
6067
6077
|
"type": "live-example",
|
|
6068
6078
|
"title": "Hand Tracking",
|
|
6069
|
-
"sceneCode": "// @renderer p5\n\nconst useHands = viji.toggle(false, { label: 'Hand Tracking', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n
|
|
6079
|
+
"sceneCode": "// @renderer p5\n\nconst useHands = viji.toggle(false, { label: 'Hand Tracking', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n p5.tint(255, 100);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n p5.noTint();\n\n if (!useHands.value) {\n cvHint(viji, p5, 'Hand Tracking');\n return;\n }\n\n viji.video.cv.hands.forEach(hand => {\n const col = hand.handedness === 'left' ? [255, 159, 243] : [84, 160, 255];\n\n p5.noStroke();\n p5.fill(col[0], col[1], col[2]);\n hand.landmarks.forEach(pt => {\n p5.circle(v.x + pt.x * v.width, v.y + pt.y * v.height, 6);\n });\n\n p5.noFill();\n p5.stroke(col[0], col[1], col[2]);\n p5.strokeWeight(2);\n p5.circle(v.x + hand.palm.x * v.width, v.y + hand.palm.y * v.height, 16);\n\n const g = hand.gestures;\n const gestures = [\n ['fist', g.fist], ['open', g.openPalm], ['peace', g.peace],\n ['thumbsUp', g.thumbsUp], ['pointing', g.pointing]\n ];\n const top = gestures.reduce((a, b) => b[1] > a[1] ? b : a);\n if (top[1] > 0.5) {\n p5.noStroke();\n p5.fill(255);\n p5.textSize(Math.min(viji.width, viji.height) * 0.03);\n p5.textAlign(p5.CENTER, p5.BOTTOM);\n p5.text(top[0], v.x + hand.palm.x * v.width, v.y + hand.bounds.y * v.height - 6);\n }\n });\n}\n",
|
|
6070
6080
|
"sceneFile": "hand-tracking-demo.scene.js",
|
|
6071
6081
|
"capabilities": {
|
|
6072
6082
|
"video": true
|
|
@@ -6107,7 +6117,7 @@ export const docsApi = {
|
|
|
6107
6117
|
{
|
|
6108
6118
|
"type": "live-example",
|
|
6109
6119
|
"title": "Pose Detection",
|
|
6110
|
-
"sceneCode": "// @renderer p5\n\nconst usePose = viji.toggle(false, { label: 'Pose Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (usePose.value) viji.video.cv.enablePoseDetection(true);\n else viji.video.cv.enablePoseDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n
|
|
6120
|
+
"sceneCode": "// @renderer p5\n\nconst usePose = viji.toggle(false, { label: 'Pose Detection', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (usePose.value) viji.video.cv.enablePoseDetection(true);\n else viji.video.cv.enablePoseDetection(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n p5.tint(255, 100);\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n p5.noTint();\n\n if (!usePose.value) {\n cvHint(viji, p5, 'Pose Detection');\n return;\n }\n\n const pose = viji.video.cv.pose;\n if (!pose) return;\n\n p5.noStroke();\n p5.fill(255, 107, 107);\n pose.landmarks.forEach(pt => {\n if (pt.visibility > 0.5) {\n p5.circle(v.x + pt.x * v.width, v.y + pt.y * v.height, 8);\n }\n });\n\n p5.strokeWeight(2);\n p5.noFill();\n const drawGroup = (group, col) => {\n if (group.length < 2) return;\n p5.stroke(col[0], col[1], col[2]);\n p5.beginShape();\n group.forEach(pt => p5.vertex(v.x + pt.x * v.width, v.y + pt.y * v.height));\n p5.endShape();\n };\n\n drawGroup(pose.leftArm, [255, 159, 243]);\n drawGroup(pose.rightArm, [84, 160, 255]);\n drawGroup(pose.leftLeg, [255, 159, 243]);\n drawGroup(pose.rightLeg, [84, 160, 255]);\n drawGroup(pose.torso, [254, 202, 87]);\n\n p5.noStroke();\n p5.fill(255);\n p5.textSize(Math.min(viji.width, viji.height) * 0.025);\n p5.textAlign(p5.LEFT, p5.BOTTOM);\n p5.text('Confidence: ' + (pose.confidence * 100).toFixed(0) + '%', viji.width * 0.03, viji.height - 10);\n}\n",
|
|
6111
6121
|
"sceneFile": "pose-detection-demo.scene.js",
|
|
6112
6122
|
"capabilities": {
|
|
6113
6123
|
"video": true
|
|
@@ -6153,7 +6163,7 @@ export const docsApi = {
|
|
|
6153
6163
|
{
|
|
6154
6164
|
"type": "live-example",
|
|
6155
6165
|
"title": "Body Segmentation",
|
|
6156
|
-
"sceneCode": "// @renderer p5\n\nconst useSeg = viji.toggle(false, { label: 'Body Segmentation', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useSeg.value) viji.video.cv.enableBodySegmentation(true);\n else viji.video.cv.enableBodySegmentation(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n
|
|
6166
|
+
"sceneCode": "// @renderer p5\n\nconst useSeg = viji.toggle(false, { label: 'Body Segmentation', category: 'video' });\n\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nfunction cvHint(viji, p5, label) {\n const s = Math.min(viji.width, viji.height);\n const t = (p5.sin(viji.time * 3) + 1) / 2;\n const bandH = s * 0.18;\n const bandY = (viji.height - bandH) / 2;\n p5.noStroke();\n p5.fill(255, 200, 0, (0.85 + t * 0.15) * 255);\n p5.rect(0, bandY, viji.width, bandH);\n p5.fill(17);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(s * 0.045);\n p5.textStyle(p5.BOLD);\n p5.text(`Enable \"${label}\" in parameters`, viji.width / 2, viji.height / 2);\n p5.textStyle(p5.NORMAL);\n}\n\nfunction render(viji, p5) {\n p5.background(17);\n\n if (useSeg.value) viji.video.cv.enableBodySegmentation(true);\n else viji.video.cv.enableBodySegmentation(false);\n\n if (!viji.video.isConnected || !viji.video.currentFrame) {\n p5.fill(100);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.textSize(Math.min(viji.width, viji.height) * 0.04);\n p5.text('Waiting for video...', viji.width / 2, viji.height / 2);\n return;\n }\n\n const v = videoFit(viji, 'contain');\n const frameToShow = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\n p5.image(frameToShow, v.x, v.y, v.width, v.height);\n\n if (!useSeg.value) {\n cvHint(viji, p5, 'Body Segmentation');\n return;\n }\n\n const seg = viji.video.cv.segmentation;\n if (!seg) return;\n\n let personPixels = 0;\n for (let i = 0; i < seg.mask.length; i++) {\n if (seg.mask[i] > 0) personPixels++;\n }\n const personRatio = personPixels / seg.mask.length;\n\n if (personRatio > 0.05) {\n p5.noFill();\n p5.stroke(78, 205, 196, 150);\n p5.strokeWeight(4);\n p5.rect(v.x, v.y, v.width, v.height);\n }\n\n const fontSize = Math.min(viji.width, viji.height) * 0.03;\n p5.noStroke();\n p5.fill(0, 128);\n p5.rect(0, viji.height - fontSize * 2, viji.width, fontSize * 2);\n p5.fill(255);\n p5.textSize(fontSize);\n p5.textAlign(p5.CENTER, p5.CENTER);\n p5.text(\n 'Person: ' + (personRatio * 100).toFixed(0) + '% | Mask: ' + seg.width + 'x' + seg.height,\n viji.width / 2, viji.height - fontSize\n );\n}\n",
|
|
6157
6167
|
"sceneFile": "body-segmentation-demo.scene.js",
|
|
6158
6168
|
"capabilities": {
|
|
6159
6169
|
"video": true
|
|
@@ -8048,12 +8058,12 @@ export const docsApi = {
|
|
|
8048
8058
|
"content": [
|
|
8049
8059
|
{
|
|
8050
8060
|
"type": "text",
|
|
8051
|
-
"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:videoMix label:\"Video Mix\" default:0.0 min:0.0 max:1.0 category:video\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- `videoMix` ([`@viji-slider`](../slider/)) only appears when a video/camera source 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 all four categories: `general` (always visible), `audio` (needs audio), `video` (needs camera), and `interaction` (needs mouse). Each control reveals itself as you connect the corresponding capability:"
|
|
8061
|
+
"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:videoMix label:\"Video Mix\" default:0.0 min:0.0 max:1.0 category:video\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- `videoMix` ([`@viji-slider`](../slider/)) only appears when a video/camera source 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> [!NOTE]\n> All four parameters are creative-strength sliders/colors, not on/off toggles. The host UI already controls whether each input is wired up; a scene-level \"Audio Reactive\" or \"Use Camera\" toggle would only duplicate that. See [Best Practices: No Redundant Input Toggles](/getting-started/best-practices/#no-redundant-input-toggles).\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 all four categories: `general` (always visible), `audio` (needs audio), `video` (needs camera), and `interaction` (needs mouse). Each control reveals itself as you connect the corresponding capability:"
|
|
8052
8062
|
},
|
|
8053
8063
|
{
|
|
8054
8064
|
"type": "live-example",
|
|
8055
8065
|
"title": "Shader Parameter Categories",
|
|
8056
|
-
"sceneCode": "// @renderer shader\
|
|
8066
|
+
"sceneCode": "// @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:videoMix label:\"Video Mix\" default:0.0 min:0.0 max:1.0 step:0.01 category:video\n// @viji-slider:mouseSize label:\"Mouse Glow\" default:0.15 min:0.0 max:0.5 category:interaction\n// @viji-accumulator:phase rate:1.0\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = (2.0 * gl_FragCoord.xy - u_resolution) / u_resolution.y;\n vec2 vuv = gl_FragCoord.xy / u_resolution;\n float d = length(uv);\n\n float pulse = u_audioVolume * audioPulse;\n float wave = sin(d * 15.0 - phase * 3.0) * 0.5 + 0.5;\n vec3 col = tint * wave * (1.0 + pulse);\n\n vec2 mouseUV = (2.0 * u_mouse - u_resolution) / u_resolution.y;\n float mouseDist = length(uv - mouseUV);\n float glow = mouseSize / (mouseDist + 0.05);\n col += vec3(glow * 0.3);\n\n col *= smoothstep(1.5, 0.3, d);\n\n vec3 video = texture2D(u_video, vijiVideoUV(vuv, 1)).rgb;\n col = mix(col, video, videoMix);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8057
8067
|
"sceneFile": "categories-demo.scene.glsl",
|
|
8058
8068
|
"capabilities": {
|
|
8059
8069
|
"audio": true,
|
|
@@ -8473,7 +8483,7 @@ export const docsApi = {
|
|
|
8473
8483
|
{
|
|
8474
8484
|
"type": "live-example",
|
|
8475
8485
|
"title": "Video & CV Shader",
|
|
8476
|
-
"sceneCode": "// @renderer shader\n\
|
|
8486
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 0);\n\n vec4 videoColor = texture2D(u_video, videoUV);\n\n // u_face0Center is normalized to the source video, so compare against videoUV.\n float faceDist = length(videoUV - u_face0Center);\n float highlight = smoothstep(0.3, 0.0, faceDist) * float(u_faceCount > 0);\n\n vec3 col = mix(videoColor.rgb, vec3(0.3, 0.8, 0.8), highlight * 0.4);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8477
8487
|
"sceneFile": "video-overview.scene.js",
|
|
8478
8488
|
"capabilities": {
|
|
8479
8489
|
"video": true
|
|
@@ -8534,7 +8544,7 @@ export const docsApi = {
|
|
|
8534
8544
|
{
|
|
8535
8545
|
"type": "live-example",
|
|
8536
8546
|
"title": "Video Shader",
|
|
8537
|
-
"sceneCode": "// @renderer shader\n\
|
|
8547
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1);\n vec4 video = texture2D(u_video, videoUV);\n\n float gray = dot(video.rgb, vec3(0.299, 0.587, 0.114));\n float scanline = 0.9 + 0.1 * sin(uv.y * u_resolution.y * 3.14159);\n\n vec3 col = vec3(gray) * scanline;\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8538
8548
|
"sceneFile": "basics-demo.scene.js",
|
|
8539
8549
|
"capabilities": {
|
|
8540
8550
|
"video": true
|
|
@@ -8575,7 +8585,7 @@ export const docsApi = {
|
|
|
8575
8585
|
{
|
|
8576
8586
|
"type": "live-example",
|
|
8577
8587
|
"title": "Face Detection Shader",
|
|
8578
|
-
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n//
|
|
8588
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// u_face0Center and u_face0Bounds are normalized to the source video; compare against videoUV.\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 0);\n vec4 video = texture2D(u_video, videoUV);\n\n float faceDist = length(videoUV - u_face0Center);\n float glow = smoothstep(0.25, 0.0, faceDist) * float(u_faceCount > 0);\n\n vec2 bMin = u_face0Bounds.xy;\n vec2 bMax = u_face0Bounds.xy + u_face0Bounds.zw;\n float inBox = step(bMin.x, videoUV.x) * step(videoUV.x, bMax.x) * step(bMin.y, videoUV.y) * step(videoUV.y, bMax.y);\n float border = inBox * (1.0 - step(bMin.x + 0.003, videoUV.x) * step(videoUV.x, bMax.x - 0.003)\n * step(bMin.y + 0.003, videoUV.y) * step(videoUV.y, bMax.y - 0.003));\n border *= float(u_faceCount > 0);\n\n vec3 col = video.rgb + vec3(0.0, glow * 0.4, glow * 0.4) + vec3(0.3, 0.8, 0.8) * border;\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8579
8589
|
"sceneFile": "face-detection-demo.scene.js",
|
|
8580
8590
|
"capabilities": {
|
|
8581
8591
|
"video": true
|
|
@@ -8626,7 +8636,7 @@ export const docsApi = {
|
|
|
8626
8636
|
{
|
|
8627
8637
|
"type": "live-example",
|
|
8628
8638
|
"title": "Head Pose Shader",
|
|
8629
|
-
"sceneCode": "// @renderer shader\n\
|
|
8639
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 0);\n\n float yaw = u_face0HeadPose.y / 90.0;\n float pitch = u_face0HeadPose.x / 90.0;\n\n vec4 source = texture2D(u_video, videoUV);\n\n vec2 offset = vec2(yaw, pitch) * 0.05;\n vec4 shifted = texture2D(u_video, videoUV + offset);\n\n float hasFace = float(u_faceCount > 0);\n vec3 col = mix(source.rgb, mix(source.rgb, shifted.rgb, 0.5), hasFace);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8630
8640
|
"sceneFile": "face-mesh-demo.scene.js",
|
|
8631
8641
|
"capabilities": {
|
|
8632
8642
|
"video": true
|
|
@@ -8672,7 +8682,7 @@ export const docsApi = {
|
|
|
8672
8682
|
{
|
|
8673
8683
|
"type": "live-example",
|
|
8674
8684
|
"title": "Emotion-Reactive Shader",
|
|
8675
|
-
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\
|
|
8685
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1);\n vec4 video = texture2D(u_video, videoUV);\n\n float happy = u_face0Happy;\n float sad = u_face0Sad;\n float surprised = u_face0Surprised;\n float angry = u_face0Angry;\n\n vec3 warmShift = vec3(happy * 0.3, happy * 0.15, 0.0);\n vec3 coolShift = vec3(0.0, 0.0, sad * 0.3);\n vec3 alertShift = vec3(surprised * 0.2, surprised * 0.2, 0.0);\n vec3 redShift = vec3(angry * 0.3, 0.0, 0.0);\n\n vec3 col = video.rgb + warmShift + coolShift + alertShift + redShift;\n gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}\n",
|
|
8676
8686
|
"sceneFile": "emotion-detection-demo.scene.js",
|
|
8677
8687
|
"capabilities": {
|
|
8678
8688
|
"video": true
|
|
@@ -8728,7 +8738,7 @@ export const docsApi = {
|
|
|
8728
8738
|
{
|
|
8729
8739
|
"type": "live-example",
|
|
8730
8740
|
"title": "Hand Tracking Shader",
|
|
8731
|
-
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\
|
|
8741
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 0);\n vec4 video = texture2D(u_video, videoUV);\n\n float leftDist = length(videoUV - u_leftHandPalm.xy);\n float rightDist = length(videoUV - u_rightHandPalm.xy);\n\n float leftGlow = smoothstep(0.15, 0.0, leftDist) * u_leftHandConfidence;\n float rightGlow = smoothstep(0.15, 0.0, rightDist) * u_rightHandConfidence;\n\n vec3 col = video.rgb;\n col += vec3(1.0, 0.6, 1.0) * leftGlow;\n col += vec3(0.3, 0.6, 1.0) * rightGlow;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8732
8742
|
"sceneFile": "hand-tracking-demo.scene.js",
|
|
8733
8743
|
"capabilities": {
|
|
8734
8744
|
"video": true
|
|
@@ -8779,7 +8789,7 @@ export const docsApi = {
|
|
|
8779
8789
|
{
|
|
8780
8790
|
"type": "live-example",
|
|
8781
8791
|
"title": "Pose Detection Shader",
|
|
8782
|
-
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\
|
|
8792
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 0);\n vec4 video = texture2D(u_video, videoUV);\n\n float active = u_poseDetected ? 1.0 : 0.0;\n\n float noseDist = length(videoUV - u_nosePosition);\n float lWrist = length(videoUV - u_leftWristPosition);\n float rWrist = length(videoUV - u_rightWristPosition);\n float lShoulder = length(videoUV - u_leftShoulderPosition);\n float rShoulder = length(videoUV - u_rightShoulderPosition);\n\n float glow = smoothstep(0.04, 0.0, noseDist)\n + smoothstep(0.04, 0.0, lWrist)\n + smoothstep(0.04, 0.0, rWrist)\n + smoothstep(0.03, 0.0, lShoulder)\n + smoothstep(0.03, 0.0, rShoulder);\n\n vec3 col = video.rgb + vec3(1.0, 0.4, 0.4) * glow * active * 0.6;\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8783
8793
|
"sceneFile": "pose-detection-demo.scene.js",
|
|
8784
8794
|
"capabilities": {
|
|
8785
8795
|
"video": true
|
|
@@ -8835,7 +8845,7 @@ export const docsApi = {
|
|
|
8835
8845
|
{
|
|
8836
8846
|
"type": "live-example",
|
|
8837
8847
|
"title": "Body Segmentation Shader",
|
|
8838
|
-
"sceneCode": "// @renderer shader\n\
|
|
8848
|
+
"sceneCode": "// @renderer shader\n\nvec2 vijiVideoUV(vec2 uv, int mode) {\n vec2 canvas = u_resolution;\n vec2 video = u_videoResolution;\n if (video.x == 0.0 || video.y == 0.0) return uv;\n float canvasAspect = canvas.x / canvas.y;\n float videoAspect = video.x / video.y;\n vec2 scale = vec2(1.0);\n if (mode == 1) {\n scale = canvasAspect > videoAspect\n ? vec2(1.0, videoAspect / canvasAspect)\n : vec2(canvasAspect / videoAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, videoAspect / canvasAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// u_segmentationMask is normalized to the source video, so sample it with videoUV.\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 0);\n\n vec4 video = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, videoUV)\n : texture2D(u_video, videoUV);\n\n float mask = texture2D(u_segmentationMask, videoUV).r;\n\n vec3 bgColor = vec3(0.05, 0.05, 0.15) + 0.05 * sin(uv.x * 8.0 + u_time * 2.0);\n vec3 col = mix(bgColor, video.rgb, mask);\n\n vec2 vpx = 1.0 / u_videoResolution;\n float maskL = texture2D(u_segmentationMask, videoUV + vec2(-vpx.x, 0.0)).r;\n float maskR = texture2D(u_segmentationMask, videoUV + vec2(vpx.x, 0.0)).r;\n float maskU = texture2D(u_segmentationMask, videoUV + vec2(0.0, -vpx.y)).r;\n float maskD = texture2D(u_segmentationMask, videoUV + vec2(0.0, vpx.y)).r;\n float edge = abs(maskL - maskR) + abs(maskU - maskD);\n\n col += vec3(0.3, 0.8, 0.8) * edge * 2.0;\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
|
|
8839
8849
|
"sceneFile": "body-segmentation-demo.scene.js",
|
|
8840
8850
|
"capabilities": {
|
|
8841
8851
|
"video": true
|