@viji-dev/core 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docs-api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const docsApi = {
2
2
  "version": "1.1.0",
3
- "coreVersion": "0.6.0",
4
- "generatedAt": "2026-05-11T07:11:45.518Z",
3
+ "coreVersion": "0.6.1",
4
+ "generatedAt": "2026-05-11T08:18:53.423Z",
5
5
  "navigation": [
6
6
  {
7
7
  "id": "getting-started",
@@ -1381,7 +1381,7 @@ export const docsApi = {
1381
1381
  "content": [
1382
1382
  {
1383
1383
  "type": "text",
1384
- "markdown": "# Prompt: Shader Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the shader effect you want. The prompt gives the AI everything it needs about Viji's shader renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji GLSL shader scene: a fragment shader that runs on a fullscreen quad inside a Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working GLSL code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D pattern or raymarched 3D?\", \"Hard geometric or soft organic feel?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable shader that follows every rule in this prompt. Include `@viji-*` parameter directives for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the shader works, which uniforms and parameters it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"more chaotic\", \"warmer palette\", \"make the kick punch harder\"). Treat each follow-up as a refinement: keep the working shader as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV uniforms, behavior nuances, full shader examples).\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt.\n- Never invent uniform names or directive names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Viji renders a **fullscreen quad**. Your shader defines the color of every pixel.\n- Viji **auto-injects** `precision mediump float;` and ALL uniform declarations: both built-in uniforms and parameter uniforms from `@viji-*` directives.\n- You write only helper functions and `void main() { ... }`.\n- **GLSL ES 1.00** by default. Add `#version 300 es` as the very first line for ES 3.00.\n- ES 3.00 requires `out vec4 fragColor;` (before `main`) and `fragColor = ...` instead of `gl_FragColor`.\n- ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader uses `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the first line (or after `#version 300 es` if using ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. NEVER use the `u_` prefix for your own parameter names: it is reserved for built-in uniforms. Name parameters descriptively: `speed`, `colorMix`, `intensity`.\n6. `@viji-*` parameter directives ONLY work with `//` comments. NEVER use `/* */` for directives.\n7. ALWAYS use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation: this prevents jumps when sliders change:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase); // smooth, no jumps\n ```\n The same applies to **nested** multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators:\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame), just reference it in code: Viji auto-detects and enables it.\n9. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n10. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls. This lets the host UI hide irrelevant controls when that input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level \"Audio Reactive\" / \"Show Video\" toggle just duplicates the host switch.\n ```glsl\n // Right: creative-strength sliders with the matching category.\n // @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n // @viji-slider:videoMix label:\"Video Mix\" default:0.6 min:0 max:1 group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 min:0 max:1 group:interaction category:interaction\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true category:audio\n // // @viji-toggle:showVideo label:\"Show Video\" default:true category:video\n ```\n\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Computer vision uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero unless the matching feature is activated. Use bare form for always-on or toggleable form to expose a host-side toggle.\n ```glsl\n // Bare: pipeline runs whenever a video stream is connected. No UI parameter.\n // @viji-cv:faceDetection\n // @viji-cv:handTracking\n\n // Toggleable: synthesizes a host-side toggle parameter and a `bool <featureToken>`\n // shader uniform. Slot becomes both the parameter key and the GLSL uniform name.\n // @viji-cv:faceDetection label:\"Face Detection\" default:false group:cv category:video\n ```\n The six valid tokens (JS-API parity): `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are always available: do NOT declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy 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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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.) share the same bottom-up coordinate\n // convention as videoUV — compare them against videoUV directly, no flip:\n float faceDist = length(videoUV - u_face0Center);\n // ...\n}\n```\n\nDefault to mode `1` (cover) for live camera shaders. Use mode `0` (contain) for CV-overlay shaders where features near frame edges must stay visible. Sampling `texture2D(u_video, uv)` directly is allowed only when distortion is intentional.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when the shader reads pixels from the displayed frame at CV-derived positions (compositing `u_segmentationMask` onto the body, sampling skin under face landmarks, warping the face along its mesh, texture-mapped face filters). For shaders that consume CV uniforms without sampling at CV positions (head-pose-driven color shifts, generative geometry from `u_face*` / `u_hand*` / `u_pose*`), keep `u_video`: `u_videoAnalysed` only advances when MediaPipe completes an inference, so the displayed video stutters or holds between inferences.\n\nWhen `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` to fall back during the brief startup window before the first CV result lands:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration with gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | Orientation (alpha, beta, gamma) degrees |\n| `u_deviceOrientationAbsolute` | `bool` | True if using magnetometer |\n\n### External Devices\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceCount` | `int` | Number of device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | Number of external devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Device camera resolutions |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Device connection status |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration w/ gravity |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n**Note:** device audio (when an external device provides an audio source) is exposed as `u_audioStream{i}*` scalar uniforms: same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0-7). There are NO per-device or per-stream FFT/waveform textures; only the main audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0-1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0-1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0-1 |\n\n(`i` = 0…7.) **Lightweight scalars only**: **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0-1)\n// `default:` accepts: #rrggbb, #rgb, vec3(r,g,b) in 0..1, rgb(r,g,b) in 0..255,\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:invert label:\"Invert\" default:false\n// → uniform bool invert;\n\n// @viji-select:mode label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index)\n\n// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// → uniform float count;\n\n// @viji-image:tex label:\"Texture\"\n// → uniform sampler2D tex;\n\n// @viji-button:reset label:\"Reset\"\n// → uniform bool reset; (true for one frame on press)\n\n// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]\n// → uniform vec2 origin; (default uses [x,y] array; both components -1 to 1)\n\n// @viji-accumulator:phase rate:speed\n// → uniform float phase; (CPU-side: += speed × deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow help the artist build a Viji shader scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the shader:\n- Follow every rule in this prompt.\n- Use `// @renderer shader` as the first line. Do NOT declare precision or uniforms. Use `@viji-accumulator` for parameter-driven animation. Use `@viji-slider/color/toggle` for artist controls.\n- Output the GLSL code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the shader works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the shader effect you want.\n4. The AI will return a complete Viji shader scene.\n\n> [!TIP]\n> For better results, describe the visual effect you want (patterns, colors, motion), mention data sources (audio, video, mouse), and what controls the user should have. If you have existing Shadertoy shaders to convert, use the [Convert: Shadertoy](/ai-prompts/convert-shadertoy) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: Shadertoy](/ai-prompts/convert-shadertoy): convert existing Shadertoy shaders to Viji\n- [Shader Quick Start](/shader/quickstart): your first Viji shader\n- [Shader API Reference](/shader/api-reference): full uniform reference\n- [Backbuffer & Feedback](/shader/backbuffer): previous-frame feedback effects\n- [Shadertoy Compatibility](/shader/shadertoy): compatibility layer for Shadertoy code"
1384
+ "markdown": "# Prompt: Shader Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the shader effect you want. The prompt gives the AI everything it needs about Viji's shader renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji GLSL shader scene: a fragment shader that runs on a fullscreen quad inside a Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working GLSL code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D pattern or raymarched 3D?\", \"Hard geometric or soft organic feel?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable shader that follows every rule in this prompt. Include `@viji-*` parameter directives for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the shader works, which uniforms and parameters it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"more chaotic\", \"warmer palette\", \"make the kick punch harder\"). Treat each follow-up as a refinement: keep the working shader as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact uniform names, types, and availability.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV uniforms, behavior nuances, full shader examples).\n\n**If you do NOT have web/file access:**\n- Use only the uniforms and directives explicitly named in this prompt.\n- Never invent uniform names or directive names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- Shader uniforms reference (every auto-injected uniform with type and description): https://unpkg.com/@viji-dev/core/dist/shader-uniforms.js\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Viji renders a **fullscreen quad**. Your shader defines the color of every pixel.\n- Viji **auto-injects** `precision mediump float;` and ALL uniform declarations: both built-in uniforms and parameter uniforms from `@viji-*` directives.\n- You write only helper functions and `void main() { ... }`.\n- **GLSL ES 1.00** by default. Add `#version 300 es` as the very first line for ES 3.00.\n- ES 3.00 requires `out vec4 fragColor;` (before `main`) and `fragColor = ...` instead of `gl_FragColor`.\n- ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader uses `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n\n## RULES\n\n1. ALWAYS add `// @renderer shader` as the first line (or after `#version 300 es` if using ES 3.00).\n2. NEVER declare `precision mediump float;` or `precision highp float;`: Viji auto-injects precision.\n3. NEVER redeclare built-in uniforms (`u_time`, `u_resolution`, `u_mouse`, etc.): they are auto-injected.\n4. NEVER redeclare parameter uniforms: they are auto-generated from `@viji-*` directives.\n5. NEVER use the `u_` prefix for your own parameter names: it is reserved for built-in uniforms. Name parameters descriptively: `speed`, `colorMix`, `intensity`.\n6. `@viji-*` parameter directives ONLY work with `//` comments. NEVER use `/* */` for directives.\n7. ALWAYS use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation: this prevents jumps when sliders change:\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase); // smooth, no jumps\n ```\n The same applies to **nested** multiplications: never multiply an accumulator by another parameter inside the shader. If you need two independent speeds, declare two accumulators:\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame), just reference it in code: Viji auto-detects and enables it.\n9. Remove any `#ifdef GL_ES` / `precision` blocks: Viji handles this.\n10. ALWAYS set `category:` on input-dependent `@viji-*` directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse/touch controls. This lets the host UI hide irrelevant controls when that input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level \"Audio Reactive\" / \"Show Video\" toggle just duplicates the host switch.\n ```glsl\n // Right: creative-strength sliders with the matching category.\n // @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n // @viji-slider:videoMix label:\"Video Mix\" default:0.6 min:0 max:1 group:video category:video\n // @viji-slider:mouseInfluence label:\"Mouse Influence\" default:0.3 min:0 max:1 group:interaction category:interaction\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // // @viji-toggle:audioReactive label:\"Audio Reactive\" default:true category:audio\n // // @viji-toggle:showVideo label:\"Show Video\" default:true category:video\n ```\n\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Computer vision uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero unless the matching feature is activated. Use bare form for always-on or toggleable form to expose a host-side toggle.\n ```glsl\n // Bare: pipeline runs whenever a video stream is connected. No UI parameter.\n // @viji-cv:faceDetection\n // @viji-cv:handTracking\n\n // Toggleable: synthesizes a host-side toggle parameter and a `bool <featureToken>`\n // shader uniform. Slot becomes both the parameter key and the GLSL uniform name.\n // @viji-cv:faceDetection label:\"Face Detection\" default:false group:cv category:video\n ```\n The six valid tokens (JS-API parity): `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## COMPLETE UNIFORM REFERENCE\n\nAll uniforms below are always available: do NOT declare them.\n\n### Core\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_resolution` | `vec2` | Canvas width and height in pixels |\n| `u_time` | `float` | Elapsed seconds since scene start |\n| `u_deltaTime` | `float` | Seconds since last frame |\n| `u_frame` | `int` | Current frame number |\n| `u_fps` | `float` | Target frame rate (based on host frame-rate mode) |\n\n### Mouse\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_mouse` | `vec2` | Mouse position in pixels (WebGL coords: bottom-left origin) |\n| `u_mouseInCanvas` | `bool` | True if mouse is inside canvas |\n| `u_mousePressed` | `bool` | True if any mouse button is pressed |\n| `u_mouseLeft` | `bool` | True if left button is pressed |\n| `u_mouseRight` | `bool` | True if right button is pressed |\n| `u_mouseMiddle` | `bool` | True if middle button is pressed |\n| `u_mouseDelta` | `vec2` | Mouse movement delta per frame |\n| `u_mouseWheel` | `float` | Mouse wheel scroll delta |\n| `u_mouseWasPressed` | `bool` | True on the frame a button was pressed |\n| `u_mouseWasReleased` | `bool` | True on the frame a button was released |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_keySpace` | `bool` | Spacebar |\n| `u_keyShift` | `bool` | Shift key |\n| `u_keyCtrl` | `bool` | Ctrl/Cmd key |\n| `u_keyAlt` | `bool` | Alt/Option key |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD keys |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrow keys |\n| `u_keyboard` | `sampler2D` | Full keyboard state texture (256×3, LUMINANCE). Row 0: held, Row 1: pressed this frame, Row 2: toggle. Access: `texelFetch(u_keyboard, ivec2(keyCode, row), 0).r` |\n\n### Touch\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_touchCount` | `int` | Number of active touches (0-5) |\n| `u_touch0` - `u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Pointer (unified mouse/touch)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_pointer` | `vec2` | Primary input position in pixels (WebGL coords) |\n| `u_pointerDelta` | `vec2` | Primary input movement delta |\n| `u_pointerDown` | `bool` | True if primary input is active |\n| `u_pointerWasPressed` | `bool` | True on frame input became active |\n| `u_pointerWasReleased` | `bool` | True on frame input was released |\n| `u_pointerInCanvas` | `bool` | True if inside canvas |\n\n### Audio: Scalars\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioVolume` | `float` | RMS volume 0-1 |\n| `u_audioPeak` | `float` | Peak amplitude 0-1 |\n| `u_audioVolumeSmoothed` | `float` | Smoothed volume (200ms decay) |\n| `u_audioLow` | `float` | Low band 20-120 Hz |\n| `u_audioLowMid` | `float` | Low-mid 120-400 Hz |\n| `u_audioMid` | `float` | Mid 400-1600 Hz |\n| `u_audioHighMid` | `float` | High-mid 1600-6000 Hz |\n| `u_audioHigh` | `float` | High 6000-16000 Hz |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | Smoothed band variants |\n| `u_audioKick` | `float` | Kick energy 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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// Pair with vijiVideoUV: true when videoUV is inside the fitted region.\n// Use to paint a flat bar color in contain mode's letterbox area instead of\n// sampling u_video at out-of-range UVs (which returns the CLAMP_TO_EDGE\n// edge column stretched).\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // 1 = cover (default), 0 = contain\n vec4 video = texture2D(u_video, videoUV);\n // Bar color vec3(17.0/255.0) matches the native/p5 #111 canvas background.\n vec3 col = vijiInVideo(videoUV) ? video.rgb : vec3(17.0 / 255.0);\n // CV uniforms (u_face0Center, etc.) share the same bottom-up coordinate\n // convention as videoUV — compare them against videoUV directly, no flip:\n float faceDist = length(videoUV - u_face0Center);\n // ...\n}\n```\n\nDefault to mode `1` (cover) for live camera shaders. Use mode `0` (contain) for CV-overlay shaders where features near frame edges must stay visible. Sampling `texture2D(u_video, uv)` directly is allowed only when distortion is intentional.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when the shader reads pixels from the displayed frame at CV-derived positions (compositing `u_segmentationMask` onto the body, sampling skin under face landmarks, warping the face along its mesh, texture-mapped face filters). For shaders that consume CV uniforms without sampling at CV positions (head-pose-driven color shifts, generative geometry from `u_face*` / `u_hand*` / `u_pose*`), keep `u_video`: `u_videoAnalysed` only advances when MediaPipe completes an inference, so the displayed video stutters or holds between inferences.\n\nWhen `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` to fall back during the brief startup window before the first CV result lands:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\n### CV: Face Detection\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_faceCount` | `int` | Number of detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | Bounding box (x, y, width, height) normalized 0-1 |\n| `u_face0Center` | `vec2` | Face center (x, y) normalized 0-1 |\n| `u_face0HeadPose` | `vec3` | Head rotation (pitch, yaw, roll) in degrees |\n| `u_face0Confidence` | `float` | Detection confidence 0-1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores (neutral, happy, sad, angry, surprised, disgusted, fearful) |\n\n**52 Blendshape uniforms** (all `float`, 0-1, ARKit names prefixed with `u_face0`):\n`u_face0BrowDownLeft`, `u_face0BrowDownRight`, `u_face0BrowInnerUp`, `u_face0BrowOuterUpLeft`, `u_face0BrowOuterUpRight`, `u_face0CheekPuff`, `u_face0CheekSquintLeft`, `u_face0CheekSquintRight`, `u_face0EyeBlinkLeft`, `u_face0EyeBlinkRight`, `u_face0EyeLookDownLeft`, `u_face0EyeLookDownRight`, `u_face0EyeLookInLeft`, `u_face0EyeLookInRight`, `u_face0EyeLookOutLeft`, `u_face0EyeLookOutRight`, `u_face0EyeLookUpLeft`, `u_face0EyeLookUpRight`, `u_face0EyeSquintLeft`, `u_face0EyeSquintRight`, `u_face0EyeWideLeft`, `u_face0EyeWideRight`, `u_face0JawForward`, `u_face0JawLeft`, `u_face0JawOpen`, `u_face0JawRight`, `u_face0MouthClose`, `u_face0MouthDimpleLeft`, `u_face0MouthDimpleRight`, `u_face0MouthFrownLeft`, `u_face0MouthFrownRight`, `u_face0MouthFunnel`, `u_face0MouthLeft`, `u_face0MouthLowerDownLeft`, `u_face0MouthLowerDownRight`, `u_face0MouthPressLeft`, `u_face0MouthPressRight`, `u_face0MouthPucker`, `u_face0MouthRight`, `u_face0MouthRollLower`, `u_face0MouthRollUpper`, `u_face0MouthShrugLower`, `u_face0MouthShrugUpper`, `u_face0MouthSmileLeft`, `u_face0MouthSmileRight`, `u_face0MouthStretchLeft`, `u_face0MouthStretchRight`, `u_face0MouthUpperUpLeft`, `u_face0MouthUpperUpRight`, `u_face0NoseSneerLeft`, `u_face0NoseSneerRight`, `u_face0TongueOut`.\n\n### CV: Hands\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_handCount` | `int` | Number of detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position (x, y, z) |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | Detection confidence 0-1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounding box normalized 0-1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores (fist, open, peace, thumbsUp, thumbsDown, pointing, iLoveYou) |\n| `u_rightHandFist` - `u_rightHandILoveYou` | `float` | 7 right-hand gesture scores |\n\n### CV: Pose\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_poseDetected` | `bool` | True if a pose is detected |\n| `u_poseConfidence` | `float` | Detection confidence 0-1 |\n| `u_nosePosition` | `vec2` | Nose landmark (normalized 0-1) |\n| `u_leftShoulderPosition`, `u_rightShoulderPosition` | `vec2` | Shoulder positions |\n| `u_leftElbowPosition`, `u_rightElbowPosition` | `vec2` | Elbow positions |\n| `u_leftWristPosition`, `u_rightWristPosition` | `vec2` | Wrist positions |\n| `u_leftHipPosition`, `u_rightHipPosition` | `vec2` | Hip positions |\n| `u_leftKneePosition`, `u_rightKneePosition` | `vec2` | Knee positions |\n| `u_leftAnklePosition`, `u_rightAnklePosition` | `vec2` | Ankle positions |\n\n### CV: Body Segmentation\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_segmentationMask` | `sampler2D` | Segmentation mask (0=background, 1=person) |\n| `u_segmentationRes` | `vec2` | Mask resolution in pixels |\n\n### Device Sensors\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceAcceleration` | `vec3` | Acceleration without gravity (m/s²) |\n| `u_deviceAccelerationGravity` | `vec3` | Acceleration with gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | Orientation (alpha, beta, gamma) degrees |\n| `u_deviceOrientationAbsolute` | `bool` | True if using magnetometer |\n\n### External Devices\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_deviceCount` | `int` | Number of device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | Number of external devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Device camera resolutions |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Device connection status |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration w/ gravity |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n**Note:** device audio (when an external device provides an audio source) is exposed as `u_audioStream{i}*` scalar uniforms: same per-slot names as compositor audio streams (`Connected`, `Volume`, band energies, `Brightness`, `Flatness` for `i` = 0-7). There are NO per-device or per-stream FFT/waveform textures; only the main audio source gets `u_audioFFT` and `u_audioWaveform`.\n\n### Streams (Compositor)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_videoStreamCount` | `int` | Number of active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Stream resolutions |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Stream connection status |\n\nStreams are host-provided video sources used internally by the compositor.\n\n#### Audio streams (additional sources)\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_audioStreamCount` | `int` | Number of active additional audio streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Whether that slot is actively providing audio |\n| `u_audioStream{i}Volume` | `float` | RMS-style volume 0-1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0-1 (`Low`, `LowMid`, `Mid`, `HighMid`, `High`) |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0-1 |\n\n(`i` = 0…7.) **Lightweight scalars only**: **no** `u_audioFFT` / `u_audioWaveform` per stream. Beat/BPM/trigger uniforms remain **main audio only** (`u_audioKick`, `u_audioBPM`, etc.).\n\n### Backbuffer\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `backbuffer` | `sampler2D` | Previous frame (auto-enabled when referenced) |\n\nNo `u_` prefix. RGBA 8-bit, LINEAR filtering, CLAMP_TO_EDGE wrapping. First frame samples as black. Content clears on canvas resize.\nSample: `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n\n## PARAMETER DIRECTIVES\n\nDeclare with `// @viji-TYPE:uniformName key:value ...` syntax. They become uniforms automatically.\n\n```glsl\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0 step:0.1\n// → uniform float speed;\n\n// @viji-color:tint label:\"Tint\" default:#ff6600\n// → uniform vec3 tint; (RGB 0-1)\n// `default:` accepts: #rrggbb, #rgb, vec3(r,g,b) in 0..1, rgb(r,g,b) in 0..255,\n// hsl(h, s%, l%) (h: 0..360), hsb(h, s, b) (h: 0..360, s/b: 0..100)\n\n// @viji-toggle:invert label:\"Invert\" default:false\n// → uniform bool invert;\n\n// @viji-select:mode label:\"Mode\" default:0 options:[\"Solid\",\"Gradient\",\"Noise\"]\n// → uniform int mode; (0-based index)\n\n// @viji-number:count label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// → uniform float count;\n\n// @viji-image:tex label:\"Texture\"\n// → uniform sampler2D tex;\n\n// @viji-button:reset label:\"Reset\"\n// → uniform bool reset; (true for one frame on press)\n\n// @viji-coordinate:origin label:\"Origin\" default:[0.0,0.0]\n// → uniform vec2 origin; (default uses [x,y] array; both components -1 to 1)\n\n// @viji-accumulator:phase rate:speed\n// → uniform float phase; (CPU-side: += speed × deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## TEMPLATE\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-color:baseColor label:\"Color\" default:#ff6600\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n float pulse = 1.0 + u_audioLow * 0.5;\n vec3 color = baseColor * wave * pulse;\n\n gl_FragColor = vec4(color, 1.0);\n}\n```\n\nNow help the artist build a Viji shader scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the shader:\n- Follow every rule in this prompt.\n- Use `// @renderer shader` as the first line. Do NOT declare precision or uniforms. Use `@viji-accumulator` for parameter-driven animation. Use `@viji-slider/color/toggle` for artist controls.\n- Output the GLSL code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the shader works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the shader effect you want.\n4. The AI will return a complete Viji shader scene.\n\n> [!TIP]\n> For better results, describe the visual effect you want (patterns, colors, motion), mention data sources (audio, video, mouse), and what controls the user should have. If you have existing Shadertoy shaders to convert, use the [Convert: Shadertoy](/ai-prompts/convert-shadertoy) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: Shadertoy](/ai-prompts/convert-shadertoy): convert existing Shadertoy shaders to Viji\n- [Shader Quick Start](/shader/quickstart): your first Viji shader\n- [Shader API Reference](/shader/api-reference): full uniform reference\n- [Backbuffer & Feedback](/shader/backbuffer): previous-frame feedback effects\n- [Shadertoy Compatibility](/shader/shadertoy): compatibility layer for Shadertoy code"
1385
1385
  }
1386
1386
  ]
1387
1387
  },
@@ -8063,7 +8063,7 @@ export const docsApi = {
8063
8063
  {
8064
8064
  "type": "live-example",
8065
8065
  "title": "Shader Parameter Categories",
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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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",
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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\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 vec2 videoUV = vijiVideoUV(vuv, 1);\n vec3 video = vijiInVideo(videoUV) ? texture2D(u_video, videoUV).rgb : vec3(17.0 / 255.0);\n col = mix(col, video, videoMix);\n\n gl_FragColor = vec4(col, 1.0);\n}\n",
8067
8067
  "sceneFile": "categories-demo.scene.glsl",
8068
8068
  "capabilities": {
8069
8069
  "audio": true,
@@ -8483,7 +8483,7 @@ export const docsApi = {
8483
8483
  {
8484
8484
  "type": "live-example",
8485
8485
  "title": "Video & CV Shader",
8486
- "sceneCode": "// @renderer shader\n// @viji-cv:faceDetection\n// @viji-cv:handTracking\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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",
8486
+ "sceneCode": "// @renderer shader\n// @viji-cv:faceDetection\n// @viji-cv:handTracking\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8487
8487
  "sceneFile": "video-overview.scene.js",
8488
8488
  "capabilities": {
8489
8489
  "video": true
@@ -8539,12 +8539,12 @@ export const docsApi = {
8539
8539
  "content": [
8540
8540
  {
8541
8541
  "type": "text",
8542
- "markdown": "# Video Basics\n\nThree uniforms provide access to the video stream in shaders: the video frame as a texture, its resolution, and the frame rate.\n\n## Uniform Reference\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Just-arrived video frame as a texture |\n| `u_videoResolution` | `vec2` | Video frame width and height in pixels (shared with `u_videoAnalysed`) |\n| `u_videoFrameRate` | `float` | Video frame rate in frames per second |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with the current CV uniforms (pixel-precise pairing) |\n| `u_videoAnalysedAvailable` | `bool` | True after the first CV result lands |\n\n### Sampling the Video Texture\n\nUse `texture2D(u_video, uv)` to sample the video frame. The texture coordinates are 0-1, matching the standard UV space:\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution;\nvec4 videoColor = texture2D(u_video, uv);\n```\n\nWhen no video is connected, `u_video` samples as black (`vec4(0.0)`), `u_videoResolution` is `vec2(0.0)`, and `u_videoFrameRate` is `0.0`.\n\n### `u_video` vs `u_videoAnalysed`\n\nCV results are computed asynchronously, so the displayed video and the CV uniforms are never instantaneously in sync. You always have two textures to choose between:\n\n- **`u_video`** is the just-arrived video frame. It refreshes every shader frame.\n- **`u_videoAnalysed`** is the exact frame MediaPipe ran on to produce the current `u_face*` / `u_hand*` / `u_pose*` / `u_segmentationMask` uniforms. It refreshes only when an inference completes. Both textures share `u_videoResolution`.\n\nPick by asking: *does my shader read pixels from the displayed frame at CV-derived positions?*\n\n- **No** (drawing on top of the camera, audio-reactive visuals driven by `u_face*` / `u_hand*` positions, generative effects that don't display the camera): sample `u_video`. Spatial drift between CV positions and displayed pixels under fast motion is the cost; smooth live-camera output is the benefit.\n- **Yes** (sampling skin pixels inside a face-mesh outline, compositing `u_segmentationMask` onto the body, warping the face along its mesh): sample `u_videoAnalysed` so the CV uniforms align with the pixels. The displayed image stutters or holds briefly between inferences, in exchange for pixel-for-pixel alignment.\n\nWhen you need `u_videoAnalysed`, gate on `u_videoAnalysedAvailable` so the startup window falls back cleanly:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\nBefore the first CV result lands, `u_videoAnalysed` samples a 1×1 black fallback (safe but typically not what you want as the visible source; use the gate above). Only the main video stream produces analysed frames; this uniform never updates from compositor or device streams.\n\n## Drawing Video\n\nCamera frames almost never match the canvas aspect ratio. Sampling `texture2D(u_video, uv)` with the canvas's normalized UV stretches the video: faces get squashed, circles become ovals, and CV uniforms (`u_face*`, `u_hand*`, `u_pose*`) drift off the actual feature positions. Use the `vijiVideoUV` helper below in every video / CV shader.\n\n```glsl\n// @renderer shader\n\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 // cover: scale > 1 on the cropped axis so the texture range narrows to a\n // centered sub-rectangle that fully covers the canvas.\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n // contain: scale < 1 on the letterboxed axis so the texture range\n // overshoots [0, 1] toward the edges, producing the empty bars.\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // cover\n\n vec4 video = texture2D(u_video, videoUV);\n gl_FragColor = video;\n}\n```\n\nThe second arg is `1` for cover (default for live camera feeds) or `0` for contain. See [Best Practices](/getting-started/best-practices/#cover-vs-contain) for when each is the right pick.\n\n### Aligning CV Uniforms with the Fitted Video\n\nCV uniforms (`u_face0Center`, `u_face0Bounds`, hand / pose landmark uniforms) are normalized to the source video frame, not the canvas, and use the same bottom-up coordinate convention as `videoUV` (the engine pre-flips them at the boundary so they line up with the texture sampling surface). Compare them against the same `videoUV` you sampled the video with — no manual Y-flip needed:\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1);\n\n vec4 video = texture2D(u_video, videoUV);\n\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(video.rgb, vec3(0.3, 0.8, 0.8), highlight * 0.4);\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nIn `cover` mode, `videoUV` stays inside `[0, 1]` — the texture range is narrowed to a centered sub-rectangle so the canvas is fully filled. In `contain` mode, `videoUV` may fall outside `[0, 1]` in the letterbox regions, where the texture sampler returns the clamped edge color (or transparent black, depending on wrap mode).\n\n### Stretching the Frame (Intentional Distortion Only)\n\nIf you actually want to stretch the video as an artistic effect, sample `texture2D(u_video, uv)` directly with the canvas UV. Do this only when the distortion is the intent.\n\n```glsl\nvec4 video = texture2D(u_video, uv);\n```\n\n> [!NOTE]\n> The video texture is updated every frame when a video stream is connected. When disconnected, the texture samples as black. Your shader will still compile and run: the uniforms simply hold their default values."
8542
+ "markdown": "# Video Basics\n\nThree uniforms provide access to the video stream in shaders: the video frame as a texture, its resolution, and the frame rate.\n\n## Uniform Reference\n\n| Uniform | Type | Description |\n|---------|------|-------------|\n| `u_video` | `sampler2D` | Just-arrived video frame as a texture |\n| `u_videoResolution` | `vec2` | Video frame width and height in pixels (shared with `u_videoAnalysed`) |\n| `u_videoFrameRate` | `float` | Video frame rate in frames per second |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with the current CV uniforms (pixel-precise pairing) |\n| `u_videoAnalysedAvailable` | `bool` | True after the first CV result lands |\n\n### Sampling the Video Texture\n\nUse `texture2D(u_video, uv)` to sample the video frame. The texture coordinates are 0-1, matching the standard UV space:\n\n```glsl\nvec2 uv = gl_FragCoord.xy / u_resolution;\nvec4 videoColor = texture2D(u_video, uv);\n```\n\nWhen no video is connected, `u_video` samples as black (`vec4(0.0)`), `u_videoResolution` is `vec2(0.0)`, and `u_videoFrameRate` is `0.0`.\n\n### `u_video` vs `u_videoAnalysed`\n\nCV results are computed asynchronously, so the displayed video and the CV uniforms are never instantaneously in sync. You always have two textures to choose between:\n\n- **`u_video`** is the just-arrived video frame. It refreshes every shader frame.\n- **`u_videoAnalysed`** is the exact frame MediaPipe ran on to produce the current `u_face*` / `u_hand*` / `u_pose*` / `u_segmentationMask` uniforms. It refreshes only when an inference completes. Both textures share `u_videoResolution`.\n\nPick by asking: *does my shader read pixels from the displayed frame at CV-derived positions?*\n\n- **No** (drawing on top of the camera, audio-reactive visuals driven by `u_face*` / `u_hand*` positions, generative effects that don't display the camera): sample `u_video`. Spatial drift between CV positions and displayed pixels under fast motion is the cost; smooth live-camera output is the benefit.\n- **Yes** (sampling skin pixels inside a face-mesh outline, compositing `u_segmentationMask` onto the body, warping the face along its mesh): sample `u_videoAnalysed` so the CV uniforms align with the pixels. The displayed image stutters or holds briefly between inferences, in exchange for pixel-for-pixel alignment.\n\nWhen you need `u_videoAnalysed`, gate on `u_videoAnalysedAvailable` so the startup window falls back cleanly:\n\n```glsl\nvec4 source = u_videoAnalysedAvailable\n ? texture2D(u_videoAnalysed, uv)\n : texture2D(u_video, uv);\n```\n\nBefore the first CV result lands, `u_videoAnalysed` samples a 1×1 black fallback (safe but typically not what you want as the visible source; use the gate above). Only the main video stream produces analysed frames; this uniform never updates from compositor or device streams.\n\n## Drawing Video\n\nCamera frames almost never match the canvas aspect ratio. Sampling `texture2D(u_video, uv)` with the canvas's normalized UV stretches the video: faces get squashed, circles become ovals, and CV uniforms (`u_face*`, `u_hand*`, `u_pose*`) drift off the actual feature positions. Use the `vijiVideoUV` helper below in every video / CV shader.\n\n```glsl\n// @renderer shader\n\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 // cover: scale > 1 on the cropped axis so the texture range narrows to a\n // centered sub-rectangle that fully covers the canvas.\n scale = canvasAspect > videoAspect\n ? vec2(1.0, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n // contain: scale < 1 on the letterboxed axis so the texture range\n // overshoots [0, 1] toward the edges, producing the empty bars.\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// True when the transformed UV lands inside the fitted video region.\n// Pair with vijiVideoUV to paint a fixed bar color in the letterbox regions\n// instead of sampling the texture's clamped edge there.\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1); // cover\n\n vec4 video = texture2D(u_video, videoUV);\n vec3 col = vijiInVideo(videoUV) ? video.rgb : vec3(17.0 / 255.0);\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nThe second arg is `1` for cover (default for live camera feeds) or `0` for contain. See [Best Practices](/getting-started/best-practices/#cover-vs-contain) for when each is the right pick.\n\n### Aligning CV Uniforms with the Fitted Video\n\nCV uniforms (`u_face0Center`, `u_face0Bounds`, hand / pose landmark uniforms) are normalized to the source video frame, not the canvas, and use the same bottom-up coordinate convention as `videoUV` (the engine pre-flips them at the boundary so they line up with the texture sampling surface). Compare them against the same `videoUV` you sampled the video with — no manual Y-flip needed:\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n vec2 videoUV = vijiVideoUV(uv, 1);\n\n vec4 video = texture2D(u_video, videoUV);\n\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(video.rgb, vec3(0.3, 0.8, 0.8), highlight * 0.4);\n gl_FragColor = vec4(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n```\n\nIn `cover` mode, `videoUV` stays inside `[0, 1]` — the texture range is narrowed to a centered sub-rectangle so the canvas is fully filled. In `contain` mode, `videoUV` falls outside `[0, 1]` in the letterbox regions; the engine pins every texture's wrap to `CLAMP_TO_EDGE`, so a raw `texture2D(u_video, videoUV)` there returns the camera frame's edge column stretched into the bar. Gating the final color with `vijiInVideo(videoUV)` (above) replaces that with a flat color so the bars read as empty space instead. The `vec3(17.0 / 255.0)` bar color matches the `#111` canvas background used by the native and p5 video scenes for cross-renderer parity.\n\n### Stretching the Frame (Intentional Distortion Only)\n\nIf you actually want to stretch the video as an artistic effect, sample `texture2D(u_video, uv)` directly with the canvas UV. Do this only when the distortion is the intent.\n\n```glsl\nvec4 video = texture2D(u_video, uv);\n```\n\n> [!NOTE]\n> The video texture is updated every frame when a video stream is connected. When disconnected, the texture samples as black. Your shader will still compile and run: the uniforms simply hold their default values."
8543
8543
  },
8544
8544
  {
8545
8545
  "type": "live-example",
8546
8546
  "title": "Video Shader",
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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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",
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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8548
8548
  "sceneFile": "basics-demo.scene.js",
8549
8549
  "capabilities": {
8550
8550
  "video": true
@@ -8585,7 +8585,7 @@ export const docsApi = {
8585
8585
  {
8586
8586
  "type": "live-example",
8587
8587
  "title": "Face Detection Shader",
8588
- "sceneCode": "// @renderer shader\n// @viji-cv:faceDetection\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// u_face0Center and u_face0Bounds use the same bottom-up coords as 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",
8588
+ "sceneCode": "// @renderer shader\n// @viji-cv:faceDetection\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\n// u_face0Center and u_face0Bounds use the same bottom-up coords as 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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8589
8589
  "sceneFile": "face-detection-demo.scene.js",
8590
8590
  "capabilities": {
8591
8591
  "video": true
@@ -8636,7 +8636,7 @@ export const docsApi = {
8636
8636
  {
8637
8637
  "type": "live-example",
8638
8638
  "title": "Head Pose Shader",
8639
- "sceneCode": "// @renderer shader\n// @viji-cv:faceMesh\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// Demonstrates all three head-pose components: yaw + pitch displace the\n// chromatic offset; roll rotates that offset vector so a head tilt visibly\n// rolls the aberration direction.\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; // positive = head up\n float roll = radians(u_face0HeadPose.z); // tilt angle\n\n vec4 source = texture2D(u_video, videoUV);\n\n // Yaw + pitch as direction; roll rotates the direction vector.\n vec2 dir = vec2(yaw, pitch);\n float cr = cos(roll), sr = sin(roll);\n dir = vec2(dir.x * cr - dir.y * sr, dir.x * sr + dir.y * cr);\n\n vec2 offset = dir * 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",
8639
+ "sceneCode": "// @renderer shader\n// @viji-cv:faceMesh\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\n// Demonstrates all three head-pose components: yaw + pitch displace the\n// chromatic offset; roll rotates that offset vector so a head tilt visibly\n// rolls the aberration direction.\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; // positive = head up\n float roll = radians(u_face0HeadPose.z); // tilt angle\n\n vec4 source = texture2D(u_video, videoUV);\n\n // Yaw + pitch as direction; roll rotates the direction vector.\n vec2 dir = vec2(yaw, pitch);\n float cr = cos(roll), sr = sin(roll);\n dir = vec2(dir.x * cr - dir.y * sr, dir.x * sr + dir.y * cr);\n\n vec2 offset = dir * 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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8640
8640
  "sceneFile": "face-mesh-demo.scene.js",
8641
8641
  "capabilities": {
8642
8642
  "video": true
@@ -8682,7 +8682,7 @@ export const docsApi = {
8682
8682
  {
8683
8683
  "type": "live-example",
8684
8684
  "title": "Emotion-Reactive Shader",
8685
- "sceneCode": "// @renderer shader\n// @viji-cv:emotionDetection\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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",
8685
+ "sceneCode": "// @renderer shader\n// @viji-cv:emotionDetection\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\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(vijiInVideo(videoUV) ? clamp(col, 0.0, 1.0) : vec3(17.0 / 255.0), 1.0);\n}\n",
8686
8686
  "sceneFile": "emotion-detection-demo.scene.js",
8687
8687
  "capabilities": {
8688
8688
  "video": true
@@ -8738,7 +8738,7 @@ export const docsApi = {
8738
8738
  {
8739
8739
  "type": "live-example",
8740
8740
  "title": "Hand Tracking Shader",
8741
- "sceneCode": "// @renderer shader\n// @viji-cv:handTracking\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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",
8741
+ "sceneCode": "// @renderer shader\n// @viji-cv:handTracking\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8742
8742
  "sceneFile": "hand-tracking-demo.scene.js",
8743
8743
  "capabilities": {
8744
8744
  "video": true
@@ -8789,7 +8789,7 @@ export const docsApi = {
8789
8789
  {
8790
8790
  "type": "live-example",
8791
8791
  "title": "Pose Detection Shader",
8792
- "sceneCode": "// @renderer shader\n// @viji-cv:poseDetection\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\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",
8792
+ "sceneCode": "// @renderer shader\n// @viji-cv:poseDetection\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8793
8793
  "sceneFile": "pose-detection-demo.scene.js",
8794
8794
  "capabilities": {
8795
8795
  "video": true
@@ -8845,7 +8845,7 @@ export const docsApi = {
8845
8845
  {
8846
8846
  "type": "live-example",
8847
8847
  "title": "Body Segmentation Shader",
8848
- "sceneCode": "// @renderer shader\n// @viji-cv:bodySegmentation\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\n// u_segmentationMask shares the same bottom-up coordinate convention as\n// u_video, so sample both 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",
8848
+ "sceneCode": "// @renderer shader\n// @viji-cv:bodySegmentation\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, canvasAspect / videoAspect)\n : vec2(videoAspect / canvasAspect, 1.0);\n } else {\n scale = canvasAspect > videoAspect\n ? vec2(videoAspect / canvasAspect, 1.0)\n : vec2(1.0, canvasAspect / videoAspect);\n }\n return (uv - 0.5) / scale + 0.5;\n}\n\nbool vijiInVideo(vec2 uv) {\n return all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)));\n}\n\n// u_segmentationMask shares the same bottom-up coordinate convention as\n// u_video, so sample both 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(vijiInVideo(videoUV) ? col : vec3(17.0 / 255.0), 1.0);\n}\n",
8849
8849
  "sceneFile": "body-segmentation-demo.scene.js",
8850
8850
  "capabilities": {
8851
8851
  "video": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viji-dev/core",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Universal execution engine for Viji Creative scenes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",