@viji-dev/core 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docs-api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const docsApi = {
2
2
  "version": "1.1.0",
3
- "coreVersion": "0.17.0",
4
- "generatedAt": "2026-09-12T17:25:53.411Z",
3
+ "coreVersion": "0.18.0",
4
+ "generatedAt": "2026-09-26T16:47:08.751Z",
5
5
  "navigation": [
6
6
  {
7
7
  "id": "getting-started",
@@ -1135,6 +1135,11 @@ export const docsApi = {
1135
1135
  "level": 3,
1136
1136
  "text": "Forgetting .value on Parameters"
1137
1137
  },
1138
+ {
1139
+ "id": "caching-an-image-parameters-bitmap",
1140
+ "level": 3,
1141
+ "text": "Caching an Image Parameter's Bitmap"
1142
+ },
1138
1143
  {
1139
1144
  "id": "hardcoding-pixel-values",
1140
1145
  "level": 3,
@@ -1269,7 +1274,7 @@ export const docsApi = {
1269
1274
  "content": [
1270
1275
  {
1271
1276
  "type": "text",
1272
- "markdown": "# Common Mistakes\n\nWhen something looks wrong in your scene, scan this page for a matching symptom. Each entry shows the broken version, the working replacement, and a link to where the rule is explained in full.\n\nFor the why behind these patterns, see [Best Practices](../best-practices/).\n\n## How to read this page\n\nEvery entry has the same shape: a one-line symptom, the wrong code, the right code, and a see-also link. Symptoms are grouped by where they typically come up: general code, animation timing, P5-specific quirks, and shader-specific quirks.\n\n---\n\n## General Mistakes\n\n### Using DOM APIs\n\nScenes run in a Web Worker. There is no DOM.\n\n```javascript\n// Wrong: DOM APIs don't exist in workers\nconst img = new Image();\nimg.src = 'photo.jpg';\n\ndocument.createElement('canvas');\nwindow.innerWidth;\nlocalStorage.setItem('key', 'value');\n```\n\n```javascript\n// Right: use Viji's API for inputs, fetch() for external data\nconst photo = viji.image(null, { label: 'Photo' });\nconst data = await fetch('https://cdn.example.com/data.json').then(r => r.json());\n// Use viji.canvas, viji.width, viji.height instead of window/document.\n```\n\n**See also:** [Best Practices: Worker Environment](../best-practices/#worker-environment), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Declaring Parameters Inside `render()`\n\nParameter functions register UI controls with the host. Calling them in `render()` re-registers the parameter every frame, resetting its value to the default.\n\n```javascript\n// Wrong: re-registers the slider every frame, resetting its value\nfunction render(viji) {\n const speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n}\n```\n\n```javascript\n// Right: declare once at top level, read .value in render()\nconst speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n\nfunction render(viji) {\n const s = speed.value;\n}\n```\n\n**See also:** [Best Practices: Parameters at Top Level](../best-practices/#parameters-at-top-level), [`native/parameters`](/native/parameters).\n\n---\n\n### Forgetting `.value` on Parameters\n\nParameter objects are not raw values. Read `.value` to get the current number, color, or other primitive.\n\n```javascript\n// Wrong: uses the parameter object, not its value\nconst radius = viji.slider(50, { min: 10, max: 200, label: 'Radius' });\n\nfunction render(viji) {\n ctx.arc(x, y, radius, 0, Math.PI * 2); // radius is an object, not a number\n}\n```\n\n```javascript\n// Right: access .value\nfunction render(viji) {\n ctx.arc(x, y, radius.value, 0, Math.PI * 2);\n}\n```\n\n**See also:** [`native/parameters`](/native/parameters).\n\n---\n\n### Hardcoding Pixel Values\n\nThe host controls your scene's resolution. Hardcoded values break at different sizes.\n\n```javascript\n// Wrong: only looks right at one specific resolution\nfunction render(viji) {\n ctx.arc(960, 540, 50, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: adapts to any resolution\nfunction render(viji) {\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const r = Math.min(viji.width, viji.height) * 0.05;\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n}\n```\n\n**See also:** [Best Practices: Resolution Agnosticism Across Renderers](../best-practices/#resolution-agnosticism-across-renderers), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Allocating Objects in `render()`\n\nCreating new objects every frame causes garbage collection pauses.\n\n```javascript\n// Wrong: new array and objects every frame\nfunction render(viji) {\n const particles = [];\n for (let i = 0; i < 100; i++) {\n particles.push({ x: Math.random() * viji.width, y: Math.random() * viji.height });\n }\n}\n```\n\n```javascript\n// Right: pre-allocate and mutate in place\nconst particles = Array.from({ length: 100 }, () => ({ x: 0, y: 0 }));\n\nfunction render(viji) {\n for (const p of particles) {\n p.x = Math.random() * viji.width;\n p.y = Math.random() * viji.height;\n }\n}\n```\n\n**See also:** [Best Practices: Memory in the Render Loop](../best-practices/#memory-in-the-render-loop).\n\n---\n\n### Using `viji.canvas.getContext()` Instead of `viji.useContext()`\n\nCalling `viji.canvas.getContext()` directly bypasses the Viji runtime's internal context tracking and may miss future runtime setup such as WebGL viewport initialization.\n\n```javascript\n// Wrong: bypasses Viji's context management\nconst ctx = viji.canvas.getContext('2d');\n```\n\n```javascript\n// Right: use the Viji API, which tracks the context and sets up internals\nconst ctx = viji.useContext('2d');\n```\n\nThe same applies to canvas dimensions: prefer `viji.width` / `viji.height` over `viji.canvas.width` / `viji.canvas.height`.\n\n**See also:** [Best Practices: Canvas Context Selection](../best-practices/#canvas-context-selection), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Not Checking `isConnected` for Audio/Video\n\nAudio and video streams may not be available. Without a guard, your scene reads zero values silently.\n\n```javascript\n// Wrong: no guard, silently uses zero values\nfunction render(viji) {\n const bass = viji.audio.bands.low;\n ctx.drawImage(viji.video.currentFrame, 0, 0);\n}\n```\n\n```javascript\n// Right: check connection state first\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // helper from native/video/basics\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n}\n```\n\n**See also:** [Best Practices: Audio and Video isConnected Guards](../best-practices/#audio-and-video-isconnected-guards).\n\n---\n\n### Enabling All CV Features by Default\n\nEnabling CV features without user consent wastes resources on devices that can't handle it and risks WebGL context loss.\n\n```javascript\n// Wrong: activates expensive CV on every device, regardless of capability\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\n\n```javascript\n// Right: let the user opt in, then enable from inside render()\nconst useFace = viji.toggle(false, { label: 'Enable Face Tracking', category: 'video' });\nconst useHands = viji.toggle(false, { label: 'Enable Hand Tracking', category: 'video' });\n\nfunction render(viji) {\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n}\n```\n\nThe `enableX(true)` and `enableX(false)` methods are idempotent: calling them every frame is safe, the runtime returns immediately when the requested state already matches.\n\nFor shader scenes the equivalent opt-in pattern uses the `// @viji-cv:` directive — toggleable form synthesizes the same kind of host-side parameter without requiring JS reconciliation:\n\n```glsl\n// Right (shader): user opts in via the synthesized toggle, runtime\n// reconciles the pipeline state on each frame.\n// @renderer shader\n// @viji-cv:faceDetection label:\"Face Detection\" default:false\n// @viji-cv:handTracking label:\"Hand Tracking\" default:false\n```\n\n**See also:** [Best Practices: Computer Vision Costs](../best-practices/#computer-vision-costs), [Shader Video & CV](/shader/video).\n\n---\n\n### Compositing CV Masks or Sampling Pixels Over `currentFrame`\n\nThis entry is specifically about effects that **read pixels from the displayed frame at CV-derived positions**: compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters. For these, `currentFrame` is wrong: the CV results come from a frame MediaPipe analysed ~1-3 frames earlier, so mask edges drift off the body and pixel samples land on the wrong skin.\n\n```javascript\n// Wrong: mask edges drift off the body during motion\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against currentFrame...\n}\n```\n\n```javascript\n// Right: composite the mask against the frame it was computed from\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\n ctx.drawImage(frame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against the same frame...\n}\n```\n\nThe `analysedFrame ?? currentFrame` fallback covers the brief startup window before the first CV result lands. For shaders, the equivalent is sampling `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) instead of `u_video`.\n\n**This is *not* a blanket rule against drawing CV-derived visuals over `currentFrame`.** Drawing landmark dots, particles, or other geometry on top of the live camera is a different case: there are no displayed pixels being read at CV-derived positions, just CV positions being used as drawing coordinates. For those, `currentFrame` is usually the better default; `analysedFrame` would freeze the displayed video between inferences. See the trade-off discussion in Best Practices.\n\n**See also:** [Best Practices: `currentFrame` vs `analysedFrame`](../best-practices/#currentframe-vs-analysedframe-pick-the-right-trade-off), [`native/video/face-mesh`](/native/video/face-mesh), [`native/video/body-segmentation`](/native/video/body-segmentation).\n\n---\n\n### Stretching the Video to Fill the Canvas\n\nDrawing video to `(0, 0, viji.width, viji.height)` stretches it whenever the canvas aspect does not match the camera aspect (which is almost always). Faces get squashed, circles become ovals, and CV bounding boxes drift off the actual face.\n\n```javascript\n// Wrong: stretches the video to fill the canvas\nctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n```\n\n```javascript\n// Right: preserve the source aspect ratio\nconst v = videoFit(viji); // see recipe below\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n```\n\nThe `videoFit` helper and the matching CV-coordinate remapping live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use them in every video / CV scene. Stretching is allowed only when distortion is intentional (an artistic squash effect).\n\n**See also:** [Best Practices: Drawing Video at the Right Aspect Ratio](../best-practices/#drawing-video-at-the-right-aspect-ratio).\n\n---\n\n### Missing `category` on Input-Related Parameters\n\nParameters tied to audio, video, or interaction inputs need `category` so the host UI hides them when the input is inactive. Without it, users see controls that do nothing.\n\n```javascript\n// Wrong: audio parameters are always visible, even when no audio is connected\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\nconst decay = viji.slider(0.8, { min: 0, max: 1, label: 'Audio Decay', group: 'audio' });\n```\n\n```javascript\n// Right: parameters are hidden until audio is available\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\nconst decay = viji.slider(0.8, { min: 0, max: 1, label: 'Audio Decay', group: 'audio', category: 'audio' });\n```\n\n**See also:** [Best Practices: Parameter Categories](../best-practices/#parameter-categories), [`native/parameters/categories`](/native/parameters/categories).\n\n---\n\n### Redundant Input On/Off Toggles\n\nThe host UI controls whether audio, video, and interaction inputs are wired up. A scene-level on/off toggle for whether to *react to* an already-available input is redundant: muting host audio achieves the same thing as flipping an \"Audio Reactive\" toggle off.\n\n```javascript\n// Wrong: scene-level toggles duplicate host input switches\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\nconst useCamera = viji.toggle(true, { label: 'Use Camera', category: 'video' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n```\n\n```javascript\n// Right: creative-strength parameters with the matching category;\n// `isConnected` / `isInCanvas` guards handle the \"input not available\" case\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\nThe exception is **CV feature toggles** (`enableFaceDetection`, `enableHandTracking`, etc.), which are not input toggles: they activate paid features that consume WebGL contexts and ML inference. Those stay opt-in.\n\n**See also:** [Best Practices: No Redundant Input Toggles](../best-practices/#no-redundant-input-toggles).\n\n---\n\n## Animation Timing Mistakes\n\nThe four entries below share the same root cause: when an animation value is computed from absolute time multiplied by a user-controlled parameter, changing the parameter rescales the entire phase history at once and the animation jumps. The fix is always the same shape (accumulate `speed × deltaTime` incrementally), so once you understand one you can spot the others. For the full rationale and cross-renderer narrative, see [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### Frame-Rate-Dependent Animation\n\nCounting frames or using fixed increments makes animation speed depend on the device's frame rate.\n\n```javascript\n// Wrong: faster on 120Hz displays, slower on 30Hz\nlet angle = 0;\nfunction render(viji) {\n angle += 0.02;\n}\n```\n\n```javascript\n// Right: use viji.time for constant-speed animation\nfunction render(viji) {\n const angle = viji.time * 2.0; // fixed 2 rad/s, independent of FPS\n}\n\n// Or use viji.deltaTime for accumulation\nlet position = 0;\nfunction render(viji) {\n position += velocity * viji.deltaTime;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `viji.time * speed.value` (JS / P5)\n\nMultiplying `viji.time` by a parameter causes the entire phase to jump when the slider moves, because the full time history is recalculated instantly with the new value.\n\n```javascript\n// Wrong: animation jumps when speed slider changes\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nfunction render(viji) {\n const angle = viji.time * speed.value;\n ctx.arc(x, y, Math.sin(angle) * r, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: accumulate with deltaTime, slider only affects future rate\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n ctx.arc(x, y, Math.sin(phase) * r, 0, Math.PI * 2);\n}\n```\n\nThe same pattern applies in P5: replace the canvas calls with `p5.*` equivalents and the accumulator stays the same.\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `u_time * speed` (Shader)\n\nThe same trap in shaders: multiplying `u_time` by a parameter uniform causes a phase jump on slider changes.\n\n```glsl\n// Wrong: animation jumps when speed slider changes\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\nvoid main() {\n float wave = sin(u_time * speed);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n```glsl\n// Right: @viji-accumulator integrates speed × deltaTime smoothly\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers), [`shader/parameters/accumulator`](/shader/parameters/accumulator).\n\n---\n\n### Nested Multiplication: Accumulated Value Multiplied by Parameter\n\nAfter adopting the accumulator pattern, multiplying the accumulated value by another parameter causes the same jump because the full accumulated history is rescaled.\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime; // accumulates correctly\n const rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n}\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n## P5-Specific\n\n### Missing the `p5.` Prefix\n\nViji runs P5 in **instance mode**. All P5 functions must be called on the `p5` object.\n\n```javascript\n// @renderer p5\n\n// Wrong: global P5 functions don't exist in instance mode\nfunction render(viji, p5) {\n background(0); // ReferenceError\n fill(255, 0, 0); // ReferenceError\n circle(width / 2, height / 2, 100); // ReferenceError\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: use p5. prefix for P5 functions, viji.* for dimensions\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255, 0, 0);\n p5.circle(viji.width / 2, viji.height / 2, 100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using `draw()` Instead of `render()`\n\nP5's built-in draw loop is disabled in Viji. Your function must be named `render`, not `draw`.\n\n```javascript\n// @renderer p5\n\n// Wrong: Viji never calls draw()\nfunction draw(viji, p5) {\n p5.background(0);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: Viji calls render() every frame\nfunction render(viji, p5) {\n p5.background(0);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Calling `createCanvas()`\n\nThe canvas is created and managed by Viji. Calling `createCanvas()` creates a second, invisible canvas. WEBGL mode is selected via the `// @renderer p5 webgl` directive on the first line, never by passing `p5.WEBGL` to `createCanvas`.\n\n```javascript\n// @renderer p5\n\n// Wrong: creates a separate, invisible canvas\nfunction setup(viji, p5) {\n p5.createCanvas(800, 600);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Wrong: main canvas already exists; this does not switch it to WEBGL\nfunction setup(viji, p5) {\n p5.createCanvas(p5.width, p5.height, p5.WEBGL);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: canvas is already provided, just configure settings\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: WEBGL is selected by the first-line directive; never call createCanvas\nfunction setup(viji, p5) {\n p5.angleMode(p5.RADIANS);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Expecting WEBGL Without `// @renderer p5 webgl`\n\n`// @renderer p5` alone gives a 2D main canvas. For 3D / WEBGL, the first comment must include `webgl` after `p5`.\n\n```javascript\n// @renderer p5\n\n// Wrong: this scene is 2D only; box() and WEBGL lighting won't work as intended\nfunction render(viji, p5) {\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: Viji creates the main canvas in WEBGL mode\nfunction render(viji, p5) {\n p5.background(32);\n p5.ambientLight(100);\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using P5 Event Callbacks\n\nP5 event callbacks like `mousePressed()`, `keyPressed()`, `touchStarted()` do not work in Viji's worker environment. Use Viji's interaction APIs instead.\n\n```javascript\n// @renderer p5\n\n// Wrong: these callbacks are never called\nfunction mousePressed() {\n console.log('clicked');\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: check Viji's interaction state in render()\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) {\n console.log('clicked');\n }\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart), [`native/pointer`](/native/pointer).\n\n---\n\n## Shader-Specific\n\n### Redeclaring Auto-Injected Code\n\nViji auto-injects `precision`, all built-in uniform declarations, and all parameter uniforms from `@viji-*` directives. Redeclaring any of them causes compilation errors.\n\n```glsl\n// @renderer shader\n\n// Wrong: these are already injected by Viji\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: just write your code, uniforms are available automatically\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n**See also:** [`shader/basics`](/shader/basics).\n\n---\n\n### Using `u_` Prefix for Custom Parameters\n\nThe `u_` prefix is reserved for Viji's built-in uniforms. Using it for your parameters risks naming collisions.\n\n```glsl\n// Wrong: u_ prefix is reserved\n// @viji-slider:u_speed label:\"Speed\" default:1.0\n```\n\n```glsl\n// Right: use descriptive names without u_ prefix\n// @viji-slider:speed label:\"Speed\" default:1.0\n```\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n### Missing `@renderer shader` Directive\n\nWithout the directive, your GLSL code is treated as JavaScript and throws syntax errors.\n\n```glsl\n// Wrong: no directive, treated as JavaScript\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: directive tells Viji to use the shader renderer\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n**See also:** [`shader/quickstart`](/shader/quickstart).\n\n---\n\n### Block Comments for `@viji-*` Parameters\n\nThe `@viji-*` parameter declarations only work with single-line `//` comments. Block comments `/* */` are silently ignored.\n\n```glsl\n// @renderer shader\n\n// Wrong: block comments are not parsed for parameters\n/* @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0 */\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0); // speed is undefined\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: use single-line comments for parameter declarations\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0);\n}\n```\n\n> [!NOTE]\n> The `@renderer` directive supports both `//` and `/* */` styles, but `@viji-*` parameter declarations require `//`.\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n## Related\n\n- [Best Practices](../best-practices/): cross-renderer authoring patterns and rationale.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer.\n- [Audio](/native/audio), [Video & CV](/native/video): full audio and video API."
1277
+ "markdown": "# Common Mistakes\n\nWhen something looks wrong in your scene, scan this page for a matching symptom. Each entry shows the broken version, the working replacement, and a link to where the rule is explained in full.\n\nFor the why behind these patterns, see [Best Practices](../best-practices/).\n\n## How to read this page\n\nEvery entry has the same shape: a one-line symptom, the wrong code, the right code, and a see-also link. Symptoms are grouped by where they typically come up: general code, animation timing, P5-specific quirks, and shader-specific quirks.\n\n---\n\n## General Mistakes\n\n### Using DOM APIs\n\nScenes run in a Web Worker. There is no DOM.\n\n```javascript\n// Wrong: DOM APIs don't exist in workers\nconst img = new Image();\nimg.src = 'photo.jpg';\n\ndocument.createElement('canvas');\nwindow.innerWidth;\nlocalStorage.setItem('key', 'value');\n```\n\n```javascript\n// Right: use Viji's API for inputs, fetch() for external data\nconst photo = viji.image(null, { label: 'Photo' });\nconst data = await fetch('https://cdn.example.com/data.json').then(r => r.json());\n// Use viji.canvas, viji.width, viji.height instead of window/document.\n```\n\n**See also:** [Best Practices: Worker Environment](../best-practices/#worker-environment), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Declaring Parameters Inside `render()`\n\nParameter functions register UI controls with the host. Calling them in `render()` re-registers the parameter every frame, resetting its value to the default.\n\n```javascript\n// Wrong: re-registers the slider every frame, resetting its value\nfunction render(viji) {\n const speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n}\n```\n\n```javascript\n// Right: declare once at top level, read .value in render()\nconst speed = viji.slider(1, { min: 0, max: 5, label: 'Speed' });\n\nfunction render(viji) {\n const s = speed.value;\n}\n```\n\n**See also:** [Best Practices: Parameters at Top Level](../best-practices/#parameters-at-top-level), [`native/parameters`](/native/parameters).\n\n---\n\n### Forgetting `.value` on Parameters\n\nParameter objects are not raw values. Read `.value` to get the current number, color, or other primitive.\n\n```javascript\n// Wrong: uses the parameter object, not its value\nconst radius = viji.slider(50, { min: 10, max: 200, label: 'Radius' });\n\nfunction render(viji) {\n ctx.arc(x, y, radius, 0, Math.PI * 2); // radius is an object, not a number\n}\n```\n\n```javascript\n// Right: access .value\nfunction render(viji) {\n ctx.arc(x, y, radius.value, 0, Math.PI * 2);\n}\n```\n\n**See also:** [`native/parameters`](/native/parameters).\n\n---\n\n### Caching an Image Parameter's Bitmap\n\nThe image disappears or the scene throws `InvalidStateError` after the user picks a new image. When an image parameter changes, the previous `ImageBitmap` is closed; a reference saved earlier points at a dead bitmap.\n\n```javascript\n// Wrong: captures the bitmap once; it is closed when the image changes\nconst tex = viji.image(null, { label: 'Texture' });\nconst img = tex.value;\n\nfunction render(viji) {\n if (img) ctx.drawImage(img, 0, 0, viji.width, viji.height);\n}\n```\n\n```javascript\n// Right: read .value where you draw it\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji) {\n if (tex.value) ctx.drawImage(tex.value, 0, 0, viji.width, viji.height);\n}\n```\n\nIn P5 scenes the same applies to `tex.p5`: pass it to `p5.image()` inside `render()` each frame instead of saving it.\n\n**See also:** [`native/parameters/image`](/native/parameters/image), [`p5/parameters/image`](/p5/parameters/image).\n\n---\n\n### Hardcoding Pixel Values\n\nThe host controls your scene's resolution. Hardcoded values break at different sizes.\n\n```javascript\n// Wrong: only looks right at one specific resolution\nfunction render(viji) {\n ctx.arc(960, 540, 50, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: adapts to any resolution\nfunction render(viji) {\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const r = Math.min(viji.width, viji.height) * 0.05;\n ctx.arc(cx, cy, r, 0, Math.PI * 2);\n}\n```\n\n**See also:** [Best Practices: Resolution Agnosticism Across Renderers](../best-practices/#resolution-agnosticism-across-renderers), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Allocating Objects in `render()`\n\nCreating new objects every frame causes garbage collection pauses.\n\n```javascript\n// Wrong: new array and objects every frame\nfunction render(viji) {\n const particles = [];\n for (let i = 0; i < 100; i++) {\n particles.push({ x: Math.random() * viji.width, y: Math.random() * viji.height });\n }\n}\n```\n\n```javascript\n// Right: pre-allocate and mutate in place\nconst particles = Array.from({ length: 100 }, () => ({ x: 0, y: 0 }));\n\nfunction render(viji) {\n for (const p of particles) {\n p.x = Math.random() * viji.width;\n p.y = Math.random() * viji.height;\n }\n}\n```\n\n**See also:** [Best Practices: Memory in the Render Loop](../best-practices/#memory-in-the-render-loop).\n\n---\n\n### Using `viji.canvas.getContext()` Instead of `viji.useContext()`\n\nCalling `viji.canvas.getContext()` directly bypasses the Viji runtime's internal context tracking and may miss future runtime setup such as WebGL viewport initialization.\n\n```javascript\n// Wrong: bypasses Viji's context management\nconst ctx = viji.canvas.getContext('2d');\n```\n\n```javascript\n// Right: use the Viji API, which tracks the context and sets up internals\nconst ctx = viji.useContext('2d');\n```\n\nThe same applies to canvas dimensions: prefer `viji.width` / `viji.height` over `viji.canvas.width` / `viji.canvas.height`.\n\n**See also:** [Best Practices: Canvas Context Selection](../best-practices/#canvas-context-selection), [`native/canvas-context`](/native/canvas-context).\n\n---\n\n### Not Checking `isConnected` for Audio/Video\n\nAudio and video streams may not be available. Without a guard, your scene reads zero values silently.\n\n```javascript\n// Wrong: no guard, silently uses zero values\nfunction render(viji) {\n const bass = viji.audio.bands.low;\n ctx.drawImage(viji.video.currentFrame, 0, 0);\n}\n```\n\n```javascript\n// Right: check connection state first\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // helper from native/video/basics\n ctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n}\n```\n\n**See also:** [Best Practices: Audio and Video isConnected Guards](../best-practices/#audio-and-video-isconnected-guards).\n\n---\n\n### Enabling All CV Features by Default\n\nEnabling CV features without user consent wastes resources on devices that can't handle it and risks WebGL context loss.\n\n```javascript\n// Wrong: activates expensive CV on every device, regardless of capability\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\n\n```javascript\n// Right: let the user opt in, then enable from inside render()\nconst useFace = viji.toggle(false, { label: 'Enable Face Tracking', category: 'video' });\nconst useHands = viji.toggle(false, { label: 'Enable Hand Tracking', category: 'video' });\n\nfunction render(viji) {\n if (useFace.value) viji.video.cv.enableFaceDetection(true);\n else viji.video.cv.enableFaceDetection(false);\n\n if (useHands.value) viji.video.cv.enableHandTracking(true);\n else viji.video.cv.enableHandTracking(false);\n}\n```\n\nThe `enableX(true)` and `enableX(false)` methods are idempotent: calling them every frame is safe, the runtime returns immediately when the requested state already matches.\n\nFor shader scenes the equivalent opt-in pattern uses the `// @viji-cv:` directive: the toggleable form synthesizes the same kind of host-side parameter without requiring JS reconciliation:\n\n```glsl\n// Right (shader): user opts in via the synthesized toggle, runtime\n// reconciles the pipeline state on each frame.\n// @renderer shader\n// @viji-cv:faceDetection label:\"Face Detection\" default:false\n// @viji-cv:handTracking label:\"Hand Tracking\" default:false\n```\n\n**See also:** [Best Practices: Computer Vision Costs](../best-practices/#computer-vision-costs), [Shader Video & CV](/shader/video).\n\n---\n\n### Compositing CV Masks or Sampling Pixels Over `currentFrame`\n\nThis entry is specifically about effects that **read pixels from the displayed frame at CV-derived positions**: compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters. For these, `currentFrame` is wrong: the CV results come from a frame MediaPipe analysed ~1-3 frames earlier, so mask edges drift off the body and pixel samples land on the wrong skin.\n\n```javascript\n// Wrong: mask edges drift off the body during motion\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against currentFrame...\n}\n```\n\n```javascript\n// Right: composite the mask against the frame it was computed from\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n const frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\n ctx.drawImage(frame, 0, 0, viji.width, viji.height);\n // composite viji.video.cv.segmentation against the same frame...\n}\n```\n\nThe `analysedFrame ?? currentFrame` fallback covers the brief startup window before the first CV result lands. For shaders, the equivalent is sampling `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) instead of `u_video`.\n\n**This is *not* a blanket rule against drawing CV-derived visuals over `currentFrame`.** Drawing landmark dots, particles, or other geometry on top of the live camera is a different case: there are no displayed pixels being read at CV-derived positions, just CV positions being used as drawing coordinates. For those, `currentFrame` is usually the better default; `analysedFrame` would freeze the displayed video between inferences. See the trade-off discussion in Best Practices.\n\n**See also:** [Best Practices: `currentFrame` vs `analysedFrame`](../best-practices/#currentframe-vs-analysedframe-pick-the-right-trade-off), [`native/video/face-mesh`](/native/video/face-mesh), [`native/video/body-segmentation`](/native/video/body-segmentation).\n\n---\n\n### Stretching the Video to Fill the Canvas\n\nDrawing video to `(0, 0, viji.width, viji.height)` stretches it whenever the canvas aspect does not match the camera aspect (which is almost always). Faces get squashed, circles become ovals, and CV bounding boxes drift off the actual face.\n\n```javascript\n// Wrong: stretches the video to fill the canvas\nctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n```\n\n```javascript\n// Right: preserve the source aspect ratio\nconst v = videoFit(viji); // see recipe below\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n```\n\nThe `videoFit` helper and the matching CV-coordinate remapping live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use them in every video / CV scene. Stretching is allowed only when distortion is intentional (an artistic squash effect).\n\n**See also:** [Best Practices: Drawing Video at the Right Aspect Ratio](../best-practices/#drawing-video-at-the-right-aspect-ratio).\n\n---\n\n### Missing `category` on Input-Related Parameters\n\nParameters tied to audio, video, or interaction inputs need `category` so the host UI hides them when the input is inactive. Without it, users see controls that do nothing.\n\n```javascript\n// Wrong: audio parameters are always visible, even when no audio is connected\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\nconst decay = viji.slider(0.8, { min: 0, max: 1, label: 'Audio Decay', group: 'audio' });\n```\n\n```javascript\n// Right: parameters are hidden until audio is available\nconst bassSens = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\nconst decay = viji.slider(0.8, { min: 0, max: 1, label: 'Audio Decay', group: 'audio', category: 'audio' });\n```\n\n**See also:** [Best Practices: Parameter Categories](../best-practices/#parameter-categories), [`native/parameters/categories`](/native/parameters/categories).\n\n---\n\n### Redundant Input On/Off Toggles\n\nThe host UI controls whether audio, video, and interaction inputs are wired up. A scene-level on/off toggle for whether to *react to* an already-available input is redundant: muting host audio achieves the same thing as flipping an \"Audio Reactive\" toggle off.\n\n```javascript\n// Wrong: scene-level toggles duplicate host input switches\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\nconst useCamera = viji.toggle(true, { label: 'Use Camera', category: 'video' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n```\n\n```javascript\n// Right: creative-strength parameters with the matching category;\n// `isConnected` / `isInCanvas` guards handle the \"input not available\" case\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\nThe exception is **CV feature toggles** (`enableFaceDetection`, `enableHandTracking`, etc.), which are not input toggles: they activate paid features that consume WebGL contexts and ML inference. Those stay opt-in.\n\n**See also:** [Best Practices: No Redundant Input Toggles](../best-practices/#no-redundant-input-toggles).\n\n---\n\n## Animation Timing Mistakes\n\nThe four entries below share the same root cause: when an animation value is computed from absolute time multiplied by a user-controlled parameter, changing the parameter rescales the entire phase history at once and the animation jumps. The fix is always the same shape (accumulate `speed × deltaTime` incrementally), so once you understand one you can spot the others. For the full rationale and cross-renderer narrative, see [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### Frame-Rate-Dependent Animation\n\nCounting frames or using fixed increments makes animation speed depend on the device's frame rate.\n\n```javascript\n// Wrong: faster on 120Hz displays, slower on 30Hz\nlet angle = 0;\nfunction render(viji) {\n angle += 0.02;\n}\n```\n\n```javascript\n// Right: use viji.time for constant-speed animation\nfunction render(viji) {\n const angle = viji.time * 2.0; // fixed 2 rad/s, independent of FPS\n}\n\n// Or use viji.deltaTime for accumulation\nlet position = 0;\nfunction render(viji) {\n position += velocity * viji.deltaTime;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `viji.time * speed.value` (JS / P5)\n\nMultiplying `viji.time` by a parameter causes the entire phase to jump when the slider moves, because the full time history is recalculated instantly with the new value.\n\n```javascript\n// Wrong: animation jumps when speed slider changes\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nfunction render(viji) {\n const angle = viji.time * speed.value;\n ctx.arc(x, y, Math.sin(angle) * r, 0, Math.PI * 2);\n}\n```\n\n```javascript\n// Right: accumulate with deltaTime, slider only affects future rate\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n ctx.arc(x, y, Math.sin(phase) * r, 0, Math.PI * 2);\n}\n```\n\nThe same pattern applies in P5: replace the canvas calls with `p5.*` equivalents and the accumulator stays the same.\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n### `u_time * speed` (Shader)\n\nThe same trap in shaders: multiplying `u_time` by a parameter uniform causes a phase jump on slider changes.\n\n```glsl\n// Wrong: animation jumps when speed slider changes\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\nvoid main() {\n float wave = sin(u_time * speed);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n```glsl\n// Right: @viji-accumulator integrates speed × deltaTime smoothly\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers), [`shader/parameters/accumulator`](/shader/parameters/accumulator).\n\n---\n\n### Nested Multiplication: Accumulated Value Multiplied by Parameter\n\nAfter adopting the accumulator pattern, multiplying the accumulated value by another parameter causes the same jump because the full accumulated history is rescaled.\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime; // accumulates correctly\n const rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n}\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase;\n}\n```\n\n**See also:** [Best Practices: Animation Timing Across Renderers](../best-practices/#animation-timing-across-renderers).\n\n---\n\n## P5-Specific\n\n### Missing the `p5.` Prefix\n\nViji runs P5 in **instance mode**. All P5 functions must be called on the `p5` object.\n\n```javascript\n// @renderer p5\n\n// Wrong: global P5 functions don't exist in instance mode\nfunction render(viji, p5) {\n background(0); // ReferenceError\n fill(255, 0, 0); // ReferenceError\n circle(width / 2, height / 2, 100); // ReferenceError\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: use p5. prefix for P5 functions, viji.* for dimensions\nfunction render(viji, p5) {\n p5.background(0);\n p5.fill(255, 0, 0);\n p5.circle(viji.width / 2, viji.height / 2, 100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using `draw()` Instead of `render()`\n\nP5's built-in draw loop is disabled in Viji. Your function must be named `render`, not `draw`.\n\n```javascript\n// @renderer p5\n\n// Wrong: Viji never calls draw()\nfunction draw(viji, p5) {\n p5.background(0);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: Viji calls render() every frame\nfunction render(viji, p5) {\n p5.background(0);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Calling `createCanvas()`\n\nThe canvas is created and managed by Viji. Calling `createCanvas()` creates a second, invisible canvas. WEBGL mode is selected via the `// @renderer p5 webgl` directive on the first line, never by passing `p5.WEBGL` to `createCanvas`.\n\n```javascript\n// @renderer p5\n\n// Wrong: creates a separate, invisible canvas\nfunction setup(viji, p5) {\n p5.createCanvas(800, 600);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Wrong: main canvas already exists; this does not switch it to WEBGL\nfunction setup(viji, p5) {\n p5.createCanvas(p5.width, p5.height, p5.WEBGL);\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: canvas is already provided, just configure settings\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: WEBGL is selected by the first-line directive; never call createCanvas\nfunction setup(viji, p5) {\n p5.angleMode(p5.RADIANS);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Expecting WEBGL Without `// @renderer p5 webgl`\n\n`// @renderer p5` alone gives a 2D main canvas. For 3D / WEBGL, the first comment must include `webgl` after `p5`.\n\n```javascript\n// @renderer p5\n\n// Wrong: this scene is 2D only; box() and WEBGL lighting won't work as intended\nfunction render(viji, p5) {\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n```javascript\n// @renderer p5 webgl\n\n// Right: Viji creates the main canvas in WEBGL mode\nfunction render(viji, p5) {\n p5.background(32);\n p5.ambientLight(100);\n p5.normalMaterial();\n p5.box(100);\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart).\n\n---\n\n### Using P5 Event Callbacks\n\nP5 event callbacks like `mousePressed()`, `keyPressed()`, `touchStarted()` do not work in Viji's worker environment. Use Viji's interaction APIs instead.\n\n```javascript\n// @renderer p5\n\n// Wrong: these callbacks are never called\nfunction mousePressed() {\n console.log('clicked');\n}\n```\n\n```javascript\n// @renderer p5\n\n// Right: check Viji's interaction state in render()\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) {\n console.log('clicked');\n }\n}\n```\n\n**See also:** [`p5/quickstart`](/p5/quickstart), [`native/pointer`](/native/pointer).\n\n---\n\n## Shader-Specific\n\n### Redeclaring Auto-Injected Code\n\nViji auto-injects `precision`, all built-in uniform declarations, and all parameter uniforms from `@viji-*` directives. Redeclaring any of them causes compilation errors.\n\n```glsl\n// @renderer shader\n\n// Wrong: these are already injected by Viji\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: just write your code, uniforms are available automatically\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(u_time), 1.0);\n}\n```\n\n**See also:** [`shader/basics`](/shader/basics).\n\n---\n\n### Using `u_` Prefix for Custom Parameters\n\nThe `u_` prefix is reserved for Viji's built-in uniforms. Using it for your parameters risks naming collisions.\n\n```glsl\n// Wrong: u_ prefix is reserved\n// @viji-slider:u_speed label:\"Speed\" default:1.0\n```\n\n```glsl\n// Right: use descriptive names without u_ prefix\n// @viji-slider:speed label:\"Speed\" default:1.0\n```\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n### Missing `@renderer shader` Directive\n\nWithout the directive, your GLSL code is treated as JavaScript and throws syntax errors.\n\n```glsl\n// Wrong: no directive, treated as JavaScript\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: directive tells Viji to use the shader renderer\nvoid main() {\n gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}\n```\n\n**See also:** [`shader/quickstart`](/shader/quickstart).\n\n---\n\n### Block Comments for `@viji-*` Parameters\n\nThe `@viji-*` parameter declarations only work with single-line `//` comments. Block comments `/* */` are silently ignored.\n\n```glsl\n// @renderer shader\n\n// Wrong: block comments are not parsed for parameters\n/* @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0 */\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0); // speed is undefined\n}\n```\n\n```glsl\n// @renderer shader\n\n// Right: use single-line comments for parameter declarations\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.0 max:5.0\n\nvoid main() {\n gl_FragColor = vec4(speed, 0.0, 0.0, 1.0);\n}\n```\n\n> [!NOTE]\n> The `@renderer` directive supports both `//` and `/* */` styles, but `@viji-*` parameter declarations require `//`.\n\n**See also:** [`shader/parameters`](/shader/parameters).\n\n---\n\n## Related\n\n- [Best Practices](../best-practices/): cross-renderer authoring patterns and rationale.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer.\n- [Audio](/native/audio), [Video & CV](/native/video): full audio and video API."
1273
1278
  }
1274
1279
  ]
1275
1280
  },
@@ -2421,6 +2426,11 @@ export const docsApi = {
2421
2426
  "level": 2,
2422
2427
  "text": "Return Value"
2423
2428
  },
2429
+ {
2430
+ "id": "value-lifetime",
2431
+ "level": 2,
2432
+ "text": "Value Lifetime"
2433
+ },
2424
2434
  {
2425
2435
  "id": "usage",
2426
2436
  "level": 2,
@@ -2440,7 +2450,7 @@ export const docsApi = {
2440
2450
  "content": [
2441
2451
  {
2442
2452
  "type": "text",
2443
- "markdown": "# viji.image()\n\n```\nimage(defaultValue: null, config: ImageConfig): ImageParameter\n```\n\nCreates an image upload parameter. The host renders it as a file picker or drag-and-drop area. The uploaded image becomes available as an `ImageBitmap`.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | | Always `null`: images are provided by the user |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns an `ImageParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ImageBitmap \\| null` | The uploaded image, or `null` if none provided |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Usage\n\nAlways check for `null` before drawing: the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n if (tex.value) {\n ctx.drawImage(tex.value, 0, 0, viji.width, viji.height);\n }\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
2453
+ "markdown": "# viji.image()\n\n```\nimage(defaultValue: null, config: ImageConfig): ImageParameter\n```\n\nCreates an image upload parameter. The host renders it as a file picker or drag-and-drop area. The uploaded image becomes available as an `ImageBitmap`.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | | Always `null`: images are provided by the user |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns an `ImageParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ImageBitmap \\| null` | The uploaded image, or `null` if none provided |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## Value Lifetime\n\nRead `tex.value` where you draw it, inside `render()`. When the host replaces or clears the image, the previous `ImageBitmap` is closed, so a reference saved from an earlier frame cannot be drawn any more. The parameter object itself is stable; only `.value` changes.\n\n## Usage\n\nAlways check for `null` before drawing: the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n if (tex.value) {\n ctx.drawImage(tex.value, 0, 0, viji.width, viji.height);\n }\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective."
2444
2454
  },
2445
2455
  {
2446
2456
  "type": "live-example",
@@ -5249,6 +5259,11 @@ export const docsApi = {
5249
5259
  "level": 2,
5250
5260
  "text": "The .p5 Property"
5251
5261
  },
5262
+ {
5263
+ "id": "value-lifetime",
5264
+ "level": 2,
5265
+ "text": "Value Lifetime"
5266
+ },
5252
5267
  {
5253
5268
  "id": "usage",
5254
5269
  "level": 2,
@@ -5268,7 +5283,7 @@ export const docsApi = {
5268
5283
  "content": [
5269
5284
  {
5270
5285
  "type": "text",
5271
- "markdown": "# viji.image()\n\n```\nimage(defaultValue: null, config: ImageConfig): ImageParameter\n```\n\nCreates an image upload parameter. The host renders it as a file picker or drag-and-drop area. In P5.js scenes, uploaded images include a special `.p5` property.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | | Always `null`: images are provided by the user |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns an `ImageParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ImageBitmap \\| null` | Raw image, or `null` if none provided |\n| `p5` | `p5.Image` | P5-compatible image object (only in P5 renderer) |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## The `.p5` Property\n\nIn P5.js scenes, every image parameter with a non-null value has a `.p5` property that returns a `p5.Image` object. This lets you use native P5 drawing functions like `p5.image()` and `p5.tint()`:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n }\n}\n```\n\n> [!WARNING]\n> Always use `tex.p5` (not `tex.value` directly) when passing to P5 drawing functions. The raw `ImageBitmap` from `.value` is not P5-compatible.\n\nThe `.p5` image is cached internally: accessing it multiple times per frame does not create new objects.\n\n## Usage\n\nAlways check for `null` before drawing: the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Image' });\nconst opacity = viji.slider(255, { min: 0, max: 255, step: 1, label: 'Opacity' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.tint(255, opacity.value);\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n p5.noTint();\n }\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Defining them inside `setup()` would register the parameter too late: no UI control would appear. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default."
5286
+ "markdown": "# viji.image()\n\n```\nimage(defaultValue: null, config: ImageConfig): ImageParameter\n```\n\nCreates an image upload parameter. The host renders it as a file picker or drag-and-drop area. In P5.js scenes, uploaded images include a special `.p5` property.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | | Always `null`: images are provided by the user |\n| `config.label` | `string` | Yes | | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns an `ImageParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ImageBitmap \\| null` | Raw image, or `null` if none provided |\n| `p5` | `p5.Image` | P5-compatible image object (only in P5 renderer) |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\n## The `.p5` Property\n\nIn P5.js scenes, every image parameter with a non-null value has a `.p5` property that returns a `p5.Image` object. This lets you use native P5 drawing functions like `p5.image()` and `p5.tint()`:\n\n```javascript\nconst tex = viji.image(null, { label: 'Texture' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n }\n}\n```\n\n> [!WARNING]\n> Always use `tex.p5` (not `tex.value` directly) when passing to P5 drawing functions. The raw `ImageBitmap` from `.value` is not P5-compatible.\n\nThe `.p5` image is cached internally: accessing it multiple times per frame does not create new objects. The cache is rebuilt when the image changes and dropped when it clears to `null`.\n\n## Value Lifetime\n\nRead `tex.value` and `tex.p5` where you draw them, inside `render()`. When the host replaces or clears the image, the previous `ImageBitmap` behind `.value` is closed, so a reference saved from an earlier frame cannot be drawn any more. A saved `.p5` object keeps its pixels but stops following the parameter. The parameter object itself is stable; only its properties change.\n\n## Usage\n\nAlways check for `null` before drawing: the user may not have uploaded an image yet:\n\n```javascript\nconst tex = viji.image(null, { label: 'Image' });\nconst opacity = viji.slider(255, { min: 0, max: 255, step: 1, label: 'Opacity' });\n\nfunction render(viji, p5) {\n p5.background(0);\n\n if (tex.value) {\n p5.tint(255, opacity.value);\n p5.image(tex.p5, 0, 0, p5.width, p5.height);\n p5.noTint();\n }\n}\n```\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Defining them inside `setup()` would register the parameter too late: no UI control would appear. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default."
5272
5287
  },
5273
5288
  {
5274
5289
  "type": "live-example",
@@ -1,4 +1,4 @@
1
- import { g as kn } from "./index-BxSadexI.js";
1
+ import { g as kn } from "./index-BFSuPQzu.js";
2
2
  function jn(Oe, ot) {
3
3
  for (var ve = 0; ve < ot.length; ve++) {
4
4
  const Z = ot[ve];