@viji-dev/core 0.20.0 → 0.21.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/README.md +17 -0
- package/dist/artist-dts-p5.js +1 -1
- package/dist/artist-dts.js +1 -1
- package/dist/artist-global-p5.d.ts +101 -1
- package/dist/artist-global.d.ts +101 -1
- package/dist/artist-jsdoc.d.ts +27 -0
- package/dist/assets/{viji.worker-CV0DI4PF.js → viji.worker-vXSUqDKy.js} +44 -44
- package/dist/docs-api.js +166 -20
- package/dist/{essentia-wasm.web-B9-7D9aP.js → essentia-wasm.web-DWW63eRx.js} +1 -1
- package/dist/{index-Dw5Jy7so.js → index-C9OHLFGB.js} +1284 -1063
- package/dist/index.d.ts +222 -11
- package/dist/index.js +6 -5
- package/dist/param-inference.js +1 -0
- package/dist/{scene-analysis-Bsf4JrzY.js → scene-analysis-C7cAsqhv.js} +1 -1
- package/dist/scene-analysis.js +1 -1
- package/package.json +117 -117
package/dist/docs-api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const docsApi = {
|
|
2
2
|
"version": "1.1.0",
|
|
3
|
-
"coreVersion": "0.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"coreVersion": "0.21.0",
|
|
4
|
+
"generatedAt": "2026-10-17T19:43:44.868Z",
|
|
5
5
|
"navigation": [
|
|
6
6
|
{
|
|
7
7
|
"id": "getting-started",
|
|
@@ -156,6 +156,11 @@ export const docsApi = {
|
|
|
156
156
|
"title": "Image",
|
|
157
157
|
"path": "native/parameters/image"
|
|
158
158
|
},
|
|
159
|
+
{
|
|
160
|
+
"id": "native-param-model",
|
|
161
|
+
"title": "Model",
|
|
162
|
+
"path": "native/parameters/model"
|
|
163
|
+
},
|
|
159
164
|
{
|
|
160
165
|
"id": "native-param-button",
|
|
161
166
|
"title": "Button",
|
|
@@ -423,6 +428,11 @@ export const docsApi = {
|
|
|
423
428
|
"title": "Image",
|
|
424
429
|
"path": "p5/parameters/image"
|
|
425
430
|
},
|
|
431
|
+
{
|
|
432
|
+
"id": "p5-param-model",
|
|
433
|
+
"title": "Model",
|
|
434
|
+
"path": "p5/parameters/model"
|
|
435
|
+
},
|
|
426
436
|
{
|
|
427
437
|
"id": "p5-param-button",
|
|
428
438
|
"title": "Button",
|
|
@@ -957,7 +967,7 @@ export const docsApi = {
|
|
|
957
967
|
{
|
|
958
968
|
"type": "live-example",
|
|
959
969
|
"title": "Native: Three.js",
|
|
960
|
-
"sceneCode": "const THREE = await import('https://esm.sh/three@0.
|
|
970
|
+
"sceneCode": "const THREE = await import('https://esm.sh/three@0.180.0');\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, viji.width / viji.height, 0.1, 1000);\ncamera.position.z = 3;\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshNormalMaterial();\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n\nfunction render(viji) {\n cube.rotation.x += speed.value * viji.deltaTime * 0.5;\n cube.rotation.y += speed.value * viji.deltaTime;\n\n if (viji.width !== renderer.domElement.width || viji.height !== renderer.domElement.height) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n }\n\n renderer.render(scene, camera);\n}\n",
|
|
961
971
|
"sceneFile": "renderers-threejs.scene.js"
|
|
962
972
|
},
|
|
963
973
|
{
|
|
@@ -1101,7 +1111,7 @@ export const docsApi = {
|
|
|
1101
1111
|
"content": [
|
|
1102
1112
|
{
|
|
1103
1113
|
"type": "text",
|
|
1104
|
-
"markdown": "# Best Practices\n\nThe rules below apply to every Viji scene, no matter which renderer you use. They cover the few topics where the same constraint has to hold across Native, P5, and Shader at once: animation timing, resolution, audio/video guards, and a handful of safety rules the runtime expects. Get them right and your scenes look correct at any resolution, run smoothly at any frame rate, and stay reliable across devices.\n\nLooking for a specific bug? [Common Mistakes](../common-mistakes/) has wrong/right pairs you can scan.\n\n---\n\n## Animation Timing Across Renderers\n\nViji exposes two timing values that are equivalent across all three renderers:\n\n- **`viji.time`** / **`u_time`**: seconds since the scene started. Use it for animation at a **constant** speed (oscillations, hue cycling, periodic events).\n- **`viji.deltaTime`** / **`u_deltaTime`**: seconds since the last frame. Use it when you accumulate values smoothly regardless of frame rate (movement, physics, fading), or when **animation speed is controlled by a parameter**.\n\n```javascript\n// viji.time: animation at a constant speed\nconst angle = viji.time * 2.0; // fixed 2 rad/s, no parameter involved\nconst x = Math.cos(angle) * radius;\n\n// viji.deltaTime: accumulation that stays smooth at any FPS\nposition += velocity * viji.deltaTime;\nopacity -= fadeRate * viji.deltaTime;\n```\n\n### When to use which\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| Oscillation at **constant** speed | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Depends on absolute position in time |\n| Hue cycling at constant rate | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Needs a continuous, monotonic input |\n| Oscillation at **parameter-driven** speed | accumulator with [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) / [`@viji-accumulator`](/shader/parameters/accumulator) | Avoids phase jumps when the slider changes |\n| Position accumulation | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Must advance the same amount per second, regardless of FPS |\n| Physics / velocity | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Distance = speed × time elapsed |\n| Fading / easing | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Progress should be per-second, not per-frame |\n| Periodic events | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Trigger at fixed time intervals |\n\n**Rule of thumb:** if the speed multiplier is a **constant**, `viji.time * constant` is fine. If the speed comes from a **parameter** (slider, etc.), accumulate with `viji.deltaTime` to prevent jumps.\n\n### Accumulator Pattern: Parameter-Driven Speed\n\nWhen animation speed is controlled by a user parameter, **never** multiply `viji.time` (or `u_time`) by the parameter directly. Changing the slider recalculates the entire phase history with the new value and causes a visible jump. Instead, accumulate `speed × deltaTime` incrementally so the slider only affects future frames.\n\nThe pattern is the same across all three renderers:\n\n```javascript\n// Native\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const wave = Math.sin(phase);\n}\n```\n\n```javascript\n// @renderer p5\nconst spin = viji.slider(1, { min: 0.1, max: 5, label: 'Spin' });\nlet phase = 0;\nfunction render(viji, p5) {\n phase += spin.value * viji.deltaTime;\n p5.rotate(phase);\n}\n```\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n // Use `phase` instead of `u_time * speed`\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\nFor shaders, the [`@viji-accumulator`](/shader/parameters/accumulator) directive integrates `speed × deltaTime` into the uniform automatically: there is no manual loop variable to maintain.\n\n### Nested Multiplication: The Same Trap After Accumulating\n\nAfter adopting the accumulator pattern, a common follow-up mistake is multiplying the accumulated value by another parameter. This causes the same jump because the full accumulated history is rescaled instantly:\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nphase += speed.value * viji.deltaTime; // accumulates correctly\nconst rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase; // smooth at any slider value\n}\n```\n\n**Rule of thumb:** any time a growing value (accumulated phase, elapsed time) is multiplied by a user parameter, the result will jump when that parameter changes. Each such product needs its own `deltaTime`-based accumulator (or its own `@viji-accumulator` in shaders).\n\n> [!NOTE]\n> When animation speed is driven by a parameter, always use the accumulator pattern (`deltaTime` in JS/P5, `@viji-accumulator` in shaders) to prevent phase jumps. The same rule extends to nested multiplications: never multiply an accumulated value by a parameter.\n\n**See also:** [`native/timing`](/native/timing#when-to-use-vijitime-vs-vijideltatime), [`shader/timing`](/shader/timing#speed-controlled-animation-the-accumulator), [`shader/parameters/accumulator`](/shader/parameters/accumulator#basic-usage), [`p5/timing`](/p5/timing#when-to-use-vijitime-vs-vijideltatime).\n\n---\n\n## Resolution Agnosticism Across Renderers\n\nThe host application controls your scene's resolution. It may change at any time (window resize, resolution scaling for performance, high-DPI displays). Never hardcode pixel values or assume a specific canvas size.\n\nThe dimensions are exposed differently per renderer but the rule is identical:\n\n- **Native + P5**: `viji.width` and `viji.height` (also `p5.width` / `p5.height`, which Viji keeps in sync)\n- **Shader**: `u_resolution` (a `vec2`)\n\n### Native and P5\n\n```javascript\n// Good: scales to any resolution\nconst centerX = viji.width / 2;\nconst centerY = viji.height / 2;\nconst radius = Math.min(viji.width, viji.height) * 0.1;\n\n// Bad: breaks at different resolutions\nconst centerX = 960;\nconst centerY = 540;\nconst radius = 50;\n```\n\nFor parameters that control sizes, use normalized values (0-1) and multiply by canvas dimensions:\n\n```javascript\nconst size = viji.slider(0.15, { min: 0.02, max: 0.5, label: 'Size' });\n\nfunction render(viji) {\n const pixelSize = size.value * Math.min(viji.width, viji.height);\n}\n```\n\n### Shader\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0-1 coordinates\n float aspect = u_resolution.x / u_resolution.y;\n // ...\n}\n```\n\n> [!NOTE]\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n**See also:** [`native/canvas-context`](/native/canvas-context), [`p5/canvas-resolution`](/p5/canvas-resolution), [`shader/resolution`](/shader/resolution).\n\n---\n\n## Audio and Video `isConnected` Guards\n\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` before reading audio bands, drawing video frames, or running CV inference. Without the guard your scene reads zeros or undefined values silently, with no indication that the input is missing.\n\nThe rule applies in three places:\n\n1. **Default audio source** (`viji.audio.isConnected`)\n2. **Default video source** (`viji.video.isConnected`)\n3. **Each external device entry** in `viji.devices[]` (`device.audio.isConnected`, `device.video.isConnected`)\n\n```javascript\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n // ... react to audio\n }\n\n if (viji.video.isConnected && viji.video.currentFrame) {\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n }\n\n for (const device of viji.devices) {\n if (device.audio?.isConnected) {\n const level = device.audio.volume.current;\n // ... per-device audio reaction\n }\n if (device.video?.isConnected && device.video.currentFrame) {\n // ... per-device video draw\n }\n }\n}\n```\n\nThe same checks work in P5 (replace canvas calls with `p5.image(...)`). In shaders, default audio and video uniforms safely fall back to `0.0` (or black for the `u_video` texture) when nothing is connected, so a typical scene can simply use the values without an explicit guard. When you read from a specific stream or device, gate on the corresponding boolean uniform: `u_audioStream{N}Connected`, `u_videoStream{N}Connected`, or `u_device{N}Connected`.\n\n**See also:** [`native/audio`](/native/audio), [`native/video`](/native/video), [`p5/audio`](/p5/audio), [`p5/video`](/p5/video), [`shader/audio`](/shader/audio), [`shader/video`](/shader/video).\n\n---\n\n## Worker Environment\n\nScenes run inside a Web Worker. There is no DOM. Use `viji.image()` for images, the `viji.audio` / `viji.video` APIs for media, and `fetch()` for external data.\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n**See also:** [`native/canvas-context`](/native/canvas-context) for the worker context model and the canonical `useContext()` pattern.\n\n---\n\n## Parameters at Top Level\n\nParameter helpers (`viji.slider()`, `viji.color()`, etc.) register UI controls with the host. They must run **once** during initialization, never inside `render()`.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\nFor P5, the same rule applies, with one extra constraint: parameters must also not be declared inside `setup()`, since they need to be registered before `setup()` runs.\n\n**See also:** [`native/parameters`](/native/parameters), [`p5/parameters`](/p5/parameters), [`shader/parameters`](/shader/parameters).\n\n---\n\n## Memory in the Render Loop\n\nAllocating objects, arrays, or strings inside `render()` creates garbage-collection pressure and frame drops. Pre-allocate at the top level and mutate in place.\n\n> [!TIP]\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\n> ```javascript\n> // Good: pre-allocated\n> const pos = { x: 0, y: 0 };\n> function render(viji) {\n> pos.x = viji.width / 2;\n> pos.y = viji.height / 2;\n> }\n>\n> // Bad: creates a new object every frame\n> function render(viji) {\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\n> }\n> ```\n\nThis is especially important for particle systems, arrays of positions, or any data structure that persists across frames.\n\n**See also:** [`native/quickstart`](/native/quickstart), [`p5/quickstart`](/p5/quickstart).\n\n---\n\n## Computer Vision Costs\n\nCV features (face detection, hand tracking, pose detection, etc.) are powerful but expensive. Each feature runs ML inference in its own WebGL context. Understand the relative cost before enabling more than one.\n\n| Feature | Relative Cost | WebGL Contexts | Notes |\n|---------|--------------|----------------|-------|\n| Face Detection | Low | 1 | Bounding box + basic landmarks only |\n| Face Mesh | Medium-High | 1 | 468 facial landmarks, requires more processing |\n| Emotion Detection | High | 1 | 7 expressions + 52 blendshape coefficients |\n| Hand Tracking | Medium | 1 | Up to 2 hands, 21 landmarks each |\n| Pose Detection | Medium | 1 | 33 body landmarks |\n| Body Segmentation | High | 1 | Per-pixel mask, large tensor output |\n\n> [!WARNING]\n> **WebGL Context Limits:** Each CV feature requires its own WebGL context for ML inference. Browsers typically allow 8-16 active WebGL contexts. Enabling too many CV features simultaneously can cause context eviction, potentially breaking the scene's own rendering. Use only the CV features you need.\n\n> [!TIP]\n> **Best practice:** Don't enable CV features by default. Instead, expose a toggle parameter so users can activate them on capable devices:\n> ```javascript\n> const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n> if (useFace.value) {\n> await viji.video.cv.enableFaceDetection(true);\n> }\n> ```\n\nThe cost rule applies in all three renderers. Native and P5 scenes activate CV through `viji.video.cv.enableX()` / `disableX()` from artist code (typically inside a per-frame reconciler that calls `enable*` when a toggle changes). Shader scenes activate CV through the `// @viji-cv:<featureToken>` directive — bare for always-on, or toggleable to synthesize a host-side parameter and a `bool <featureToken>` uniform mirroring the toggle. See [Shader Video & CV](/shader/video) for the directive surface.\n\n**See also:** [`native/video`](/native/video), [`p5/video`](/p5/video), [`shader/video`](/shader/video).\n\n## `currentFrame` vs `analysedFrame`: Pick the Right Trade-off\n\nCV results are computed asynchronously from the video stream. By the time `viji.video.cv.faces` (or `hands` / `pose` / `segmentation`) is available, the camera has moved on. With CV enabled you always have two frames to choose between, and they fail in different ways:\n\n- **`viji.video.currentFrame`** is the just-arrived video frame. It refreshes every frame your scene runs.\n- **`viji.video.cv.analysedFrame`** is the exact frame MediaPipe ran on to produce the current CV results. It refreshes only when an inference completes, which is not every frame.\n\nEach pairing has a different failure mode:\n\n| Choice | Mismatch | What you see |\n|---|---|---|\n| `currentFrame` + CV results | Spatial: landmarks and mask lag the displayed pixels by one inference | Overlay trails the face/body during fast motion |\n| `analysedFrame` + CV results | Temporal: the displayed image freezes briefly between inferences | The video itself stutters or holds, then jumps |\n\nYou cannot avoid both. Pick the mismatch that matters less for your scene.\n\n### How to choose\n\nAsk: *does my effect read pixels from the displayed frame at CV-derived positions?*\n\n- **No.** Drawing landmark dots, particles attached to fingertips, audio-reactive visuals driven by CV positions, generative geometry, abstract scenes that do not display the camera at all: use `currentFrame`. Spatial drift on a small overlay is much easier to live with than the displayed video stuttering.\n- **Yes.** Compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters: use `analysedFrame`. Spatial alignment is the whole point of the effect, and the visible stutter is the cost.\n\nDrawing landmark dots over `currentFrame` is **not** a mistake. It is a deliberate choice to keep the live-camera feel.\n\n```javascript\n// Live camera with landmark dots: currentFrame is the right default\nctx.drawImage(viji.video.currentFrame, 0, 0, w, h);\nviji.video.cv.faces.forEach(face => { /* dots */ });\n\n// Mask compositing or pixel sampling under landmarks: analysedFrame is required\nconst frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\nctx.drawImage(frame, 0, 0, w, h);\n// composite mask against the same frame, or sample skin tone at landmark positions...\n```\n\nFor shaders, `u_video` and `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) follow the same trade-off. Sample `u_videoAnalysed` only when the effect reads pixels at CV-derived positions; otherwise `u_video` is freshest and simplest.\n\n`analysedFrame` is `null` until the first CV inference completes after a feature is enabled (typically under one second). The `?? currentFrame` fallback covers that startup window, but be aware the source switches when the first inference lands; a brief visual hitch is normal.\n\n`analysedFrame` is populated only on the main stream's CV surface (`viji.video.cv`). CV features can be enabled per stream on `viji.videoStreams[i].cv` with the same verbs, but `analysedFrame` stays `null` there (use `currentFrame`). Device videos (`viji.devices[i].video?.cv`) do not run CV; their `analysedFrame` always reads `null`.\n\n---\n\n## Drawing Video at the Right Aspect Ratio\n\nCamera frames almost never match the canvas aspect ratio. Drawing the video to `(0, 0, viji.width, viji.height)` stretches it visibly: faces get squashed or elongated, circles become ovals, and CV bounding boxes drift off the actual face.\n\nThe fix is a small helper that returns the destination rectangle for `drawImage` (or `p5.image`) and lets you map CV coordinates into the same rectangle. The recipe and full code live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use those helpers in every video / CV scene.\n\n### Cover vs Contain\n\n| Mode | What it does | Use when |\n|---|---|---|\n| `cover` (default) | Video fills the canvas, edges cropped when aspects mismatch | Live camera feeds, filter-style scenes, anywhere stretching would look wrong but black bars would too |\n| `contain` | Full video shown, letterboxed when aspects mismatch | CV demos where bounding boxes near frame edges must stay visible, body segmentation where the user might be at the edge of the camera view, severe aspect mismatches |\n\n**Default to `cover`.** It matches the convention from CSS `object-fit: cover` and most camera apps: the camera fills the visible area cleanly, and the small edge crop is normal. Switch to `contain` only when losing the edges of the frame would lose meaning, which is mostly CV-overlay scenes.\n\n**Stretching `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional** (an artistic effect that wants the squash). On every other scene it is a quality bug.\n\n---\n\n## Parameter Categories\n\nParameters that depend on an external input (audio, video / camera / CV, or user interaction) must include the matching `category` so the host UI can show or hide them based on whether that input is active.\n\n| Input | Required `category` |\n|-------|---------------------|\n| Audio-related parameters (volume sensitivity, bass reactivity, beat response) | `'audio'` |\n| Video/camera/CV-related parameters (video opacity, CV sensitivity, segmentation toggles) | `'video'` |\n| Interaction-related parameters (mouse attraction, keyboard speed, touch sensitivity) | `'interaction'` |\n| Everything else (colors, sizes, speeds, shapes) | `'general'` (default: can be omitted) |\n\n**Native / P5:**\n\n```javascript\n// Wrong\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\n\n// Right\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n```\n\n**Shader:** use the `category:` key in `@viji-*` directives:\n\n```glsl\n// @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n```\n\n> [!NOTE]\n> `group` controls layout (which section the parameter appears in). `category` controls visibility (whether the parameter is shown at all). They are orthogonal: use both when appropriate.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [`p5/parameters/categories`](/p5/parameters/categories), [`shader/parameters/categories`](/shader/parameters/categories).\n\n---\n\n## No Redundant Input Toggles\n\nThe host UI controls whether audio, video, and interaction inputs are available to the scene. A scene-level on/off toggle for whether to *react to* an already-available input is redundant: muting host audio achieves the same thing as flipping an \"Audio Reactive\" toggle off, and any creative-strength slider at zero is also already an off-switch.\n\n**The rule.** Don't add `viji.toggle` parameters whose only effect is gating whether the scene reads from `viji.audio`, `viji.video`, `viji.mouse`, `viji.keyboard`, or `viji.touch`. Use `isConnected` / `isInCanvas` guards plus a creative-strength parameter (slider, color, etc.) tagged with the matching `category` instead.\n\n```javascript\n// Wrong: redundant on/off toggles\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\nconst useCamera = viji.toggle(true, { label: 'Use Camera', category: 'video' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n\n// Right: creative-strength parameters with the matching category\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\n**Why.** The host already exposes input switches in its UI. A scene-level \"Audio Reactive (on/off)\" toggle just duplicates that switch one layer down, and any creative-strength slider going to zero (`bassSensitivity = 0`) achieves the off state without an extra control. Two controls for the same thing is friction for the user and a quality bug for our example scenes.\n\n**The exception: CV feature toggles.** `viji.video.cv.enableFaceDetection`, `enableFaceMesh`, `enableHandTracking`, etc. are *not* input toggles; they activate paid features that consume WebGL contexts and ML inference cycles. Those have to stay opt-in. The line: if the toggle's effect is \"should the scene read an already-available input?\" → remove. If it's \"should the runtime spend resources to enable a feature?\" → keep.\n\nIn shader scenes the same rule applies, expressed as a directive: `// @viji-cv:faceDetection label:\"Face Detection\" default:false` (toggleable form) is the shader equivalent of the JS toggle pattern above. Bare `// @viji-cv:faceDetection` is the always-on equivalent and skips the parameter entirely.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [Common Mistakes: Redundant Input Toggles](../common-mistakes/#redundant-input-onoff-toggles).\n\n---\n\n## Canvas Context Selection\n\nUse `viji.useContext()` to obtain a 2D or WebGL context for the scene canvas, not `viji.canvas.getContext()`. A canvas only supports one context type for its entire lifetime: pick one and stay with it.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call `useContext('2d')` and later call `useContext('webgl')` (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n**See also:** [`native/canvas-context`](/native/canvas-context).\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/): symptom catalog with wrong/right pairs.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer."
|
|
1114
|
+
"markdown": "# Best Practices\n\nThe rules below apply to every Viji scene, no matter which renderer you use. They cover the few topics where the same constraint has to hold across Native, P5, and Shader at once: animation timing, resolution, audio/video guards, and a handful of safety rules the runtime expects. Get them right and your scenes look correct at any resolution, run smoothly at any frame rate, and stay reliable across devices.\n\nLooking for a specific bug? [Common Mistakes](../common-mistakes/) has wrong/right pairs you can scan.\n\n---\n\n## Animation Timing Across Renderers\n\nViji exposes two timing values that are equivalent across all three renderers:\n\n- **`viji.time`** / **`u_time`**: seconds since the scene started. Use it for animation at a **constant** speed (oscillations, hue cycling, periodic events).\n- **`viji.deltaTime`** / **`u_deltaTime`**: seconds since the last frame. Use it when you accumulate values smoothly regardless of frame rate (movement, physics, fading), or when **animation speed is controlled by a parameter**.\n\n```javascript\n// viji.time: animation at a constant speed\nconst angle = viji.time * 2.0; // fixed 2 rad/s, no parameter involved\nconst x = Math.cos(angle) * radius;\n\n// viji.deltaTime: accumulation that stays smooth at any FPS\nposition += velocity * viji.deltaTime;\nopacity -= fadeRate * viji.deltaTime;\n```\n\n### When to use which\n\n| Use Case | Property | Why |\n|----------|----------|-----|\n| Oscillation at **constant** speed | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Depends on absolute position in time |\n| Hue cycling at constant rate | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Needs a continuous, monotonic input |\n| Oscillation at **parameter-driven** speed | accumulator with [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) / [`@viji-accumulator`](/shader/parameters/accumulator) | Avoids phase jumps when the slider changes |\n| Position accumulation | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Must advance the same amount per second, regardless of FPS |\n| Physics / velocity | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Distance = speed × time elapsed |\n| Fading / easing | [`viji.deltaTime`](/native/timing#vijideltatime-frame-delta) | Progress should be per-second, not per-frame |\n| Periodic events | [`viji.time`](/native/timing#vijitime-absolute-time) / [`u_time`](/shader/timing#u_time-absolute-time) | Trigger at fixed time intervals |\n\n**Rule of thumb:** if the speed multiplier is a **constant**, `viji.time * constant` is fine. If the speed comes from a **parameter** (slider, etc.), accumulate with `viji.deltaTime` to prevent jumps.\n\n### Accumulator Pattern: Parameter-Driven Speed\n\nWhen animation speed is controlled by a user parameter, **never** multiply `viji.time` (or `u_time`) by the parameter directly. Changing the slider recalculates the entire phase history with the new value and causes a visible jump. Instead, accumulate `speed × deltaTime` incrementally so the slider only affects future frames.\n\nThe pattern is the same across all three renderers:\n\n```javascript\n// Native\nconst speed = viji.slider(1, { min: 0.1, max: 5 });\nlet phase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n const wave = Math.sin(phase);\n}\n```\n\n```javascript\n// @renderer p5\nconst spin = viji.slider(1, { min: 0.1, max: 5, label: 'Spin' });\nlet phase = 0;\nfunction render(viji, p5) {\n phase += spin.value * viji.deltaTime;\n p5.rotate(phase);\n}\n```\n\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n // Use `phase` instead of `u_time * speed`\n float wave = sin(phase);\n gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);\n}\n```\n\nFor shaders, the [`@viji-accumulator`](/shader/parameters/accumulator) directive integrates `speed × deltaTime` into the uniform automatically: there is no manual loop variable to maintain.\n\n### Nested Multiplication: The Same Trap After Accumulating\n\nAfter adopting the accumulator pattern, a common follow-up mistake is multiplying the accumulated value by another parameter. This causes the same jump because the full accumulated history is rescaled instantly:\n\n```javascript\n// Wrong: rotation jumps when rotationSpeed slider changes\nphase += speed.value * viji.deltaTime; // accumulates correctly\nconst rotation = phase * rotationSpeed.value; // jumps when rotationSpeed changes\n```\n\n```javascript\n// Right: each parameter-driven value gets its own accumulator\nlet phase = 0;\nlet rotPhase = 0;\nfunction render(viji) {\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotationSpeed.value * viji.deltaTime;\n const rotation = rotPhase; // smooth at any slider value\n}\n```\n\n**Rule of thumb:** any time a growing value (accumulated phase, elapsed time) is multiplied by a user parameter, the result will jump when that parameter changes. Each such product needs its own `deltaTime`-based accumulator (or its own `@viji-accumulator` in shaders).\n\n> [!NOTE]\n> When animation speed is driven by a parameter, always use the accumulator pattern (`deltaTime` in JS/P5, `@viji-accumulator` in shaders) to prevent phase jumps. The same rule extends to nested multiplications: never multiply an accumulated value by a parameter.\n\n**See also:** [`native/timing`](/native/timing#when-to-use-vijitime-vs-vijideltatime), [`shader/timing`](/shader/timing#speed-controlled-animation-the-accumulator), [`shader/parameters/accumulator`](/shader/parameters/accumulator#basic-usage), [`p5/timing`](/p5/timing#when-to-use-vijitime-vs-vijideltatime).\n\n---\n\n## Resolution Agnosticism Across Renderers\n\nThe host application controls your scene's resolution. It may change at any time (window resize, resolution scaling for performance, high-DPI displays). Never hardcode pixel values or assume a specific canvas size.\n\nThe dimensions are exposed differently per renderer but the rule is identical:\n\n- **Native + P5**: `viji.width` and `viji.height` (also `p5.width` / `p5.height`, which Viji keeps in sync)\n- **Shader**: `u_resolution` (a `vec2`)\n\n### Native and P5\n\n```javascript\n// Good: scales to any resolution\nconst centerX = viji.width / 2;\nconst centerY = viji.height / 2;\nconst radius = Math.min(viji.width, viji.height) * 0.1;\n\n// Bad: breaks at different resolutions\nconst centerX = 960;\nconst centerY = 540;\nconst radius = 50;\n```\n\nFor parameters that control sizes, use normalized values (0-1) and multiply by canvas dimensions:\n\n```javascript\nconst size = viji.slider(0.15, { min: 0.02, max: 0.5, label: 'Size' });\n\nfunction render(viji) {\n const pixelSize = size.value * Math.min(viji.width, viji.height);\n}\n```\n\n### Shader\n\n```glsl\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution; // normalized 0-1 coordinates\n float aspect = u_resolution.x / u_resolution.y;\n // ...\n}\n```\n\n> [!NOTE]\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n**See also:** [`native/canvas-context`](/native/canvas-context), [`p5/canvas-resolution`](/p5/canvas-resolution), [`shader/resolution`](/shader/resolution).\n\n---\n\n## Audio and Video `isConnected` Guards\n\nAudio and video streams are provided by the host and may not always be available. Always check `isConnected` before reading audio bands, drawing video frames, or running CV inference. Without the guard your scene reads zeros or undefined values silently, with no indication that the input is missing.\n\nThe rule applies in three places:\n\n1. **Default audio source** (`viji.audio.isConnected`)\n2. **Default video source** (`viji.video.isConnected`)\n3. **Each external device entry** in `viji.devices[]` (`device.audio.isConnected`, `device.video.isConnected`)\n\n```javascript\nfunction render(viji) {\n if (viji.audio.isConnected) {\n const bass = viji.audio.bands.low;\n // ... react to audio\n }\n\n if (viji.video.isConnected && viji.video.currentFrame) {\n ctx.drawImage(viji.video.currentFrame, 0, 0, viji.width, viji.height);\n }\n\n for (const device of viji.devices) {\n if (device.audio?.isConnected) {\n const level = device.audio.volume.current;\n // ... per-device audio reaction\n }\n if (device.video?.isConnected && device.video.currentFrame) {\n // ... per-device video draw\n }\n }\n}\n```\n\nThe same checks work in P5 (replace canvas calls with `p5.image(...)`). In shaders, default audio and video uniforms safely fall back to `0.0` (or black for the `u_video` texture) when nothing is connected, so a typical scene can simply use the values without an explicit guard. When you read from a specific stream or device, gate on the corresponding boolean uniform: `u_audioStream{N}Connected`, `u_videoStream{N}Connected`, or `u_device{N}Connected`.\n\n**See also:** [`native/audio`](/native/audio), [`native/video`](/native/video), [`p5/audio`](/p5/audio), [`p5/video`](/p5/video), [`shader/audio`](/shader/audio), [`shader/video`](/shader/video).\n\n---\n\n## Worker Environment\n\nScenes run inside a Web Worker. There is no DOM. Use [`viji.image()`](/native/parameters/image) for images and [`viji.model()`](/native/parameters/model) for 3D models, the `viji.audio` / `viji.video` APIs for media, and `fetch()` for external data.\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) are provided through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n**See also:** [`native/canvas-context`](/native/canvas-context) for the worker context model and the canonical `useContext()` pattern.\n\n---\n\n## Parameters at Top Level\n\nParameter helpers (`viji.slider()`, `viji.color()`, etc.) register UI controls with the host. They must run **once** during initialization, never inside `render()`.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\nFor P5, the same rule applies, with one extra constraint: parameters must also not be declared inside `setup()`, since they need to be registered before `setup()` runs.\n\n**See also:** [`native/parameters`](/native/parameters), [`p5/parameters`](/p5/parameters), [`shader/parameters`](/shader/parameters).\n\n---\n\n## Memory in the Render Loop\n\nAllocating objects, arrays, or strings inside `render()` creates garbage-collection pressure and frame drops. Pre-allocate at the top level and mutate in place.\n\n> [!TIP]\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\n> ```javascript\n> // Good: pre-allocated\n> const pos = { x: 0, y: 0 };\n> function render(viji) {\n> pos.x = viji.width / 2;\n> pos.y = viji.height / 2;\n> }\n>\n> // Bad: creates a new object every frame\n> function render(viji) {\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\n> }\n> ```\n\nThis is especially important for particle systems, arrays of positions, or any data structure that persists across frames.\n\n**See also:** [`native/quickstart`](/native/quickstart), [`p5/quickstart`](/p5/quickstart).\n\n---\n\n## Computer Vision Costs\n\nCV features (face detection, hand tracking, pose detection, etc.) are powerful but expensive. Each feature runs ML inference in its own WebGL context. Understand the relative cost before enabling more than one.\n\n| Feature | Relative Cost | WebGL Contexts | Notes |\n|---------|--------------|----------------|-------|\n| Face Detection | Low | 1 | Bounding box + basic landmarks only |\n| Face Mesh | Medium-High | 1 | 468 facial landmarks, requires more processing |\n| Emotion Detection | High | 1 | 7 expressions + 52 blendshape coefficients |\n| Hand Tracking | Medium | 1 | Up to 2 hands, 21 landmarks each |\n| Pose Detection | Medium | 1 | 33 body landmarks |\n| Body Segmentation | High | 1 | Per-pixel mask, large tensor output |\n\n> [!WARNING]\n> **WebGL Context Limits:** Each CV feature requires its own WebGL context for ML inference. Browsers typically allow 8-16 active WebGL contexts. Enabling too many CV features simultaneously can cause context eviction, potentially breaking the scene's own rendering. Use only the CV features you need.\n\n> [!TIP]\n> **Best practice:** Don't enable CV features by default. Instead, expose a toggle parameter so users can activate them on capable devices:\n> ```javascript\n> const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n> if (useFace.value) {\n> await viji.video.cv.enableFaceDetection(true);\n> }\n> ```\n\nThe cost rule applies in all three renderers. Native and P5 scenes activate CV through `viji.video.cv.enableX()` / `disableX()` from artist code (typically inside a per-frame reconciler that calls `enable*` when a toggle changes). Shader scenes activate CV through the `// @viji-cv:<featureToken>` directive — bare for always-on, or toggleable to synthesize a host-side parameter and a `bool <featureToken>` uniform mirroring the toggle. See [Shader Video & CV](/shader/video) for the directive surface.\n\n**See also:** [`native/video`](/native/video), [`p5/video`](/p5/video), [`shader/video`](/shader/video).\n\n## `currentFrame` vs `analysedFrame`: Pick the Right Trade-off\n\nCV results are computed asynchronously from the video stream. By the time `viji.video.cv.faces` (or `hands` / `pose` / `segmentation`) is available, the camera has moved on. With CV enabled you always have two frames to choose between, and they fail in different ways:\n\n- **`viji.video.currentFrame`** is the just-arrived video frame. It refreshes every frame your scene runs.\n- **`viji.video.cv.analysedFrame`** is the exact frame MediaPipe ran on to produce the current CV results. It refreshes only when an inference completes, which is not every frame.\n\nEach pairing has a different failure mode:\n\n| Choice | Mismatch | What you see |\n|---|---|---|\n| `currentFrame` + CV results | Spatial: landmarks and mask lag the displayed pixels by one inference | Overlay trails the face/body during fast motion |\n| `analysedFrame` + CV results | Temporal: the displayed image freezes briefly between inferences | The video itself stutters or holds, then jumps |\n\nYou cannot avoid both. Pick the mismatch that matters less for your scene.\n\n### How to choose\n\nAsk: *does my effect read pixels from the displayed frame at CV-derived positions?*\n\n- **No.** Drawing landmark dots, particles attached to fingertips, audio-reactive visuals driven by CV positions, generative geometry, abstract scenes that do not display the camera at all: use `currentFrame`. Spatial drift on a small overlay is much easier to live with than the displayed video stuttering.\n- **Yes.** Compositing the segmentation mask onto the body, sampling skin tone under a face landmark, warping the face along its mesh, texture-mapped face filters: use `analysedFrame`. Spatial alignment is the whole point of the effect, and the visible stutter is the cost.\n\nDrawing landmark dots over `currentFrame` is **not** a mistake. It is a deliberate choice to keep the live-camera feel.\n\n```javascript\n// Live camera with landmark dots: currentFrame is the right default\nctx.drawImage(viji.video.currentFrame, 0, 0, w, h);\nviji.video.cv.faces.forEach(face => { /* dots */ });\n\n// Mask compositing or pixel sampling under landmarks: analysedFrame is required\nconst frame = viji.video.cv.analysedFrame ?? viji.video.currentFrame;\nctx.drawImage(frame, 0, 0, w, h);\n// composite mask against the same frame, or sample skin tone at landmark positions...\n```\n\nFor shaders, `u_video` and `u_videoAnalysed` (gated by `u_videoAnalysedAvailable`) follow the same trade-off. Sample `u_videoAnalysed` only when the effect reads pixels at CV-derived positions; otherwise `u_video` is freshest and simplest.\n\n`analysedFrame` is `null` until the first CV inference completes after a feature is enabled (typically under one second). The `?? currentFrame` fallback covers that startup window, but be aware the source switches when the first inference lands; a brief visual hitch is normal.\n\n`analysedFrame` is populated only on the main stream's CV surface (`viji.video.cv`). CV features can be enabled per stream on `viji.videoStreams[i].cv` with the same verbs, but `analysedFrame` stays `null` there (use `currentFrame`). Device videos (`viji.devices[i].video?.cv`) do not run CV; their `analysedFrame` always reads `null`.\n\n---\n\n## Drawing Video at the Right Aspect Ratio\n\nCamera frames almost never match the canvas aspect ratio. Drawing the video to `(0, 0, viji.width, viji.height)` stretches it visibly: faces get squashed or elongated, circles become ovals, and CV bounding boxes drift off the actual face.\n\nThe fix is a small helper that returns the destination rectangle for `drawImage` (or `p5.image`) and lets you map CV coordinates into the same rectangle. The recipe and full code live on the per-renderer basics pages: [native](/native/video/basics/#drawing-video), [p5](/p5/video/basics/#drawing-video), [shader](/shader/video/basics/#drawing-video). Use those helpers in every video / CV scene.\n\n### Cover vs Contain\n\n| Mode | What it does | Use when |\n|---|---|---|\n| `cover` (default) | Video fills the canvas, edges cropped when aspects mismatch | Live camera feeds, filter-style scenes, anywhere stretching would look wrong but black bars would too |\n| `contain` | Full video shown, letterboxed when aspects mismatch | CV demos where bounding boxes near frame edges must stay visible, body segmentation where the user might be at the edge of the camera view, severe aspect mismatches |\n\n**Default to `cover`.** It matches the convention from CSS `object-fit: cover` and most camera apps: the camera fills the visible area cleanly, and the small edge crop is normal. Switch to `contain` only when losing the edges of the frame would lose meaning, which is mostly CV-overlay scenes.\n\n**Stretching `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional** (an artistic effect that wants the squash). On every other scene it is a quality bug.\n\n---\n\n## Parameter Categories\n\nParameters that depend on an external input (audio, video / camera / CV, or user interaction) must include the matching `category` so the host UI can show or hide them based on whether that input is active.\n\n| Input | Required `category` |\n|-------|---------------------|\n| Audio-related parameters (volume sensitivity, bass reactivity, beat response) | `'audio'` |\n| Video/camera/CV-related parameters (video opacity, CV sensitivity, segmentation toggles) | `'video'` |\n| Interaction-related parameters (mouse attraction, keyboard speed, touch sensitivity) | `'interaction'` |\n| Everything else (colors, sizes, speeds, shapes) | `'general'` (default: can be omitted) |\n\n**Native / P5:**\n\n```javascript\n// Wrong\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio' });\n\n// Right\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n```\n\n**Shader:** use the `category:` key in `@viji-*` directives:\n\n```glsl\n// @viji-slider:bassSensitivity label:\"Bass Sensitivity\" default:1.5 min:0 max:3 group:audio category:audio\n```\n\n> [!NOTE]\n> `group` controls layout (which section the parameter appears in). `category` controls visibility (whether the parameter is shown at all). They are orthogonal: use both when appropriate.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [`p5/parameters/categories`](/p5/parameters/categories), [`shader/parameters/categories`](/shader/parameters/categories).\n\n---\n\n## No Redundant Input Toggles\n\nThe host UI controls whether audio, video, and interaction inputs are available to the scene. A scene-level on/off toggle for whether to *react to* an already-available input is redundant: muting host audio achieves the same thing as flipping an \"Audio Reactive\" toggle off, and any creative-strength slider at zero is also already an off-switch.\n\n**The rule.** Don't add `viji.toggle` parameters whose only effect is gating whether the scene reads from `viji.audio`, `viji.video`, `viji.mouse`, `viji.keyboard`, or `viji.touch`. Use `isConnected` / `isInCanvas` guards plus a creative-strength parameter (slider, color, etc.) tagged with the matching `category` instead.\n\n```javascript\n// Wrong: redundant on/off toggles\nconst audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\nconst useCamera = viji.toggle(true, { label: 'Use Camera', category: 'video' });\nconst showMouse = viji.toggle(true, { label: 'Mouse Trail', category: 'interaction' });\n\n// Right: creative-strength parameters with the matching category\nconst bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', category: 'audio' });\nconst videoOpacity = viji.slider(0.6, { min: 0, max: 1, label: 'Video Opacity', category: 'video' });\nconst mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', category: 'interaction' });\n```\n\n**Why.** The host already exposes input switches in its UI. A scene-level \"Audio Reactive (on/off)\" toggle just duplicates that switch one layer down, and any creative-strength slider going to zero (`bassSensitivity = 0`) achieves the off state without an extra control. Two controls for the same thing is friction for the user and a quality bug for our example scenes.\n\n**The exception: CV feature toggles.** `viji.video.cv.enableFaceDetection`, `enableFaceMesh`, `enableHandTracking`, etc. are *not* input toggles; they activate paid features that consume WebGL contexts and ML inference cycles. Those have to stay opt-in. The line: if the toggle's effect is \"should the scene read an already-available input?\" → remove. If it's \"should the runtime spend resources to enable a feature?\" → keep.\n\nIn shader scenes the same rule applies, expressed as a directive: `// @viji-cv:faceDetection label:\"Face Detection\" default:false` (toggleable form) is the shader equivalent of the JS toggle pattern above. Bare `// @viji-cv:faceDetection` is the always-on equivalent and skips the parameter entirely.\n\n**See also:** [`native/parameters/categories`](/native/parameters/categories), [Common Mistakes: Redundant Input Toggles](../common-mistakes/#redundant-input-onoff-toggles).\n\n---\n\n## Canvas Context Selection\n\nUse `viji.useContext()` to obtain a 2D or WebGL context for the scene canvas, not `viji.canvas.getContext()`. A canvas only supports one context type for its entire lifetime: pick one and stay with it.\n\n> [!WARNING]\n> A canvas only supports one context type. If you call `useContext('2d')` and later call `useContext('webgl')` (or vice versa), the second call returns `null`. Choose one context type and use it for the entire scene.\n\n**See also:** [`native/canvas-context`](/native/canvas-context).\n\n---\n\n## Related\n\n- [Common Mistakes](../common-mistakes/): symptom catalog with wrong/right pairs.\n- [Renderers Overview](../renderers-overview/): choosing the right renderer."
|
|
1105
1115
|
}
|
|
1106
1116
|
]
|
|
1107
1117
|
},
|
|
@@ -1317,7 +1327,7 @@ export const docsApi = {
|
|
|
1317
1327
|
"content": [
|
|
1318
1328
|
{
|
|
1319
1329
|
"type": "text",
|
|
1320
|
-
"markdown": "# Create Your First Scene\n\nNew to Viji? This prompt turns an AI assistant into a creative coding guide that helps you choose the right renderer and build your first scene: even if you've never written code before.\n\n## How It Works\n\n1. Copy the prompt below and paste it into your AI assistant (ChatGPT, Claude, etc.).\n2. Describe what you want to create: as simple or detailed as you like.\n3. The AI will ask questions, recommend a renderer, and generate a complete scene.\n\n## Renderer Quick Comparison\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| **Language** | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js | GLSL (GPU fragment shader) |\n| **Best for** | Full control, Three.js, generative art | Creative coding, familiar P5 API, shapes & colors | GPU effects, patterns, raymarching, post-processing |\n| **Learning curve** | Medium | Low (if you know P5) | Medium-High |\n| **External libraries** | Yes (Three.js, etc.) | P5.js built-in | No |\n| **3D support** | Yes (WebGL, Three.js) | Yes (via `// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n\n## The Prompt\n\n````\nYou are a creative coding assistant for the Viji platform. Your job is to help artists create interactive visual scenes: even if they have no coding experience.\n\n## YOUR BEHAVIOR\n\n1. Ask the artist what they want to create. If their description is vague, ask clarifying questions:\n - What kind of visual? (patterns, shapes, particles, video effects, 3D, etc.)\n - Should it react to audio/music?\n - Should it use a camera/video?\n - Should it respond to mouse/touch/device tilt?\n - What mood or style? (abstract, organic, geometric, glitchy, minimal, etc.)\n2. Recommend a renderer based on what the artist wants to achieve. Coding experience is NOT a selection criterion: with your help, every renderer is accessible to artists at every level. Choose the renderer whose strengths best fit the goal:\n - **Per-pixel GPU effects** (raymarching, fractals, distance fields, full-screen post-processing, generative patterns that read every pixel) → recommend **Shader**.\n - **3D scenes with Three.js, custom WebGL or WebGPU, pixel-perfect Canvas 2D, or any external library via `await import()`** → recommend **Native**.\n - **Classic creative-coding shapes** (`p5.ellipse`, `p5.line`, `p5.bezier`), **color modes, transforms, particle systems with simple drawing**, or **an existing P5 sketch the artist already has** → recommend **P5**.\n - Audio reactivity, camera/CV reactivity, and parameters work in every renderer: pick based on the visual style, not on data sources.\n - You may briefly mention that Shader (GLSL) and Native + Three.js have a steeper conceptual learning curve, but never use this as the primary selection criterion. The artist asked for help; help them with the renderer that fits the goal.\n3. Generate a complete, working scene with parameters for everything the artist might want to adjust.\n4. Explain what the code does in simple terms.\n5. Suggest ways to iterate and improve, and invite the artist to ask for changes.\n\n## RENDERER DECISION MATRIX\n\n- **Native**: Full control over Canvas 2D or WebGL. Supports `await import()` for external libraries like Three.js. Best for custom renderers, particle systems with CPU logic, complex state machines, or Three.js 3D scenes.\n- **P5**: Uses P5.js v1.9.4 with familiar `setup()`/`render()` pattern. Best for creative coding with shapes, colors, transforms, text. Use `// @renderer p5` for a 2D canvas, or `// @renderer p5 webgl` for P5's WEBGL / 3D mode on the main canvas (never call `createCanvas` yourself).\n- **Shader**: GLSL fragment shader on a GPU fullscreen quad. Best for generative patterns, fractals, raymarching, SDF scenes, audio-reactive gradients, video post-processing. Extremely fast: runs entirely on the GPU.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules, tables, and templates 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 resources. Use them to verify exact API names, types, and uniform availability for the renderer you recommend.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (e.g., specific P5.js function syntax, advanced CV data structures, behavior nuances, or full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference (all auto-injected uniforms with types and descriptions): 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- Bundled Viji + P5.js v1.9.4 types (large file: only fetch when verifying P5 syntax): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- P5.js v1.9.4 reference (HTML): https://p5js.org/reference/\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## VIJI ARCHITECTURE (all renderers)\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is NO DOM access.\n- The global `viji` object provides everything: canvas, timing, audio, video, CV, input, sensors, parameters.\n- **Top-level code** runs once (parameters, state, imports).\n- **`render(viji)` / `render(viji, p5)` / `void main()`** runs every frame.\n- `fetch()` is available. `window`, `document`, `Image()` are NOT.\n\n## SCENE STRUCTURE PER RENDERER\n\n### Native\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx...\n}\n```\n\n### P5\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.circle(), p5.rect(), etc. (all need p5. prefix)\n}\n```\n\n### Shader (GLSL)\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## PARAMETERS (all renderers)\n\nArtists control scenes through parameters: declared once, shown as UI controls.\n\n**Native/P5 syntax** (top-level):\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // → .value: number\nviji.color(default, { label, alpha }) // → .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb, .hsb, .alpha\nviji.gradient([{ color, position }], { label, minStops, maxStops, alpha }) // → .value: sorted stops, .at(t): hex at t // default required\nviji.toggle(default, { label }) // → .value: boolean\nviji.select(default, { options: [...], label }) // → .value: string|number\nviji.number(default, { min, max, step, label }) // → .value: number\nviji.text(default, { label, maxLength }) // → .value: string\nviji.image(null, { label }) // → .value: ImageBitmap|null\nviji.button({ label }) // → .value: boolean (1 frame)\nviji.coordinate(defaultValue, { step, label, description, group, category }) // → .value: { x, y } // both -1 to 1\n```\n\n**Shader syntax** (comment directives):\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 → uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 → uniform vec3 name; (alpha:true → vec4)\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] → vec3 name(float t) sampler // stops are #hex@position; alpha:true → vec4\n// @viji-toggle:name label:\"Toggle\" default:false → uniform bool name;\n// @viji-select:name label:\"Mode\" default:\"A\" options:[\"A\",\"B\"] → uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 → uniform float name;\n// @viji-image:name label:\"Texture\" → uniform sampler2D name;\n// @viji-button:name label:\"Reset\" → uniform bool name;\n// @viji-coordinate:name label:\"Label\" default:[0.0,0.0] → uniform vec2 name; // both -1 to 1\n// @viji-accumulator:name rate:speed → uniform float name;\n```\n\n## AUDIO: `viji.audio` / Shader uniforms\n\nALWAYS check `isConnected` / `u_audioVolume > 0.0` before using audio data.\n\nKey members (Native/P5):\n- `isConnected`\n- `volume.current`, `volume.peak`, `volume.smoothed` (smoothed = 200ms decay envelope of `current`)\n- `bands.low`, `bands.lowMid`, `bands.mid`, `bands.highMid`, `bands.high` (instant band energies, 0-1)\n- `bands.lowSmoothed`, `bands.lowMidSmoothed`, `bands.midSmoothed`, `bands.highMidSmoothed`, `bands.highSmoothed` (each is a 150ms decay envelope of the matching instant band, sibling property; not nested)\n- `beat.kick`, `beat.snare`, `beat.hat`, `beat.any` (instant energies, 0-1)\n- `beat.kickSmoothed`, `beat.snareSmoothed`, `beat.hatSmoothed`, `beat.anySmoothed` (500ms decay envelopes, sibling properties)\n- `beat.triggers.kick`, `beat.triggers.snare`, `beat.triggers.hat`, `beat.triggers.any` (boolean, true for one frame on the corresponding beat)\n- `beat.bpm`, `beat.confidence`, `beat.isLocked`\n- `spectral.brightness`, `spectral.flatness`\n- `getFrequencyData()` -> `Uint8Array`, `getWaveform()` -> `Float32Array`\n\nKey shader uniforms: `u_audioVolume`, `u_audioLow`-`u_audioHigh`, `u_audioKick`-`u_audioAny`, `u_audioKickTrigger`-`u_audioAnyTrigger`, `u_audioBPM`, `u_audioBrightness`, `u_audioFlatness`, `u_audioFFT`, `u_audioWaveform`. Smoothed scalar variants exist as `u_audioVolumeSmoothed`, `u_audioLowSmoothed`-`u_audioHighSmoothed`, `u_audioKickSmoothed`-`u_audioAnySmoothed`.\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` exposing the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` (and each `*Smoothed` sibling), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as above. **No** beat detection, BPM, triggers, or events: those exist only on the main `viji.audio`.\n\n## VIDEO & CV: `viji.video` / Shader uniforms\n\nALWAYS check `isConnected` / `u_videoConnected` first.\n\nKey members (Native/P5):\n- `isConnected`, `currentFrame`, `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`\n- CV toggle: `cv.enableFaceDetection(bool)`, `cv.enableFaceMesh(bool)`, `cv.enableEmotionDetection(bool)`, `cv.enableHandTracking(bool)`, `cv.enablePoseDetection(bool)`, `cv.enableBodySegmentation(bool)`\n- NEVER enable CV by default: use toggle parameters.\n- Data: `faces[]` (FaceData), `hands[]` (HandData), `pose` (PoseData|null), `segmentation` (SegmentationData|null)\n\nShader CV activation: use `// @viji-cv:<featureToken>` directives at the top of the shader. Bare form (`// @viji-cv:faceDetection`) is always-on, no UI parameter. Toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) synthesizes a host-side toggle parameter keyed by the feature token, plus a `bool <featureToken>` shader uniform mirroring the toggle. Six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\nKey shader uniforms: `u_video`, `u_videoResolution`, `u_videoConnected`, `u_faceCount`, `u_face0*`, `u_handCount`, `u_leftHand*`, `u_rightHand*`, `u_poseDetected`, `u_pose*Position`, `u_segmentationMask`.\n\n## INPUT\n\n**Pointer** (unified): `viji.pointer.x/y`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas` / Shader: `u_pointer`, `u_pointerDown`, `u_pointerWasPressed`\n**Mouse**: `viji.mouse.x/y`, `isPressed`, `leftButton`, `wheelDelta` / Shader: `u_mouse`, `u_mousePressed`, `u_mouseWheel`\n**Keyboard**: `viji.keyboard.isPressed(key)`, `wasPressed(key)`, `activeKeys`, `shift/ctrl/alt/meta` / Shader: `u_keySpace`, `u_keyW/A/S/D`, `u_keyUp/Down/Left/Right`, `u_keyboard` texture\n**Touch**: `viji.touches.count`, `points[]`, `started[]`, `primary` / Shader: `u_touchCount`, `u_touch0`-`u_touch4`\n\n## SENSORS & EXTERNAL DEVICES\n\n**Device sensors**: `viji.device.motion` (acceleration, rotationRate), `viji.device.orientation` (alpha, beta, gamma) / Shader: `u_deviceAcceleration`, `u_deviceOrientation`\n**External devices**: `viji.devices[]` (each entry: `id`, `name`, `motion`, `orientation`, `video`, `audio`) / Shader: `u_device0`-`u_device7` camera textures, plus per-device sensor and connection uniforms. Device audio is exposed as `u_audioStream{i}*` scalar uniforms (volume, bands, spectral; no beat/BPM/FFT/waveform: those are main audio only).\n\n## CRITICAL RULES (all renderers)\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`. `fetch()` IS available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` / `main()`.\n3. ALWAYS use `viji.width`/`viji.height` or `u_resolution`: NEVER hardcode pixel sizes.\n4. ALWAYS use `viji.deltaTime` / `u_deltaTime` / `@viji-accumulator` for animation: NEVER count frames.\n5. NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`), it causes animation jumps when the parameter changes. Use a `deltaTime` accumulator instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter, give each speed its own accumulator.\n6. NEVER allocate objects/arrays inside `render()`: pre-allocate at top level.\n7. ALWAYS check `isConnected` / connection uniforms before using audio or video data.\n8. NEVER enable CV features by default: use toggle parameters.\n9. ALWAYS set `category` on input-dependent parameters: `category: 'audio'` for audio controls, `category: 'video'` for video/CV controls, `category: 'interaction'` for mouse/keyboard/touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input.\n10. In P5: ALWAYS prefix every P5 function/constant with `p5.`. NEVER use `createCanvas()`.\n11. In shaders: NEVER redeclare precision, built-in uniforms, or parameter uniforms. NEVER use `u_` prefix for parameter names.\n\n## FOR ADVANCED FEATURES\n\nWhen the artist needs the full API surface (detailed CV data structures, all 52 face blendshapes, full external-device interfaces, backbuffer/feedback in shaders, host-supplied additional sources `viji.videoStreams[]` / `viji.audioStreams[]` used by Viji's compositor), use the renderer-specific prompts:\n- **Native**: Use the \"Prompt: Native Scenes\" page for the complete API reference\n- **P5**: Use the \"Prompt: P5 Scenes\" page for the complete API reference + P5 mapping\n- **Shader**: Use the \"Prompt: Shader Scenes\" page for all 270+ uniforms and directive details\n\nNow help the artist create their Viji scene. Start by asking what they want to build.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. Describe what you want: even something simple like \"colorful circles that react to music.\"\n4. The AI will guide you through choosing a renderer and building a scene.\n\n> [!TIP]\n> Don't worry about technical details: the AI will handle those. Focus on describing what you want to **see** and **feel**. Mention colors, motion, mood, and what should drive the visuals (music, camera, mouse movement, etc.).\n\n## Next Steps\n\nOnce you've created your first scene and want more control, use the full renderer-specific prompts:\n\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): exhaustive Native API prompt\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): exhaustive P5 API prompt\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt): exhaustive Shader API prompt\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n\n## Related\n\n- [Overview](/getting-started/overview): what Viji is and how it works\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
|
|
1330
|
+
"markdown": "# Create Your First Scene\n\nNew to Viji? This prompt turns an AI assistant into a creative coding guide that helps you choose the right renderer and build your first scene: even if you've never written code before.\n\n## How It Works\n\n1. Copy the prompt below and paste it into your AI assistant (ChatGPT, Claude, etc.).\n2. Describe what you want to create: as simple or detailed as you like.\n3. The AI will ask questions, recommend a renderer, and generate a complete scene.\n\n## Renderer Quick Comparison\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| **Language** | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js | GLSL (GPU fragment shader) |\n| **Best for** | Full control, Three.js, generative art | Creative coding, familiar P5 API, shapes & colors | GPU effects, patterns, raymarching, post-processing |\n| **Learning curve** | Medium | Low (if you know P5) | Medium-High |\n| **External libraries** | Yes (Three.js, etc.) | P5.js built-in | No |\n| **3D support** | Yes (WebGL, Three.js) | Yes (via `// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n\n## The Prompt\n\n````\nYou are a creative coding assistant for the Viji platform. Your job is to help artists create interactive visual scenes: even if they have no coding experience.\n\n## YOUR BEHAVIOR\n\n1. Ask the artist what they want to create. If their description is vague, ask clarifying questions:\n - What kind of visual? (patterns, shapes, particles, video effects, 3D, etc.)\n - Should it react to audio/music?\n - Should it use a camera/video?\n - Should it respond to mouse/touch/device tilt?\n - What mood or style? (abstract, organic, geometric, glitchy, minimal, etc.)\n2. Recommend a renderer based on what the artist wants to achieve. Coding experience is NOT a selection criterion: with your help, every renderer is accessible to artists at every level. Choose the renderer whose strengths best fit the goal:\n - **Per-pixel GPU effects** (raymarching, fractals, distance fields, full-screen post-processing, generative patterns that read every pixel) → recommend **Shader**.\n - **3D scenes with Three.js, custom WebGL or WebGPU, pixel-perfect Canvas 2D, or any external library via `await import()`** → recommend **Native**.\n - **Classic creative-coding shapes** (`p5.ellipse`, `p5.line`, `p5.bezier`), **color modes, transforms, particle systems with simple drawing**, or **an existing P5 sketch the artist already has** → recommend **P5**.\n - Audio reactivity, camera/CV reactivity, and parameters work in every renderer: pick based on the visual style, not on data sources.\n - You may briefly mention that Shader (GLSL) and Native + Three.js have a steeper conceptual learning curve, but never use this as the primary selection criterion. The artist asked for help; help them with the renderer that fits the goal.\n3. Generate a complete, working scene with parameters for everything the artist might want to adjust.\n4. Explain what the code does in simple terms.\n5. Suggest ways to iterate and improve, and invite the artist to ask for changes.\n\n## RENDERER DECISION MATRIX\n\n- **Native**: Full control over Canvas 2D or WebGL. Supports `await import()` for external libraries like Three.js. Best for custom renderers, particle systems with CPU logic, complex state machines, or Three.js 3D scenes.\n- **P5**: Uses P5.js v1.9.4 with familiar `setup()`/`render()` pattern. Best for creative coding with shapes, colors, transforms, text. Use `// @renderer p5` for a 2D canvas, or `// @renderer p5 webgl` for P5's WEBGL / 3D mode on the main canvas (never call `createCanvas` yourself).\n- **Shader**: GLSL fragment shader on a GPU fullscreen quad. Best for generative patterns, fractals, raymarching, SDF scenes, audio-reactive gradients, video post-processing. Extremely fast: runs entirely on the GPU.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules, tables, and templates 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 resources. Use them to verify exact API names, types, and uniform availability for the renderer you recommend.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (e.g., specific P5.js function syntax, advanced CV data structures, behavior nuances, or full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- Shader uniforms reference (all auto-injected uniforms with types and descriptions): 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- Bundled Viji + P5.js v1.9.4 types (large file: only fetch when verifying P5 syntax): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- P5.js v1.9.4 reference (HTML): https://p5js.org/reference/\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## VIJI ARCHITECTURE (all renderers)\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is NO DOM access.\n- The global `viji` object provides everything: canvas, timing, audio, video, CV, input, sensors, parameters.\n- **Top-level code** runs once (parameters, state, imports).\n- **`render(viji)` / `render(viji, p5)` / `void main()`** runs every frame.\n- `fetch()` is available. `window`, `document`, `Image()` are NOT.\n\n## SCENE STRUCTURE PER RENDERER\n\n### Native\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx...\n}\n```\n\n### P5\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.circle(), p5.rect(), etc. (all need p5. prefix)\n}\n```\n\n### Shader (GLSL)\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## PARAMETERS (all renderers)\n\nArtists control scenes through parameters: declared once, shown as UI controls.\n\n**Native/P5 syntax** (top-level):\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // → .value: number\nviji.color(default, { label, alpha }) // → .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb, .hsb, .alpha\nviji.gradient([{ color, position }], { label, minStops, maxStops, alpha }) // → .value: sorted stops, .at(t): hex at t // default required\nviji.toggle(default, { label }) // → .value: boolean\nviji.select(default, { options: [...], label }) // → .value: string|number\nviji.number(default, { min, max, step, label }) // → .value: number\nviji.text(default, { label, maxLength }) // → .value: string\nviji.image(null, { label }) // → .value: ImageBitmap|null\nviji.model(null, { label, formats }) // → .value: ModelValue|null ({ bytes, url, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }); formats is required: list only the formats your loader parses; parse with your own loader once per new value\nviji.button({ label }) // → .value: boolean (1 frame)\nviji.coordinate(defaultValue, { step, label, description, group, category }) // → .value: { x, y } // both -1 to 1\n```\n\n**Shader syntax** (comment directives):\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 → uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 → uniform vec3 name; (alpha:true → vec4)\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] → vec3 name(float t) sampler // stops are #hex@position; alpha:true → vec4\n// @viji-toggle:name label:\"Toggle\" default:false → uniform bool name;\n// @viji-select:name label:\"Mode\" default:\"A\" options:[\"A\",\"B\"] → uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 → uniform float name;\n// @viji-image:name label:\"Texture\" → uniform sampler2D name;\n// @viji-button:name label:\"Reset\" → uniform bool name;\n// @viji-coordinate:name label:\"Label\" default:[0.0,0.0] → uniform vec2 name; // both -1 to 1\n// @viji-accumulator:name rate:speed → uniform float name;\n```\n\n## AUDIO: `viji.audio` / Shader uniforms\n\nALWAYS check `isConnected` / `u_audioVolume > 0.0` before using audio data.\n\nKey members (Native/P5):\n- `isConnected`\n- `volume.current`, `volume.peak`, `volume.smoothed` (smoothed = 200ms decay envelope of `current`)\n- `bands.low`, `bands.lowMid`, `bands.mid`, `bands.highMid`, `bands.high` (instant band energies, 0-1)\n- `bands.lowSmoothed`, `bands.lowMidSmoothed`, `bands.midSmoothed`, `bands.highMidSmoothed`, `bands.highSmoothed` (each is a 150ms decay envelope of the matching instant band, sibling property; not nested)\n- `beat.kick`, `beat.snare`, `beat.hat`, `beat.any` (instant energies, 0-1)\n- `beat.kickSmoothed`, `beat.snareSmoothed`, `beat.hatSmoothed`, `beat.anySmoothed` (500ms decay envelopes, sibling properties)\n- `beat.triggers.kick`, `beat.triggers.snare`, `beat.triggers.hat`, `beat.triggers.any` (boolean, true for one frame on the corresponding beat)\n- `beat.bpm`, `beat.confidence`, `beat.isLocked`\n- `spectral.brightness`, `spectral.flatness`\n- `getFrequencyData()` -> `Uint8Array`, `getWaveform()` -> `Float32Array`\n\nKey shader uniforms: `u_audioVolume`, `u_audioLow`-`u_audioHigh`, `u_audioKick`-`u_audioAny`, `u_audioKickTrigger`-`u_audioAnyTrigger`, `u_audioBPM`, `u_audioBrightness`, `u_audioFlatness`, `u_audioFFT`, `u_audioWaveform`. Smoothed scalar variants exist as `u_audioVolumeSmoothed`, `u_audioLowSmoothed`-`u_audioHighSmoothed`, `u_audioKickSmoothed`-`u_audioAnySmoothed`.\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` exposing the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` (and each `*Smoothed` sibling), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as above. **No** beat detection, BPM, triggers, or events: those exist only on the main `viji.audio`.\n\n## VIDEO & CV: `viji.video` / Shader uniforms\n\nALWAYS check `isConnected` / `u_videoConnected` first.\n\nKey members (Native/P5):\n- `isConnected`, `currentFrame`, `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`\n- CV toggle: `cv.enableFaceDetection(bool)`, `cv.enableFaceMesh(bool)`, `cv.enableEmotionDetection(bool)`, `cv.enableHandTracking(bool)`, `cv.enablePoseDetection(bool)`, `cv.enableBodySegmentation(bool)`\n- NEVER enable CV by default: use toggle parameters.\n- Data: `faces[]` (FaceData), `hands[]` (HandData), `pose` (PoseData|null), `segmentation` (SegmentationData|null)\n\nShader CV activation: use `// @viji-cv:<featureToken>` directives at the top of the shader. Bare form (`// @viji-cv:faceDetection`) is always-on, no UI parameter. Toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) synthesizes a host-side toggle parameter keyed by the feature token, plus a `bool <featureToken>` shader uniform mirroring the toggle. Six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\nKey shader uniforms: `u_video`, `u_videoResolution`, `u_videoConnected`, `u_faceCount`, `u_face0*`, `u_handCount`, `u_leftHand*`, `u_rightHand*`, `u_poseDetected`, `u_pose*Position`, `u_segmentationMask`.\n\n## INPUT\n\n**Pointer** (unified): `viji.pointer.x/y`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas` / Shader: `u_pointer`, `u_pointerDown`, `u_pointerWasPressed`\n**Mouse**: `viji.mouse.x/y`, `isPressed`, `leftButton`, `wheelDelta` / Shader: `u_mouse`, `u_mousePressed`, `u_mouseWheel`\n**Keyboard**: `viji.keyboard.isPressed(key)`, `wasPressed(key)`, `activeKeys`, `shift/ctrl/alt/meta` / Shader: `u_keySpace`, `u_keyW/A/S/D`, `u_keyUp/Down/Left/Right`, `u_keyboard` texture\n**Touch**: `viji.touches.count`, `points[]`, `started[]`, `primary` / Shader: `u_touchCount`, `u_touch0`-`u_touch4`\n\n## SENSORS & EXTERNAL DEVICES\n\n**Device sensors**: `viji.device.motion` (acceleration, rotationRate), `viji.device.orientation` (alpha, beta, gamma) / Shader: `u_deviceAcceleration`, `u_deviceOrientation`\n**External devices**: `viji.devices[]` (each entry: `id`, `name`, `motion`, `orientation`, `video`, `audio`) / Shader: `u_device0`-`u_device7` camera textures, plus per-device sensor and connection uniforms. Device audio is exposed as `u_audioStream{i}*` scalar uniforms (volume, bands, spectral; no beat/BPM/FFT/waveform: those are main audio only).\n\n## CRITICAL RULES (all renderers)\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`. `fetch()` IS available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` / `main()`.\n3. ALWAYS use `viji.width`/`viji.height` or `u_resolution`: NEVER hardcode pixel sizes.\n4. ALWAYS use `viji.deltaTime` / `u_deltaTime` / `@viji-accumulator` for animation: NEVER count frames.\n5. NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`), it causes animation jumps when the parameter changes. Use a `deltaTime` accumulator instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter, give each speed its own accumulator.\n6. NEVER allocate objects/arrays inside `render()`: pre-allocate at top level.\n7. ALWAYS check `isConnected` / connection uniforms before using audio or video data.\n8. NEVER enable CV features by default: use toggle parameters.\n9. ALWAYS set `category` on input-dependent parameters: `category: 'audio'` for audio controls, `category: 'video'` for video/CV controls, `category: 'interaction'` for mouse/keyboard/touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input.\n10. In P5: ALWAYS prefix every P5 function/constant with `p5.`. NEVER use `createCanvas()`.\n11. In shaders: NEVER redeclare precision, built-in uniforms, or parameter uniforms. NEVER use `u_` prefix for parameter names.\n\n## FOR ADVANCED FEATURES\n\nWhen the artist needs the full API surface (detailed CV data structures, all 52 face blendshapes, full external-device interfaces, backbuffer/feedback in shaders, host-supplied additional sources `viji.videoStreams[]` / `viji.audioStreams[]` used by Viji's compositor), use the renderer-specific prompts:\n- **Native**: Use the \"Prompt: Native Scenes\" page for the complete API reference\n- **P5**: Use the \"Prompt: P5 Scenes\" page for the complete API reference + P5 mapping\n- **Shader**: Use the \"Prompt: Shader Scenes\" page for all 270+ uniforms and directive details\n\nNow help the artist create their Viji scene. Start by asking what they want to build.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. Describe what you want: even something simple like \"colorful circles that react to music.\"\n4. The AI will guide you through choosing a renderer and building a scene.\n\n> [!TIP]\n> Don't worry about technical details: the AI will handle those. Focus on describing what you want to **see** and **feel**. Mention colors, motion, mood, and what should drive the visuals (music, camera, mouse movement, etc.).\n\n## Next Steps\n\nOnce you've created your first scene and want more control, use the full renderer-specific prompts:\n\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): exhaustive Native API prompt\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): exhaustive P5 API prompt\n- [Prompt: Shader Scenes](/ai-prompts/shader-prompt): exhaustive Shader API prompt\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n\n## Related\n\n- [Overview](/getting-started/overview): what Viji is and how it works\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
|
|
1321
1331
|
}
|
|
1322
1332
|
]
|
|
1323
1333
|
},
|
|
@@ -1345,7 +1355,7 @@ export const docsApi = {
|
|
|
1345
1355
|
"content": [
|
|
1346
1356
|
{
|
|
1347
1357
|
"type": "text",
|
|
1348
|
-
"markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene: a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples of useful questions: \"Should this react to audio or stay purely visual?\", \"Should it use the camera or only mouse input?\", \"Roughly how dense / how minimal do you want it?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust (speed, density, colors, mode toggles).\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"make it smoother\", \"add a trail\", \"make it audio-reactive on the kick\"). Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact API names and types.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, behavior nuances, full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Use them instead of parsing hex by hand. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time`: elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\n - `viji.deltaTime`: seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER multiply `viji.time` by a parameter for animation speed: it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\n ```javascript\n // WRONG: jumps when speed changes:\n const t = viji.time * speed.value;\n // RIGHT: smooth:\n let phase = 0; // top level\n phase += speed.value * viji.deltaTime; // in render()\n ```\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\n ```javascript\n // WRONG: jumps when rotSpeed changes:\n const rot = phase * rotSpeed.value;\n // RIGHT: give it its own accumulator:\n let rotPhase = 0; // top level\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\n ```\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')`: Canvas 2D\n - `viji.useContext('webgl')`: WebGL 1\n - `viji.useContext('webgl2')`: WebGL 2\n - `viji.useContext('webgpu')`: WebGPU\n Calling a different type after the first returns `null`.\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n12. Be mindful of WebGL context limits: each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n14. ALWAYS type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so errors can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\n ```javascript\n function render(viji: VijiAPI) { ... }\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/native/api-reference) lists every `viji` member with its exact type.\n15. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Get WebGPU context (null when unsupported) |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.gpu` | `GPUCanvasContext` | Shortcut (after useContext('webgpu')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results (`faces`, `hands`, `pose`, `segmentation`) live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds. Define this `videoFit` helper at module scope and use it for every video / CV scene:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (default) or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0-1 to the source video frame, not the canvas.\n// Map them through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live camera feeds (fills the canvas, edges cropped). Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible (full frame shown with letterbox bars). Stretching directly to `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n\n**`currentFrame` vs `analysedFrame`.** Default to `currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need (4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\n- `confidence: number | null` (0..1) — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\n- `headPose: {pitch, yaw, roll} | null` (degrees) — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow help the artist build a Viji native scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want: be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Native Quick Start](/native/quickstart): your first Viji native scene\n- [Native API Reference](/native/api-reference): full API reference\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
|
|
1358
|
+
"markdown": "# Prompt: Native Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji to generate a correct, working native scene.\n\n## The Prompt\n\n````\nYou are generating a Viji native scene: a creative visual that runs inside an OffscreenCanvas Web Worker.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples of useful questions: \"Should this react to audio or stay purely visual?\", \"Should it use the camera or only mouse input?\", \"Roughly how dense / how minimal do you want it?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust (speed, density, colors, mode toggles).\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes (\"make it smoother\", \"add a trail\", \"make it audio-reactive on the kick\"). Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resource. Use it to verify exact API names and types.\n- ON DEMAND: fetch from the Tier-2 resource when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, behavior nuances, full examples).\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or uniform names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state, imports). Top-level `await` is supported for dynamic imports.\n- **`function render(viji) { ... }`** is called every frame. This is where you draw.\n- There is **no `setup()` function** in native scenes. All initialization goes at the top level.\n\n## RULES\n\n1. NEVER access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` ARE available.\n2. ALWAYS declare parameters at the TOP LEVEL, never inside `render()`:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n function render(viji) { /* use speed.value */ }\n ```\n3. ALWAYS read parameters via `.value`: `speed.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Use them instead of parsing hex by hand. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n5. ALWAYS use `viji.time` or `viji.deltaTime` for animation. NEVER count frames or assume a fixed frame rate.\n - `viji.time`: elapsed seconds. Use for constant-speed oscillations only: `sin(viji.time * 2.0)`.\n - `viji.deltaTime`: seconds since last frame. Use for accumulators: `angle += speed.value * viji.deltaTime;`\n6. NEVER multiply `viji.time` by a parameter for animation speed: it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator:\n ```javascript\n // WRONG: jumps when speed changes:\n const t = viji.time * speed.value;\n // RIGHT: smooth:\n let phase = 0; // top level\n phase += speed.value * viji.deltaTime; // in render()\n ```\n This also applies to **nested** multiplications. If `phase` is already an accumulator, NEVER multiply it by another parameter:\n ```javascript\n // WRONG: jumps when rotSpeed changes:\n const rot = phase * rotSpeed.value;\n // RIGHT: give it its own accumulator:\n let rotPhase = 0; // top level\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime; // in render()\n ```\n7. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. ALWAYS call `viji.useContext()` to get a canvas context. Choose ONE type and use it for the entire scene:\n - `viji.useContext('2d')`: Canvas 2D\n - `viji.useContext('webgl')`: WebGL 1\n - `viji.useContext('webgl2')`: WebGL 2\n - `viji.useContext('webgpu')`: WebGPU\n Calling a different type after the first returns `null`.\n9. ALWAYS check `viji.audio.isConnected` before using audio data.\n10. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n11. NEVER enable CV features by default. Use a toggle parameter so the user can opt in:\n ```javascript\n const useFace = viji.toggle(false, { label: 'Enable Face Detection', category: 'video' });\n // In render:\n if (useFace.value) await viji.video.cv.enableFaceDetection(true);\n else await viji.video.cv.enableFaceDetection(false);\n ```\n12. Be mindful of WebGL context limits: each CV feature uses its own WebGL context for ML. Enabling too many can cause context loss.\n13. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n14. ALWAYS type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so errors can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\n ```javascript\n function render(viji: VijiAPI) { ... }\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/native/api-reference) lists every `viji` member with its exact type.\n15. For external libraries, use dynamic import with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.180.0');\n ```\n Pass `viji.canvas` to the library's renderer. ALWAYS pass `false` as the third argument to Three.js `setSize()`.\n\n## COMPLETE API REFERENCE\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Get WebGPU context (null when unsupported) |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.gpu` | `GPUCanvasContext` | Shortcut (after useContext('webgpu')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required: list only the formats your loader parses; parse with your own loader once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results (`faces`, `hands`, `pose`, `segmentation`) live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\n**Drawing video: preserve aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds. Define this `videoFit` helper at module scope and use it for every video / CV scene:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (default) or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0-1 to the source video frame, not the canvas.\n// Map them through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live camera feeds (fills the canvas, edges cropped). Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible (full frame shown with letterbox bars). Stretching directly to `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n\n**`currentFrame` vs `analysedFrame`.** Default to `currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need (4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\n- `confidence: number | null` (0..1) — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\n- `headPose: {pitch, yaw, roll} | null` (degrees) — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n### External Libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // false = no CSS styles\n```\n\nALWAYS pin library versions. ALWAYS pass `viji.canvas` to the renderer. Handle resize in `render()`.\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 6). Same for nested: never multiply an accumulator by another parameter.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 13).\n6. For WebGL scenes with Three.js, handle resize by comparing `viji.width/height` with previous values.\n\n## TEMPLATE\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n\nNow help the artist build a Viji native scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `viji.deltaTime` for animation. Use parameters for anything the user might want to adjust. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want: be as specific as you like.\n4. The AI will return a complete Viji native scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have (sliders, toggles, color pickers).\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Native Quick Start](/native/quickstart): your first Viji native scene\n- [Native API Reference](/native/api-reference): full API reference\n- [Best Practices](/getting-started/best-practices): essential patterns for reliable scenes\n- [Common Mistakes](/getting-started/common-mistakes): pitfalls to avoid"
|
|
1349
1359
|
}
|
|
1350
1360
|
]
|
|
1351
1361
|
},
|
|
@@ -1373,7 +1383,7 @@ export const docsApi = {
|
|
|
1373
1383
|
"content": [
|
|
1374
1384
|
{
|
|
1375
1385
|
"type": "text",
|
|
1376
|
-
"markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene: a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js v1.9.4.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D canvas or WEBGL / 3D mode?\", \"Should it use the camera or only mouse input?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes. Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or P5 function names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete docs (every Viji page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene's needs.\n2. ALWAYS use `render(viji, p5)`: not `draw()`. ALWAYS use `setup(viji, p5)`: not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255; matches `colorMode(RGB, 255)`) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100; matches `colorMode(HSB, 360, 100, 100)`); prefer those over parsing hex. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed, it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications, never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG: jumps: const t = viji.time * speed.value;\n // WRONG: nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only: `p5.drawingContext` is WebGL, not Canvas 2D. **Always preserve the source aspect ratio with the `videoFit` helper.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n ```javascript\n function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // 'cover' (default) or 'contain'\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n // CV coords (face.bounds, hand.landmarks, etc.) are normalized 0-1 to the\n // SOURCE video frame, not the canvas. Map through v: x = v.x + pt.x * v.width.\n ```\n Default to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. Stretching with `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)`: Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context: never use Canvas 2D-only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default: use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes: the canvas is managed by P5.\n32. ALWAYS type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/p5/api-reference) lists every `viji` member with its exact type.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5 using the `videoFit` helper (see Drawing & Canvas section above) to preserve source aspect: `const v = videoFit(viji); p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);`. Never `(0, 0, viji.width, viji.height)` unless distortion is intentional.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need.\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`, not on `viji.video` directly):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` | Remove: canvas is provided |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow help the artist build a Viji P5 scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5): convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart): your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference): full API reference\n- [Drawing with P5](/p5/drawing): Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/): full P5.js documentation"
|
|
1386
|
+
"markdown": "# Prompt: P5 Scenes\n\nCopy the prompt below and paste it into your AI assistant. Then describe the scene you want. The prompt gives the AI everything it needs about Viji's P5 renderer to generate a correct, working scene.\n\n## The Prompt\n\n````\nYou are generating a Viji P5.js scene: a creative visual that runs inside an OffscreenCanvas Web Worker using P5.js v1.9.4.\nArtists describe what they want; you collaborate with them to produce complete, working scene code. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the artist's brief is vague, missing a key data source, or has multiple plausible interpretations, ask one or two short clarifying questions before generating code. Examples: \"Should this react to audio or stay purely visual?\", \"2D canvas or WEBGL / 3D mode?\", \"Should it use the camera or only mouse input?\". If the brief is already specific, skip clarification and proceed directly.\n2. **Generate.** Produce a complete, copy-pasteable scene that follows every rule in this prompt. Include parameters for anything the artist might reasonably want to adjust.\n3. **Explain.** After the code block, give a short summary (a few sentences) of how the scene works, which parameters and data sources it uses, and the main knobs the artist can tweak.\n4. **Iterate.** Invite the artist to ask for changes. Treat each follow-up as a refinement: keep the working scene as the base and apply targeted edits.\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt are a summary; if anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before generating code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax.\n- ON DEMAND: fetch from Tier-2 resources when the artist requests something not fully covered by the rules and tables in this prompt (advanced CV data structures, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt.\n- Never invent property, method, or P5 function names from memory.\n- If the artist asks for something not covered here, say so and ask the artist what they want; do NOT fabricate.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete docs (every Viji page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## ARCHITECTURE\n\n- Scenes run in a **Web Worker** with an **OffscreenCanvas**. There is no DOM.\n- Viji automatically loads **P5.js v1.9.4** when you use `// @renderer p5` or `// @renderer p5 webgl`.\n- The global `viji` object provides canvas, timing, audio, video, CV, input, sensors, and parameters.\n- **Top-level code** runs once (initialization, parameter declarations, state).\n- **`function render(viji, p5) { ... }`** is called every frame. This is where you draw.\n- Optional **`function setup(viji, p5) { ... }`** runs once for configuration (e.g., `p5.colorMode()`).\n- P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix.\n\n## RULES\n\n1. ALWAYS add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the very first line, matching the scene's needs.\n2. ALWAYS use `render(viji, p5)`: not `draw()`. ALWAYS use `setup(viji, p5)`: not `setup()`.\n3. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`, `random()` → `p5.random()`\n This applies to ALL P5 functions and constants without exception.\n4. NEVER call `createCanvas()`. The canvas is created and managed by Viji.\n5. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, `viji.model(null, { label: 'Name' })` for 3D models, or `fetch()` in `setup()`.\n6. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n7. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadShader()`. Use `viji.image()` or `fetch()`. Use `loadModel()` only with `viji.model().url`, once per new value (not per frame), passing the format as the file type: `p5.loadModel(m.value.url, true, ok, err, '.' + m.value.format)` (`.obj` and `.stl` only; needs `// @renderer p5 webgl`).\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`.\n9. NEVER use `createCapture()`, `createVideo()`. Use `viji.video.*` instead.\n10. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n11. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n12. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`.\n13. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255; matches `colorMode(RGB, 255)`) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100; matches `colorMode(HSB, 360, 100, 100)`); prefer those over parsing hex. Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), and CSS `'rgb(...)'` / `'hsl(...)'` strings.\n14. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n15. ALWAYS use `viji.deltaTime` for frame-rate-independent animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. NEVER multiply `viji.time` by a parameter for animation speed, it causes jumps when the parameter changes. ALWAYS use a `deltaTime` accumulator (rule 15). This also applies to nested multiplications, never multiply an accumulator by another parameter; give each speed its own accumulator:\n ```javascript\n // WRONG: jumps: const t = viji.time * speed.value;\n // WRONG: nested: const rot = phase * rotSpeed.value;\n // RIGHT:\n let phase = 0, rotPhase = 0; // top level\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. For image parameters displayed with P5, use `.p5` (not `.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. For video frames: in **2D** (`// @renderer p5`) you may use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In **WEBGL** (`// @renderer p5 webgl`), use `p5.image(viji.video.currentFrame, ...)` only: `p5.drawingContext` is WebGL, not Canvas 2D. **Always preserve the source aspect ratio with the `videoFit` helper.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n ```javascript\n function videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n }\n if (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji); // 'cover' (default) or 'contain'\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n }\n // CV coords (face.bounds, hand.landmarks, etc.) are normalized 0-1 to the\n // SOURCE video frame, not the canvas. Map through v: x = v.x + pt.x * v.width.\n ```\n Default to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. Stretching with `(0, 0, viji.width, viji.height)` is allowed only when distortion is intentional.\n20. `p5.createGraphics()` works (creates OffscreenCanvas internally). Use for off-screen buffers.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is NOT available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode:** Use `// @renderer p5` for a **2D** main canvas. For **WEBGL / 3D**, the first line MUST be `// @renderer p5 webgl`. NEVER call `createCanvas()` or `createCanvas(..., p5.WEBGL)`: Viji creates the canvas in the correct mode.\n24. In **WEBGL** scenes, `p5.drawingContext` is a WebGL context: never use Canvas 2D-only APIs on it. Use P5 3D drawing, `p5.image()` / textures for images and video.\n25. `p5.createGraphics(w, h)` is **2D only**. `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n26. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work (2D scenes; WEBGL pixel readback follows P5.js rules).\n27. ALWAYS check `viji.audio.isConnected` before using audio data.\n28. ALWAYS check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n29. NEVER enable CV features by default: use toggle parameters for user opt-in.\n30. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio-related, `category: 'video'` for video/camera/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host UI hide irrelevant controls when the input is inactive. **Use creative-strength sliders, not on/off toggles**: the host UI already controls whether each input is wired up, so a scene-level `toggle(true, { label: 'Audio Reactive' })` just duplicates the host switch. CV feature toggles (`enableFaceDetection`, etc.) are the exception and stay opt-in.\n ```javascript\n // Right: creative-strength sliders with the matching category.\n const bassSensitivity = viji.slider(1.5, { min: 0, max: 3, label: 'Bass Sensitivity', group: 'audio', category: 'audio' });\n const mouseAttraction = viji.slider(0.5, { min: 0, max: 1, label: 'Mouse Attraction', group: 'interaction', category: 'interaction' });\n\n // Wrong: scene-level on/off toggle for an input the host already gates.\n // const audioReact = viji.toggle(true, { label: 'Audio Reactive', category: 'audio' });\n // const followMouse = viji.toggle(true, { label: 'Follow Mouse', category: 'interaction' });\n ```\n31. `viji.useContext()` is NOT available in P5 scenes: the canvas is managed by P5.\n32. ALWAYS type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n All Viji types are ambient globals: `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>`; the named type exists). The [API Reference](/p5/api-reference) lists every `viji` member with its exact type.\n\n## COMPLETE API REFERENCE\n\nAll `viji.*` members are identical to the native renderer (same object, same types).\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\nNote: `viji.useContext()` is NOT available in P5. The canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required; use ['obj', 'stl'] (what p5 loadModel reads); no .p5; load with p5.loadModel(value.url, true, ok, err, '.' + value.format) once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n**`device.audio`** (when an external device in `viji.devices[]` connects with audio): an `AudioStreamAPI` with the same `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` and each `*Smoothed` sibling (`lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed`), `spectral.{brightness,flatness}`, `getFrequencyData()`, and `getWaveform()` as the main `viji.audio` table. **No** `beat`, BPM, triggers, or events: those are main-audio only. Host-supplied additional audio sources (`viji.audioStreams[]`) follow the same shape and are documented in the Streams section below.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n\nDraw video with P5 using the `videoFit` helper (see Drawing & Canvas section above) to preserve source aspect: `const v = videoFit(viji); p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);`. Never `(0, 0, viji.width, viji.height)` unless distortion is intentional.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces/hands/pose/segmentation`\n\nEach CV feature is independent and populates only its own subset of fields. Each active feature uses its own WebGL context for MediaPipe — enable only what you need.\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true/false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true/false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true/false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true/false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`, not on `viji.video` directly):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\n`id` always present. Other fields populated only by their source model and read `null` (or `[]` for `landmarks`) otherwise — always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection. Names: browDownLeft/Right, browInnerUp, browOuterUpLeft/Right, cheekPuff, cheekSquintLeft/Right, eyeBlinkLeft/Right, eyeLookDownLeft/Right, eyeLookInLeft/Right, eyeLookOutLeft/Right, eyeLookUpLeft/Right, eyeSquintLeft/Right, eyeWideLeft/Right, jawForward/Left/Open/Right, mouthClose/DimpleLeft/Right/FrownLeft/Right/Funnel/Left/LowerDownLeft/Right/PressLeft/Right/Pucker/Right/RollLower/Upper/ShrugLower/Upper/SmileLeft/Right/StretchLeft/Right/UpperUpLeft/Right, noseSneerLeft/Right, tongueOut.\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1 confidence).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `motion` | `DeviceMotionData\\|null` | Accelerometer/gyroscope |\n| `orientation` | `DeviceOrientationData\\|null` | Device orientation |\n\n**DeviceMotionData:** `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n**DeviceOrientationData:** `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\nArray of connected external devices. Each `DeviceState`:\n`id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same as viji.video but without CV), `audio` (AudioStreamAPI|null, lightweight analysis only; no beat/BPM/triggers).\n\n### Streams: `viji.videoStreams`\n\n`VideoAPI[]`: additional video sources provided by the host application (used by the compositor for scene mixing). May be empty. Each element has the same shape as `viji.video`.\n\n### Streams: `viji.audioStreams`\n\n`AudioStreamAPI[]`: additional audio sources from the host (e.g. multi-source mixing). May be empty. Lightweight interface: volume, bands, spectral features, `getFrequencyData()`, `getWaveform()`: **not** the full `AudioAPI` (no beat detection, BPM, triggers, or events).\n\n## P5 ↔ VIJI MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` | Remove: canvas is provided |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `loadModel(path)` | `viji.model(null, { label: 'Model', formats: ['obj', 'stl'] })`; when `.value` changes, `p5.loadModel(value.url, true, ok, err, '.' + value.format)` |\n| `save()` | Remove: host handles capture |\n\n## BEST PRACTICES\n\n1. NEVER use `viji.time * speed.value`: use a `deltaTime` accumulator instead (see rule 16). Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n2. Guard audio/video with `isConnected` checks.\n3. Pre-allocate all objects/arrays at top level: never inside `render()`.\n4. For CV, use toggle parameters: never enable by default.\n5. ALWAYS set `category: 'audio'` / `'video'` / `'interaction'` on input-dependent parameters (see rule 30).\n6. Use `p5.drawingContext.drawImage()` for video frames (faster than wrapping).\n7. Use `p5.createGraphics()` for off-screen buffers when needed.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n\nNow help the artist build a Viji P5 scene based on their description below.\n\nIf the brief is vague, ambiguous, or missing a key data source, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you generate the scene:\n- Follow every rule in this prompt.\n- Use `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line. Prefix ALL P5 functions with `p5.`. Use `viji.deltaTime` for animation. Use parameters for anything adjustable. Check `isConnected` before using audio or video.\n- Output the scene code in a single fenced code block.\n- After the code block, write a short explanation (a few sentences) of how the scene works and what the artist can tweak.\n- Invite the artist to ask for changes.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant (ChatGPT, Claude, etc.).\n3. After the prompt, describe the scene you want.\n4. The AI will return a complete Viji P5 scene.\n\n> [!TIP]\n> For better results, mention which data sources you want (audio, video, camera, mouse) and what kind of controls the user should have. If you have existing P5 sketches to convert, use the [Convert: P5 Sketches](/ai-prompts/convert-p5) prompt instead.\n\n## Related\n\n- [Create Your First Scene](/ai-prompts/create-first-scene): guided prompt for beginners\n- [Prompting Tips](/ai-prompts/prompting-tips): how to get better results from AI\n- [Convert: P5 Sketches](/ai-prompts/convert-p5): convert existing P5 sketches to Viji\n- [P5 Quick Start](/p5/quickstart): your first Viji P5 scene\n- [P5 API Reference](/p5/api-reference): full API reference\n- [Drawing with P5](/p5/drawing): Viji-specific P5 drawing guide\n- [p5js.org Reference](https://p5js.org/reference/): full P5.js documentation"
|
|
1377
1387
|
}
|
|
1378
1388
|
]
|
|
1379
1389
|
},
|
|
@@ -1507,7 +1517,7 @@ export const docsApi = {
|
|
|
1507
1517
|
"content": [
|
|
1508
1518
|
{
|
|
1509
1519
|
"type": "text",
|
|
1510
|
-
"markdown": "# Convert: P5 Sketches to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the P5.js sketch you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji-P5 scene.\n\n## The Prompt\n\n````\nYou are converting a standard P5.js sketch into a Viji-P5 scene.\nViji scenes run inside an OffscreenCanvas Web Worker using P5.js v1.9.4. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source sketch is incomplete (missing `setup` or `draw`), uses libraries you cannot identify (`p5.sound`, custom `loadX` calls, third-party add-ons), or relies on `index.html` HTML elements, ask the artist for the missing pieces or for permission to drop them before generating code. If the sketch is self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji-P5 scene that follows every rule in this prompt. Preserve the artist's visual intent and parameter ranges; replace only the Viji-incompatible parts.\n3. **Explain.** After the code block, give a short summary of the key changes you made (renamed `draw` to `render`, added `p5.` prefix, replaced `mouseX` with `viji.pointer.x`, removed `loadImage`, etc.). Flag any features you had to drop or simplify because they are incompatible with the Viji worker environment.\n4. **Iterate.** Invite the artist to ask for refinements (\"the colors look off\", \"the animation is too fast\", \"I want a slider for the speed\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common conversion mappings, but they do NOT cover the full Viji or P5 API surface. If anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax for any call this prompt does not list.\n- ON DEMAND: fetch from Tier-2 resources when the source sketch uses a Viji-side feature this prompt does not map (advanced CV data, device sensors, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard P5.js v1.x API for direct ports.\n- Never invent Viji property, method, or P5 function names from memory.\n- If the source sketch uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- Companion prompt for any Viji feature this conversion prompt does not cover: https://unpkg.com/@viji-dev/core/dist/docs-api.js (search for \"p5-prompt\")\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS set the first line from the sketch's canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or 3D primitives on the main canvas. NEVER keep `createCanvas()`: Viji creates the canvas.\n2. ALWAYS rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it doesn't exist, do NOT add one.\n4. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`\n This applies to ALL P5 functions and constants without exception.\n5. NEVER call `createCanvas()`. The canvas is created and managed by Viji. WEBGL is selected only with `// @renderer p5 webgl`, not with `createCanvas(..., p5.WEBGL)`.\n6. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in an async `setup()` for data.\n7. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Instead, check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` (works for both mouse and touch) or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` (works for both mouse and touch) or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: use `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. These are host-level concerns.\n9. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n10. NEVER use `createCapture()` or `createVideo()`. Use `viji.video.*` instead.\n11. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n12. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n13. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\n ```javascript\n // CORRECT\n const size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\n function render(viji, p5) { p5.circle(0, 0, size.value); }\n\n // WRONG: creates a new parameter every frame\n function render(viji, p5) { const size = viji.slider(50, { ... }); }\n ```\n14. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100) for direct use with `colorMode(RGB, 255)` / `colorMode(HSB, 360, 100, 100)`.\n15. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n16. ALWAYS use `viji.deltaTime` for frame-rate-independent animation. Replace `frameCount * 0.01` patterns with a deltaTime accumulator:\n ```javascript\n let angle = 0;\n function render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n }\n ```\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes jumps when the parameter changes. Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n19. For image parameters displayed with P5, use `photo.p5` (not `photo.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n20. ALWAYS type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## API MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` / `createCanvas(w, h, WEBGL)` | Remove: use `// @renderer p5` or `// @renderer p5 webgl` |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host uses `captureFrame()` |\n\nThe mapping above covers the most common direct ports. The complete Viji API surface is below: use it for any feature the source sketch reaches for that this table does not list (audio analysis, video frames, CV data, touch, device sensors, etc.).\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical to the Native renderer (same object, same types). Access it inside `setup(viji, p5)` and `render(viji, p5)`.\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n`viji.useContext()` is NOT available in P5: the canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\nCommon P5 sound conversions: `new p5.AudioIn() ... mic.getLevel()` → `viji.audio.volume.current`; `fft.analyze()` → `viji.audio.getFrequencyData()`; `p5.Amplitude` → `viji.audio.volume.smoothed`.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\nReplace `createCapture(VIDEO)` with `viji.video.currentFrame`. **Always preserve the source aspect ratio with `videoFit`.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (live cameras, default) or 'contain' (CV overlays)\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0-1 to the source frame; map through v:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n // ...\n});\n```\n\nUse `p5.image(...)` in both 2D and WEBGL P5 modes, or `p5.drawingContext.drawImage(...)` in 2D only.\n\nDefault to `viji.video.currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\nP5 `touchStarted()` / `touchMoved()` / `touchEnded()` callbacks do not fire. Read `viji.touches.started` / `.moved` / `.ended` inside `render()` instead.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## P5-SPECIFIC GOTCHAS\n\nThese behaviors are different from running P5 in a browser tab:\n\n- **Fonts:** `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is NOT available.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes (`// @renderer p5 webgl`) it is a WebGL context: never use Canvas-2D-only APIs on it; use P5 3D drawing or `p5.image()` for textures and video.\n- **`viji.useContext()`** is NOT available in P5: the canvas and 2D context are managed by P5 internally.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(angle * 30 % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nNow convert the P5.js sketch I provide.\n\nIf the sketch is incomplete or uses features that are incompatible with the Viji worker environment, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule and mapping above. The **API MAPPING** table covers the most common direct ports; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any feature the source sketch reaches for that the mapping table does not list (audio analysis, video frames, CV data, touch, device sensors). For any P5 call you are unsure about, consult the p5.js v1.x reference linked in **REFERENCE**. The canonical companion generation prompt is `p5-prompt` (in the `docs-api.js` bundle).\n- Output the Viji-P5 scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify.\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the P5.js sketch you want to convert.\n4. The AI will return a Viji-compatible scene.\n\nFor a detailed human-readable guide, see [Converting P5 Sketches](/p5/converting-sketches#step-by-step).\n\n## Related\n\n- [Converting P5 Sketches](/p5/converting-sketches#step-by-step): step-by-step manual conversion guide\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): AI prompt for creating new P5 scenes from scratch\n- [P5 Quick Start](/p5/quickstart): your first Viji-P5 scene"
|
|
1520
|
+
"markdown": "# Convert: P5 Sketches to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the P5.js sketch you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji-P5 scene.\n\n## The Prompt\n\n````\nYou are converting a standard P5.js sketch into a Viji-P5 scene.\nViji scenes run inside an OffscreenCanvas Web Worker using P5.js v1.9.4. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source sketch is incomplete (missing `setup` or `draw`), uses libraries you cannot identify (`p5.sound`, custom `loadX` calls, third-party add-ons), or relies on `index.html` HTML elements, ask the artist for the missing pieces or for permission to drop them before generating code. If the sketch is self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji-P5 scene that follows every rule in this prompt. Preserve the artist's visual intent and parameter ranges; replace only the Viji-incompatible parts.\n3. **Explain.** After the code block, give a short summary of the key changes you made (renamed `draw` to `render`, added `p5.` prefix, replaced `mouseX` with `viji.pointer.x`, removed `loadImage`, etc.). Flag any features you had to drop or simplify because they are incompatible with the Viji worker environment.\n4. **Iterate.** Invite the artist to ask for refinements (\"the colors look off\", \"the animation is too fast\", \"I want a slider for the speed\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common conversion mappings, but they do NOT cover the full Viji or P5 API surface. If anything ever conflicts, the linked files win. Viji pins **p5.js v1.9.4**: when in doubt about a P5 call, the p5.js v1.x reference is the truth.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resources. Use them to verify exact Viji API names and types, and to check P5 function syntax for any call this prompt does not list.\n- ON DEMAND: fetch from Tier-2 resources when the source sketch uses a Viji-side feature this prompt does not map (advanced CV data, device sensors, full Viji examples) or when you need authoritative TypeScript signatures for a P5 function.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard P5.js v1.x API for direct ports.\n- Never invent Viji property, method, or P5 function names from memory.\n- If the source sketch uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n- P5.js v1.x reference (HTML, authoritative for P5 syntax): https://p5js.org/reference/\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Bundled Viji + P5.js v1.9.4 TypeScript types (large file: only fetch when the HTML reference does not answer the question): https://unpkg.com/@viji-dev/core/dist/artist-global-p5.d.ts\n- Companion prompt for any Viji feature this conversion prompt does not cover: https://unpkg.com/@viji-dev/core/dist/docs-api.js (search for \"p5-prompt\")\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS set the first line from the sketch's canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or 3D primitives on the main canvas. NEVER keep `createCanvas()`: Viji creates the canvas.\n2. ALWAYS rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it doesn't exist, do NOT add one.\n4. ALWAYS prefix every P5 function and constant with `p5.`:\n - `background(0)` → `p5.background(0)`\n - `fill(255)` → `p5.fill(255)`\n - `PI` → `p5.PI`, `TWO_PI` → `p5.TWO_PI`, `HSB` → `p5.HSB`\n - `createVector(1, 0)` → `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` → `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` → `p5.noise(x)`\n This applies to ALL P5 functions and constants without exception.\n5. NEVER call `createCanvas()`. The canvas is created and managed by Viji. WEBGL is selected only with `// @renderer p5 webgl`, not with `createCanvas(..., p5.WEBGL)`.\n6. NEVER use `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` in an async `setup()` for data.\n7. NEVER use P5 event callbacks: `mousePressed()`, `mouseDragged()`, `mouseReleased()`, `keyPressed()`, `keyReleased()`, `keyTyped()`, `touchStarted()`, `touchMoved()`, `touchEnded()`. Instead, check state in `render()`:\n - `mouseIsPressed` → `viji.pointer.isDown` (works for both mouse and touch) or `viji.mouse.isPressed`\n - `mouseX` / `mouseY` → `viji.pointer.x` / `viji.pointer.y` (works for both mouse and touch) or `viji.mouse.x` / `viji.mouse.y`\n - `keyIsPressed` → `viji.keyboard.isPressed('keyName')`\n - For press-edge detection: use `viji.pointer.wasPressed` / `viji.pointer.wasReleased`.\n8. NEVER use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. These are host-level concerns.\n9. NEVER use `loadImage()`, `loadFont()`, `loadJSON()`, `loadShader()`. Use `viji.image()` or `fetch()`. Use `loadModel()` only with `viji.model().url`, once per new value, passing the format as the file type: `p5.loadModel(m.value.url, true, ok, err, '.' + m.value.format)`.\n10. NEVER use `createCapture()` or `createVideo()`. Use `viji.video.*` instead.\n11. NEVER use `p5.dom` or `p5.sound` libraries. Use Viji parameters for UI and `viji.audio.*` for audio.\n12. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n13. ALWAYS declare parameters at the TOP LEVEL, never inside `render()` or `setup()`:\n ```javascript\n // CORRECT\n const size = viji.slider(50, { min: 10, max: 200, label: 'Size' });\n function render(viji, p5) { p5.circle(0, 0, size.value); }\n\n // WRONG: creates a new parameter every frame\n function render(viji, p5) { const size = viji.slider(50, { ... }); }\n ```\n14. ALWAYS read parameters via `.value`: `size.value`, `color.value`, `toggle.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100) for direct use with `colorMode(RGB, 255)` / `colorMode(HSB, 360, 100, 100)`.\n15. ALWAYS use `viji.width` and `viji.height` for canvas dimensions. NEVER hardcode pixel sizes.\n16. ALWAYS use `viji.deltaTime` for frame-rate-independent animation. Replace `frameCount * 0.01` patterns with a deltaTime accumulator:\n ```javascript\n let angle = 0;\n function render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n }\n ```\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes jumps when the parameter changes. Same for nested: never multiply an accumulator by another parameter; give each speed its own accumulator.\n17. NEVER allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n19. For image parameters displayed with P5, use `photo.p5` (not `photo.value`) with `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n20. ALWAYS type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## API MAPPING\n\n| Standard P5.js | Viji-P5 |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | Use `viji.time` or `viji.deltaTime` accumulator |\n| `frameRate(n)` | Remove: host controls frame rate |\n| `createCanvas(w, h)` / `createCanvas(w, h, WEBGL)` | Remove: use `// @renderer p5` or `// @renderer p5 webgl` |\n| `preload()` | Remove: use `viji.image()` or `fetch()` in `setup()` |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })` |\n| `save()` | Remove: host uses `captureFrame()` |\n\nThe mapping above covers the most common direct ports. The complete Viji API surface is below: use it for any feature the source sketch reaches for that this table does not list (audio analysis, video frames, CV data, touch, device sensors, etc.).\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical to the Native renderer (same object, same types). Access it inside `setup(viji, p5)` and `render(viji, p5)`.\n\n### Canvas & Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (managed by P5) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n`viji.useContext()` is NOT available in P5: the canvas context is managed by P5 internally.\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null, p5: P5Image }\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required; use ['obj', 'stl'] (what p5 loadModel reads); no .p5; load with p5.loadModel(value.url, true, ok, err, '.' + value.format) once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\nCommon P5 sound conversions: `new p5.AudioIn() ... mic.getLevel()` → `viji.audio.volume.current`; `fft.analyze()` → `viji.audio.getFrequencyData()`; `p5.Amplitude` → `viji.audio.volume.smoothed`.\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Just-arrived video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data of `currentFrame` for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. See the Computer Vision section below.\n\nReplace `createCapture(VIDEO)` with `viji.video.currentFrame`. **Always preserve the source aspect ratio with `videoFit`.** Drawing to `(0, 0, viji.width, viji.height)` stretches the video and misaligns CV bounds.\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' (live cameras, default) or 'contain' (CV overlays)\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0-1 to the source frame; map through v:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n // ...\n});\n```\n\nUse `p5.image(...)` in both 2D and WEBGL P5 modes, or `p5.drawingContext.drawImage(...)` in 2D only.\n\nDefault to `viji.video.currentFrame` for displayed video. Reach for `viji.video.cv.analysedFrame ?? viji.video.currentFrame` only when the effect reads pixels from the displayed frame at CV-derived positions (compositing the segmentation mask onto the body, sampling skin under a face landmark, warping the face along its mesh, texture-mapped face filters). For drawing landmark dots, particles, or any overlay that doesn't sample the displayed frame at CV positions, `currentFrame` is the better default: `analysedFrame` advances only when MediaPipe completes an inference, so reaching for it without a reason makes the displayed video stutter or hold between inferences.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\nP5 `touchStarted()` / `touchMoved()` / `touchEnded()` callbacks do not fire. Read `viji.touches.started` / `.moved` / `.ended` inside `render()` instead.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## P5-SPECIFIC GOTCHAS\n\nThese behaviors are different from running P5 in a browser tab:\n\n- **Fonts:** `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is NOT available.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is NOT supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes (`// @renderer p5 webgl`) it is a WebGL context: never use Canvas-2D-only APIs on it; use P5 3D drawing or `p5.image()` for textures and video.\n- **`viji.useContext()`** is NOT available in P5: the canvas and 2D context are managed by P5 internally.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## TEMPLATE\n\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(angle * 30 % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nNow convert the P5.js sketch I provide.\n\nIf the sketch is incomplete or uses features that are incompatible with the Viji worker environment, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule and mapping above. The **API MAPPING** table covers the most common direct ports; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any feature the source sketch reaches for that the mapping table does not list (audio analysis, video frames, CV data, touch, device sensors). For any P5 call you are unsure about, consult the p5.js v1.x reference linked in **REFERENCE**. The canonical companion generation prompt is `p5-prompt` (in the `docs-api.js` bundle).\n- Output the Viji-P5 scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify.\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the P5.js sketch you want to convert.\n4. The AI will return a Viji-compatible scene.\n\nFor a detailed human-readable guide, see [Converting P5 Sketches](/p5/converting-sketches#step-by-step).\n\n## Related\n\n- [Converting P5 Sketches](/p5/converting-sketches#step-by-step): step-by-step manual conversion guide\n- [Prompt: P5 Scenes](/ai-prompts/p5-prompt): AI prompt for creating new P5 scenes from scratch\n- [P5 Quick Start](/p5/quickstart): your first Viji-P5 scene"
|
|
1511
1521
|
}
|
|
1512
1522
|
]
|
|
1513
1523
|
},
|
|
@@ -1563,7 +1573,7 @@ export const docsApi = {
|
|
|
1563
1573
|
"content": [
|
|
1564
1574
|
{
|
|
1565
1575
|
"type": "text",
|
|
1566
|
-
"markdown": "# Convert: Three.js to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the Three.js code you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji native scene with Three.js.\n\n## The Prompt\n\n````\nYou are converting a standalone Three.js application into a Viji native scene.\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source code is incomplete (missing init or render loop), uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js), or relies on DOM elements outside the canvas, ask the artist for the missing pieces or for permission to drop the framework wrapper before generating code. If the code is plain Three.js and self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji native scene that follows every rule in this prompt. Preserve the artist's visual intent, scene graph, and material setup; replace only the Viji-incompatible parts (window/document access, `requestAnimationFrame`, `THREE.Clock`, DOM event listeners).\n3. **Explain.** After the code block, give a short summary of the key changes you made (e.g., \"wrapped scene init in top-level code, moved per-frame logic into `render(viji)`, replaced `clock.getDelta()` with `viji.deltaTime`, replaced mouse listeners with `viji.pointer`\"). Flag any features you had to drop or simplify (e.g., `OrbitControls`, postprocessing that depends on DOM events).\n4. **Iterate.** Invite the artist to ask for refinements (\"add a slider for camera distance\", \"make the cube react to audio\", \"swap the material for a wireframe\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common Three.js → Viji mappings, but they do NOT cover the full Viji API surface. If anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resource. Use it to verify exact Viji API names, parameter types, and any Viji feature the source code may reach for (audio, video, CV, sensors, parameters).\n- ON DEMAND: fetch from the Tier-2 resource when the source uses a Viji-side feature this prompt does not map (advanced CV data structures, device sensors, full Viji examples) or when you need authoritative TypeScript signatures.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard Three.js API for direct ports.\n- Never invent Viji property or method names from memory.\n- If the source uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Companion prompt for any Viji feature this conversion prompt does not cover (search for \"native-prompt\" in the docs above).\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS import Three.js dynamically at the top level using `await import()`:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n NEVER use `<script>` tags, `require()`, or static `import` statements.\n ALWAYS pin the version number in the URL.\n\n2. ALWAYS use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n ALWAYS pass `false` as the third argument to `setSize()`: this prevents Three.js from setting CSS styles, which would fail in the worker.\n\n3. NEVER use `requestAnimationFrame()`. Viji controls the render loop. Write all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. ALWAYS handle resize by checking `viji.width` / `viji.height` in `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n renderer.render(scene, camera);\n }\n ```\n\n5. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n\n6. NEVER use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. Replace hardcoded values with Viji parameters declared at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const color = viji.color('#049ef4', { label: 'Color' });\n ```\n NEVER declare parameters inside `render()`.\n ALWAYS read via `.value`: `speed.value`, `color.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Three.js color objects can take `.rgb` directly, e.g. `material.color.setRGB(c.rgb.r/255, c.rgb.g/255, c.rgb.b/255)`, or just keep using `material.color.set(c.value)`.\n\n8. ALWAYS use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n NEVER use `clock.getDelta()` or `Date.now()`. Remove any `THREE.Clock` usage.\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes animation jumps when the parameter changes. Same for nested multiplications: never multiply an accumulator by another parameter; give each speed its own accumulator.\n\n9. Replace mouse/keyboard event listeners with Viji APIs:\n - `event.clientX` → `viji.pointer.x` (works for both mouse and touch) or `viji.mouse.x`\n - `event.clientY` → `viji.pointer.y` or `viji.mouse.y`\n - Mouse buttons → `viji.mouse.leftButton`, `viji.mouse.rightButton`\n - Key presses → `viji.keyboard.isPressed('keyName')`\n\n10. `OrbitControls` and other controls that depend on DOM events will NOT work in the worker.\n For camera interaction, read `viji.pointer` (handles both mouse and touch) and update the camera manually.\n\n11. For Three.js addons, import from the examples directory:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n ALWAYS use the same Three.js version for addons as for the main library.\n\n12. For textures from file inputs, use Viji's image parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n // In render():\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n ```\n\n13. For video textures, use `viji.video`:\n ```javascript\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n ```\n\n14. NEVER allocate new objects inside `render()`. Pre-create vectors, colors, and materials at the top level.\n\n15. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n\n16. Remove any `window.addEventListener('resize', ...)`: resize is handled in `render()` (see rule 4).\n\n17. Remove any CSS, HTML, or DOM manipulation code. Viji scenes produce only canvas output.\n\n18. ALWAYS type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical across all renderers (same object, same types). The mapping table above covers the most common Three.js → Viji ports; the reference below is the full surface for any Viji-side feature the source code reaches for (audio analysis, video frames, CV data, touch, device sensors).\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (pass to Three.js renderer) |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\nWhen using Three.js, do NOT call `viji.useContext()`: pass `viji.canvas` to the `THREE.WebGLRenderer({ canvas: viji.canvas })` and Three.js manages its own GL context.\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. For Three.js video textures, see rule 13.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## TEMPLATE\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 1, 3);\ncamera.lookAt(0, 0, 0);\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshStandardMaterial({ color: color.value });\nconst mesh = new THREE.Mesh(geometry, material);\nscene.add(mesh);\n\nscene.add(new THREE.DirectionalLight(0xffffff, 1.5));\nscene.add(new THREE.AmbientLight(0x404040));\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n mesh.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n```\n\nNow convert the Three.js code I provide.\n\nIf the source is incomplete or uses a framework that needs to be unwrapped, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule above. The conversion-specific rules (1-17) cover the Three.js → Viji mappings most artists need; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any Viji-side feature the source code reaches for (audio, video, CV, touch, device sensors, parameters beyond the basics). For details not in this prompt, consult the **REFERENCE** links: the canonical companion generation prompt is `native-prompt` (in the `docs-api.js` bundle).\n- Output the Viji scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify (controls, postprocessing, framework wrappers).\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the Three.js code you want to convert.\n4. The AI will return a Viji-compatible native scene.\n\n> [!NOTE]\n> This prompt handles standard Three.js scenes. If the original code uses a framework (React Three Fiber, Drei, etc.), you may need to manually extract the Three.js scene setup first.\n\n## Related\n\n- [External Libraries](/native/external-libraries): detailed guide for using Three.js and other libraries in Viji\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): AI prompt for creating new native scenes from scratch\n- [Native Quick Start](/native/quickstart): your first Viji native scene"
|
|
1576
|
+
"markdown": "# Convert: Three.js to Viji\n\nCopy the prompt below and paste it into your AI assistant along with the Three.js code you want to convert. The prompt contains all the rules the AI needs to produce a correct Viji native scene with Three.js.\n\n## The Prompt\n\n````\nYou are converting a standalone Three.js application into a Viji native scene.\nViji scenes run inside an OffscreenCanvas Web Worker. Apply every rule below exactly.\n\n## YOUR BEHAVIOR\n\n1. **Clarify when needed.** If the source code is incomplete (missing init or render loop), uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js), or relies on DOM elements outside the canvas, ask the artist for the missing pieces or for permission to drop the framework wrapper before generating code. If the code is plain Three.js and self-contained, skip clarification and proceed.\n2. **Convert.** Produce a complete, copy-pasteable Viji native scene that follows every rule in this prompt. Preserve the artist's visual intent, scene graph, and material setup; replace only the Viji-incompatible parts (window/document access, `requestAnimationFrame`, `THREE.Clock`, DOM event listeners).\n3. **Explain.** After the code block, give a short summary of the key changes you made (e.g., \"wrapped scene init in top-level code, moved per-frame logic into `render(viji)`, replaced `clock.getDelta()` with `viji.deltaTime`, replaced mouse listeners with `viji.pointer`\"). Flag any features you had to drop or simplify (e.g., `OrbitControls`, postprocessing that depends on DOM events).\n4. **Iterate.** Invite the artist to ask for refinements (\"add a slider for camera distance\", \"make the cube react to audio\", \"swap the material for a wireframe\").\n\n## REFERENCE (source of truth)\n\nThe resources below are AUTHORITATIVE. The rules and tables in this prompt focus on the most common Three.js → Viji mappings, but they do NOT cover the full Viji API surface. If anything ever conflicts, the linked files win.\n\n**If you have web/file access:**\n- REQUIRED before converting code: fetch and skim the Tier-1 resource. Use it to verify exact Viji API names, parameter types, and any Viji feature the source code may reach for (audio, video, CV, sensors, parameters).\n- ON DEMAND: fetch from the Tier-2 resource when the source uses a Viji-side feature this prompt does not map (advanced CV data structures, device sensors, full Viji examples) or when you need authoritative TypeScript signatures.\n\n**If you do NOT have web/file access:**\n- Use only the API surface explicitly named in this prompt and the standard Three.js API for direct ports.\n- Never invent Viji property or method names from memory.\n- If the source uses something not covered here, say so and ask the artist how they want it handled; do NOT fabricate a Viji equivalent.\n\n**Tier 1 (always consult when accessible):**\n- TypeScript API types (Viji JS surface, all renderers): https://unpkg.com/@viji-dev/core/dist/artist-global.d.ts\n\n**Tier 2 (consult when needed):**\n- Complete Viji docs (every page + every live example): https://unpkg.com/@viji-dev/core/dist/docs-api.js\n- Companion prompt for any Viji feature this conversion prompt does not cover (search for \"native-prompt\" in the docs above).\n\n**Last-resort lookup:** https://www.npmjs.com/package/@viji-dev/core\n\n## RULES\n\n1. ALWAYS import Three.js dynamically at the top level using `await import()`:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.180.0');\n ```\n NEVER use `<script>` tags, `require()`, or static `import` statements.\n ALWAYS pin the version number in the URL. Pin 0.170 or newer: versions below 0.170 cannot load glb textures inside a worker on Safari and iOS.\n\n2. ALWAYS use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n ALWAYS pass `false` as the third argument to `setSize()`: this prevents Three.js from setting CSS styles, which would fail in the worker.\n\n3. NEVER use `requestAnimationFrame()`. Viji controls the render loop. Write all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. ALWAYS handle resize by checking `viji.width` / `viji.height` in `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n renderer.render(scene, camera);\n }\n ```\n\n5. NEVER access `window`, `document`, `Image()`, or `localStorage`. `fetch()` IS available.\n\n6. NEVER use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. Replace hardcoded values with Viji parameters declared at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const color = viji.color('#049ef4', { label: 'Color' });\n ```\n NEVER declare parameters inside `render()`.\n ALWAYS read via `.value`: `speed.value`, `color.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }`, h in 0..360, s/b in 0..100). Three.js color objects can take `.rgb` directly, e.g. `material.color.setRGB(c.rgb.r/255, c.rgb.g/255, c.rgb.b/255)`, or just keep using `material.color.set(c.value)`.\n\n8. ALWAYS use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n NEVER use `clock.getDelta()` or `Date.now()`. Remove any `THREE.Clock` usage.\n NEVER multiply `viji.time` by a parameter (`viji.time * speed.value`): it causes animation jumps when the parameter changes. Same for nested multiplications: never multiply an accumulator by another parameter; give each speed its own accumulator.\n\n9. Replace mouse/keyboard event listeners with Viji APIs:\n - `event.clientX` → `viji.pointer.x` (works for both mouse and touch) or `viji.mouse.x`\n - `event.clientY` → `viji.pointer.y` or `viji.mouse.y`\n - Mouse buttons → `viji.mouse.leftButton`, `viji.mouse.rightButton`\n - Key presses → `viji.keyboard.isPressed('keyName')`\n\n10. `OrbitControls` and other controls that depend on DOM events will NOT work in the worker.\n For camera interaction, read `viji.pointer` (handles both mouse and touch) and update the camera manually.\n\n11. For Three.js addons, import from the examples directory:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.180.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n ALWAYS use the same Three.js version for addons as for the main library. When the model is a user asset, read it from `viji.model()` instead of a hardcoded URL:\n ```javascript\n const asset = viji.model(null, { label: 'Model', formats: ['glb'] });\n const loader = new GLTFLoader();\n let loaded = null;\n\n function render(viji) {\n if (asset.value !== loaded) {\n loaded = asset.value;\n if (loaded) loader.parse(loaded.bytes, '', (gltf) => scene.add(gltf.scene), (err) => console.error(err));\n }\n renderer.render(scene, camera);\n }\n ```\n `asset.value` is `{ bytes, url, format }` or `null`; `loader.load(asset.value.url, ...)` also works. Parse once per new value, NEVER per frame.\n\n12. For textures from file inputs, use Viji's image parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n // In render():\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n ```\n\n13. For video textures, use `viji.video`:\n ```javascript\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n ```\n\n14. NEVER allocate new objects inside `render()`. Pre-create vectors, colors, and materials at the top level.\n\n15. ALWAYS set `category` on parameters that depend on an external input: `category: 'audio'` for audio, `category: 'video'` for video/CV, `category: 'interaction'` for mouse/keyboard/touch. This lets the host hide irrelevant controls when the input is inactive.\n\n16. Remove any `window.addEventListener('resize', ...)`: resize is handled in `render()` (see rule 4).\n\n17. Remove any CSS, HTML, or DOM manipulation code. Viji scenes produce only canvas output.\n\n18. ALWAYS type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## COMPLETE VIJI API REFERENCE\n\nThe `viji` object is identical across all renderers (same object, same types). The mapping table above covers the most common Three.js → Viji ports; the reference below is the full surface for any Viji-side feature the source code reaches for (audio analysis, video frames, CV data, touch, device sensors).\n\n### Canvas & Context\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.canvas` | `OffscreenCanvas` | The canvas element (pass to Three.js renderer) |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Get 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Get WebGL 1 context |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Get WebGL 2 context |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut (after useContext('2d')) |\n| `viji.gl` | `WebGLRenderingContext` | Shortcut (after useContext('webgl')) |\n| `viji.width` | `number` | Current canvas width in pixels |\n| `viji.height` | `number` | Current canvas height in pixels |\n\nWhen using Three.js, do NOT call `viji.useContext()`: pass `viji.canvas` to the `THREE.WebGLRenderer({ canvas: viji.canvas })` and Three.js manages its own GL context.\n\n### Timing\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (based on host frame-rate mode) |\n\n### Parameters\n\nDeclare at top level. Read `.value` inside `render()`. All support `{ label, description?, group?, category? }`.\nCategory values: `'audio'`, `'video'`, `'interaction'`, `'general'`.\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // { value: number }; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // { value: '#rrggbb' (or '#rrggbbaa' with alpha: true), rgb: { r, g, b } in 0..255, hsb: { h: 0..360, s/b: 0..100 }, alpha: 0..1 }; alpha-bearing inputs only with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] required (2..8 stops); { value: sorted stops, at(t): hex color at t }\nviji.toggle(default, { label, group?, category? }) // { value: boolean }\nviji.select(default, { options: [...], label, group?, category? }) // { value: string|number }\nviji.number(default, { min?, max?, step?, label, group?, category? }) // { value: number }\nviji.text(default, { label, group?, category?, maxLength? }) // { value: string }\nviji.image(null, { label, group?, category? }) // { value: ImageBitmap|null }\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required: list only the formats your loader parses; parse with your own loader once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // { value: boolean } (true one frame)\nviji.coordinate(default, { step?, label, group?, category? }) // { value: { x, y } } (both -1 to 1)\n```\n\n### Audio: `viji.audio`\n\nALWAYS check `viji.audio.isConnected` first.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether audio source is active |\n| `volume.current` | `number` | RMS volume 0-1 |\n| `volume.peak` | `number` | Peak amplitude 0-1 |\n| `volume.smoothed` | `number` | Smoothed volume (200ms decay) |\n| `bands.low` | `number` | 20-120 Hz energy 0-1 |\n| `bands.lowMid` | `number` | 120-400 Hz energy 0-1 |\n| `bands.mid` | `number` | 400-1600 Hz energy 0-1 |\n| `bands.highMid` | `number` | 1600-6000 Hz energy 0-1 |\n| `bands.high` | `number` | 6000-16000 Hz energy 0-1 |\n| `bands.lowSmoothed` … `bands.highSmoothed` | `number` | Smoothed variants of each band |\n| `beat.kick` | `number` | Kick energy curve 0-1, 300ms decay; peaks on each detected kick |\n| `beat.snare` | `number` | Snare energy curve 0-1, 300ms decay |\n| `beat.hat` | `number` | Hi-hat energy curve 0-1, 300ms decay |\n| `beat.any` | `number` | Any-beat energy curve 0-1, 300ms decay |\n| `beat.kickSmoothed` … `beat.anySmoothed` | `number` | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick` | `boolean` | True for exactly one frame on a kick, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.snare` | `boolean` | True for exactly one frame on a snare, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.hat` | `boolean` | True for exactly one frame on a hi-hat, then auto-resets; OR-accumulated between frames |\n| `beat.triggers.any` | `boolean` | True for exactly one frame on any beat, then auto-resets |\n| `beat.events` | `Array<{type,time,strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in ms; cleared each frame |\n| `beat.bpm` | `number` | `0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on |\n| `beat.confidence` | `number` | BPM tracking confidence 0-1 |\n| `beat.isLocked` | `boolean` | True on stable tempo lock |\n| `spectral.brightness` | `number` | Spectral centroid 0-1 |\n| `spectral.flatness` | `number` | Spectral flatness 0-1 |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins (0-255) |\n| `getWaveform()` | `Float32Array` | Time-domain waveform (−1 to 1) |\n\n### Video: `viji.video`\n\nALWAYS check `viji.video.isConnected` first. Check `currentFrame` before drawing.\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isConnected` | `boolean` | Whether video source is active |\n| `currentFrame` | `OffscreenCanvas\\|ImageBitmap\\|null` | Current video frame |\n| `frameWidth` | `number` | Frame width in pixels |\n| `frameHeight` | `number` | Frame height in pixels |\n| `frameRate` | `number` | Video frame rate |\n| `getFrameData()` | `ImageData\\|null` | Pixel data for CPU access |\n| `cv` | `VideoCVAPI` | Computer-vision surface (see below) |\n\nCV-paired outputs (`analysedFrame`, `getAnalysedFrameData()`) and detection results live on `viji.video.cv`, not on `viji.video` directly. For Three.js video textures, see rule 13.\n\n### Computer Vision: `viji.video.cv` & `viji.video.cv.faces / hands / pose / segmentation`\n\nEnable features via toggle parameters (NEVER enable by default):\n\n```javascript\nawait viji.video.cv.enableFaceDetection(true/false);\nawait viji.video.cv.enableFaceMesh(true/false); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true/false); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true/false);\nawait viji.video.cv.enablePoseDetection(true/false);\nawait viji.video.cv.enableBodySegmentation(true/false);\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nCV-paired outputs (also on `viji.video.cv`):\n- `analysedFrame: OffscreenCanvas | null`: the exact frame that produced the current CV results. `null` until the first inference lands. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`viji.video.cv.faces: FaceData[]`**\nEach face: `id` (number), `bounds` ({x,y,width,height}), `center` ({x,y}), `confidence` (0-1), `landmarks` ({x,y,z?}[]), `expressions` ({neutral,happy,sad,angry,surprised,disgusted,fearful} all 0-1), `headPose` ({pitch,yaw,roll} in degrees), `blendshapes` (52 ARKit coefficients 0-1: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut).\n\n**`viji.video.cv.hands: HandData[]`**\nEach hand: `id` (number), `handedness` ('left'|'right'), `confidence` (0-1), `bounds` ({x,y,width,height}), `landmarks` ({x,y,z}[], 21 points), `palm` ({x,y,z}), `gestures` ({fist,openPalm,peace,thumbsUp,thumbsDown,pointing,iLoveYou} all 0-1).\n\n**`viji.video.cv.pose: PoseData | null`**\n`confidence` (0-1), `landmarks` ({x,y,z,visibility}[], 33 points), plus body-part arrays: `face` ({x,y}[]), `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**\n`mask` (Uint8Array; each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input: Pointer (unified mouse/touch): `viji.pointer`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Movement since last frame |\n| `isDown` | `boolean` | True if pressed/touching |\n| `wasPressed` | `boolean` | True on press frame |\n| `wasReleased` | `boolean` | True on release frame |\n| `isInCanvas` | `boolean` | True if inside canvas |\n| `type` | `string` | `'mouse'`, `'touch'`, or `'none'` |\n\n### Input: Mouse: `viji.mouse`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `x`, `y` | `number` | Position in pixels |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `isPressed` | `boolean` | Any button pressed |\n| `leftButton`, `rightButton`, `middleButton` | `boolean` | Specific buttons |\n| `deltaX`, `deltaY` | `number` | Movement delta |\n| `wheelDelta` | `number` | Scroll wheel delta |\n| `wheelX`, `wheelY` | `number` | Horizontal/vertical scroll |\n| `wasPressed`, `wasReleased`, `wasMoved` | `boolean` | Frame-edge events |\n\n### Input: Keyboard: `viji.keyboard`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `isPressed(key)` | `boolean` | True while key is held |\n| `wasPressed(key)` | `boolean` | True on key-down frame |\n| `wasReleased(key)` | `boolean` | True on key-up frame |\n| `activeKeys` | `Set<string>` | Currently held keys |\n| `pressedThisFrame` | `Set<string>` | Keys pressed this frame |\n| `releasedThisFrame` | `Set<string>` | Keys released this frame |\n| `lastKeyPressed` | `string` | Most recent key-down |\n| `lastKeyReleased` | `string` | Most recent key-up |\n| `shift`, `ctrl`, `alt`, `meta` | `boolean` | Modifier states |\n\n### Input: Touch: `viji.touches`\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `count` | `number` | Active touch count |\n| `points` | `TouchPoint[]` | All active touches |\n| `started` | `TouchPoint[]` | Touches started this frame |\n| `moved` | `TouchPoint[]` | Touches moved this frame |\n| `ended` | `TouchPoint[]` | Touches ended this frame |\n| `primary` | `TouchPoint\\|null` | First active touch |\n\n**TouchPoint:** `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity` ({x,y}), `isNew`, `isActive`, `isEnding`.\n\n### Device Sensors: `viji.device`\n\n`viji.device.motion` (DeviceMotionData|null): `acceleration` ({x,y,z} m/s²), `accelerationIncludingGravity`, `rotationRate` ({alpha,beta,gamma} deg/s), `interval` (ms).\n`viji.device.orientation` (DeviceOrientationData|null): `alpha` (0-360° compass), `beta` (−180-180° tilt), `gamma` (−90-90° tilt), `absolute` (boolean).\n\n### External Devices: `viji.devices`\n\n`DeviceState[]`: connected external devices. Each entry: `id` (string), `name` (string), `motion` (DeviceMotionData|null), `orientation` (DeviceOrientationData|null), `video` (VideoAPI|null, same shape as `viji.video` but without CV), `audio` (AudioStreamAPI|null, lightweight subset of `viji.audio`: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` + each `*Smoothed`, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. **No** beat / BPM / triggers / events).\n\n### Streams: `viji.videoStreams` and `viji.audioStreams`\n\n`viji.videoStreams: VideoAPI[]` and `viji.audioStreams: AudioStreamAPI[]`: additional video/audio sources provided by the host application (used internally by Viji's compositor for scene mixing). May be empty. Audio streams use the AudioStreamAPI shape (no beat / BPM / triggers / events).\n\n## TEMPLATE\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 1, 3);\ncamera.lookAt(0, 0, 0);\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshStandardMaterial({ color: color.value });\nconst mesh = new THREE.Mesh(geometry, material);\nscene.add(mesh);\n\nscene.add(new THREE.DirectionalLight(0xffffff, 1.5));\nscene.add(new THREE.AmbientLight(0x404040));\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n mesh.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n```\n\nNow convert the Three.js code I provide.\n\nIf the source is incomplete or uses a framework that needs to be unwrapped, ask one or two clarifying questions first (see YOUR BEHAVIOR above). Otherwise, proceed.\n\nWhen you produce the conversion:\n- Apply every rule above. The conversion-specific rules (1-17) cover the Three.js → Viji mappings most artists need; the **COMPLETE VIJI API REFERENCE** above lists the full Viji surface for any Viji-side feature the source code reaches for (audio, video, CV, touch, device sensors, parameters beyond the basics). For details not in this prompt, consult the **REFERENCE** links: the canonical companion generation prompt is `native-prompt` (in the `docs-api.js` bundle).\n- Output the Viji scene code in a single fenced code block.\n- After the code block, write a short summary of the key changes and flag anything you had to drop or simplify (controls, postprocessing, framework wrappers).\n- Invite the artist to ask for refinements.\n````\n\n## Usage\n\n1. Copy the entire prompt block above.\n2. Paste it into your AI assistant.\n3. After the prompt, paste the Three.js code you want to convert.\n4. The AI will return a Viji-compatible native scene.\n\n> [!NOTE]\n> This prompt handles standard Three.js scenes. If the original code uses a framework (React Three Fiber, Drei, etc.), you may need to manually extract the Three.js scene setup first.\n\n## Related\n\n- [External Libraries](/native/external-libraries): detailed guide for using Three.js and other libraries in Viji\n- [Prompt: Native Scenes](/ai-prompts/native-prompt): AI prompt for creating new native scenes from scratch\n- [Native Quick Start](/native/quickstart): your first Viji native scene"
|
|
1567
1577
|
}
|
|
1568
1578
|
]
|
|
1569
1579
|
},
|
|
@@ -1626,7 +1636,7 @@ export const docsApi = {
|
|
|
1626
1636
|
},
|
|
1627
1637
|
{
|
|
1628
1638
|
"type": "text",
|
|
1629
|
-
"markdown": "### What's Happening\n\n**Top level (runs once):**\n\n- `viji.color()` and `viji.slider()` create parameters that appear in the UI. They must be declared at the top level, not inside `render()`.\n\n**`render(viji)` (called every frame):**\n\n- `viji.useContext('2d')` returns a standard `CanvasRenderingContext2D`. Call it once; the context is cached.\n- `viji.width` and `viji.height` give the current canvas size. Use them instead of hardcoded pixels.\n- `viji.time` is elapsed seconds since the scene started. Use it for animation.\n- Each parameter's `.value` is read inside `render()` and updates live as the user moves sliders.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Scene Structure\n\nA native scene has two parts:\n\n```javascript\n// 1. Top level: initialization, parameters, state\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\n\n// 2. render(): called every frame\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n // ... draw using ctx\n}\n```\n\n- **No `setup()` function.** All initialization happens at the top level. Top-level `await` is supported for dynamic imports.\n- **`render(viji)` is the only required function.** It receives the Viji API object with canvas, timing, audio, video, parameters, and interaction data.\n\n## Canvas Context\n\nUse `viji.useContext()` to get a rendering context:\n\n```javascript\nconst ctx = viji.useContext('2d'); // Canvas 2D\nconst gl = viji.useContext('webgl'); // WebGL 1\nconst gl2 = viji.useContext('webgl2'); // WebGL 2\n```\n\nPick one and use it for the entire scene. A canvas only supports one context type, so requesting a different type returns `null`.\n\n## Essential Patterns\n\n> [!NOTE]\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) come through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n> [!TIP]\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\n> ```javascript\n> // Good: pre-allocated\n> const pos = { x: 0, y: 0 };\n> function render(viji) {\n> pos.x = viji.width / 2;\n> pos.y = viji.height / 2;\n> }\n>\n> // Bad: creates a new object every frame\n> function render(viji) {\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\n> }\n> ```\n\n## External Libraries\n\nTop-level `await` lets you load libraries from a CDN:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.
|
|
1639
|
+
"markdown": "### What's Happening\n\n**Top level (runs once):**\n\n- `viji.color()` and `viji.slider()` create parameters that appear in the UI. They must be declared at the top level, not inside `render()`.\n\n**`render(viji)` (called every frame):**\n\n- `viji.useContext('2d')` returns a standard `CanvasRenderingContext2D`. Call it once; the context is cached.\n- `viji.width` and `viji.height` give the current canvas size. Use them instead of hardcoded pixels.\n- `viji.time` is elapsed seconds since the scene started. Use it for animation.\n- Each parameter's `.value` is read inside `render()` and updates live as the user moves sliders.\n\n> [!NOTE]\n> Parameters must be defined at the top level of your scene, not inside `render()`. They are registered once during initialization. Defining them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Scene Structure\n\nA native scene has two parts:\n\n```javascript\n// 1. Top level: initialization, parameters, state\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\n\n// 2. render(): called every frame\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n // ... draw using ctx\n}\n```\n\n- **No `setup()` function.** All initialization happens at the top level. Top-level `await` is supported for dynamic imports.\n- **`render(viji)` is the only required function.** It receives the Viji API object with canvas, timing, audio, video, parameters, and interaction data.\n\n## Canvas Context\n\nUse `viji.useContext()` to get a rendering context:\n\n```javascript\nconst ctx = viji.useContext('2d'); // Canvas 2D\nconst gl = viji.useContext('webgl'); // WebGL 1\nconst gl2 = viji.useContext('webgl2'); // WebGL 2\n```\n\nPick one and use it for the entire scene. A canvas only supports one context type, so requesting a different type returns `null`.\n\n## Essential Patterns\n\n> [!NOTE]\n> Always use `viji.width` and `viji.height` for positioning and sizing, and `viji.deltaTime` for frame-rate-independent animation. Never hardcode pixel values or assume a specific frame rate.\n\n> [!WARNING]\n> Scenes run in a Web Worker. There is no `window`, `document`, `Image()`, `localStorage`, or any DOM API. All inputs (audio, video, images) come through the Viji API. One exception: `fetch()` is available, so you can load external data (JSON, etc.) from CDNs.\n\n> [!TIP]\n> Avoid allocating objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse them:\n> ```javascript\n> // Good: pre-allocated\n> const pos = { x: 0, y: 0 };\n> function render(viji) {\n> pos.x = viji.width / 2;\n> pos.y = viji.height / 2;\n> }\n>\n> // Bad: creates a new object every frame\n> function render(viji) {\n> const pos = { x: viji.width / 2, y: viji.height / 2 };\n> }\n> ```\n\n## External Libraries\n\nTop-level `await` lets you load libraries from a CDN:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\n// ... set up your Three.js scene at the top level\nfunction render(viji) { /* ... */ }\n```\n\nSee [External Libraries](/native/external-libraries#threejs) for detailed patterns.\n\n## TypeScript Support\n\nThe Viji editor provides built-in TypeScript support with autocomplete, inline hints, and type checking. All Viji types, `VijiAPI`, `SliderParameter`, `AudioAPI`, `MouseAPI`, and others, are available globally without imports. Add type annotations for richer editor assistance:\n\n```typescript\nfunction render(viji: VijiAPI) {\n const ctx = viji.useContext('2d'); // ctx is typed as CanvasRenderingContext2D\n // ... full autocomplete for viji.audio, viji.mouse, etc.\n}\n```\n\nTypeScript is optional. The same code works as plain JavaScript without the annotations."
|
|
1630
1640
|
},
|
|
1631
1641
|
{
|
|
1632
1642
|
"type": "live-example",
|
|
@@ -1954,6 +1964,11 @@ export const docsApi = {
|
|
|
1954
1964
|
"level": 3,
|
|
1955
1965
|
"text": "Video and Image Textures"
|
|
1956
1966
|
},
|
|
1967
|
+
{
|
|
1968
|
+
"id": "3d-model-assets",
|
|
1969
|
+
"level": 3,
|
|
1970
|
+
"text": "3D Model Assets"
|
|
1971
|
+
},
|
|
1957
1972
|
{
|
|
1958
1973
|
"id": "other-libraries",
|
|
1959
1974
|
"level": 2,
|
|
@@ -1983,17 +1998,17 @@ export const docsApi = {
|
|
|
1983
1998
|
"content": [
|
|
1984
1999
|
{
|
|
1985
2000
|
"type": "text",
|
|
1986
|
-
"markdown": "# External Libraries\n\nThe native renderer supports **top-level `await`**, which means you can dynamically import any ES module from a CDN directly in your scene code. No bundler, no install step: just `await import()`.\n\n> [!TIP]\n> Have an existing Three.js project? See [Convert: Three.js](/ai-prompts/convert-threejs) for a ready-to-paste AI prompt that adapts your code to Viji automatically.\n\n## Importing a Library\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.
|
|
2001
|
+
"markdown": "# External Libraries\n\nThe native renderer supports **top-level `await`**, which means you can dynamically import any ES module from a CDN directly in your scene code. No bundler, no install step: just `await import()`.\n\n> [!TIP]\n> Have an existing Three.js project? See [Convert: Three.js](/ai-prompts/convert-threejs) for a ready-to-paste AI prompt that adapts your code to Viji automatically.\n\n## Importing a Library\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\n```\n\nThis works because Viji scene code runs in a module-like context where top-level `await` is available. The import happens once during scene initialization, before `render()` is ever called.\n\n> [!TIP]\n> [esm.sh](https://esm.sh) is a recommended CDN for ES modules. It serves any npm package as a standard ES module and handles dependency resolution automatically.\n\n## Three.js\n\nThree.js is the most common external library used with Viji. It works naturally in native mode because it accepts an external canvas, has no automatic render loop, and uses standard WebGL."
|
|
1987
2002
|
},
|
|
1988
2003
|
{
|
|
1989
2004
|
"type": "live-example",
|
|
1990
2005
|
"title": "Three.js: Lit Cube",
|
|
1991
|
-
"sceneCode": "const THREE = await import('https://esm.sh/three@0.
|
|
2006
|
+
"sceneCode": "const THREE = await import('https://esm.sh/three@0.180.0');\n\nconst rotationSpeed = viji.slider(1, { min: 0.1, max: 5, label: 'Rotation Speed' });\nconst cubeColor = viji.color('#049ef4', { label: 'Cube Color' });\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(60, viji.width / viji.height, 0.1, 100);\ncamera.position.set(2, 1.5, 3);\ncamera.lookAt(0, 0, 0);\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshStandardMaterial({ color: cubeColor.value });\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\nconst light = new THREE.DirectionalLight(0xffffff, 1.5);\nlight.position.set(3, 4, 2);\nscene.add(light);\nscene.add(new THREE.AmbientLight(0x404040));\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n cube.rotation.x += rotationSpeed.value * viji.deltaTime * 0.5;\n cube.rotation.y += rotationSpeed.value * viji.deltaTime;\n material.color.set(cubeColor.value);\n\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n",
|
|
1992
2007
|
"sceneFile": "threejs-scene.scene.js"
|
|
1993
2008
|
},
|
|
1994
2009
|
{
|
|
1995
2010
|
"type": "text",
|
|
1996
|
-
"markdown": "### Key Integration Points\n\n**Canvas:** Pass `viji.canvas` to the Three.js renderer:\n\n```javascript\nconst renderer = new THREE.WebGLRenderer({\n canvas: viji.canvas,\n antialias: true\n});\n```\n\n**Resize:** Three.js doesn't auto-resize. Check for dimension changes in `render()`:\n\n```javascript\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n```\n\n> [!IMPORTANT]\n> Pass `false` as the third argument to `renderer.setSize()`: this prevents Three.js from setting CSS styles on the canvas, which would fail in the worker environment.\n\n**No `requestAnimationFrame`:** Viji controls the render loop. Write your animation logic inside `render(viji)` and call `renderer.render(scene, camera)` at the end.\n\n**Parameters:** All Viji parameter types work as expected:\n\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nfunction render(viji) {\n cube.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n renderer.render(scene, camera);\n}\n```\n\n### Three.js Addons\n\nImport addons from the Three.js examples directory:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.
|
|
2011
|
+
"markdown": "### Key Integration Points\n\n**Canvas:** Pass `viji.canvas` to the Three.js renderer:\n\n```javascript\nconst renderer = new THREE.WebGLRenderer({\n canvas: viji.canvas,\n antialias: true\n});\n```\n\n**Resize:** Three.js doesn't auto-resize. Check for dimension changes in `render()`:\n\n```javascript\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n```\n\n> [!IMPORTANT]\n> Pass `false` as the third argument to `renderer.setSize()`: this prevents Three.js from setting CSS styles on the canvas, which would fail in the worker environment.\n\n**No `requestAnimationFrame`:** Viji controls the render loop. Write your animation logic inside `render(viji)` and call `renderer.render(scene, camera)` at the end.\n\n**Parameters:** All Viji parameter types work as expected:\n\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#049ef4', { label: 'Color' });\n\nfunction render(viji) {\n cube.rotation.y += speed.value * viji.deltaTime;\n material.color.set(color.value);\n renderer.render(scene, camera);\n}\n```\n\n### Three.js Addons\n\nImport addons from the Three.js examples directory:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\nconst { OrbitControls } = await import('https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js');\nconst { GLTFLoader } = await import('https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js');\nconst { EffectComposer } = await import('https://esm.sh/three@0.180.0/examples/jsm/postprocessing/EffectComposer.js');\n```\n\n> [!NOTE]\n> `OrbitControls` requires DOM events and will not work as expected in the worker environment. For camera interaction, use [`viji.pointer`](/native/pointer) to drive the camera manually: it handles both mouse and touch input.\n\n### Video and Image Textures\n\nThree.js can consume Viji's video frames and image parameters directly:\n\n```javascript\nconst photo = viji.image(null, { label: 'Texture' });\nlet texture = null;\n\nfunction render(viji) {\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n videoMaterial.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n\n renderer.render(scene, camera);\n}\n```\n\n### 3D Model Assets\n\nUser-uploaded models arrive through [`viji.model()`](/native/parameters/model): the parameter hands you the file as raw bytes plus a blob URL, tagged with its format, and you parse it with the Three.js loader of your choice (`GLTFLoader.parse(value.bytes, '')`, `loader.load(value.url)`, `PLYLoader`, `STLLoader`, and so on). Nothing is parsed for you, so any format a loader understands works, and the loaded geometry stays yours to dispose. See the [Model parameter](/native/parameters/model) page for the parse-on-change pattern, point clouds, and the full format list.\n\n## Other Libraries\n\nAny library published as an ES module works the same way. Examples:\n\n**Math / utility libraries:**\n\n```javascript\nconst { vec3, mat4 } = await import('https://esm.sh/gl-matrix@3.4.3');\nconst SimplexNoise = (await import('https://esm.sh/simplex-noise@4.0.1')).createNoise3D;\n```\n\n**2D rendering libraries:**\n\n```javascript\nconst PIXI = await import('https://esm.sh/pixi.js@7.3.2');\n```\n\n> [!NOTE]\n> Some libraries expect a DOM environment and may not work in the worker context. Libraries that accept an external canvas or work with pure data (math, noise, data structures) are the best candidates.\n\n## WebGL Context\n\nWhen using a library that creates its own WebGL context (like Three.js or Pixi.js), keep in mind:\n\n- The library consumes the canvas's WebGL context. You cannot also call `viji.useContext('2d')` or `viji.useContext('webgl')` for the same canvas: pick one rendering approach.\n- CV features (face detection, hand tracking, etc.) use additional WebGL contexts internally. Browsers have a limit of 8-16 contexts. Combining a WebGL library with multiple CV features may cause context loss.\n\nSee [Canvas & Context](/native/canvas-context) for details on `useContext()`.\n\n## Machine Learning Libraries\n\nTensorFlow.js and ONNX Runtime Web both work inside scenes. Import the library at the top level, run inference outside `render()` (inference takes longer than a frame), and draw the most recent result each frame. Model files downloaded with `fetch()` are cached by the engine on the host, so a model downloads once and later scene loads read it from disk. Only large static files are cached: small responses such as live API data stay fresh on every call. Note that `import()` module loads are not cached this way; libraries are small compared to models, so this rarely matters.\n\n**ONNX Runtime Web** (version 1.27 and newer): import the module build from jsdelivr and tell the runtime where to download its WebAssembly files. Both values are URLs on the same CDN:\n\n```javascript\nconst ort = await import('https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/ort.webgpu.min.mjs');\nort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/';\n\nconst modelBytes = new Uint8Array(await (await fetch(modelUrl)).arrayBuffer());\nconst session = await ort.InferenceSession.create(modelBytes, {\n executionProviders: ['webgpu']\n});\n```\n\n> [!NOTE]\n> Import ONNX Runtime Web from jsdelivr, not esm.sh. The esm.sh build injects Node.js compatibility code that breaks the runtime during startup.\n\n**TensorFlow.js**: `importScripts('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js')` loads the classic bundle and exposes the global `tf`. The WebGL backend initializes automatically on first use.\n\n**Transformers.js**: import the plain-file build and turn off its Cache Storage use. The scene worker cannot access Cache Storage; plain fetch downloads work and are cached by the engine on the host:\n\n```javascript\nconst tf = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.1/dist/transformers.min.js');\ntf.env.useBrowserCache = false;\n```\n\n## Tips\n\n- **Pin library versions** in the import URL (e.g., `three@0.180.0`). Unpinned versions may break when the library updates. For Three.js, pin 0.170 or newer: versions below 0.170 cannot load glb textures inside a worker on Safari and iOS.\n- **Import once at the top level.** Never put `await import()` inside `render()`: it would re-fetch the library every frame.\n- **Check worker compatibility** before choosing a library. Libraries that require `document`, `window`, or `Image()` will not work. `fetch()` is available.\n- **Use `viji.deltaTime`** for animation instead of the library's own timer, to stay frame-rate-independent and synchronized with other Viji features.\n- **Spawning a helper worker?** Build it from a `data:` URL: `new Worker('data:application/javascript,' + encodeURIComponent(workerSource))`. In Chrome, only `data:` workers can use WebGPU and other capabilities that need a secure context. Workers built from `blob:` URLs still run everywhere, they just cannot use those capabilities in Chrome.\n\n## Related\n\n- [Canvas & Context](/native/canvas-context): `useContext('2d')`, `useContext('webgl')`, `useContext('webgl2')`\n- [Native Quick Start](/native/quickstart): building native scenes from scratch\n- [Parameters](/native/parameters): sliders, colors, toggles\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
|
|
1997
2012
|
}
|
|
1998
2013
|
]
|
|
1999
2014
|
},
|
|
@@ -2031,7 +2046,7 @@ export const docsApi = {
|
|
|
2031
2046
|
"content": [
|
|
2032
2047
|
{
|
|
2033
2048
|
"type": "text",
|
|
2034
|
-
"markdown": "# Parameters\n\nParameters are the primary way to give users control over your scene. You define them at the top level of your code, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Read `.value` inside `render()` to get the current state. Values update in real time as users interact with the controls.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB), optional alpha channel |\n| [Gradient](gradient/) | [`viji.gradient(default, config)`](gradient/) | `{ color, position }[]` | Color ramp editor. Sorted stops plus `at(t)` sampling. |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = color.value;\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `render()`. They are registered once during initialization. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on active capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, organize parameters into logical sections and control their visibility:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp with `at(t)` sampling\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [P5 Parameters](/p5/parameters): same system in the P5 renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
|
|
2049
|
+
"markdown": "# Parameters\n\nParameters are the primary way to give users control over your scene. You define them at the top level of your code, and Viji renders corresponding UI controls (sliders, color pickers, toggles, etc.) in the host application. Read `.value` inside `render()` to get the current state. Values update in real time as users interact with the controls.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB), optional alpha channel |\n| [Gradient](gradient/) | [`viji.gradient(default, config)`](gradient/) | `{ color, position }[]` | Color ramp editor. Sorted stops plus `at(t)` sampling. |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures |\n| [Model](model/) | [`viji.model(default, config)`](model/) | `ModelValue \\| null` | User-uploaded 3D models (glb, obj, stl, ply, las, laz, splat), parsed by your own loader |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n ctx.fillStyle = color.value;\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `render()`. They are registered once during initialization. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default and making user changes ineffective.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on active capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, organize parameters into logical sections and control their visibility:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp with `at(t)` sampling\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [P5 Parameters](/p5/parameters): same system in the P5 renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
|
|
2035
2050
|
}
|
|
2036
2051
|
]
|
|
2037
2052
|
},
|
|
@@ -2464,6 +2479,69 @@ export const docsApi = {
|
|
|
2464
2479
|
}
|
|
2465
2480
|
]
|
|
2466
2481
|
},
|
|
2482
|
+
"native-param-model": {
|
|
2483
|
+
"id": "native-param-model",
|
|
2484
|
+
"title": "Model Parameter",
|
|
2485
|
+
"description": "Accept user-uploaded 3D model files (glb, obj, stl, ply, las, laz, splat) and parse them with your own loader.",
|
|
2486
|
+
"headings": [
|
|
2487
|
+
{
|
|
2488
|
+
"id": "parameters",
|
|
2489
|
+
"level": 2,
|
|
2490
|
+
"text": "Parameters"
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
"id": "return-value",
|
|
2494
|
+
"level": 2,
|
|
2495
|
+
"text": "Return Value"
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
"id": "value-lifetime",
|
|
2499
|
+
"level": 2,
|
|
2500
|
+
"text": "Value Lifetime"
|
|
2501
|
+
},
|
|
2502
|
+
{
|
|
2503
|
+
"id": "usage",
|
|
2504
|
+
"level": 2,
|
|
2505
|
+
"text": "Usage"
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
"id": "loading-by-url",
|
|
2509
|
+
"level": 2,
|
|
2510
|
+
"text": "Loading by URL"
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
"id": "point-clouds",
|
|
2514
|
+
"level": 2,
|
|
2515
|
+
"text": "Point Clouds"
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
"id": "formats",
|
|
2519
|
+
"level": 2,
|
|
2520
|
+
"text": "Formats"
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
"id": "related",
|
|
2524
|
+
"level": 2,
|
|
2525
|
+
"text": "Related"
|
|
2526
|
+
}
|
|
2527
|
+
],
|
|
2528
|
+
"content": [
|
|
2529
|
+
{
|
|
2530
|
+
"type": "text",
|
|
2531
|
+
"markdown": "# viji.model()\n\n```\nmodel(defaultValue: null, config: ModelConfig): ModelParameter\n```\n\nCreates a 3D model upload parameter. The host renders it as a file picker for model files. The uploaded file becomes available as a `ModelValue`: the raw bytes, a blob URL, and a format tag. Viji never parses the file. Pass `bytes` to a loader's `parse()` or `url` to its `load()`.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | | Always `null`: models are provided by the user |\n| `config.label` | `string` | No | variable name | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.formats` | `ModelFormat[]` | Yes | | Formats your scene can parse, and nothing more. The host filters its file picker by this list and files of other formats are rejected. Must not be empty. See [Formats](#formats) |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ModelParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ModelValue \\| null` | The uploaded model, or `null` if none provided |\n| `formats` | `ReadonlyArray<ModelFormat>` | The formats this parameter accepts (`config.formats`, deduplicated) |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\nA `ModelValue` has three properties:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `bytes` | `ArrayBuffer` | The whole file. `bytes.byteLength` is the file size. Pass it to a loader's `parse()` |\n| `url` | `string` | A blob URL for loaders that take a URL (`loader.load(url)`). Created on first read and cached |\n| `format` | `ModelFormat` | Format tag: `'glb'`, `'obj'`, `'stl'`, `'ply'`, `'las'`, `'laz'` or `'splat'` |\n\n## Value Lifetime\n\nRead `value` where you parse it. When the host replaces or clears the model, `url` is revoked and `bytes` is dropped by core; geometry you already built from it stays valid.\n\nRead `url` where you load. A URL from a replaced value rejects when loaded afterwards; revocation does not abort a load that is already in flight. Reading `url` on a value the host already replaced throws.\n\nSome loaders transfer the buffer to their own worker and detach `value.bytes`. Pass `bytes.slice(0)` to those, or disable their worker (for loaders.gl, `{ worker: false }`).\n\nThe parameter object itself is stable; only `.value` changes. Compare `value` by identity to detect a new upload: each upload is a new object.\n\n## Usage\n\nAlways check for `null` before parsing: the user may not have uploaded a model yet. Parse once per upload, not every frame:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\nconst { GLTFLoader } = await import('https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js');\n\nconst statue = viji.model(null, { label: 'Statue', formats: ['glb'] });\nconst loader = new GLTFLoader();\nlet loaded = null;\n// scene, camera and renderer are set up as on the External Libraries page\n\nfunction render(viji) {\n if (statue.value !== loaded) {\n loaded = statue.value;\n if (loaded) {\n loader.parse(loaded.bytes, '', (gltf) => scene.add(gltf.scene), (err) => console.error(err));\n }\n }\n\n renderer.render(scene, camera);\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."
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
"type": "live-example",
|
|
2535
|
+
"title": "Model Upload",
|
|
2536
|
+
"sceneCode": "const THREE = await import('https://esm.sh/three@0.180.0');\nconst { GLTFLoader } = await import('https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js');\n\nconst statue = viji.model(null, { label: 'Model', formats: ['glb'] });\nconst size = viji.slider(1, { min: 0.3, max: 2, step: 0.05, label: 'Size' });\nconst bg = viji.color('#0f0f1a', { label: 'Background' });\n\nconst scene = new THREE.Scene();\nscene.background = new THREE.Color(bg.value);\n\nconst camera = new THREE.PerspectiveCamera(45, viji.width / viji.height, 0.01, 100);\ncamera.position.set(0, 0.5, 3.2);\ncamera.lookAt(0, 0, 0);\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nscene.add(new THREE.HemisphereLight(0xffffff, 0x404060, 2));\nconst key = new THREE.DirectionalLight(0xffffff, 2.5);\nkey.position.set(3, 4, 2);\nscene.add(key);\n\n// Everything the user uploads hangs off this pivot so the whole model spins.\nconst pivot = new THREE.Group();\nscene.add(pivot);\n\n// Shown while no model is loaded.\nconst placeholder = new THREE.Mesh(\n new THREE.IcosahedronGeometry(0.8, 1),\n new THREE.MeshBasicMaterial({ color: 0x8888aa, wireframe: true })\n);\npivot.add(placeholder);\n\nconst loader = new GLTFLoader();\nlet loaded = null; // the ModelValue currently parsed\nlet current = null; // the parsed scene graph\n\nfunction dispose(root) {\n root.traverse((obj) => {\n if (obj.geometry) obj.geometry.dispose();\n if (!obj.material) return;\n const materials = Array.isArray(obj.material) ? obj.material : [obj.material];\n for (const m of materials) {\n for (const key of Object.keys(m)) {\n if (m[key] && m[key].isTexture) m[key].dispose();\n }\n m.dispose();\n }\n });\n}\n\nfunction show(root) {\n if (current) {\n pivot.remove(current);\n dispose(current);\n }\n current = root;\n placeholder.visible = !root;\n if (!root) return;\n\n // Center the model and scale it to fit a 1.6 unit box.\n const box = new THREE.Box3().setFromObject(root);\n const extent = box.getSize(new THREE.Vector3());\n const center = box.getCenter(new THREE.Vector3());\n const scale = 1.6 / Math.max(extent.x, extent.y, extent.z, 1e-6);\n root.position.copy(center).multiplyScalar(-scale);\n root.scale.setScalar(scale);\n pivot.add(root);\n}\n\nfunction load(value) {\n loaded = value;\n if (!value) {\n show(null);\n return;\n }\n // parse(bytes) needs no copy: the bytes are already in this worker.\n loader.parse(value.bytes, '', (gltf) => {\n if (statue.value !== value) return; // replaced while parsing\n show(gltf.scene);\n }, (err) => {\n console.error('Could not parse the glb file', err);\n });\n}\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n // A new upload is a new object: compare by identity.\n if (statue.value !== loaded) load(statue.value);\n\n pivot.rotation.y = viji.time * 0.6;\n pivot.scale.setScalar(size.value);\n scene.background.set(bg.value);\n\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n renderer.render(scene, camera);\n}\n",
|
|
2537
|
+
"sceneFile": "model-basic.scene.js"
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
"type": "text",
|
|
2541
|
+
"markdown": "> [!IMPORTANT]\n> Pin three.js 0.170 or newer. Versions below 0.170 cannot load glb textures inside a worker on Safari and iOS: every texture fails silently and the model renders untextured.\n\n## Loading by URL\n\nLoaders that only take a URL use `value.url`. Read it at the moment you start the load:\n\n```javascript\nfunction load(value) {\n loader.load(value.url, (gltf) => {\n if (statue.value !== value) return; // replaced while loading\n scene.add(gltf.scene);\n }, undefined, (err) => console.error(err));\n}\n```\n\n`parse(bytes)` is the cheaper path: the bytes are already in your worker, and no blob copy is made. Prefer it when the loader offers one.\n\n## Point Clouds\n\nPLY files load with the three.js `PLYLoader`; LAS and LAZ files load with `@loaders.gl/las`. Both produce a `THREE.Points` object:\n\n```javascript\nconst { PLYLoader } = await import('https://esm.sh/three@0.180.0/examples/jsm/loaders/PLYLoader.js');\nconst { parse } = await import('https://esm.sh/@loaders.gl/core@4.3.3');\nconst { LASLoader } = await import('https://esm.sh/@loaders.gl/las@4.3.3');\n\nconst cloud = viji.model(null, { label: 'Point Cloud', formats: ['ply', 'las', 'laz'] });\nconst plyLoader = new PLYLoader();\n\nasync function toGeometry(value) {\n if (value.format === 'ply') {\n return plyLoader.parse(value.bytes);\n }\n // worker: false keeps the bytes in this worker; the default would detach value.bytes\n const las = await parse(value.bytes, LASLoader, { worker: false });\n const geometry = new THREE.BufferGeometry();\n geometry.setAttribute('position', new THREE.BufferAttribute(las.attributes.POSITION.value, 3));\n const color = las.attributes.COLOR_0;\n if (color) {\n geometry.setAttribute('color', new THREE.BufferAttribute(color.value, color.size, true));\n }\n return geometry;\n}\n\nasync function show(value) {\n const geometry = await toGeometry(value);\n if (cloud.value !== value) { geometry.dispose(); return; }\n geometry.center();\n const material = new THREE.PointsMaterial({ size: 0.01, vertexColors: geometry.hasAttribute('color') });\n scene.add(new THREE.Points(geometry, material));\n}\n```\n\nSplat files (`.splat`) have no header: the file is a sequence of 32-byte records (three `float32` position, three `float32` scale, four `uint8` RGBA, four `uint8` rotation quaternion). Read them with a `DataView` or hand `bytes` to a Gaussian splat library.\n\n## Formats\n\n| Format | Extension | MIME type | Typical loader |\n|--------|-----------|-----------|----------------|\n| `glb` | `.glb` | `model/gltf-binary` | three.js `GLTFLoader.parse(bytes, '')` or `.load(url)` |\n| `obj` | `.obj` | `model/obj` | three.js `OBJLoader.parse(text)` with `new TextDecoder().decode(bytes)` |\n| `stl` | `.stl` | `model/stl` | three.js `STLLoader.parse(bytes)` (ascii and binary) |\n| `ply` | `.ply` | `application/x-ply` | three.js `PLYLoader.parse(bytes)` (meshes and point clouds, per-vertex color) |\n| `las` | `.las` | `application/vnd.las` | loaders.gl `parse(bytes, LASLoader, { worker: false })` |\n| `laz` | `.laz` | `application/vnd.laszip` | loaders.gl `parse(bytes, LASLoader, { worker: false })` |\n| `splat` | `.splat` | `application/x-splat` | 32-byte records; Gaussian splat libraries |\n\n`.gltf` is not accepted: it is a multi-file format (JSON plus external buffers and textures), and one asset is one file. Export to `.glb` instead. Textures compressed with Draco or KTX2 inside a glb need the matching three.js decoder (`DRACOLoader`, `KTX2Loader`) with its decoder path set to a CDN.\n\n## Related\n\n- [Image](../image/): image upload parameter\n- [Slider](../slider/): numeric slider parameter\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility\n- [External Libraries](/native/external-libraries): importing three.js and other loaders\n- [P5 Model](/p5/parameters/model): P5 equivalent with `p5.loadModel()`\n- Shader renderer: no equivalent. `@viji-model` is a parse error."
|
|
2542
|
+
}
|
|
2543
|
+
]
|
|
2544
|
+
},
|
|
2467
2545
|
"native-param-button": {
|
|
2468
2546
|
"id": "native-param-button",
|
|
2469
2547
|
"title": "Button Parameter",
|
|
@@ -4864,7 +4942,7 @@ export const docsApi = {
|
|
|
4864
4942
|
"content": [
|
|
4865
4943
|
{
|
|
4866
4944
|
"type": "text",
|
|
4867
|
-
"markdown": "# Parameters\n\nParameters give users real-time control over your P5 scene. Define them at the top level, and Viji renders corresponding UI controls in the host application. Read `.value` inside `render()` to get the current state.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB), optional alpha channel |\n| [Gradient](gradient/) | [`viji.gradient(default, config)`](gradient/) | `{ color, position }[]` | Color ramp editor. Sorted stops plus `at(t)` sampling. |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures (use `.p5` with P5 drawing functions) |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// @renderer p5\n\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji, p5) {\n const r = parseInt(color.value.slice(1, 3), 16);\n const g = parseInt(color.value.slice(3, 5), 16);\n const b = parseInt(color.value.slice(5, 7), 16);\n p5.fill(r, g, b);\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Declaring them inside `setup()` would register the parameter too late: no UI control would appear. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default.\n\n## Image Parameters in P5\n\nWhen using [`viji.image()`](image/) with P5 drawing functions, use the `.p5` property instead of `.value`:\n\n```javascript\nconst photo = viji.image(null, { label: 'Photo' });\n\nfunction render(viji, p5) {\n if (photo.value) {\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nThe `.p5` property wraps the raw image data in a P5-compatible object. Use `.value` to check if an image is loaded, and `.p5` when passing to P5 drawing functions.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp with `at(t)` sampling\n- [Image](image/): image parameters with the `.p5` property\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [Native Parameters](/native/parameters): same system in the native renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
|
|
4945
|
+
"markdown": "# Parameters\n\nParameters give users real-time control over your P5 scene. Define them at the top level, and Viji renders corresponding UI controls in the host application. Read `.value` inside `render()` to get the current state.\n\n## Parameter Types\n\n| Type | Function | Value | Use For |\n|---|---|---|---|\n| [Slider](slider/) | [`viji.slider(default, config)`](slider/) | `number` | Continuous numeric ranges (speed, size, opacity) |\n| [Number](number/) | [`viji.number(default, config)`](number/) | `number` | Precise numeric input (counts, thresholds) |\n| [Color](color/) | [`viji.color(default, config)`](color/) | `string` | Color picker for tints and palettes (hex / RGB / HSB), optional alpha channel |\n| [Gradient](gradient/) | [`viji.gradient(default, config)`](gradient/) | `{ color, position }[]` | Color ramp editor. Sorted stops plus `at(t)` sampling. |\n| [Toggle](toggle/) | [`viji.toggle(default, config)`](toggle/) | `boolean` | On/off switches (enable audio, show trail) |\n| [Select](select/) | [`viji.select(default, config)`](select/) | `string \\| number` | Dropdown from predefined options (blend mode, shape type) |\n| [Text](text/) | [`viji.text(default, config)`](text/) | `string` | Free-form text input (titles, labels) |\n| [Image](image/) | [`viji.image(default, config)`](image/) | `ImageBitmap \\| null` | User-uploaded images and textures (use `.p5` with P5 drawing functions) |\n| [Model](model/) | [`viji.model(default, config)`](model/) | `ModelValue \\| null` | User-uploaded 3D models (pass `.value.url` to `p5.loadModel()` in WEBGL scenes) |\n| [Button](button/) | [`viji.button(config)`](button/) | `boolean` | Momentary trigger: true for 1 frame (resets, spawns) |\n| [Coordinate](coordinate/) | [`viji.coordinate(default, config)`](coordinate/) | `{ x: number, y: number }` | 2D coordinate pad. Both axes range from -1 to 1. |\n\n## Basic Pattern\n\n```javascript\n// @renderer p5\n\n// 1. Define at top level: runs once\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst color = viji.color('#ff6600', { label: 'Color' });\nconst mirror = viji.toggle(false, { label: 'Mirror' });\n\n// 2. Read .value in render(): updates in real-time\nfunction render(viji, p5) {\n const r = parseInt(color.value.slice(1, 3), 16);\n const g = parseInt(color.value.slice(3, 5), 16);\n const b = parseInt(color.value.slice(5, 7), 16);\n p5.fill(r, g, b);\n // speed.value, mirror.value, etc.\n}\n```\n\n> [!WARNING]\n> Parameters must be declared at the **top level** of your scene, never inside `setup()` or `render()`. They are registered once and sent to the host before either function runs. Declaring them inside `setup()` would register the parameter too late: no UI control would appear. Declaring them inside `render()` would re-register the parameter every frame, resetting its value to the default.\n\n## Image Parameters in P5\n\nWhen using [`viji.image()`](image/) with P5 drawing functions, use the `.p5` property instead of `.value`:\n\n```javascript\nconst photo = viji.image(null, { label: 'Photo' });\n\nfunction render(viji, p5) {\n if (photo.value) {\n p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n}\n```\n\nThe `.p5` property wraps the raw image data in a P5-compatible object. Use `.value` to check if an image is loaded, and `.p5` when passing to P5 drawing functions.\n\n## Common Config Keys\n\nAll parameter types share these optional configuration keys:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `label` | `string` | *(optional, recommended)* | Display name shown in the parameter UI. Defaults to the variable name you assign the parameter to. |\n| `description` | `string` | | Tooltip or help text |\n| `group` | `string` | `'general'` | Group name for organizing parameters: see [Grouping](grouping/) |\n| `category` | `ParameterCategory` | `'general'` | Controls visibility based on capabilities: see [Categories](categories/) |\n\n## Organization\n\nAs scenes grow, you'll want to organize parameters into logical sections and control when they're visible:\n\n- **[Grouping](grouping/)**: Collect related parameters under named groups (e.g., \"animation\", \"shape\", \"audio\"). Parameters with the same `group` string appear together in the UI.\n- **[Categories](categories/)**: Tag parameters as `'general'`, `'audio'`, `'video'`, or `'interaction'` to automatically show/hide them based on what inputs are currently active.\n\n## Related\n\n- [Slider](slider/): the most common parameter type\n- [Coordinate](coordinate/): 2D position control\n- [Gradient](gradient/): color ramp with `at(t)` sampling\n- [Image](image/): image parameters with the `.p5` property\n- [Grouping](grouping/): organizing parameters into named groups\n- [Categories](categories/): visibility based on capabilities\n- [Native Parameters](/native/parameters): same system in the native renderer\n- [Shader Parameters](/shader/parameters): comment-directive syntax for shaders\n- [Best Practices](/getting-started/best-practices): essential patterns for all renderers"
|
|
4868
4946
|
}
|
|
4869
4947
|
]
|
|
4870
4948
|
},
|
|
@@ -5297,6 +5375,74 @@ export const docsApi = {
|
|
|
5297
5375
|
}
|
|
5298
5376
|
]
|
|
5299
5377
|
},
|
|
5378
|
+
"p5-param-model": {
|
|
5379
|
+
"id": "p5-param-model",
|
|
5380
|
+
"title": "Model Parameter",
|
|
5381
|
+
"description": "Accept user-uploaded 3D model files in P5.js WEBGL scenes and load them with p5.loadModel().",
|
|
5382
|
+
"headings": [
|
|
5383
|
+
{
|
|
5384
|
+
"id": "parameters",
|
|
5385
|
+
"level": 2,
|
|
5386
|
+
"text": "Parameters"
|
|
5387
|
+
},
|
|
5388
|
+
{
|
|
5389
|
+
"id": "return-value",
|
|
5390
|
+
"level": 2,
|
|
5391
|
+
"text": "Return Value"
|
|
5392
|
+
},
|
|
5393
|
+
{
|
|
5394
|
+
"id": "value-lifetime",
|
|
5395
|
+
"level": 2,
|
|
5396
|
+
"text": "Value Lifetime"
|
|
5397
|
+
},
|
|
5398
|
+
{
|
|
5399
|
+
"id": "usage",
|
|
5400
|
+
"level": 2,
|
|
5401
|
+
"text": "Usage"
|
|
5402
|
+
},
|
|
5403
|
+
{
|
|
5404
|
+
"id": "loading-by-url",
|
|
5405
|
+
"level": 2,
|
|
5406
|
+
"text": "Loading by URL"
|
|
5407
|
+
},
|
|
5408
|
+
{
|
|
5409
|
+
"id": "material-libraries",
|
|
5410
|
+
"level": 3,
|
|
5411
|
+
"text": "Material libraries"
|
|
5412
|
+
},
|
|
5413
|
+
{
|
|
5414
|
+
"id": "point-clouds",
|
|
5415
|
+
"level": 2,
|
|
5416
|
+
"text": "Point Clouds"
|
|
5417
|
+
},
|
|
5418
|
+
{
|
|
5419
|
+
"id": "formats",
|
|
5420
|
+
"level": 2,
|
|
5421
|
+
"text": "Formats"
|
|
5422
|
+
},
|
|
5423
|
+
{
|
|
5424
|
+
"id": "related",
|
|
5425
|
+
"level": 2,
|
|
5426
|
+
"text": "Related"
|
|
5427
|
+
}
|
|
5428
|
+
],
|
|
5429
|
+
"content": [
|
|
5430
|
+
{
|
|
5431
|
+
"type": "text",
|
|
5432
|
+
"markdown": "# viji.model()\n\n```\nmodel(defaultValue: null, config: ModelConfig): ModelParameter\n```\n\nCreates a 3D model upload parameter. The host renders it as a file picker for model files. The uploaded file becomes available as a `ModelValue`: the raw bytes, a blob URL, and a format tag. Viji never parses the file. In P5 scenes, pass `value.url` to `p5.loadModel()` and draw the result with `p5.model()`.\n\nModel values have no `.p5` property: `p5.loadModel()` builds the `p5.Geometry` from the URL itself.\n\n## Parameters\n\n| Name | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|\n| `defaultValue` | `null` | Yes | | Always `null`: models are provided by the user |\n| `config.label` | `string` | No | variable name | Display name shown in the parameter UI |\n| `config.description` | `string` | No | | Tooltip or help text |\n| `config.formats` | `ModelFormat[]` | Yes | | Formats your scene can parse, and nothing more. The host filters its file picker by this list and files of other formats are rejected. Must not be empty. See [Formats](#formats) |\n| `config.group` | `string` | No | `'general'` | Group name: see [Grouping](../grouping/) |\n| `config.category` | `ParameterCategory` | No | `'general'` | Visibility category: see [Categories](../categories/) |\n\n## Return Value\n\nReturns a `ModelParameter` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `value` | `ModelValue \\| null` | The uploaded model, or `null` if none provided |\n| `formats` | `ReadonlyArray<ModelFormat>` | The formats this parameter accepts (`config.formats`, deduplicated) |\n| `label` | `string` | Display label |\n| `description` | `string \\| undefined` | Description text |\n| `group` | `string` | Group name |\n| `category` | `ParameterCategory` | Parameter category |\n\nA `ModelValue` has three properties:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `bytes` | `ArrayBuffer` | The whole file. `bytes.byteLength` is the file size |\n| `url` | `string` | A blob URL for `p5.loadModel()`. Created on first read and cached |\n| `format` | `ModelFormat` | Format tag: `'glb'`, `'obj'`, `'stl'`, `'ply'`, `'las'`, `'laz'` or `'splat'` |\n\n## Value Lifetime\n\nRead `value` where you load it. When the host replaces or clears the model, `url` is revoked and `bytes` is dropped by core; geometry you already built from it stays valid.\n\nRead `url` where you load. A URL from a replaced value rejects when loaded afterwards; revocation does not abort a load that is already in flight. Reading `url` on a value the host already replaced throws.\n\nSome loaders transfer the buffer to their own worker and detach `value.bytes`. Pass `bytes.slice(0)` to those, or disable their worker (for loaders.gl, `{ worker: false }`). `p5.loadModel()` reads the URL and does not touch `bytes`.\n\nThe parameter object itself is stable; only `.value` changes. Compare `value` by identity to detect a new upload: each upload is a new object.\n\n## Usage\n\n`p5.loadModel()` is asynchronous and needs the file type as its last argument, because a blob URL has no extension. Build it from `value.format`. Load once per upload, not every frame, and always check for `null` first:\n\n```javascript\n// @renderer p5 webgl\n\nconst statue = viji.model(null, { label: 'Statue', formats: ['obj', 'stl'] });\nlet loaded = null;\nlet mesh = null;\n\nfunction render(viji, p5) {\n if (statue.value !== loaded) {\n loaded = statue.value;\n mesh = null;\n if (loaded) {\n const value = loaded;\n p5.loadModel(value.url, true, (m) => {\n if (statue.value === value) mesh = m; // ignore a load the user already replaced\n }, (err) => console.error(err), '.' + value.format);\n }\n }\n\n p5.background(0);\n if (mesh) {\n p5.normalMaterial();\n p5.model(mesh);\n }\n}\n```\n\nThe second argument (`true`) normalizes the model to a standard size, so uploads of any scale fill a similar part of the canvas.\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."
|
|
5433
|
+
},
|
|
5434
|
+
{
|
|
5435
|
+
"type": "live-example",
|
|
5436
|
+
"title": "Model Upload",
|
|
5437
|
+
"sceneCode": "// @renderer p5 webgl\n\nconst statue = viji.model(null, { label: 'Model', formats: ['obj', 'stl'] });\nconst size = viji.slider(1, { min: 0.3, max: 2, step: 0.05, label: 'Size' });\nconst bg = viji.color('#0f0f1a', { label: 'Background' });\n\nlet loaded = null; // the ModelValue currently loaded\nlet mesh = null; // the p5.Geometry built from it\n\nfunction load(p5, value) {\n loaded = value;\n mesh = null;\n if (!value) return;\n // A blob URL has no extension, so pass the format as the file type.\n p5.loadModel(value.url, true, (m) => {\n if (statue.value !== value) return; // replaced while loading\n mesh = m;\n }, (err) => {\n console.error('Could not load the model', err);\n }, '.' + value.format);\n}\n\nfunction render(viji, p5) {\n // A new upload is a new object: compare by identity.\n if (statue.value !== loaded) load(p5, statue.value);\n\n p5.background(bg.value);\n\n // Orbit the camera around the model.\n const radius = Math.min(viji.width, viji.height) * 1.1;\n const angle = viji.time * 0.6;\n p5.camera(\n Math.sin(angle) * radius, -radius * 0.35, Math.cos(angle) * radius,\n 0, 0, 0,\n 0, 1, 0\n );\n\n // Normalized models span about 200 units; scale that to the canvas.\n // Transforms live inside push() / pop(): the matrix is not reset between\n // frames, so an unwrapped scale() would compound every frame.\n const unit = Math.min(viji.width, viji.height) / 400;\n p5.push();\n p5.scale(unit * size.value);\n // p5 WEBGL's y axis points down the screen: flip once so a y-up model file is upright.\n p5.scale(1, -1, 1);\n\n if (mesh) {\n p5.noStroke();\n p5.normalMaterial();\n p5.model(mesh);\n } else {\n p5.noFill();\n p5.stroke(136, 136, 170);\n p5.strokeWeight(1 / unit);\n p5.box(160);\n }\n p5.pop();\n}\n",
|
|
5438
|
+
"sceneFile": "model-p5.scene.js"
|
|
5439
|
+
},
|
|
5440
|
+
{
|
|
5441
|
+
"type": "text",
|
|
5442
|
+
"markdown": "## Loading by URL\n\n`p5.loadModel()` only accepts a URL, so `value.url` is the path for P5 scenes. Read it at the moment you start the load, as in the example above; a URL saved from an earlier value rejects once the host replaces the model.\n\n### Material libraries\n\nAn `.obj` file that names a material library (`mtllib something.mtl`) makes P5 fetch that file next to the model's URL. A blob URL has no neighbours, so the browser logs a failed request for `blob:null/something.mtl` and P5 warns that it is proceeding without materials. The geometry still loads; only the per-material colours are skipped, and `normalMaterial()` or your own `fill()` takes over. The messages are harmless. To avoid them, build the URL yourself from a copy of the bytes with the `mtllib` line removed:\n\n```javascript\nconst text = new TextDecoder().decode(value.bytes).replace(/^mtllib .*$/m, '');\nconst url = URL.createObjectURL(new Blob([text], { type: 'model/obj' }));\np5.loadModel(url, true, ok, err, '.obj');\n```\n\nRevoke that URL with `URL.revokeObjectURL(url)` once the model has loaded; core only manages the URLs it created.\n\n## Point Clouds\n\nP5's `loadModel()` reads `.obj` and `.stl` files only. For point cloud formats (`ply`, `las`, `laz`, `splat`) parse `value.bytes` yourself and draw the vertices with `p5.point()` or `p5.beginShape(p5.POINTS)`. A `DataView` over `value.bytes` is enough for the fixed 32-byte records of a `.splat` file; for the other formats, use the Native renderer with three.js or loaders.gl (see [Native Model](/native/parameters/model#point-clouds)).\n\n## Formats\n\n| Format | Extension | MIME type | Loads with |\n|--------|-----------|-----------|------------|\n| `glb` | `.glb` | `model/gltf-binary` | Not supported by `p5.loadModel()`; parse `bytes` yourself |\n| `obj` | `.obj` | `model/obj` | `p5.loadModel(value.url, true, ok, err, '.obj')` |\n| `stl` | `.stl` | `model/stl` | `p5.loadModel(value.url, true, ok, err, '.stl')` (ascii and binary) |\n| `ply` | `.ply` | `application/x-ply` | Not supported by `p5.loadModel()`; parse `bytes` yourself |\n| `las` | `.las` | `application/vnd.las` | Not supported by `p5.loadModel()`; parse `bytes` yourself |\n| `laz` | `.laz` | `application/vnd.laszip` | Not supported by `p5.loadModel()`; parse `bytes` yourself |\n| `splat` | `.splat` | `application/x-splat` | 32-byte records; read with a `DataView` |\n\nRestrict `config.formats` to what your scene loads (`['obj', 'stl']` for `p5.loadModel()`), so the host picker only offers files the scene can show. `.gltf` is not accepted: it is a multi-file format, and one asset is one file.\n\n## Related\n\n- [Image](../image/): image upload parameter with the `.p5` property\n- [Slider](../slider/): numeric slider parameter\n- [Grouping](../grouping/): organizing parameters into named groups\n- [Categories](../categories/): controlling parameter visibility\n- [Drawing with P5](/p5/drawing): the WEBGL main canvas\n- [Native Model](/native/parameters/model): Native renderer equivalent with three.js loaders\n- Shader renderer: no equivalent. `@viji-model` is a parse error."
|
|
5443
|
+
}
|
|
5444
|
+
]
|
|
5445
|
+
},
|
|
5300
5446
|
"p5-param-button": {
|
|
5301
5447
|
"id": "p5-param-button",
|
|
5302
5448
|
"title": "Button Parameter",
|
|
@@ -10087,15 +10233,15 @@ export const docsApi = {
|
|
|
10087
10233
|
},
|
|
10088
10234
|
"ai": {
|
|
10089
10235
|
"systemPrompts": {
|
|
10090
|
-
"base": "# Viji API: Base Reference\n\nThis document is the renderer-agnostic foundation for generating Viji scenes. It is loaded into every AI turn alongside the renderer-specific reference for the renderer in use.\n\n## Architecture\n\nViji scenes run inside a **Web Worker** on an **OffscreenCanvas**. There is no DOM.\n\n- Top-level code runs once when the scene loads (parameter declarations, state, imports).\n- A `render` function (or, for shaders, `void main()`) runs every frame.\n- `fetch()` and `await import()` are available. `window`, `document`, `Image()`, and `localStorage` are not.\n- The global `viji` object exposes everything: canvas, timing, audio, video, computer vision, input, sensors, and parameters. Each renderer also has its own surface (P5's `p5.*` API; the shader's auto-injected uniforms).\n\n## Renderers\n\nThree renderers. Pick by the visual goal, not by perceived difficulty.\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| Language | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js 1.9.4 | GLSL fragment shader |\n| Best for | Full control, Three.js, generative art, pixel-perfect Canvas 2D | Creative coding with shapes, colors, transforms; ports of existing P5 sketches | GPU effects, patterns, raymarching, SDF, post-processing |\n| 3D | Yes (WebGL, Three.js via `await import()`) | Yes (`// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n| External libraries | Yes (any ESM via `await import()`) | P5.js built-in | None (GPU only) |\n\n### Decision criteria\n\n- **Per-pixel GPU effects** (raymarching, fractals, distance fields, fullscreen post-processing, patterns that read every pixel): use **Shader**.\n- **3D with Three.js, custom WebGL, pixel-perfect Canvas 2D, or any external ESM library**: use **Native**.\n- **Classic P5 shapes and transforms** (`ellipse`, `line`, `bezier`, color modes, `push`/`pop`), or a port of an existing P5 sketch: use **P5**.\n- **When ambiguous, default to Native**. Audio reactivity, camera/CV, parameters, and input work in every renderer. Pick on visual style, not data sources.\n\n## Scene shape per renderer\n\nEach example below is the minimum viable entry point. Full surface lives in the matching renderer reference.\n\n**Native**:\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx\n}\n```\n\n**P5**:\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.* prefixed calls\n}\n```\n\n**Shader (GLSL)**:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## Cross-cutting rules (apply to every renderer)\n\n1. **Never** touch the DOM. No `window`, `document`, `Image()`, `localStorage`. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the **top level**, never inside `render()` or `main()`. Parameters declared inside a render call are recreated every frame and break the host UI.\n3. **Always** use `viji.width` / `viji.height` (or `u_resolution` in shaders) for canvas dimensions. Never hardcode pixel sizes.\n4. **Always** use `viji.deltaTime` (or `u_deltaTime` / `@viji-accumulator` in shaders) for animation. Never count frames or assume a fixed frame rate.\n5. **Never** multiply `viji.time` (or `u_time`) by a parameter to drive animation speed. It causes visible jumps when the slider changes. Use a `deltaTime` accumulator at the top level instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter. Each independent speed gets its own accumulator.\n6. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n7. **Always** check `viji.audio.isConnected` (or `u_audioVolume > 0.0`) before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` (or `u_videoConnected`) before reading video. When an input is not connected, degrade gracefully — keep rendering the scene's normal idle/ambient visual. Do **not** draw on-canvas text telling the user to connect or turn on a camera, microphone, or other input; the host provides those controls. (The connection-demo examples draw \"Waiting for…\" text only to illustrate this check itself — don't copy that into real scenes unless the user explicitly asks for an on-canvas prompt.)\n8. **CV features are independent**: each `enableX()` populates only its own subset of fields. Face detection populates `face.bounds` / `center` / `confidence`; face mesh populates `face.landmarks` / `headPose`; emotion detection populates `face.expressions` / `blendshapes`. Fields whose source model is not enabled read as `null` (or `[]` for `landmarks`) — always null-check before reading. CV is expensive and is **not** host-gated, so it is the one feature class that legitimately belongs behind a parameter toggle — but gate by intent: if a CV feature is the scene's defining purpose (the scene is meaningless without it), enable it always-on from module scope; if CV is optional/supplementary, or the scene uses several heavy features, gate each behind a `viji.toggle` (shader: the toggleable `@viji-cv:` form) so the user opts into the cost — and make sure the base visual stands on its own when a toggle is off (no \"enable in parameters\" banner). Each active CV feature uses its own WebGL context; 4+ on low-end hardware can hit context limits, so enable only what you need.\n9. **Always** set `category` on parameters that depend on an external input: `category: 'audio'` for audio-driven controls, `category: 'video'` for video / CV-driven controls, `category: 'interaction'` for mouse / keyboard / touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input. **Never** add on/off parameter toggles to enable/disable built-in inputs or device features — audio, video, mouse, keyboard, touch, motion/orientation sensors, and external devices are all gated by the host's own controls. Read them in code behind `isConnected` / `isInCanvas` / null checks. A parameter that shapes how an input drives the visual (e.g. a sensitivity slider) is fine; an on/off enable toggle for the input itself is not. If the user explicitly asks for one, advise that the host already provides it before complying. (CV features are the sole exception — see the CV rule above.)\n10. **In P5**: prefix every P5 function and constant with `p5.` (instance mode). Never call `createCanvas()`; Viji creates the canvas.\n11. **In shaders**: never redeclare `precision`, never redeclare built-in uniforms (`u_time`, `u_resolution`, etc.), never redeclare parameter uniforms. Never use the `u_` prefix for your own parameter names; it is reserved for Viji's built-ins.\n\n## Parameter declaration shape (cross-renderer)\n\nAssign each parameter to its own top-level `const` with a unique camelCase\nname (`const speed = viji.slider(1, { label: 'Speed' })`) and read it with\n`.value`. `label` is optional but recommended (falls back to the variable\nname). Native and P5 use JavaScript calls at the top level:\n\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // .value: number\nviji.color(default, { label, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb, .hsb, .alpha\nviji.gradient([{ color, position }], { label, minStops, maxStops, alpha }) // .value: sorted stops, .at(t): hex at t; default required (2..8 stops)\nviji.toggle(default, { label }) // .value: boolean\nviji.select(default, { options, label }) // .value: string | number\nviji.number(default, { min, max, step, label }) // .value: number\nviji.text(default, { label, maxLength }) // .value: string\nviji.image(null, { label }) // .value: ImageBitmap | null\nviji.button({ label }) // .value: boolean (true for 1 frame)\nviji.coordinate({ x: 0, y: 0 }, { step, label, group, category }) // .value: { x, y }, range -1..1\n```\n\n**`slider` / `number` `step` default**: when omitted, `step` auto-derives from `min`/`max` as the largest power of 10 giving ~100 positions across the range (e.g., range `0..1` → `0.01`; range `0.1..5` → `0.01`; range `0..100` → `1`). Specify explicitly only when you need a different grid: `step: 1` for integer counts (`{ min: 3, max: 30, step: 1 }`), `step: 0.001` for fine precision.\n\nShaders use GLSL comment directives that compile to typed uniforms:\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 -> uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 -> uniform vec3 name; (alpha:true -> vec4; 8-digit default needs alpha:true)\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] -> vec3 name(float t) sampler + arrays (alpha:true -> vec4; stops are #hex@position)\n// @viji-toggle:name label:\"Toggle\" default:false -> uniform bool name;\n// @viji-select:name label:\"Mode\" default:\"A\" options:[\"A\",\"B\"] -> uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 -> uniform float name;\n// @viji-image:name label:\"Texture\" -> uniform sampler2D name;\n// @viji-button:name label:\"Reset\" -> uniform bool name;\n// @viji-coordinate:name label:\"Pos\" default:[0.0,0.0] -> uniform vec2 name; // range -1..1\n// @viji-accumulator:name rate:speed -> uniform float name;\n```\n\nDirectives use `//` line comments only. Block comments (`/* */`) are not parsed.\n\n## Where to look next\n\nFor the full API surface of the renderer in use, consult the matching reference: Native, P5, or Shader. Each renderer reference includes the complete data shapes for audio, video, CV, input, sensors, external devices, and renderer-specific patterns (e.g. `viji.useContext` for Native, `videoFit` for P5, the complete uniform table for Shader).\n",
|
|
10091
|
-
"native": "# Viji API: Native Renderer Reference\n\nNative scenes run JavaScript on an `OffscreenCanvas` in a Web Worker, with full access to Canvas 2D, WebGL, WebGL2, and WebGPU contexts. External libraries load via `await import()`. Top-level code runs once; a `render(viji)` function runs every frame.\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters.\n- Call `viji.useContext('2d' | 'webgl' | 'webgl2' | 'webgpu')` once at the top of `render()` (or top-level) to obtain a rendering context. After the first call, the matching shortcut is populated: `viji.ctx` / `viji.gl` / `viji.gpu`.\n- **Pick one context type for the entire scene.** Calling `useContext` with a different type after the first call returns `null`.\n- **WebGPU**: request the adapter and device at top level. `useContext('webgpu')` returns `null` where unsupported: guard and fall back.\n- There is **no `setup()` function** in Native. All initialization goes at the top level (which supports `await` for dynamic imports).\n- The DOM is unavailable. `fetch()` and `await import()` are available.\n\n## Rules\n\n1. **Never** access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()`. `label` is optional but recommended (falls back to the variable name).\n3. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }` with `h` in 0..360, `s` / `b` in 0..100). Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n5. **Always** use `viji.deltaTime` for animation. Use `viji.time` only for constant-speed oscillations (`Math.sin(viji.time * 2.0)`). For anything driven by a parameter, use a `deltaTime` accumulator.\n6. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator at the top level. This also applies to nested multiplications: each independent speed gets its own accumulator.\n\n ```javascript\n // WRONG\n const t = viji.time * speed.value;\n const rot = phase * rotSpeed.value;\n\n // RIGHT\n let phase = 0, rotPhase = 0;\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n7. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. **Always** call `viji.useContext()` to get a context. Pick one type and use it for the entire scene.\n9. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n10. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle` so the user opts into the cost.\n11. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n12. **Always** type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so the error can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\n\n ```javascript\n function render(viji: VijiAPI) { ... }\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\n ```\n13. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `native-api-reference` docs page instead of guessing.\n\n## API reference\n\n### Canvas and context\n\n| Member | Type | Description |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | The canvas |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Acquire 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Acquire WebGL 1 |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Acquire WebGL 2 |\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Acquire WebGPU |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut after `useContext('2d')` |\n| `viji.gl` | `WebGLRenderingContext \\| WebGL2RenderingContext` | Shortcut after WebGL context acquired |\n| `viji.gpu` | `GPUCanvasContext` | Shortcut after `useContext('webgpu')` |\n| `viji.width`, `viji.height` | `number` | Current canvas dimensions in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|---|---|---|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (host-controlled) |\n\n### Parameters\n\nDeclare at top level. All accept `{ label, description?, group?, category? }`. Category values: `'audio'`, `'video'`, `'interaction'`, `'general'` (default).\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.button({ label, description?, group?, category? }) // .value: boolean (true for 1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first.\n\n| Member | Type | Notes |\n|---|---|---|\n| `isConnected` | `boolean` | Audio source active |\n| `volume.current`, `volume.peak`, `volume.smoothed` | `number` 0..1 | `smoothed` = 200ms decay envelope of `current` |\n| `bands.low`, `lowMid`, `mid`, `highMid`, `high` | `number` 0..1 | Instant band energies (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\n| `bands.lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed` | `number` 0..1 | 150ms decay envelope siblings (not nested) |\n| `beat.kick`, `snare`, `hat`, `any` | `number` 0..1 | Beat energy curves with a 300ms decay; peak on each detected beat then fall off |\n| `beat.kickSmoothed`, `snareSmoothed`, `hatSmoothed`, `anySmoothed` | `number` 0..1 | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick`, `snare`, `hat`, `any` | `boolean` | True for exactly one frame on the matching beat, then auto-resets. OR-accumulated between frames so no beat is lost |\n| `beat.events` | `Array<{type, time, strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in milliseconds; cleared each frame |\n| `beat.bpm`, `beat.confidence`, `beat.isLocked` | `number`, `number`, `boolean` | `bpm` is `0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120` as a fallback before lock-on. `confidence` in 0..1. `isLocked` is `true` on stable tempo lock |\n| `spectral.brightness`, `spectral.flatness` | `number` 0..1 | Spectral centroid; spectral flatness |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins 0..255 |\n| `getWaveform()` | `Float32Array` | Time-domain waveform -1..1 |\n\nExternal-device audio (`viji.devices[i].audio`) and host-supplied audio streams (`viji.audioStreams[i]`) use the `AudioStreamAPI` shape: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` plus each `*Smoothed` sibling, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. No `beat`, BPM, triggers, or events on streams: those exist only on the main `viji.audio`.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing.\n\n| Member | Type | Notes |\n|---|---|---|\n| `isConnected` | `boolean` | Video source active |\n| `currentFrame` | `OffscreenCanvas \\| ImageBitmap \\| null` | Most recent frame |\n| `frameWidth`, `frameHeight` | `number` | Source frame size |\n| `frameRate` | `number` | Source frame rate |\n| `getFrameData()` | `ImageData \\| null` | Pixel data of `currentFrame` |\n| `cv` | `VideoCVAPI` | Computer-vision surface: see below |\n\nCV data outputs (`analysedFrame`, `getAnalysedFrameData()`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs (`enableFaceDetection`, etc.) all live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches video and misaligns CV bounds. Use this helper at module scope:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0..1 to the source frame. Map through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. `viji.video.currentFrame` is the right default for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions (segmentation compositing, face-mesh texturing, landmark-anchored color sampling). The common fallback pattern is `viji.video.cv.analysedFrame ?? viji.video.currentFrame` during the brief window before the first CV inference lands.\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature consumes a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional — without it, the model loads\n// asynchronously while render runs and data fields populate when ready.\n// Safe to call from module scope (always-on CV) or per-frame inside render()\n// gated by a viji.toggle(...) (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\nawait viji.video.cv.enableHandTracking(true | false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the exact frame paired with the current CV results. `null` until the first inference lands after a feature is enabled. Use the pattern `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during the startup window.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`, cached and re-extracted only on new CV results.\n\n**`viji.video.cv.faces: FaceData[]`**: `id` always present. Other fields are populated only by their source model and read `null` (or empty array for `landmarks`) otherwise — always null-check before reading.\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\n- `confidence: number | null` (0..1) — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\n- `headPose: { pitch, yaw, roll } | null` (degrees) — populated by face mesh\n- `expressions: { neutral, happy, sad, angry, surprised, disgusted, fearful } | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` — populated by emotion detection; 52 ARKit coefficients (`browDownLeft`/`Right`, `browInnerUp`, `browOuterUpLeft`/`Right`, `cheekPuff`, `cheekSquintLeft`/`Right`, `eyeBlinkLeft`/`Right`, `eyeLookDown`/`In`/`Out`/`UpLeft`/`Right`, `eyeSquintLeft`/`Right`, `eyeWideLeft`/`Right`, `jawForward`/`Left`/`Open`/`Right`, `mouthClose`/`DimpleLeft`/`Right`/`FrownLeft`/`Right`/`Funnel`/`Left`/`LowerDownLeft`/`Right`/`PressLeft`/`Right`/`Pucker`/`Right`/`RollLower`/`Upper`/`ShrugLower`/`Upper`/`SmileLeft`/`Right`/`StretchLeft`/`Right`/`UpperUpLeft`/`Right`, `noseSneerLeft`/`Right`, `tongueOut`: all 0..1)\n\nIf you need bounds while only running face mesh, either also enable face detection or compute the bounding box from `face.landmarks` min/max yourself.\n\n**`viji.video.cv.hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks: {x,y,z}[]` (21 points), `palm: {x,y,z}`, `gestures: { fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou }` (0..1 confidence each).\n\n**`viji.video.cv.pose: PoseData | null`**: `confidence`, `landmarks: {x,y,z,visibility}[]` (33 points), plus body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`. Note: the shader `u_segmentationMask` sampler is sampled as a normalized `float` (0.0 = background, 1.0 = person); the JS `mask` byte values are 0/1, not 0/255.\n\n### Input\n\n**Pointer** (unified mouse and touch): `viji.pointer`\n| Member | Type | Notes |\n|---|---|---|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Per-frame movement |\n| `isDown` | `boolean` | Pressed or touching |\n| `wasPressed`, `wasReleased` | `boolean` | One-frame edge triggers |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `type` | `'mouse' \\| 'touch' \\| 'none'` | Active input type |\n\n**Mouse**: `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n\n**Keyboard**: `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys: Set<string>`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n\n**Touch**: `viji.touches`: `count`, `points: TouchPoint[]`, `started`, `moved`, `ended` (each a `TouchPoint[]`), `primary: TouchPoint | null`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity: {x,y}`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n**`viji.device`**: host device motion and orientation:\n- `motion: DeviceMotionData | null`: `acceleration: {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate: {alpha,beta,gamma}` (deg/s), `interval` (ms).\n- `orientation: DeviceOrientationData | null`: `alpha` (0-360° compass), `beta` (-180..180° front-back tilt), `gamma` (-90..90° left-right tilt), `absolute`.\n\n**`viji.devices`**: `DeviceState[]`, externally connected devices. Each: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, same shape as `viji.video` but without CV), `audio` (`AudioStreamAPI | null`, lightweight; no beat or BPM).\n\n### Host-supplied streams\n\n- `viji.videoStreams: VideoAPI[]`: extra video sources from the host compositor. Same shape as `viji.video`, including a per-stream `cv` surface with the same enable verbs and result fields; `cv.analysedFrame` is populated on the main stream only. May be empty.\n- `viji.audioStreams: AudioStreamAPI[]`: extra audio sources from the host. Lightweight shape (no beat or BPM). May be empty.\n\n### External libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // always pass false to skip CSS styling\n```\n\nPin library versions in the import URL. Always pass `viji.canvas` to the renderer. Always pass `false` as the third argument to Three.js `setSize`. Handle resize by comparing `viji.width` / `viji.height` against previous values inside `render()`. Transformers.js needs `env.useBrowserCache = false` (Cache Storage is unavailable in the scene worker; model downloads are cached by the engine anyway). Load ONNX Runtime Web 1.27+ from jsdelivr (`https://cdn.jsdelivr.net/npm/onnxruntime-web@<version>/dist/ort.webgpu.min.mjs`, and set `ort.env.wasm.wasmPaths` to that `dist/` folder); esm.sh builds of it are broken. Run model inference off the render path: start it at an interval when not already running, keep the latest result, and draw that.\n\n## Template\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n",
|
|
10092
|
-
"p5": "# Viji API: P5 Renderer Reference\n\nP5 scenes use P5.js 1.9.4 inside a Web Worker on an `OffscreenCanvas`. P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix. The first line of the scene must be `// @renderer p5` (2D) or `// @renderer p5 webgl` (3D / WEBGL).\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters: identical to Native.\n- The canvas and its rendering context are managed by P5. **Do not call `createCanvas()`.** Viji creates the canvas in the mode declared by the `// @renderer` directive.\n- `viji.useContext()` is **not available** in P5 mode (the context belongs to P5).\n- Top-level code runs once. `function setup(viji, p5)` runs once for configuration. `function render(viji, p5)` runs every frame.\n- The `p5` instance passed to `setup` and `render` is the P5 instance. Use `p5.background`, `p5.fill`, `p5.circle`, `p5.PI`, etc. Unprefixed names will not resolve.\n\n## Rules\n\n1. **Always** add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line.\n2. **Always** use `render(viji, p5)` (not `draw()`) and `setup(viji, p5)` (not `setup()`).\n3. **Always** prefix every P5 function and constant with `p5.`:\n - `background(0)` -> `p5.background(0)`\n - `fill(255)` -> `p5.fill(255)`\n - `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`\n - `createVector(1, 0)` -> `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` -> `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`\n4. **Never** call `createCanvas()`. The canvas is created and managed by Viji.\n5. **Never** use `preload()`. Use `viji.image(null, { label })` for images, or `fetch()` inside `setup()`.\n6. **Never** use P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edges.\n7. **Never** use `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n8. **Never** use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** use `createCapture()` or `createVideo()`. Use `viji.video.*`.\n10. **Never** use `p5.dom` or `p5.sound`. Use Viji parameters and `viji.audio.*`.\n11. **Never** access `window`, `document`, `Image()`, `localStorage`. `fetch()` is available.\n12. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()` or `setup()`. `label` is optional but recommended (falls back to the variable name).\n13. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (matches `colorMode(RGB, 255)`) and `.hsb` (matches `colorMode(HSB, 360, 100, 100)`). Color defaults accept hex, `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS color strings.\n14. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n15. **Always** use `viji.deltaTime` for animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator. This also applies to nested multiplications; each independent speed gets its own accumulator.\n17. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. **Image parameters with P5:** use `.p5` (a `P5Image`) instead of `.value` when passing to `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. **Video drawing.** In 2D, use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In WEBGL, use `p5.image(viji.video.currentFrame, ...)` only (`p5.drawingContext` is WebGL, not Canvas 2D). **Always preserve aspect ratio** via the `videoFit` helper below.\n20. `p5.createGraphics(w, h)` is **2D only** (`createGraphics(w, h, p5.WEBGL)` is not supported). It creates an OffscreenCanvas internally.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is not available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode is declared by the `// @renderer` directive.** Use `// @renderer p5` for 2D, `// @renderer p5 webgl` for WEBGL. Never call `createCanvas()` or `createCanvas(..., p5.WEBGL)`.\n24. In WEBGL scenes, `p5.drawingContext` is a WebGL context. Use P5 3D drawing primitives or `p5.image()` / textures for images and video.\n25. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes; WEBGL pixel readback follows standard P5.js rules.\n26. **Always** check `viji.audio.isConnected` before reading audio data.\n27. **Always** check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n28. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle`.\n29. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n30. **Always** type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n31. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `p5-api-reference` docs page instead of guessing.\n\n## Video aspect helper\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nif (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji, 'cover');\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n}\n\n// CV bounds and landmarks are normalized 0..1 to the source frame.\n// Map them through v to align with the drawn video.\n```\n\n## P5 to Viji mapping\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `mousePressed()` / `mouseReleased()` callbacks | Check `viji.pointer.wasPressed` / `wasReleased` inside `render()`. |\n| `keyPressed()` / `keyReleased()` callbacks | Check `viji.keyboard.wasPressed(key)` / `wasReleased(key)` inside `render()`. |\n\n## API reference\n\nAll `viji.*` members are identical to Native (same object, same types).\n\n### Canvas and timing\n\n| Member | Type | Notes |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | Managed by P5 |\n| `viji.width`, `viji.height` | `number` | Canvas dimensions |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate |\n\n### Parameters\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null, .p5: P5Image\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first. Members:\n\n`isConnected` (boolean); `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (0..1; ranges 20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (0..1, 300ms decay curves; peak on each detected beat); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for exactly one frame, OR-accumulated between frames); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; `time` ms; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins, 0..255); `getWaveform(): Float32Array` (2048 samples, -1..1).\n\nExternal-device audio (`viji.devices[i].audio`) and host streams (`viji.audioStreams[i]`) follow the `AudioStreamAPI` shape: `isConnected`, `volume`, `bands` (instant + smoothed siblings), `spectral`, `getFrequencyData`, `getWaveform`. No beat, BPM, triggers, or events on streams.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing. Members:\n\n`isConnected` (boolean); `currentFrame` (`OffscreenCanvas | ImageBitmap | null`); `frameWidth`, `frameHeight`, `frameRate`; `getFrameData(): ImageData | null`; `cv: VideoCVAPI` (CV outputs and verbs live here, not on `viji.video` directly).\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature uses a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true | false); // viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the frame paired with the current CV results. `null` until the first inference lands. Use `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during startup.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`faces: FaceData[]`**: `id` always present. Other fields populated only by their source model; otherwise `null` (`landmarks` is `[]` when empty). Always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks` (21 points), `palm`, `gestures` (`fist`, `openPalm`, `peace`, `thumbsUp`, `thumbsDown`, `pointing`, `iLoveYou` 0..1).\n\n**`pose: PoseData | null`**: `confidence`, `landmarks` (33 points), body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input\n\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`).\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n`viji.device.motion`: `acceleration {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate {alpha,beta,gamma}` (deg/s), `interval` (ms).\n`viji.device.orientation`: `alpha` (0-360°), `beta` (-180..180°), `gamma` (-90..90°), `absolute`.\n\n`viji.devices: DeviceState[]`: each entry: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, no CV), `audio` (`AudioStreamAPI | null`, lightweight).\n\n### Host streams\n\n`viji.videoStreams: VideoAPI[]`: extra video sources. Same shape as `viji.video`, including a per-stream `cv` surface (same verbs and result fields; `cv.analysedFrame` is main-stream only). May be empty.\n`viji.audioStreams: AudioStreamAPI[]`: extra audio sources. May be empty. Lightweight shape (no beat / BPM).\n\n## Template\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n",
|
|
10236
|
+
"base": "# Viji API: Base Reference\n\nThis document is the renderer-agnostic foundation for generating Viji scenes. It is loaded into every AI turn alongside the renderer-specific reference for the renderer in use.\n\n## Architecture\n\nViji scenes run inside a **Web Worker** on an **OffscreenCanvas**. There is no DOM.\n\n- Top-level code runs once when the scene loads (parameter declarations, state, imports).\n- A `render` function (or, for shaders, `void main()`) runs every frame.\n- `fetch()` and `await import()` are available. `window`, `document`, `Image()`, and `localStorage` are not.\n- The global `viji` object exposes everything: canvas, timing, audio, video, computer vision, input, sensors, and parameters. Each renderer also has its own surface (P5's `p5.*` API; the shader's auto-injected uniforms).\n\n## Renderers\n\nThree renderers. Pick by the visual goal, not by perceived difficulty.\n\n| | Native | P5 | Shader |\n|---|---|---|---|\n| Language | JavaScript (Canvas 2D / WebGL) | JavaScript + P5.js 1.9.4 | GLSL fragment shader |\n| Best for | Full control, Three.js, generative art, pixel-perfect Canvas 2D | Creative coding with shapes, colors, transforms; ports of existing P5 sketches | GPU effects, patterns, raymarching, SDF, post-processing |\n| 3D | Yes (WebGL, Three.js via `await import()`) | Yes (`// @renderer p5 webgl`) | Yes (raymarching, SDF) |\n| External libraries | Yes (any ESM via `await import()`) | P5.js built-in | None (GPU only) |\n\n### Decision criteria\n\n- **Per-pixel GPU effects** (raymarching, fractals, distance fields, fullscreen post-processing, patterns that read every pixel): use **Shader**.\n- **3D with Three.js, custom WebGL, pixel-perfect Canvas 2D, or any external ESM library**: use **Native**.\n- **Classic P5 shapes and transforms** (`ellipse`, `line`, `bezier`, color modes, `push`/`pop`), or a port of an existing P5 sketch: use **P5**.\n- **When ambiguous, default to Native**. Audio reactivity, camera/CV, parameters, and input work in every renderer. Pick on visual style, not data sources.\n\n## Scene shape per renderer\n\nEach example below is the minimum viable entry point. Full surface lives in the matching renderer reference.\n\n**Native**:\n```javascript\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n ctx.clearRect(0, 0, viji.width, viji.height);\n // draw with ctx\n}\n```\n\n**P5**:\n```javascript\n// @renderer p5\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nlet angle = 0;\nfunction setup(viji, p5) { p5.colorMode(p5.HSB, 360, 100, 100); }\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(0);\n // draw with p5.* prefixed calls\n}\n```\n\n**Shader (GLSL)**:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n// @viji-accumulator:phase rate:speed\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n gl_FragColor = vec4(uv, sin(phase) * 0.5 + 0.5, 1.0);\n}\n```\n\n## Cross-cutting rules (apply to every renderer)\n\n1. **Never** touch the DOM. No `window`, `document`, `Image()`, `localStorage`. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the **top level**, never inside `render()` or `main()`. Parameters declared inside a render call are recreated every frame and break the host UI.\n3. **Always** use `viji.width` / `viji.height` (or `u_resolution` in shaders) for canvas dimensions. Never hardcode pixel sizes.\n4. **Always** use `viji.deltaTime` (or `u_deltaTime` / `@viji-accumulator` in shaders) for animation. Never count frames or assume a fixed frame rate.\n5. **Never** multiply `viji.time` (or `u_time`) by a parameter to drive animation speed. It causes visible jumps when the slider changes. Use a `deltaTime` accumulator at the top level instead. In shaders, use `@viji-accumulator`. This also applies to nested multiplications: never multiply an accumulator by another parameter. Each independent speed gets its own accumulator.\n6. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n7. **Always** check `viji.audio.isConnected` (or `u_audioVolume > 0.0`) before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` (or `u_videoConnected`) before reading video. When an input is not connected, degrade gracefully — keep rendering the scene's normal idle/ambient visual. Do **not** draw on-canvas text telling the user to connect or turn on a camera, microphone, or other input; the host provides those controls. (The connection-demo examples draw \"Waiting for…\" text only to illustrate this check itself — don't copy that into real scenes unless the user explicitly asks for an on-canvas prompt.)\n8. **CV features are independent**: each `enableX()` populates only its own subset of fields. Face detection populates `face.bounds` / `center` / `confidence`; face mesh populates `face.landmarks` / `headPose`; emotion detection populates `face.expressions` / `blendshapes`. Fields whose source model is not enabled read as `null` (or `[]` for `landmarks`) — always null-check before reading. CV is expensive and is **not** host-gated, so it is the one feature class that legitimately belongs behind a parameter toggle — but gate by intent: if a CV feature is the scene's defining purpose (the scene is meaningless without it), enable it always-on from module scope; if CV is optional/supplementary, or the scene uses several heavy features, gate each behind a `viji.toggle` (shader: the toggleable `@viji-cv:` form) so the user opts into the cost — and make sure the base visual stands on its own when a toggle is off (no \"enable in parameters\" banner). Each active CV feature uses its own WebGL context; 4+ on low-end hardware can hit context limits, so enable only what you need.\n9. **Always** set `category` on parameters that depend on an external input: `category: 'audio'` for audio-driven controls, `category: 'video'` for video / CV-driven controls, `category: 'interaction'` for mouse / keyboard / touch controls. Omit `category` (defaults to `'general'`) for parameters that work without external input. **Never** add on/off parameter toggles to enable/disable built-in inputs or device features — audio, video, mouse, keyboard, touch, motion/orientation sensors, and external devices are all gated by the host's own controls. Read them in code behind `isConnected` / `isInCanvas` / null checks. A parameter that shapes how an input drives the visual (e.g. a sensitivity slider) is fine; an on/off enable toggle for the input itself is not. If the user explicitly asks for one, advise that the host already provides it before complying. (CV features are the sole exception — see the CV rule above.)\n10. **In P5**: prefix every P5 function and constant with `p5.` (instance mode). Never call `createCanvas()`; Viji creates the canvas.\n11. **In shaders**: never redeclare `precision`, never redeclare built-in uniforms (`u_time`, `u_resolution`, etc.), never redeclare parameter uniforms. Never use the `u_` prefix for your own parameter names; it is reserved for Viji's built-ins.\n\n## Parameter declaration shape (cross-renderer)\n\nAssign each parameter to its own top-level `const` with a unique camelCase\nname (`const speed = viji.slider(1, { label: 'Speed' })`) and read it with\n`.value`. `label` is optional but recommended (falls back to the variable\nname). Native and P5 use JavaScript calls at the top level:\n\n```javascript\nviji.slider(default, { min, max, step, label, group, category }) // .value: number\nviji.color(default, { label, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb, .hsb, .alpha\nviji.gradient([{ color, position }], { label, minStops, maxStops, alpha }) // .value: sorted stops, .at(t): hex at t; default required (2..8 stops)\nviji.toggle(default, { label }) // .value: boolean\nviji.select(default, { options, label }) // .value: string | number\nviji.number(default, { min, max, step, label }) // .value: number\nviji.text(default, { label, maxLength }) // .value: string\nviji.image(null, { label }) // .value: ImageBitmap | null\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required: list only the formats your loader parses; parse with your own loader once per new value, never per frame\nviji.button({ label }) // .value: boolean (true for 1 frame)\nviji.coordinate({ x: 0, y: 0 }, { step, label, group, category }) // .value: { x, y }, range -1..1\n```\n\n**`slider` / `number` `step` default**: when omitted, `step` auto-derives from `min`/`max` as the largest power of 10 giving ~100 positions across the range (e.g., range `0..1` → `0.01`; range `0.1..5` → `0.01`; range `0..100` → `1`). Specify explicitly only when you need a different grid: `step: 1` for integer counts (`{ min: 3, max: 30, step: 1 }`), `step: 0.001` for fine precision.\n\nShaders use GLSL comment directives that compile to typed uniforms:\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 -> uniform float name;\n// @viji-color:name label:\"Color\" default:#ff6600 -> uniform vec3 name; (alpha:true -> vec4; 8-digit default needs alpha:true)\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] -> vec3 name(float t) sampler + arrays (alpha:true -> vec4; stops are #hex@position)\n// @viji-toggle:name label:\"Toggle\" default:false -> uniform bool name;\n// @viji-select:name label:\"Mode\" default:\"A\" options:[\"A\",\"B\"] -> uniform int name;\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 -> uniform float name;\n// @viji-image:name label:\"Texture\" -> uniform sampler2D name;\n// @viji-button:name label:\"Reset\" -> uniform bool name;\n// @viji-coordinate:name label:\"Pos\" default:[0.0,0.0] -> uniform vec2 name; // range -1..1\n// @viji-accumulator:name rate:speed -> uniform float name;\n```\n\nDirectives use `//` line comments only. Block comments (`/* */`) are not parsed.\n\n## Where to look next\n\nFor the full API surface of the renderer in use, consult the matching reference: Native, P5, or Shader. Each renderer reference includes the complete data shapes for audio, video, CV, input, sensors, external devices, and renderer-specific patterns (e.g. `viji.useContext` for Native, `videoFit` for P5, the complete uniform table for Shader).\n",
|
|
10237
|
+
"native": "# Viji API: Native Renderer Reference\n\nNative scenes run JavaScript on an `OffscreenCanvas` in a Web Worker, with full access to Canvas 2D, WebGL, WebGL2, and WebGPU contexts. External libraries load via `await import()`. Top-level code runs once; a `render(viji)` function runs every frame.\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters.\n- Call `viji.useContext('2d' | 'webgl' | 'webgl2' | 'webgpu')` once at the top of `render()` (or top-level) to obtain a rendering context. After the first call, the matching shortcut is populated: `viji.ctx` / `viji.gl` / `viji.gpu`.\n- **Pick one context type for the entire scene.** Calling `useContext` with a different type after the first call returns `null`.\n- **WebGPU**: request the adapter and device at top level. `useContext('webgpu')` returns `null` where unsupported: guard and fall back.\n- There is **no `setup()` function** in Native. All initialization goes at the top level (which supports `await` for dynamic imports).\n- The DOM is unavailable. `fetch()` and `await import()` are available.\n\n## Rules\n\n1. **Never** access `window`, `document`, `Image()`, `localStorage`, or any DOM API. `fetch()` and `await import()` are available.\n2. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()`. `label` is optional but recommended (falls back to the variable name).\n3. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (`{ r, g, b }` in 0..255) and `.hsb` (`{ h, s, b }` with `h` in 0..360, `s` / `b` in 0..100). Color defaults accept hex (`'#ff6600'`, `'#f60'`), `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS `'rgb(...)'` / `'hsl(...)'` strings.\n4. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n5. **Always** use `viji.deltaTime` for animation. Use `viji.time` only for constant-speed oscillations (`Math.sin(viji.time * 2.0)`). For anything driven by a parameter, use a `deltaTime` accumulator.\n6. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator at the top level. This also applies to nested multiplications: each independent speed gets its own accumulator.\n\n ```javascript\n // WRONG\n const t = viji.time * speed.value;\n const rot = phase * rotSpeed.value;\n\n // RIGHT\n let phase = 0, rotPhase = 0;\n phase += speed.value * viji.deltaTime;\n rotPhase += speed.value * rotSpeed.value * viji.deltaTime;\n ```\n7. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n8. **Always** call `viji.useContext()` to get a context. Pick one type and use it for the entire scene.\n9. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n10. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle` so the user opts into the cost.\n11. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n12. **Always** type any function parameter that receives the `viji` instance, including `render(viji)` and any helpers that take `viji`. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time so the error can be corrected before runtime. Scene code is transpiled via sucrase before execution (`transforms: ['typescript']`), so TS annotations are stripped at runtime — they are safe to include.\n\n ```javascript\n function render(viji: VijiAPI) { ... }\n function applyGlow(viji: VijiAPI, intensity: number) { ... }\n ```\n13. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `native-api-reference` docs page instead of guessing.\n\n## API reference\n\n### Canvas and context\n\n| Member | Type | Description |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | The canvas |\n| `viji.useContext('2d')` | `OffscreenCanvasRenderingContext2D` | Acquire 2D context |\n| `viji.useContext('webgl')` | `WebGLRenderingContext` | Acquire WebGL 1 |\n| `viji.useContext('webgl2')` | `WebGL2RenderingContext` | Acquire WebGL 2 |\n| `viji.useContext('webgpu')` | `GPUCanvasContext \\| null` | Acquire WebGPU |\n| `viji.ctx` | `OffscreenCanvasRenderingContext2D` | Shortcut after `useContext('2d')` |\n| `viji.gl` | `WebGLRenderingContext \\| WebGL2RenderingContext` | Shortcut after WebGL context acquired |\n| `viji.gpu` | `GPUCanvasContext` | Shortcut after `useContext('webgpu')` |\n| `viji.width`, `viji.height` | `number` | Current canvas dimensions in pixels |\n\n### Timing\n\n| Member | Type | Description |\n|---|---|---|\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate (host-controlled) |\n\n### Parameters\n\nDeclare at top level. All accept `{ label, description?, group?, category? }`. Category values: `'audio'`, `'video'`, `'interaction'`, `'general'` (default).\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required: list only the formats your loader parses; parse with your own loader once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // .value: boolean (true for 1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first.\n\n| Member | Type | Notes |\n|---|---|---|\n| `isConnected` | `boolean` | Audio source active |\n| `volume.current`, `volume.peak`, `volume.smoothed` | `number` 0..1 | `smoothed` = 200ms decay envelope of `current` |\n| `bands.low`, `lowMid`, `mid`, `highMid`, `high` | `number` 0..1 | Instant band energies (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\n| `bands.lowSmoothed`, `lowMidSmoothed`, `midSmoothed`, `highMidSmoothed`, `highSmoothed` | `number` 0..1 | 150ms decay envelope siblings (not nested) |\n| `beat.kick`, `snare`, `hat`, `any` | `number` 0..1 | Beat energy curves with a 300ms decay; peak on each detected beat then fall off |\n| `beat.kickSmoothed`, `snareSmoothed`, `hatSmoothed`, `anySmoothed` | `number` 0..1 | Smoother 500ms decay envelopes; use for ambient pulses |\n| `beat.triggers.kick`, `snare`, `hat`, `any` | `boolean` | True for exactly one frame on the matching beat, then auto-resets. OR-accumulated between frames so no beat is lost |\n| `beat.events` | `Array<{type, time, strength}>` | All beats detected since the last frame; `type` is `'kick' \\| 'snare' \\| 'hat'` (never `'any'`); `time` in milliseconds; cleared each frame |\n| `beat.bpm`, `beat.confidence`, `beat.isLocked` | `number`, `number`, `boolean` | `bpm` is `0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120` as a fallback before lock-on. `confidence` in 0..1. `isLocked` is `true` on stable tempo lock |\n| `spectral.brightness`, `spectral.flatness` | `number` 0..1 | Spectral centroid; spectral flatness |\n| `getFrequencyData()` | `Uint8Array` | Raw FFT bins 0..255 |\n| `getWaveform()` | `Float32Array` | Time-domain waveform -1..1 |\n\nExternal-device audio (`viji.devices[i].audio`) and host-supplied audio streams (`viji.audioStreams[i]`) use the `AudioStreamAPI` shape: `isConnected`, `volume.{current,peak,smoothed}`, `bands.{low...high}` plus each `*Smoothed` sibling, `spectral.{brightness,flatness}`, `getFrequencyData()`, `getWaveform()`. No `beat`, BPM, triggers, or events on streams: those exist only on the main `viji.audio`.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing.\n\n| Member | Type | Notes |\n|---|---|---|\n| `isConnected` | `boolean` | Video source active |\n| `currentFrame` | `OffscreenCanvas \\| ImageBitmap \\| null` | Most recent frame |\n| `frameWidth`, `frameHeight` | `number` | Source frame size |\n| `frameRate` | `number` | Source frame rate |\n| `getFrameData()` | `ImageData \\| null` | Pixel data of `currentFrame` |\n| `cv` | `VideoCVAPI` | Computer-vision surface: see below |\n\nCV data outputs (`analysedFrame`, `getAnalysedFrameData()`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs (`enableFaceDetection`, etc.) all live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect ratio.** Camera frames almost never match the canvas aspect. Drawing to `(0, 0, viji.width, viji.height)` stretches video and misaligns CV bounds. Use this helper at module scope:\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nconst v = videoFit(viji); // 'cover' or 'contain'\nctx.drawImage(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n\n// CV coords are normalized 0..1 to the source frame. Map through v to align with the fitted video:\nviji.video.cv.faces.forEach(face => {\n const bx = v.x + face.bounds.x * v.width;\n const by = v.y + face.bounds.y * v.height;\n const bw = face.bounds.width * v.width;\n const bh = face.bounds.height * v.height;\n ctx.strokeRect(bx, by, bw, bh);\n});\n```\n\nDefault to `'cover'` for live cameras. Use `'contain'` for CV-overlay scenes where bounding boxes near frame edges must stay visible. `viji.video.currentFrame` is the right default for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions (segmentation compositing, face-mesh texturing, landmark-anchored color sampling). The common fallback pattern is `viji.video.cv.analysedFrame ?? viji.video.currentFrame` during the brief window before the first CV inference lands.\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature consumes a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional — without it, the model loads\n// asynchronously while render runs and data fields populate when ready.\n// Safe to call from module scope (always-on CV) or per-frame inside render()\n// gated by a viji.toggle(...) (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // populates face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // populates face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // populates face.expressions + face.blendshapes; also loads landmarker (face.landmarks + face.headPose populated)\nawait viji.video.cv.enableHandTracking(true | false); // populates viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // populates viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // populates viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the exact frame paired with the current CV results. `null` until the first inference lands after a feature is enabled. Use the pattern `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during the startup window.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`, cached and re-extracted only on new CV results.\n\n**`viji.video.cv.faces: FaceData[]`**: `id` always present. Other fields are populated only by their source model and read `null` (or empty array for `landmarks`) otherwise — always null-check before reading.\n- `bounds: {x,y,width,height} | null` (normalized 0..1) — populated by face detection\n- `center: {x,y} | null` (normalized 0..1) — populated by face detection\n- `confidence: number | null` (0..1) — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled; 468 pts when populated)\n- `headPose: { pitch, yaw, roll } | null` (degrees) — populated by face mesh\n- `expressions: { neutral, happy, sad, angry, surprised, disgusted, fearful } | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` — populated by emotion detection; 52 ARKit coefficients (`browDownLeft`/`Right`, `browInnerUp`, `browOuterUpLeft`/`Right`, `cheekPuff`, `cheekSquintLeft`/`Right`, `eyeBlinkLeft`/`Right`, `eyeLookDown`/`In`/`Out`/`UpLeft`/`Right`, `eyeSquintLeft`/`Right`, `eyeWideLeft`/`Right`, `jawForward`/`Left`/`Open`/`Right`, `mouthClose`/`DimpleLeft`/`Right`/`FrownLeft`/`Right`/`Funnel`/`Left`/`LowerDownLeft`/`Right`/`PressLeft`/`Right`/`Pucker`/`Right`/`RollLower`/`Upper`/`ShrugLower`/`Upper`/`SmileLeft`/`Right`/`StretchLeft`/`Right`/`UpperUpLeft`/`Right`, `noseSneerLeft`/`Right`, `tongueOut`: all 0..1)\n\nIf you need bounds while only running face mesh, either also enable face detection or compute the bounding box from `face.landmarks` min/max yourself.\n\n**`viji.video.cv.hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks: {x,y,z}[]` (21 points), `palm: {x,y,z}`, `gestures: { fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou }` (0..1 confidence each).\n\n**`viji.video.cv.pose: PoseData | null`**: `confidence`, `landmarks: {x,y,z,visibility}[]` (33 points), plus body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`viji.video.cv.segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`. Note: the shader `u_segmentationMask` sampler is sampled as a normalized `float` (0.0 = background, 1.0 = person); the JS `mask` byte values are 0/1, not 0/255.\n\n### Input\n\n**Pointer** (unified mouse and touch): `viji.pointer`\n| Member | Type | Notes |\n|---|---|---|\n| `x`, `y` | `number` | Position in pixels |\n| `deltaX`, `deltaY` | `number` | Per-frame movement |\n| `isDown` | `boolean` | Pressed or touching |\n| `wasPressed`, `wasReleased` | `boolean` | One-frame edge triggers |\n| `isInCanvas` | `boolean` | Inside canvas bounds |\n| `type` | `'mouse' \\| 'touch' \\| 'none'` | Active input type |\n\n**Mouse**: `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n\n**Keyboard**: `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys: Set<string>`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n\n**Touch**: `viji.touches`: `count`, `points: TouchPoint[]`, `started`, `moved`, `ended` (each a `TouchPoint[]`), `primary: TouchPoint | null`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity: {x,y}`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n**`viji.device`**: host device motion and orientation:\n- `motion: DeviceMotionData | null`: `acceleration: {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate: {alpha,beta,gamma}` (deg/s), `interval` (ms).\n- `orientation: DeviceOrientationData | null`: `alpha` (0-360° compass), `beta` (-180..180° front-back tilt), `gamma` (-90..90° left-right tilt), `absolute`.\n\n**`viji.devices`**: `DeviceState[]`, externally connected devices. Each: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, same shape as `viji.video` but without CV), `audio` (`AudioStreamAPI | null`, lightweight; no beat or BPM).\n\n### Host-supplied streams\n\n- `viji.videoStreams: VideoAPI[]`: extra video sources from the host compositor. Same shape as `viji.video`, including a per-stream `cv` surface with the same enable verbs and result fields; `cv.analysedFrame` is populated on the main stream only. May be empty.\n- `viji.audioStreams: AudioStreamAPI[]`: extra audio sources from the host. Lightweight shape (no beat or BPM). May be empty.\n\n### External libraries\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false); // always pass false to skip CSS styling\n```\n\nPin library versions in the import URL. Always pass `viji.canvas` to the renderer. Always pass `false` as the third argument to Three.js `setSize`. Handle resize by comparing `viji.width` / `viji.height` against previous values inside `render()`. Transformers.js needs `env.useBrowserCache = false` (Cache Storage is unavailable in the scene worker; model downloads are cached by the engine anyway). Load ONNX Runtime Web 1.27+ from jsdelivr (`https://cdn.jsdelivr.net/npm/onnxruntime-web@<version>/dist/ort.webgpu.min.mjs`, and set `ort.env.wasm.wasmPaths` to that `dist/` folder); esm.sh builds of it are broken. Run model inference off the render path: start it at an interval when not already running, keep the latest result, and draw that.\n\n## Template\n\n```javascript\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(12, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction render(viji) {\n const ctx = viji.useContext('2d');\n angle += speed.value * viji.deltaTime;\n\n ctx.fillStyle = bgColor.value;\n ctx.fillRect(0, 0, viji.width, viji.height);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = Math.min(viji.width, viji.height) * 0.3;\n const dotSize = Math.min(viji.width, viji.height) * 0.02;\n const n = Math.floor(count.value);\n\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n const hue = (i / n) * 360;\n ctx.beginPath();\n ctx.arc(x, y, dotSize, 0, Math.PI * 2);\n ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;\n ctx.fill();\n }\n}\n```\n",
|
|
10238
|
+
"p5": "# Viji API: P5 Renderer Reference\n\nP5 scenes use P5.js 1.9.4 inside a Web Worker on an `OffscreenCanvas`. P5 runs in **instance mode**: every P5 function and constant requires the `p5.` prefix. The first line of the scene must be `// @renderer p5` (2D) or `// @renderer p5 webgl` (3D / WEBGL).\n\n## Architecture\n\n- The global `viji` object exposes canvas, timing, audio, video, CV, input, sensors, parameters: identical to Native.\n- The canvas and its rendering context are managed by P5. **Do not call `createCanvas()`.** Viji creates the canvas in the mode declared by the `// @renderer` directive.\n- `viji.useContext()` is **not available** in P5 mode (the context belongs to P5).\n- Top-level code runs once. `function setup(viji, p5)` runs once for configuration. `function render(viji, p5)` runs every frame.\n- The `p5` instance passed to `setup` and `render` is the P5 instance. Use `p5.background`, `p5.fill`, `p5.circle`, `p5.PI`, etc. Unprefixed names will not resolve.\n\n## Rules\n\n1. **Always** add `// @renderer p5` (2D) or `// @renderer p5 webgl` (WEBGL) as the first line.\n2. **Always** use `render(viji, p5)` (not `draw()`) and `setup(viji, p5)` (not `setup()`).\n3. **Always** prefix every P5 function and constant with `p5.`:\n - `background(0)` -> `p5.background(0)`\n - `fill(255)` -> `p5.fill(255)`\n - `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`\n - `createVector(1, 0)` -> `p5.createVector(1, 0)`\n - `map(v, 0, 1, 0, 255)` -> `p5.map(v, 0, 1, 0, 255)`\n - `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`\n4. **Never** call `createCanvas()`. The canvas is created and managed by Viji.\n5. **Never** use `preload()`. Use `viji.image(null, { label })` for images, `viji.model(null, { label, formats })` for 3D models, or `fetch()` inside `setup()`.\n6. **Never** use P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edges.\n7. **Never** use `loadImage()`, `loadFont()`, `loadJSON()`, `loadShader()`. Use `viji.image()` or `fetch()`. Use `loadModel()` only with `viji.model().url`, once per new value (not per frame), passing the format as the file type: `p5.loadModel(m.value.url, true, ok, err, '.' + m.value.format)` (`.obj` and `.stl` only; needs `// @renderer p5 webgl`).\n8. **Never** use `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** use `createCapture()` or `createVideo()`. Use `viji.video.*`.\n10. **Never** use `p5.dom` or `p5.sound`. Use Viji parameters and `viji.audio.*`.\n11. **Never** access `window`, `document`, `Image()`, `localStorage`. `fetch()` is available.\n12. **Always** declare parameters at the top level as individual `const`s (`const speed = viji.slider(...)`), never inside `render()` or `setup()`. `label` is optional but recommended (falls back to the variable name).\n13. **Always** read parameters via `.value`. Color parameters also expose `.rgb` (matches `colorMode(RGB, 255)`) and `.hsb` (matches `colorMode(HSB, 360, 100, 100)`). Color defaults accept hex, `{ r, g, b }` (0..255), `{ h, s, b }` (0..360 / 0..100), or CSS color strings.\n14. **Always** use `viji.width` and `viji.height` for canvas dimensions. Never hardcode pixel sizes.\n15. **Always** use `viji.deltaTime` for animation:\n ```javascript\n let angle = 0;\n function render(viji, p5) { angle += speed.value * viji.deltaTime; }\n ```\n16. **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Use a `deltaTime` accumulator. This also applies to nested multiplications; each independent speed gets its own accumulator.\n17. **Never** allocate objects, arrays, or strings inside `render()`. Pre-allocate at the top level and reuse.\n18. **Image parameters with P5:** use `.p5` (a `P5Image`) instead of `.value` when passing to `p5.image()`:\n ```javascript\n const photo = viji.image(null, { label: 'Photo' });\n function render(viji, p5) {\n if (photo.value) p5.image(photo.p5, 0, 0, viji.width, viji.height);\n }\n ```\n19. **Video drawing.** In 2D, use `p5.image(viji.video.currentFrame, ...)` or `p5.drawingContext.drawImage(...)`. In WEBGL, use `p5.image(viji.video.currentFrame, ...)` only (`p5.drawingContext` is WebGL, not Canvas 2D). **Always preserve aspect ratio** via the `videoFit` helper below.\n20. `p5.createGraphics(w, h)` is **2D only** (`createGraphics(w, h, p5.WEBGL)` is not supported). It creates an OffscreenCanvas internally.\n21. Fonts: `p5.textFont()` only with CSS generic names (`monospace`, `serif`, `sans-serif`). `loadFont()` is not available.\n22. `p5.tint()` and `p5.blendMode()` work normally.\n23. **Canvas mode is declared by the `// @renderer` directive.** Use `// @renderer p5` for 2D, `// @renderer p5 webgl` for WEBGL. Never call `createCanvas()` or `createCanvas(..., p5.WEBGL)`.\n24. In WEBGL scenes, `p5.drawingContext` is a WebGL context. Use P5 3D drawing primitives or `p5.image()` / textures for images and video.\n25. `p5.pixelDensity()` defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes; WEBGL pixel readback follows standard P5.js rules.\n26. **Always** check `viji.audio.isConnected` before reading audio data.\n27. **Always** check `viji.video.isConnected && viji.video.currentFrame` before drawing video. When not connected, keep rendering the idle visual — don't draw on-canvas \"connect a camera/mic\" instructions.\n28. Enable CV by intent: always-on from module scope when CV is the scene's defining purpose, otherwise gated behind a `viji.toggle`.\n29. **Always** set `category` on input-dependent parameters (`'audio'`, `'video'`, `'interaction'`). Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV is the only exception (it gets a toggle when not always-on).\n30. **Always** type any function parameter that receives the `viji` instance or the `p5` instance, including `setup(viji, p5)`, `render(viji, p5)`, and any helpers that take either. Monaco's TS checker flags API misuse (wrong nesting, typo'd property names) at edit time. Scene code is transpiled via sucrase before execution, so TS annotations are stripped at runtime — safe to include. Note: the p5-instance type is the lowercase namespace `p5` (same name as the conventional parameter; TS resolves the value and the type from separate namespaces, so `p5: p5` is legal and common).\n\n ```javascript\n function setup(viji: VijiAPI, p5: p5) { ... }\n function render(viji: VijiAPI, p5: p5) { ... }\n function videoFit(viji: VijiAPI, mode: 'cover' | 'contain') { ... }\n ```\n31. All Viji types are ambient globals — `VijiAPI`, the parameter and config types (`SliderParameter`, `SliderConfig`, ...), input and CV types (`TouchPoint`, `FaceData`, ...). Annotate with them directly; never import, alias, or re-derive them (no `ReturnType<typeof viji.slider>` — the named type exists). For the exact type of any `viji` member, fetch the `p5-api-reference` docs page instead of guessing.\n\n## Video aspect helper\n\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\n\nif (viji.video.isConnected && viji.video.currentFrame) {\n const v = videoFit(viji, 'cover');\n p5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n}\n\n// CV bounds and landmarks are normalized 0..1 to the source frame.\n// Map them through v to align with the drawn video.\n```\n\n## P5 to Viji mapping\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `loadModel(path)` | `viji.model(null, { label: 'Model', formats: ['obj', 'stl'] })`; when `.value` changes, `p5.loadModel(value.url, true, ok, err, '.' + value.format)`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `mousePressed()` / `mouseReleased()` callbacks | Check `viji.pointer.wasPressed` / `wasReleased` inside `render()`. |\n| `keyPressed()` / `keyReleased()` callbacks | Check `viji.keyboard.wasPressed(key)` / `wasReleased(key)` inside `render()`. |\n\n## API reference\n\nAll `viji.*` members are identical to Native (same object, same types).\n\n### Canvas and timing\n\n| Member | Type | Notes |\n|---|---|---|\n| `viji.canvas` | `OffscreenCanvas` | Managed by P5 |\n| `viji.width`, `viji.height` | `number` | Canvas dimensions |\n| `viji.time` | `number` | Seconds since scene start |\n| `viji.deltaTime` | `number` | Seconds since last frame |\n| `viji.frameCount` | `number` | Total frames rendered |\n| `viji.fps` | `number` | Target frame rate |\n\n### Parameters\n\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null, .p5: P5Image\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required; use ['obj', 'stl'] (what p5 loadModel reads); no .p5; load with p5.loadModel(value.url, true, ok, err, '.' + value.format) once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n// slider/number `step` defaults to a power of 10 giving ~100 positions across min..max\n// (e.g., range 0..1 → 0.01, range 0..100 → 1). Specify `step: 1` for integer counts.\n```\n\n### Audio: `viji.audio`\n\nCheck `isConnected` first. Members:\n\n`isConnected` (boolean); `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (0..1; ranges 20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (0..1, 300ms decay curves; peak on each detected beat); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for exactly one frame, OR-accumulated between frames); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; `time` ms; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins, 0..255); `getWaveform(): Float32Array` (2048 samples, -1..1).\n\nExternal-device audio (`viji.devices[i].audio`) and host streams (`viji.audioStreams[i]`) follow the `AudioStreamAPI` shape: `isConnected`, `volume`, `bands` (instant + smoothed siblings), `spectral`, `getFrequencyData`, `getWaveform`. No beat, BPM, triggers, or events on streams.\n\n### Video: `viji.video`\n\nCheck `isConnected && currentFrame` before drawing. Members:\n\n`isConnected` (boolean); `currentFrame` (`OffscreenCanvas | ImageBitmap | null`); `frameWidth`, `frameHeight`, `frameRate`; `getFrameData(): ImageData | null`; `cv: VideoCVAPI` (CV outputs and verbs live here, not on `viji.video` directly).\n\n### Computer Vision: `viji.video.cv`\n\nEach CV feature is independent and populates only its own subset of fields. Enable only what you need (each active feature uses a separate WebGL context for MediaPipe; 4+ on low-end hardware risks context-limit failures).\n\n```javascript\n// Verbs return Promise<void>. await is optional. Safe to call from module\n// scope (always-on CV) or per-frame inside render() gated by a viji.toggle(...)\n// (opt-in CV). Idempotent + reference-counted.\nawait viji.video.cv.enableFaceDetection(true | false); // face.bounds, face.center, face.confidence\nawait viji.video.cv.enableFaceMesh(true | false); // face.landmarks (468 pts) + face.headPose\nawait viji.video.cv.enableEmotionDetection(true | false); // face.expressions + face.blendshapes; also loads landmarker\nawait viji.video.cv.enableHandTracking(true | false); // viji.video.cv.hands[]\nawait viji.video.cv.enablePoseDetection(true | false); // viji.video.cv.pose\nawait viji.video.cv.enableBodySegmentation(true | false); // viji.video.cv.segmentation\nviji.video.cv.getActiveFeatures(); // CVFeature[]\nviji.video.cv.isProcessing(); // boolean\n```\n\nData outputs on `viji.video.cv`:\n- `analysedFrame: OffscreenCanvas | null`: the frame paired with the current CV results. `null` until the first inference lands. Use `viji.video.cv.analysedFrame ?? viji.video.currentFrame` to fall back during startup.\n- `getAnalysedFrameData(): ImageData | null`: pixel data of `analysedFrame`.\n\n**`faces: FaceData[]`**: `id` always present. Other fields populated only by their source model; otherwise `null` (`landmarks` is `[]` when empty). Always null-check.\n- `bounds: {x,y,width,height} | null` — populated by face detection\n- `center: {x,y} | null` — populated by face detection\n- `confidence: number | null` — populated by face detection\n- `landmarks: {x,y,z?}[]` (empty `[]` unless face mesh enabled)\n- `headPose: {pitch, yaw, roll} | null` — populated by face mesh\n- `expressions: {neutral, happy, sad, angry, surprised, disgusted, fearful} | null` (0..1 each) — populated by emotion detection\n- `blendshapes | null` (52 ARKit coefficients 0..1) — populated by emotion detection\n\nIf you need `bounds` while only running face mesh, either also enable face detection or compute it from `face.landmarks` min/max.\n\n**`hands: HandData[]`**: `id`, `handedness: 'left' | 'right'`, `confidence`, `bounds`, `landmarks` (21 points), `palm`, `gestures` (`fist`, `openPalm`, `peace`, `thumbsUp`, `thumbsDown`, `pointing`, `iLoveYou` 0..1).\n\n**`pose: PoseData | null`**: `confidence`, `landmarks` (33 points), body-part arrays `face`, `torso`, `leftArm`, `rightArm`, `leftLeg`, `rightLeg`.\n\n**`segmentation: SegmentationData | null`**: `mask: Uint8Array` (each byte is `0` for background or `1` for person; length = `width * height`), `width`, `height`.\n\n### Input\n\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`).\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `pressedThisFrame`, `releasedThisFrame`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`. Each `TouchPoint`: `id`, `x`, `y`, `pressure`, `radius`, `radiusX`, `radiusY`, `rotationAngle`, `force`, `isInCanvas`, `deltaX`, `deltaY`, `velocity`, `isNew`, `isActive`, `isEnding`.\n\n### Sensors and external devices\n\n`viji.device.motion`: `acceleration {x,y,z}` (m/s²), `accelerationIncludingGravity`, `rotationRate {alpha,beta,gamma}` (deg/s), `interval` (ms).\n`viji.device.orientation`: `alpha` (0-360°), `beta` (-180..180°), `gamma` (-90..90°), `absolute`.\n\n`viji.devices: DeviceState[]`: each entry: `id`, `name`, `motion`, `orientation`, `video` (`VideoAPI | null`, no CV), `audio` (`AudioStreamAPI | null`, lightweight).\n\n### Host streams\n\n`viji.videoStreams: VideoAPI[]`: extra video sources. Same shape as `viji.video`, including a per-stream `cv` surface (same verbs and result fields; `cv.analysedFrame` is main-stream only). May be empty.\n`viji.audioStreams: AudioStreamAPI[]`: extra audio sources. May be empty. Lightweight shape (no beat / BPM).\n\n## Template\n\n```javascript\n// @renderer p5\n\nconst bgColor = viji.color('#1a1a2e', { label: 'Background' });\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' }); // implicit step → 0.01\nconst count = viji.slider(8, { min: 3, max: 30, step: 1, label: 'Count' }); // explicit step for integer counts\n\nlet angle = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n angle += speed.value * viji.deltaTime;\n p5.background(bgColor.value);\n\n const cx = viji.width / 2;\n const cy = viji.height / 2;\n const radius = p5.min(viji.width, viji.height) * 0.3;\n const dotSize = p5.min(viji.width, viji.height) * 0.04;\n const n = p5.floor(count.value);\n\n p5.noStroke();\n for (let i = 0; i < n; i++) {\n const a = angle + (i / n) * p5.TWO_PI;\n const x = cx + p5.cos(a) * radius;\n const y = cy + p5.sin(a) * radius;\n p5.fill((i / n) * 360, 80, 90);\n p5.circle(x, y, dotSize);\n }\n}\n```\n",
|
|
10093
10239
|
"shader": "# Viji API: Shader Renderer Reference\n\nShader scenes are GLSL fragment shaders running on a fullscreen quad. Write helper functions and `void main()`. Precision and all uniforms (built-in plus those generated from `@viji-*` directives) are auto-injected by Viji. The first line must be `// @renderer shader` (or `#version 300 es` followed by `// @renderer shader` for GLSL ES 3.00).\n\n## Architecture\n\n- The shader is **GLSL ES 1.00** by default. Opt into ES 3.00 by making `#version 300 es` the literal first line.\n- Viji auto-injects `precision mediump float;` and every uniform declaration. Never declare them yourself.\n- ES 3.00 requires `out vec4 fragColor;` before `main` and `fragColor = ...` instead of `gl_FragColor`. ES 3.00 uses `texture()` instead of `texture2D()`.\n- If the shader references `fwidth`, Viji auto-injects `#extension GL_OES_standard_derivatives : enable`.\n- The `backbuffer` sampler is auto-enabled when referenced. It carries the previous frame.\n\n## Rules\n\n1. **Always** add `// @renderer shader` as the first line (or as line 2 after `#version 300 es`).\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`, audio / video / CV uniforms, 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 Viji's built-ins. Use descriptive names (`speed`, `colorMix`, `intensity`).\n6. `@viji-*` parameter directives work with `//` comments only. Block comments (`/* */`) are not parsed.\n7. **Always** use `@viji-accumulator` instead of `u_time * speed` for parameter-driven animation. Multiplying `u_time` by a parameter causes visible jumps when sliders change.\n\n ```glsl\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float wave = sin(phase);\n ```\n\n This also applies to **nested** multiplications. Each independent speed needs its own accumulator:\n\n ```glsl\n // @viji-accumulator:phase rate:speed\n // @viji-accumulator:rotPhase rate:rotSpeed\n ```\n8. For `backbuffer` (previous frame): reference it directly in code. Viji auto-detects and enables it. Sample with `texture2D(backbuffer, uv)` (ES 1.00) or `texture(backbuffer, uv)` (ES 3.00).\n9. Remove any `#ifdef GL_ES` / `precision` blocks; Viji handles them.\n10. **Always** set `category` on input-dependent directives: `category:audio`, `category:video`, `category:interaction`. Don't add on/off toggles to enable/disable host-gated inputs (audio, video, mouse, keyboard, touch, sensors, devices) — the host controls those. CV activations are the exception (the `@viji-cv:` directives below).\n11. **CV features must be activated via `// @viji-cv:<featureToken>` directives.** Without the matching `@viji-cv:` activation, CV uniforms (`u_faceCount`, `u_face0*`, `u_handCount`, `u_poseDetected`, `u_segmentationMask`, etc.) read zero. Choose the form by intent: use the bare form (`// @viji-cv:faceDetection`) for always-on detection when CV is the scene's defining purpose, or the toggleable form (`// @viji-cv:faceDetection label:\"Face Detection\" default:false`) — which synthesizes both a host-side toggle parameter and a `bool <featureToken>` shader uniform mirroring its state — when CV is optional or the scene uses several heavy features. The six tokens: `faceDetection`, `faceMesh`, `emotionDetection`, `handTracking`, `poseDetection`, `bodySegmentation`.\n\n## Parameter directives\n\n```glsl\n// @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0 step:0.1\n// -> uniform float name;\n\n// @viji-color:name label:\"Color\" default:#ff6600\n// (alpha:true -> uniform vec4 name; an 8-digit default without alpha:true is an error)\n\n// @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] minStops:2 maxStops:8\n// -> vec3 name(float t) sampler (vec4 with alpha:true) over uniform arrays name_colors[N], name_positions[N], name_count\n// stops are #hex@position (hex only), default required; call name(t) in main(), never declare the uniforms\n// -> uniform vec3 name; (RGB 0..1)\n// default: accepts #rrggbb, #rgb, vec3(r,g,b) (0..1), rgb(r,g,b) (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:name label:\"Toggle\" default:false\n// -> uniform bool name;\n\n// @viji-select:name label:\"Mode\" default:\"Solid\" options:[\"Solid\",\"Gradient\",\"Noise\"]\n// -> uniform int name; (0-based index of the selected option)\n\n// @viji-number:name label:\"Count\" default:10.0 min:1.0 max:100.0 step:1.0\n// -> uniform float name;\n\n// @viji-image:name label:\"Texture\"\n// -> uniform sampler2D name;\n\n// @viji-button:name label:\"Reset\"\n// -> uniform bool name; (true for one frame on press)\n\n// @viji-coordinate:name label:\"Origin\" default:[0.0,0.0]\n// -> uniform vec2 name; (each component -1..1)\n\n// @viji-accumulator:name rate:speed\n// -> uniform float name; (CPU-side: += rate * deltaTime each frame)\n```\n\nAll directives support `group:\"GroupName\"` and `category:\"audio|video|interaction|general\"`.\n\n## 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` | 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 (host-controlled) |\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` | Mouse inside canvas |\n| `u_mousePressed` | `bool` | Any button pressed |\n| `u_mouseLeft`, `u_mouseRight`, `u_mouseMiddle` | `bool` | Specific buttons |\n| `u_mouseDelta` | `vec2` | Per-frame movement |\n| `u_mouseWheel` | `float` | Scroll wheel delta |\n| `u_mouseWasPressed`, `u_mouseWasReleased` | `bool` | One-frame edges |\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` | Primary input active |\n| `u_pointerWasPressed`, `u_pointerWasReleased` | `bool` | One-frame edges |\n| `u_pointerInCanvas` | `bool` | Inside canvas |\n\n### Keyboard\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_keySpace`, `u_keyShift`, `u_keyCtrl`, `u_keyAlt` | `bool` | Modifier and space |\n| `u_keyW`, `u_keyA`, `u_keyS`, `u_keyD` | `bool` | WASD |\n| `u_keyUp`, `u_keyDown`, `u_keyLeft`, `u_keyRight` | `bool` | Arrows |\n| `u_keyboard` | `sampler2D` | Full keyboard state (256x3 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` | Active touches (0-5) |\n| `u_touch0`-`u_touch4` | `vec2` | Touch point positions in pixels |\n\n### Audio (scalars)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioVolume`, `u_audioPeak` | `float` | RMS / peak 0..1 |\n| `u_audioVolumeSmoothed` | `float` | 200ms decay envelope of `u_audioVolume` |\n| `u_audioLow`, `u_audioLowMid`, `u_audioMid`, `u_audioHighMid`, `u_audioHigh` | `float` | Band energies 0..1 (20-120, 120-400, 400-1600, 1600-6000, 6000-16000 Hz) |\n| `u_audioLowSmoothed` - `u_audioHighSmoothed` | `float` | 150ms decay envelopes of the bands |\n| `u_audioKick`, `u_audioSnare`, `u_audioHat`, `u_audioAny` | `float` | Beat energy curves with 300ms decay; peak on each detected beat then fall off |\n| `u_audioKickSmoothed` - `u_audioAnySmoothed` | `float` | Smoother 500ms decay envelopes |\n| `u_audioKickTrigger`, `u_audioSnareTrigger`, `u_audioHatTrigger`, `u_audioAnyTrigger` | `bool` | True for exactly one frame on the matching beat, then auto-resets; OR-accumulated between frames |\n| `u_audioBPM`, `u_audioConfidence`, `u_audioIsLocked` | `float`, `float`, `bool` | `u_audioBPM` is `0.0` when no audio is connected; once audio connects it tracks the detected tempo (clamped to 60..240) with `120.0` as a fallback before lock-on. `u_audioConfidence` 0..1. `u_audioIsLocked` true on stable tempo lock |\n| `u_audioBrightness`, `u_audioFlatness` | `float` | Spectral features 0..1 |\n\n### Audio (textures, main source only)\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioFFT` | `sampler2D` | FFT spectrum (1024 bins, 0..255) |\n| `u_audioWaveform` | `sampler2D` | Time-domain waveform (-1..1) |\n\nAdditional audio streams and device audio expose scalar uniforms only (no FFT / waveform textures): see \"Streams\" below.\n\n### Video\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_video` | `sampler2D` | Most recent video frame |\n| `u_videoAnalysed` | `sampler2D` | Frame paired with current CV uniforms |\n| `u_videoAnalysedAvailable` | `bool` | True after first CV result lands |\n| `u_videoResolution` | `vec2` | Source frame size in pixels |\n| `u_videoFrameRate` | `float` | Source frame rate |\n| `u_videoConnected` | `bool` | Video source active |\n\n**Aspect handling.** Camera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with canvas UV stretches the video and misaligns CV uniforms. Use these helpers at the top of every video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, video edges cropped)\n// 0 = contain (fits video, canvas letterboxed)\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\nDefault to mode `1` (cover) for live cameras. Use `0` (contain) for CV-overlay shaders where features near edges must stay visible. CV uniforms (`u_face0Center`, etc.) share the same bottom-up convention as `videoUV`; compare them directly without flipping.\n\n**`u_video` vs `u_videoAnalysed`.** Default to `u_video` for displayed video. Reach for `u_videoAnalysed` only when sampling pixels at CV-derived positions (segmentation compositing onto the body, sampling skin under face landmarks, face-mesh texturing). For shaders that consume CV uniforms without sampling at CV positions, stay on `u_video` (`u_videoAnalysed` advances only on inference completion and will stutter the displayed video). When `u_videoAnalysed` is the right choice, gate on `u_videoAnalysedAvailable` for the brief startup window:\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` | Detected faces (0-1) |\n| `u_face0Bounds` | `vec4` | `(x, y, width, height)` normalized 0..1 |\n| `u_face0Center` | `vec2` | Center normalized 0..1 |\n| `u_face0HeadPose` | `vec3` | `(pitch, yaw, roll)` in degrees |\n| `u_face0Confidence` | `float` | 0..1 |\n| `u_face0Neutral` - `u_face0Fearful` | `float` | 7 expression scores 0..1 |\n\n**52 blendshape uniforms** (all `float`, 0..1, ARKit names prefixed with `u_face0`):\n`BrowDownLeft`, `BrowDownRight`, `BrowInnerUp`, `BrowOuterUpLeft`, `BrowOuterUpRight`, `CheekPuff`, `CheekSquintLeft`, `CheekSquintRight`, `EyeBlinkLeft`, `EyeBlinkRight`, `EyeLookDownLeft`, `EyeLookDownRight`, `EyeLookInLeft`, `EyeLookInRight`, `EyeLookOutLeft`, `EyeLookOutRight`, `EyeLookUpLeft`, `EyeLookUpRight`, `EyeSquintLeft`, `EyeSquintRight`, `EyeWideLeft`, `EyeWideRight`, `JawForward`, `JawLeft`, `JawOpen`, `JawRight`, `MouthClose`, `MouthDimpleLeft`, `MouthDimpleRight`, `MouthFrownLeft`, `MouthFrownRight`, `MouthFunnel`, `MouthLeft`, `MouthLowerDownLeft`, `MouthLowerDownRight`, `MouthPressLeft`, `MouthPressRight`, `MouthPucker`, `MouthRight`, `MouthRollLower`, `MouthRollUpper`, `MouthShrugLower`, `MouthShrugUpper`, `MouthSmileLeft`, `MouthSmileRight`, `MouthStretchLeft`, `MouthStretchRight`, `MouthUpperUpLeft`, `MouthUpperUpRight`, `NoseSneerLeft`, `NoseSneerRight`, `TongueOut`.\n\n### CV: hands\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_handCount` | `int` | Detected hands (0-2) |\n| `u_leftHandPalm`, `u_rightHandPalm` | `vec3` | Palm position `(x, y, z)` |\n| `u_leftHandConfidence`, `u_rightHandConfidence` | `float` | 0..1 |\n| `u_leftHandBounds`, `u_rightHandBounds` | `vec4` | Bounds normalized 0..1 |\n| `u_leftHandFist` - `u_leftHandILoveYou` | `float` | 7 left-hand gesture scores |\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` | Pose detected |\n| `u_poseConfidence` | `float` | 0..1 |\n| `u_nosePosition` | `vec2` | Nose landmark, normalized 0..1 |\n| `u_leftShoulderPosition` … `u_rightAnklePosition` | `vec2` | Joint landmarks (shoulders, elbows, wrists, hips, knees, ankles) |\n\n### CV: body segmentation\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_segmentationMask` | `sampler2D` | 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 including gravity (m/s²) |\n| `u_deviceRotationRate` | `vec3` | Rotation rate (deg/s) |\n| `u_deviceOrientation` | `vec3` | `(alpha, beta, gamma)` in degrees |\n| `u_deviceOrientationAbsolute` | `bool` | Using magnetometer |\n\n### External devices\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_deviceCount` | `int` | Device video sources (0-8) |\n| `u_externalDeviceCount` | `int` | External devices (0-8) |\n| `u_device0` - `u_device7` | `sampler2D` | Device camera textures |\n| `u_device0Resolution` - `u_device7Resolution` | `vec2` | Per-device resolution |\n| `u_device0Connected` - `u_device7Connected` | `bool` | Per-device connection |\n| `u_device0Acceleration` - `u_device7Acceleration` | `vec3` | Per-device acceleration (no gravity) |\n| `u_device0AccelerationGravity` - `u_device7AccelerationGravity` | `vec3` | Per-device acceleration (with gravity) |\n| `u_device0RotationRate` - `u_device7RotationRate` | `vec3` | Per-device rotation rate |\n| `u_device0Orientation` - `u_device7Orientation` | `vec3` | Per-device orientation |\n\n### Streams (compositor)\n\nVideo streams (host-provided):\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_videoStreamCount` | `int` | Active streams (0-8) |\n| `u_videoStream0` - `u_videoStream7` | `sampler2D` | Stream textures |\n| `u_videoStream0Resolution` - `u_videoStream7Resolution` | `vec2` | Per-stream resolution |\n| `u_videoStream0Connected` - `u_videoStream7Connected` | `bool` | Per-stream connection |\n\nAudio streams (additional sources, including device audio):\n\n| Uniform | Type | Description |\n|---|---|---|\n| `u_audioStreamCount` | `int` | Active streams (0-8) |\n| `u_audioStream0Connected` - `u_audioStream7Connected` | `bool` | Per-slot active |\n| `u_audioStream{i}Volume` | `float` | Volume 0..1 |\n| `u_audioStream{i}Low` - `u_audioStream{i}High` | `float` | Band energies 0..1 |\n| `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` | `float` | Spectral features 0..1 |\n\n`i` ranges 0..7. **Scalars only**: no `u_audioFFT` / `u_audioWaveform` per stream. Beat / BPM / trigger uniforms are main audio only.\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. Clears on canvas resize.\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"
|
|
10094
10240
|
},
|
|
10095
10241
|
"conversionGuides": {
|
|
10096
|
-
"p5": "# Converting P5.js Sketches to Viji P5\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing P5.js sketch into a Viji scene. It is self-contained: it includes both the source-to-target mapping and the Viji P5 target reference needed to produce correct converted output, without requiring the Viji P5 system prompt to also be loaded.\n\n## Source platform: standard P5.js sketches\n\nStandard P5.js sketches run in a browser tab. They typically:\n- Use `setup()` to call `createCanvas(w, h)` and configure the sketch (`colorMode`, `frameRate`, etc.).\n- Use `draw()` as the per-frame loop (`background`, `fill`, shape primitives, etc.).\n- Use global `mouseX`, `mouseY`, `mouseIsPressed`, `keyIsPressed`, `frameCount`, `width`, `height`.\n- Define event callbacks at the top level: `mousePressed`, `keyPressed`, `touchStarted`, etc.\n- Use `preload()` to fetch assets via `loadImage`, `loadFont`, `loadJSON`.\n- Possibly use `createCapture(VIDEO)`, `p5.AudioIn()`, `p5.sound`, `p5.dom` add-ons.\n\nP5.js v1.9.4 is what Viji pins. When in doubt about a P5 function, the p5.js v1.x reference is authoritative.\n\n## Target platform: Viji P5\n\nViji P5 scenes run inside a Web Worker on an `OffscreenCanvas` and use P5.js v1.9.4 in **instance mode**. The DOM is unavailable. Every P5 function and constant requires the `p5.` prefix. The canvas is created and managed by Viji; never call `createCanvas()`.\n\n## Conversion rules\n\n1. **Always** set the first line based on the source canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or any 3D primitives. Never keep `createCanvas()`.\n2. Rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it does not exist, do not add one.\n4. **Always** prefix every P5 function and constant with `p5.`. No exceptions: `background` -> `p5.background`, `fill` -> `p5.fill`, `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`, `createVector(...)` -> `p5.createVector(...)`, `map(...)` -> `p5.map(...)`, `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`.\n5. **Never** keep `createCanvas()`. The canvas is created by Viji. WEBGL is selected only via `// @renderer p5 webgl`, never via `createCanvas(..., p5.WEBGL)`.\n6. **Never** keep `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` inside an async `setup()` for other data.\n7. **Never** keep P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edge detection.\n8. **Never** keep `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, or `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** keep `loadImage()`, `loadFont()`, `loadJSON()`, `loadModel()`, `loadShader()`. Use `viji.image()` or `fetch()`.\n10. **Never** keep `createCapture()` or `createVideo()`. Use `viji.video.*`.\n11. **Never** keep `p5.dom` or `p5.sound` add-ons. Replace UI with Viji parameters and audio with `viji.audio.*`.\n12. **Never** keep `window`, `document`, `Image()`, or `localStorage`. `fetch()` is available.\n13. Lift constants and tunable values to top-level parameters using `viji.slider`, `viji.color`, `viji.toggle`, etc. Read via `.value` inside `render()`.\n14. Replace `frameCount`-based timing with a `viji.deltaTime` accumulator. Never multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator.\n15. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n16. **Never** enable CV features by default; use toggle parameters.\n17. **Always** set `category` on input-dependent parameters: `'audio'`, `'video'`, `'interaction'`.\n18. **Always** type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Direct mapping table\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. Use `// @renderer p5`. |\n| `createCanvas(w, h, WEBGL)` | Remove. Use `// @renderer p5 webgl` as the first line. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `loadFont(url)` | Not available. Use CSS generic font names with `p5.textFont('monospace' \\| 'serif' \\| 'sans-serif')`. |\n| `loadJSON(url)` | `await fetch(url).then(r => r.json())` inside async `setup()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `createCapture(VIDEO)` | `viji.video.currentFrame` (plus `viji.video.isConnected` check). |\n| `new p5.AudioIn(); mic.getLevel()` | `viji.audio.volume.current` (after `viji.audio.isConnected` check). |\n| `new p5.FFT(); fft.analyze()` | `viji.audio.getFrequencyData()` (`Uint8Array` of 1024 bins). |\n| `mousePressed()` callback | `if (viji.pointer.wasPressed) { ... }` inside `render()`. |\n| `keyPressed()` callback | `if (viji.keyboard.wasPressed(key)) { ... }` inside `render()`. |\n| `touchStarted()` callback | `viji.touches.started.forEach(t => ...)` inside `render()`. |\n\n## Target API reference (Viji P5)\n\nThe `viji` object is identical to Native. Access it inside `setup(viji, p5)` and `render(viji, p5)`. `viji.useContext()` is **not available** in P5; the context is managed by P5.\n\n**Canvas and timing**: `viji.canvas` (`OffscreenCanvas`), `viji.width`, `viji.height`, `viji.time`, `viji.deltaTime`, `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap|null, .p5: P5Image\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\nFor images displayed with P5, pass `param.p5` (a `P5Image`) to `p5.image()`, not `param.value`.\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves; peak on detected beats); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1).\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. Members: `currentFrame` (`OffscreenCanvas | ImageBitmap | null`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv` (VideoCVAPI). CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect handling** (use for every video / CV conversion):\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\nconst v = videoFit(viji, 'cover');\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0..1 to the source frame; map through v to align.\n```\nDefault `viji.video.currentFrame` for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\n**CV: `viji.video.cv`**: enable explicitly (never by default):\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (frame paired with current CV results), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures(): CVFeature[]`, `isProcessing(): boolean`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes}`; `blendshapes` are 52 ARKit coefficients 0..1), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures: {fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou}}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type`.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`: each `TouchPoint` has `id`, `x`, `y`, `pressure`, `radius`, `velocity`, etc.\n\n**Sensors / streams**: `viji.device.{motion, orientation}`, `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]` (lightweight `AudioStreamAPI`: no beat / BPM).\n\n## P5 gotchas in the worker\n\n- **Fonts**: `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is unavailable.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is not supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes, it is a WebGL context; do not call Canvas-2D APIs on it.\n- **`viji.useContext()`** is not available in P5; the context belongs to P5.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## Worked example\n\nSource P5 sketch:\n```javascript\nlet angle = 0;\nfunction setup() {\n createCanvas(800, 600);\n colorMode(HSB, 360, 100, 100);\n}\nfunction draw() {\n angle += 0.02;\n background(0, 0, 10);\n const x = width / 2 + cos(angle) * width * 0.3;\n const y = height / 2 + sin(angle) * height * 0.3;\n noStroke();\n fill((frameCount * 0.5) % 360, 80, 100);\n circle(x, y, width * 0.05);\n}\nfunction mousePressed() {\n angle = 0;\n}\n```\n\nConverted Viji P5 scene:\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst reactivity = viji.slider(0, { min: 0, max: 1, label: 'Audio Reactivity', category: 'audio' });\n\nlet angle = 0;\nlet huePhase = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) angle = 0;\n\n const audioBoost = viji.audio.isConnected ? viji.audio.volume.smoothed * reactivity.value : 0;\n angle += (speed.value + audioBoost * 4) * viji.deltaTime;\n huePhase += 30 * viji.deltaTime;\n\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(huePhase % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nKey changes: removed `createCanvas`, added `// @renderer p5`; renamed `draw` to `render(viji, p5)`; gave `setup` the `(viji, p5)` signature; added `p5.` prefix everywhere; replaced `width`/`height` with `viji.width`/`viji.height`; replaced the implicit `0.02` frame-rate-coupled animation with a `viji.deltaTime` accumulator driven by a `speed` slider; replaced `(frameCount * 0.5) % 360` hue with a separate `huePhase` accumulator (so changing speed never jumps the color); replaced the `mousePressed()` callback with a `viji.pointer.wasPressed` check inside `render`; added an opt-in audio-reactive `reactivity` slider with `category: 'audio'`.\n",
|
|
10242
|
+
"p5": "# Converting P5.js Sketches to Viji P5\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing P5.js sketch into a Viji scene. It is self-contained: it includes both the source-to-target mapping and the Viji P5 target reference needed to produce correct converted output, without requiring the Viji P5 system prompt to also be loaded.\n\n## Source platform: standard P5.js sketches\n\nStandard P5.js sketches run in a browser tab. They typically:\n- Use `setup()` to call `createCanvas(w, h)` and configure the sketch (`colorMode`, `frameRate`, etc.).\n- Use `draw()` as the per-frame loop (`background`, `fill`, shape primitives, etc.).\n- Use global `mouseX`, `mouseY`, `mouseIsPressed`, `keyIsPressed`, `frameCount`, `width`, `height`.\n- Define event callbacks at the top level: `mousePressed`, `keyPressed`, `touchStarted`, etc.\n- Use `preload()` to fetch assets via `loadImage`, `loadFont`, `loadJSON`.\n- Possibly use `createCapture(VIDEO)`, `p5.AudioIn()`, `p5.sound`, `p5.dom` add-ons.\n\nP5.js v1.9.4 is what Viji pins. When in doubt about a P5 function, the p5.js v1.x reference is authoritative.\n\n## Target platform: Viji P5\n\nViji P5 scenes run inside a Web Worker on an `OffscreenCanvas` and use P5.js v1.9.4 in **instance mode**. The DOM is unavailable. Every P5 function and constant requires the `p5.` prefix. The canvas is created and managed by Viji; never call `createCanvas()`.\n\n## Conversion rules\n\n1. **Always** set the first line based on the source canvas mode: `// @renderer p5` for 2D (default), or `// @renderer p5 webgl` if the sketch used `createCanvas(w, h, WEBGL)` or any 3D primitives. Never keep `createCanvas()`.\n2. Rename `draw()` to `render(viji, p5)`.\n3. If `setup()` exists, change its signature to `setup(viji, p5)`. If it does not exist, do not add one.\n4. **Always** prefix every P5 function and constant with `p5.`. No exceptions: `background` -> `p5.background`, `fill` -> `p5.fill`, `PI` -> `p5.PI`, `TWO_PI` -> `p5.TWO_PI`, `HSB` -> `p5.HSB`, `createVector(...)` -> `p5.createVector(...)`, `map(...)` -> `p5.map(...)`, `noise(x)` -> `p5.noise(x)`, `random()` -> `p5.random()`.\n5. **Never** keep `createCanvas()`. The canvas is created by Viji. WEBGL is selected only via `// @renderer p5 webgl`, never via `createCanvas(..., p5.WEBGL)`.\n6. **Never** keep `preload()`. Use `viji.image(null, { label: 'Name' })` for images, or `fetch()` inside an async `setup()` for other data.\n7. **Never** keep P5 event callbacks (`mousePressed`, `mouseDragged`, `mouseReleased`, `keyPressed`, `keyReleased`, `keyTyped`, `touchStarted`, `touchMoved`, `touchEnded`). Read state in `render()` via `viji.pointer`, `viji.mouse`, `viji.keyboard`, `viji.touches`. Use `wasPressed` / `wasReleased` for one-frame edge detection.\n8. **Never** keep `p5.frameRate()`, `p5.save()`, `p5.saveCanvas()`, or `p5.saveFrames()`. The host controls frame rate and capture.\n9. **Never** keep `loadImage()`, `loadFont()`, `loadJSON()`, `loadShader()`. Use `viji.image()` or `fetch()`. Keep `loadModel()` only with `viji.model().url`, once per new value, passing the format as the file type: `p5.loadModel(m.value.url, true, ok, err, '.' + m.value.format)`.\n10. **Never** keep `createCapture()` or `createVideo()`. Use `viji.video.*`.\n11. **Never** keep `p5.dom` or `p5.sound` add-ons. Replace UI with Viji parameters and audio with `viji.audio.*`.\n12. **Never** keep `window`, `document`, `Image()`, or `localStorage`. `fetch()` is available.\n13. Lift constants and tunable values to top-level parameters using `viji.slider`, `viji.color`, `viji.toggle`, etc. Read via `.value` inside `render()`.\n14. Replace `frameCount`-based timing with a `viji.deltaTime` accumulator. Never multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator.\n15. **Always** check `viji.audio.isConnected` before reading audio data. Check `viji.video.isConnected && viji.video.currentFrame` before drawing video.\n16. **Never** enable CV features by default; use toggle parameters.\n17. **Always** set `category` on input-dependent parameters: `'audio'`, `'video'`, `'interaction'`.\n18. **Always** type the `viji` and `p5` parameters on `setup`, `render`, and any helpers — `function render(viji: VijiAPI, p5: p5) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Direct mapping table\n\n| Standard P5.js | Viji P5 equivalent |\n|---|---|\n| `width` / `height` | `viji.width` / `viji.height` |\n| `mouseX` / `mouseY` | `viji.pointer.x` / `viji.pointer.y` (or `viji.mouse.x` / `viji.mouse.y`) |\n| `mouseIsPressed` | `viji.pointer.isDown` (or `viji.mouse.isPressed`) |\n| `mouseButton === LEFT` | `viji.mouse.leftButton` |\n| `keyIsPressed` | `viji.keyboard.activeKeys.size > 0` |\n| `keyIsDown(code)` | `viji.keyboard.isPressed('keyName')` |\n| `key` | `viji.keyboard.lastKeyPressed` |\n| `frameCount` | `viji.frameCount` (or a `viji.deltaTime` accumulator) |\n| `frameRate(n)` | Remove. Host controls frame rate. |\n| `createCanvas(w, h)` | Remove. Canvas is provided. Use `// @renderer p5`. |\n| `createCanvas(w, h, WEBGL)` | Remove. Use `// @renderer p5 webgl` as the first line. |\n| `preload()` | Remove. Use `viji.image()` or `fetch()` in `setup()`. |\n| `loadImage(url)` | `viji.image(null, { label: 'Image' })`; pass `.p5` to `p5.image()`. |\n| `loadFont(url)` | Not available. Use CSS generic font names with `p5.textFont('monospace' \\| 'serif' \\| 'sans-serif')`. |\n| `loadJSON(url)` | `await fetch(url).then(r => r.json())` inside async `setup()`. |\n| `save()` / `saveCanvas()` | Remove. Host handles capture. |\n| `createCapture(VIDEO)` | `viji.video.currentFrame` (plus `viji.video.isConnected` check). |\n| `new p5.AudioIn(); mic.getLevel()` | `viji.audio.volume.current` (after `viji.audio.isConnected` check). |\n| `new p5.FFT(); fft.analyze()` | `viji.audio.getFrequencyData()` (`Uint8Array` of 1024 bins). |\n| `mousePressed()` callback | `if (viji.pointer.wasPressed) { ... }` inside `render()`. |\n| `keyPressed()` callback | `if (viji.keyboard.wasPressed(key)) { ... }` inside `render()`. |\n| `touchStarted()` callback | `viji.touches.started.forEach(t => ...)` inside `render()`. |\n\n## Target API reference (Viji P5)\n\nThe `viji` object is identical to Native. Access it inside `setup(viji, p5)` and `render(viji, p5)`. `viji.useContext()` is **not available** in P5; the context is managed by P5.\n\n**Canvas and timing**: `viji.canvas` (`OffscreenCanvas`), `viji.width`, `viji.height`, `viji.time`, `viji.deltaTime`, `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap|null, .p5: P5Image\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required; use ['obj', 'stl'] (what p5 loadModel reads); no .p5; load with p5.loadModel(value.url, true, ok, err, '.' + value.format) once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\nFor images displayed with P5, pass `param.p5` (a `P5Image`) to `p5.image()`, not `param.value`.\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) and the sibling `*Smoothed` envelopes (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves; peak on detected beats); `beat.{kickSmoothed, snareSmoothed, hatSmoothed, anySmoothed}` (500ms decay envelopes); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`; cleared each frame); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked` (boolean); `spectral.{brightness, flatness}` (0..1); `getFrequencyData(): Uint8Array` (1024 FFT bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1).\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. Members: `currentFrame` (`OffscreenCanvas | ImageBitmap | null`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv` (VideoCVAPI). CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs live on `viji.video.cv`, not on `viji.video` directly.\n\n**Aspect handling** (use for every video / CV conversion):\n```javascript\nfunction videoFit(viji, mode = 'cover') {\n const vw = viji.video.frameWidth, vh = viji.video.frameHeight;\n const w = viji.width, h = viji.height;\n if (!vw || !vh) return { x: 0, y: 0, width: 0, height: 0 };\n const scale = mode === 'cover' ? Math.max(w / vw, h / vh) : Math.min(w / vw, h / vh);\n const dw = vw * scale, dh = vh * scale;\n return { x: (w - dw) / 2, y: (h - dh) / 2, width: dw, height: dh };\n}\nconst v = videoFit(viji, 'cover');\np5.image(viji.video.currentFrame, v.x, v.y, v.width, v.height);\n// CV coords are normalized 0..1 to the source frame; map through v to align.\n```\nDefault `viji.video.currentFrame` for displayed video; reach for `viji.video.cv.analysedFrame` only when sampling pixels at CV-derived positions. Common fallback pattern: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\n**CV: `viji.video.cv`**: enable explicitly (never by default):\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (frame paired with current CV results), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures(): CVFeature[]`, `isProcessing(): boolean`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes}`; `blendshapes` are 52 ARKit coefficients 0..1), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures: {fist, openPalm, peace, thumbsUp, thumbsDown, pointing, iLoveYou}}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type`.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`: each `TouchPoint` has `id`, `x`, `y`, `pressure`, `radius`, `velocity`, etc.\n\n**Sensors / streams**: `viji.device.{motion, orientation}`, `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]` (lightweight `AudioStreamAPI`: no beat / BPM).\n\n## P5 gotchas in the worker\n\n- **Fonts**: `p5.textFont()` only with CSS generic names (`'monospace'`, `'serif'`, `'sans-serif'`). `loadFont()` is unavailable.\n- **`p5.createGraphics(w, h)`** works (creates an internal OffscreenCanvas). `createGraphics(w, h, p5.WEBGL)` is not supported.\n- **`p5.pixelDensity()`** defaults to 1 in the worker. `p5.loadPixels()` and `p5.pixels[]` work in 2D scenes.\n- **`p5.drawingContext`** is a 2D context only in 2D scenes. In WEBGL scenes, it is a WebGL context; do not call Canvas-2D APIs on it.\n- **`viji.useContext()`** is not available in P5; the context belongs to P5.\n- **`p5.tint()` and `p5.blendMode()`** work normally.\n\n## Worked example\n\nSource P5 sketch:\n```javascript\nlet angle = 0;\nfunction setup() {\n createCanvas(800, 600);\n colorMode(HSB, 360, 100, 100);\n}\nfunction draw() {\n angle += 0.02;\n background(0, 0, 10);\n const x = width / 2 + cos(angle) * width * 0.3;\n const y = height / 2 + sin(angle) * height * 0.3;\n noStroke();\n fill((frameCount * 0.5) % 360, 80, 100);\n circle(x, y, width * 0.05);\n}\nfunction mousePressed() {\n angle = 0;\n}\n```\n\nConverted Viji P5 scene:\n```javascript\n// @renderer p5\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst reactivity = viji.slider(0, { min: 0, max: 1, label: 'Audio Reactivity', category: 'audio' });\n\nlet angle = 0;\nlet huePhase = 0;\n\nfunction setup(viji, p5) {\n p5.colorMode(p5.HSB, 360, 100, 100);\n}\n\nfunction render(viji, p5) {\n if (viji.pointer.wasPressed) angle = 0;\n\n const audioBoost = viji.audio.isConnected ? viji.audio.volume.smoothed * reactivity.value : 0;\n angle += (speed.value + audioBoost * 4) * viji.deltaTime;\n huePhase += 30 * viji.deltaTime;\n\n p5.background(0, 0, 10);\n const x = viji.width / 2 + p5.cos(angle) * viji.width * 0.3;\n const y = viji.height / 2 + p5.sin(angle) * viji.height * 0.3;\n p5.noStroke();\n p5.fill(huePhase % 360, 80, 100);\n p5.circle(x, y, viji.width * 0.05);\n}\n```\n\nKey changes: removed `createCanvas`, added `// @renderer p5`; renamed `draw` to `render(viji, p5)`; gave `setup` the `(viji, p5)` signature; added `p5.` prefix everywhere; replaced `width`/`height` with `viji.width`/`viji.height`; replaced the implicit `0.02` frame-rate-coupled animation with a `viji.deltaTime` accumulator driven by a `speed` slider; replaced `(frameCount * 0.5) % 360` hue with a separate `huePhase` accumulator (so changing speed never jumps the color); replaced the `mousePressed()` callback with a `viji.pointer.wasPressed` check inside `render`; added an opt-in audio-reactive `reactivity` slider with `category: 'audio'`.\n",
|
|
10097
10243
|
"shadertoy": "# Converting Shadertoy Shaders to Viji Shader Scenes\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert an existing Shadertoy shader into a Viji shader scene. It is self-contained: it includes both the source-to-target mapping and the Viji shader target reference needed to produce correct converted output, without requiring the Viji shader system prompt to also be loaded.\n\n## Source platform: Shadertoy\n\nShadertoy shaders are GLSL fragment shaders authored against a fixed API:\n- Entry point: `void mainImage(out vec4 fragColor, in vec2 fragCoord)`.\n- Built-in uniforms: `iResolution` (`vec3`: `.xy` size, `.z` aspect), `iTime`, `iTimeDelta`, `iFrame`, `iMouse` (`vec4`: `.xy` current position, `.zw` click origin), `iDate` (year, month, day, seconds), `iChannel0`-`iChannel3` (textures whose meaning depends on the channel binding: Image, FFT, Sound, Music, Microphone, Keyboard, Cubemap).\n- Side metadata: `iChannelResolution[i]`, `iChannelTime[i]`, `iSampleRate`.\n- Multi-buffer pipelines (Buffer A, B, C, D), Cubemap buffer, Sound output buffer, VR mode (`mainVR`). None of these have Viji equivalents.\n\n## Target platform: Viji shader\n\nGLSL fragment shader on a fullscreen quad. Standard `void main()` entry. Precision and all uniforms (built-ins and `@viji-*` directive uniforms) are auto-injected by Viji. **Never declare them.** Artists declare parameters via `// @viji-*` line-comment directives, which compile to typed uniforms.\n\nGLSL ES 1.00 by default. Opt into ES 3.00 by making `#version 300 es` the literal first line.\n\n## Conversion rules\n\n1. **Always** add `// @renderer shader` as the very first line (or as line 2 after `#version 300 es`).\n2. **Never** declare `precision mediump float;` or `precision highp float;`. Viji auto-injects precision.\n3. **Never** redeclare built-in uniforms. They are auto-injected.\n4. **Never** redeclare parameter uniforms. They are auto-generated from `@viji-*` directives.\n5. Convert the `mainImage` signature to standard `void main()`:\n - Replace `fragCoord` with `gl_FragCoord.xy`.\n - Replace `fragColor` (the `out` parameter) with `gl_FragColor` (ES 1.00) or the declared `out vec4 fragColor` (ES 3.00).\n - Remove the `mainImage` wrapper entirely.\n\n6. Replace Shadertoy uniforms with Viji equivalents:\n\n | Shadertoy | Viji | Notes |\n |---|---|---|\n | `iResolution.xy` | `u_resolution` | Viji's `u_resolution` is `vec2`. |\n | `iResolution.z` | `u_resolution.x / u_resolution.y` | Aspect ratio. |\n | `iResolution.x` / `.y` | `u_resolution.x` / `.y` | Direct match. |\n | `iTime` | `u_time` | Elapsed seconds. |\n | `iTimeDelta` | `u_deltaTime` | Seconds since last frame. |\n | `iFrame` | `u_frame` (`int`) | Frame counter. |\n | `iMouse.xy` | `u_mouse` | Current mouse position in pixels. |\n | `iMouse.z` | `u_mouseLeft ? u_mouse.x : 0.0` | Viji does not track click origin. |\n | `iMouse.w` | `u_mouseLeft ? u_mouse.y : 0.0` | Viji does not track click origin. |\n | `iChannelN` (static Image) | `@viji-image:channelN label:\"…\"` | See rule 7. |\n | `iChannelN` (FFT) | `u_audioFFT` | Auto-injected `sampler2D`, 1024 bins 0..255. |\n | `iChannelN` (Sound / Music / Microphone) | `u_audioFFT` and/or `u_audioWaveform` | See two-row note in rule 7. |\n | `iChannelN` (Keyboard) | `u_keyboard` | See rule 8. |\n | `iChannelN` (Cubemap) | Not supported | Only 2D textures available. |\n | `iChannelResolution[i]` | Not available | Track manually if needed. |\n | `iChannelTime[i]` | Not available | Per-channel time is not tracked. |\n | `iDate` | Not available | Use `u_time`. |\n | `iSampleRate` | Not available | Not applicable. |\n\n7. **Static-image `iChannel` slots** become `@viji-image` parameters:\n ```glsl\n // @viji-image:channel0 label:\"Texture 1\"\n ```\n Then replace `texture(iChannel0, uv)` with `texture2D(channel0, uv)` (ES 1.00) or `texture(channel0, uv)` (ES 3.00).\n\n **Audio iChannel slots** (Sound / Music / Microphone) sample the auto-injected audio textures directly:\n ```glsl\n float spectrum = texture2D(u_audioFFT, vec2(uv.x, 0.0)).r;\n float waveform = texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r;\n ```\n\n **Two-row audio channel**: Shadertoy's Sound / Music / Microphone iChannels encode FFT in row 0 (`y` near 0.0) and waveform in row 1 (`y` near 0.5+) of a single texture. When converting:\n - `texture(iChannelN, vec2(uv.x, 0.0)).r` (row 0) -> `texture2D(u_audioFFT, vec2(uv.x, 0.0)).r`.\n - `texture(iChannelN, vec2(uv.x, 0.5)).r` or `vec2(uv.x, 0.75)` (row 1) -> `texture2D(u_audioWaveform, vec2(uv.x, 0.0)).r`.\n - When unsure: prefer `u_audioFFT` for spectrum-style visuals, `u_audioWaveform` for oscilloscope-style visuals.\n\n8. **Keyboard `iChannel` slots** become `u_keyboard`:\n ```glsl\n // Shadertoy: texelFetch(iChannel0, ivec2(KEY, 0), 0).x\n // Viji: texelFetch(u_keyboard, ivec2(KEY, 0), 0).x\n ```\n `u_keyboard` is a built-in `sampler2D` (256x3). Row 0 = held, row 1 = pressed-this-frame, row 2 = toggle. Do not declare it.\n\n9. `iResolution` used as `vec3`: replace with `vec3(u_resolution, u_resolution.x / u_resolution.y)`, or refactor to use the `vec2` form directly.\n\n10. **Parameter-driven animation must use `@viji-accumulator`, never `u_time * speed`.** Multiplying `u_time` by a parameter causes visible jumps when the slider changes.\n\n ```glsl\n // WRONG (source pattern)\n float t = iTime * speed;\n\n // RIGHT (converted)\n // @viji-slider:speed label:\"Speed\" default:1.0 min:0.1 max:5.0\n // @viji-accumulator:phase rate:speed\n float t = phase;\n ```\n\n Same rule for nested multiplications. Each independent speed gets its own accumulator.\n\n11. Adding artist-controllable parameters uses `@viji-*` directives:\n ```glsl\n // @viji-slider:name label:\"Label\" default:1.0 min:0.0 max:5.0\n // @viji-color:name label:\"Label\" default:#ff6600 (alpha:true -> vec4; then #rrggbbaa / rgba() defaults are allowed)\n // @viji-gradient:name label:\"Ramp\" default:[#ff0000@0,#0000ff@1] (sample with name(t); alpha:true -> vec4)\n // @viji-toggle:name label:\"Label\" default:true\n // @viji-select:name label:\"Mode\" default:\"A\" options:[\"A\",\"B\",\"C\"]\n // @viji-image:name label:\"Texture\"\n // @viji-button:name label:\"Reset\"\n // @viji-coordinate:name label:\"Origin\" default:[0.0,0.0]\n // @viji-accumulator:name rate:speed\n ```\n Never use the `u_` prefix for parameter names; it is reserved for built-in uniforms. Directives work with `//` line comments only.\n\n12. **`#version 300 es`** stays as the literal first line if present. Then `// @renderer shader` on line 2. Replace `gl_FragColor = ...` with a declared `out vec4 fragColor;` (before `main`) and `fragColor = ...`. Replace `texture2D()` with `texture()`.\n\n13. Remove any `#ifdef GL_ES` / `precision` blocks. Viji handles them.\n\n14. **Always** set `category` on input-dependent directives: `category:audio` for audio controls, `category:video` for video controls, `category:interaction` for mouse / touch controls.\n\n15. **Single Buffer feedback** becomes Viji's `backbuffer`:\n ```glsl\n vec4 prev = texture2D(backbuffer, uv);\n ```\n `backbuffer` is auto-detected and enabled when referenced. RGBA 8-bit, `LINEAR`, `CLAMP_TO_EDGE`, no `u_` prefix. First frame samples as black.\n\n## Unsupported features (warn the artist)\n\n- **Multi-buffer pipelines** (Buffer A -> B -> C -> D): only single `backbuffer` is available.\n- **Cubemap buffer** (`samplerCube`): not supported.\n- **3D textures** (`sampler3D`): not supported.\n- **`iChannelTime[i]`**, **`iChannelResolution[i]`**, **`iSampleRate`**, **`iDate`**: not available.\n- **Sound output buffer**: not supported.\n- **`mainVR()`**: not supported.\n- **Texture wrap/filter modes**: Viji uses fixed `CLAMP_TO_EDGE` + `LINEAR`. Use `fract(uv)` for repeat.\n\n## Target uniform reference (Viji shader)\n\nAll uniforms below are auto-injected. Never declare them.\n\n**Core**: `u_resolution` (`vec2`), `u_time` (`float`), `u_deltaTime` (`float`), `u_frame` (`int`), `u_fps` (`float`).\n\n**Mouse**: `u_mouse` (`vec2`, pixels, bottom-left origin), `u_mouseInCanvas`, `u_mousePressed`, `u_mouseLeft`, `u_mouseRight`, `u_mouseMiddle` (`bool`), `u_mouseDelta` (`vec2`), `u_mouseWheel` (`float`), `u_mouseWasPressed`, `u_mouseWasReleased` (`bool`).\n\n**Pointer (unified mouse / touch)**: `u_pointer` (`vec2`), `u_pointerDelta` (`vec2`), `u_pointerDown`, `u_pointerWasPressed`, `u_pointerWasReleased`, `u_pointerInCanvas` (`bool`).\n\n**Keyboard**: `u_keySpace`, `u_keyShift`, `u_keyCtrl`, `u_keyAlt`, `u_keyW`/`A`/`S`/`D`, `u_keyUp`/`Down`/`Left`/`Right` (`bool`). `u_keyboard` (`sampler2D`, 256x3): row 0 held, row 1 pressed-this-frame, row 2 toggle.\n\n**Touch**: `u_touchCount` (`int`), `u_touch0`-`u_touch4` (`vec2`).\n\n**Audio scalars**: `u_audioVolume`, `u_audioPeak`, `u_audioVolumeSmoothed` (200ms decay), `u_audioLow`/`LowMid`/`Mid`/`HighMid`/`High` (band energies 20-120 / 120-400 / 400-1600 / 1600-6000 / 6000-16000 Hz), `u_audioLowSmoothed`-`u_audioHighSmoothed` (150ms decay), `u_audioKick`/`Snare`/`Hat`/`Any` (300ms decay curves), `u_audioKickSmoothed`-`u_audioAnySmoothed` (500ms decay), `u_audioKickTrigger`/`SnareTrigger`/`HatTrigger`/`AnyTrigger` (`bool`, true for one frame, OR-accumulated), `u_audioBPM` (`0.0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120.0` as a fallback before lock-on), `u_audioConfidence`, `u_audioIsLocked` (`bool`), `u_audioBrightness`, `u_audioFlatness`.\n\n**Audio textures (main source only)**: `u_audioFFT` (`sampler2D`, 1024 FFT bins 0..255), `u_audioWaveform` (`sampler2D`, time-domain -1..1).\n\n**Video**: `u_video` (`sampler2D`), `u_videoAnalysed` (`sampler2D`, paired with CV uniforms), `u_videoAnalysedAvailable` (`bool`), `u_videoResolution` (`vec2`), `u_videoFrameRate` (`float`), `u_videoConnected` (`bool`). Use the `vijiVideoUV` helper below for aspect handling.\n\n**CV face**: `u_faceCount` (`int`), `u_face0Bounds` (`vec4`, normalized 0..1), `u_face0Center` (`vec2`, normalized), `u_face0HeadPose` (`vec3` degrees), `u_face0Confidence` (`float`), `u_face0Neutral`-`u_face0Fearful` (7 expressions 0..1), 52 blendshape uniforms prefixed `u_face0` (e.g. `u_face0JawOpen`, `u_face0EyeBlinkLeft`, `u_face0MouthSmileRight`). Activated via `// @viji-cv:faceDetection` / `// @viji-cv:faceMesh` / `// @viji-cv:emotionDetection`.\n\n**CV hands**: `u_handCount` (`int`), `u_leftHandPalm`/`u_rightHandPalm` (`vec3`), `u_leftHandConfidence`/`u_rightHandConfidence`, `u_leftHandBounds`/`u_rightHandBounds` (`vec4`), 7 gesture uniforms per hand (`Fist`, `OpenPalm`, `Peace`, `ThumbsUp`, `ThumbsDown`, `Pointing`, `ILoveYou`). Activated via `// @viji-cv:handTracking`.\n\n**CV pose**: `u_poseDetected` (`bool`), `u_poseConfidence`, joint landmarks (`u_nosePosition`, `u_leftShoulderPosition`-`u_rightAnklePosition`, all `vec2` normalized 0..1). Activated via `// @viji-cv:poseDetection`.\n\n**CV segmentation**: `u_segmentationMask` (`sampler2D`, 0 = background, 1 = person), `u_segmentationRes` (`vec2`). Activated via `// @viji-cv:bodySegmentation`.\n\n**Device sensors**: `u_deviceAcceleration` (`vec3`, m/s² without gravity), `u_deviceAccelerationGravity`, `u_deviceRotationRate` (deg/s), `u_deviceOrientation` (`vec3`, degrees), `u_deviceOrientationAbsolute` (`bool`).\n\n**External devices**: `u_deviceCount` (`int`, 0..8), `u_device0`-`u_device7` (`sampler2D` camera textures), `u_device0Resolution`-`u_device7Resolution` (`vec2`), `u_device0Connected`-`u_device7Connected` (`bool`), plus per-device acceleration / rotation / orientation `vec3` uniforms.\n\n**Compositor streams**: `u_videoStreamCount`, `u_videoStream0`-`u_videoStream7` (`sampler2D`), with `*Resolution` and `*Connected` per slot. `u_audioStreamCount`, `u_audioStream0Connected`-`u_audioStream7Connected` (`bool`), `u_audioStream{i}Volume`, `u_audioStream{i}Low`-`u_audioStream{i}High`, `u_audioStream{i}Brightness`, `u_audioStream{i}Flatness` (`float` 0..1). Scalars only on streams: no per-stream FFT / waveform. Beat / BPM / trigger uniforms are main audio only.\n\n**Backbuffer**: `backbuffer` (`sampler2D`, no `u_` prefix). Auto-enabled when referenced.\n\n## Video aspect helper\n\nCamera frames almost never match the canvas aspect. Sampling `texture2D(u_video, uv)` with canvas UV stretches the video and misaligns CV uniforms. Use these at the top of any video / CV shader:\n\n```glsl\n// mode: 1 = cover (fills canvas, video edges cropped)\n// 0 = contain (fits video, canvas letterboxed)\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\n## Worked example\n\nSource Shadertoy shader:\n```glsl\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = fragCoord / iResolution.xy;\n float t = iTime * 0.5;\n float wave = sin(uv.x * 10.0 + t) * 0.5 + 0.5;\n vec3 col = vec3(uv, wave);\n fragColor = vec4(col, 1.0);\n}\n```\n\nConverted Viji shader:\n```glsl\n// @renderer shader\n// @viji-slider:speed label:\"Speed\" default:0.5 min:0.1 max:3.0\n// @viji-color:tint label:\"Tint\" default:#ffffff\n// @viji-accumulator:phase rate:speed\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_resolution;\n float wave = sin(uv.x * 10.0 + phase) * 0.5 + 0.5;\n vec3 col = vec3(uv, wave) * tint;\n gl_FragColor = vec4(col, 1.0);\n}\n```\n\nKey changes: removed the `mainImage(out vec4 fragColor, in vec2 fragCoord)` wrapper and used `void main()` with `gl_FragCoord.xy`; replaced `iResolution.xy` with `u_resolution`; replaced `iTime * 0.5` with a `@viji-accumulator` keyed to a `speed` slider (so changing speed never jumps the animation); added a `@viji-color` for the tint so the artist can adjust it; preserved the visual intent (UV gradient + horizontal wave).\n",
|
|
10098
|
-
"threejs": "# Converting Three.js Applications to Viji Native\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert a standalone Three.js application into a Viji scene. The target renderer is Viji **Native** (Three.js loads as an ESM dynamic import). This guide is self-contained: it includes both the source-to-target mapping and the Viji Native target reference needed to produce correct converted output, without requiring the Viji Native system prompt to also be loaded.\n\n## Source platform: standalone Three.js\n\nStandalone Three.js apps typically:\n- Create their own `<canvas>` element or accept one from the DOM.\n- Construct a `THREE.Scene`, `THREE.PerspectiveCamera`, and `THREE.WebGLRenderer`.\n- Use `requestAnimationFrame()` as the per-frame loop.\n- Use `THREE.Clock` / `clock.getDelta()` for timing.\n- Attach `window.addEventListener('resize', ...)` and use `window.innerWidth` / `window.innerHeight`.\n- Listen to DOM mouse / keyboard events, or use `OrbitControls`.\n- Load assets via `THREE.TextureLoader`, `GLTFLoader`, etc.\n\n## Target platform: Viji Native + Three.js\n\nViji Native scenes run on an `OffscreenCanvas` inside a Web Worker. The DOM is unavailable. Three.js loads as an ESM dynamic import. The canvas is created by Viji and passed to the Three.js renderer. Viji controls the render loop; the artist's `render(viji)` function runs each frame.\n\n## Conversion rules\n\n1. **Always** import Three.js dynamically at the top level with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.160.0');\n ```\n Never use `<script>` tags, `require()`, or static `import` statements.\n\n2. **Always** use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n **Always** pass `false` as the third argument to `setSize()`. The Worker has no DOM, so any attempt to update CSS styles (Three.js's default) will throw.\n\n3. **Never** use `requestAnimationFrame()`. Viji controls the render loop. Put all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. **Always** handle resize by checking `viji.width` / `viji.height` inside `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n // ...\n renderer.render(scene, camera);\n }\n ```\n Remove any `window.addEventListener('resize', ...)`. Resize is handled here.\n\n5. **Never** access `window`, `document`, `Image()`, or `localStorage`. `fetch()` and `await import()` are available.\n\n6. **Never** use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. **Lift hardcoded values to Viji parameters** at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const meshColor = viji.color('#049ef4', { label: 'Color' });\n ```\n Read via `.value` inside `render()`. Three.js colors accept the hex string: `material.color.set(meshColor.value)`. For more control, color parameters also expose `.rgb` (0..255) and `.hsb`.\n\n8. **Always** use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n Remove `THREE.Clock` and `clock.getDelta()`. Never use `Date.now()` or `performance.now()` directly.\n\n **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator at the top level.\n\n9. **Replace mouse / keyboard event listeners with Viji input APIs**:\n - `event.clientX` -> `viji.pointer.x` (works for both mouse and touch), or `viji.mouse.x`.\n - `event.clientY` -> `viji.pointer.y`, or `viji.mouse.y`.\n - Mouse buttons -> `viji.mouse.leftButton`, `viji.mouse.rightButton`, `viji.mouse.middleButton`.\n - Key presses -> `viji.keyboard.isPressed('keyName')`, `viji.keyboard.wasPressed('keyName')` for one-frame edges.\n\n10. **`OrbitControls` and other DOM-event-based controls do not work** in the Worker. For camera interaction, read `viji.pointer` and `viji.mouse.wheelDelta` directly and update the camera manually. (You can typically build a working orbit with: pointer delta when down -> yaw / pitch; wheel delta -> radius.)\n\n11. **Three.js addons** import from the examples directory with the same pinned version:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.160.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n Always pin the same version for addons as for the main library.\n\n12. **File textures** become `viji.image` parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n let texture = null;\n\n function render(viji) {\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n // ...\n }\n ```\n\n13. **Video textures** use `viji.video`:\n ```javascript\n let videoTexture = null;\n\n function render(viji) {\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n // ...\n }\n ```\n\n14. **Never** allocate new objects (vectors, colors, materials, geometries) inside `render()`. Pre-create at the top level and mutate in place. Three.js reuses `Vector3` and `Color` mutably; this is the idiomatic pattern.\n\n15. **Always** set `category` on parameters that depend on an external input: `category: 'audio'`, `category: 'video'`, `category: 'interaction'`.\n\n16. **Remove** any CSS, HTML, or DOM manipulation code. Viji scenes produce canvas output only.\n\n17. If the source uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js): flag this to the artist. These cannot be converted directly; they would need to be rewritten as plain Three.js first.\n\n18. **Always** type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Target API reference (Viji Native + Three.js patterns)\n\n**Canvas and context**: `viji.canvas` (`OffscreenCanvas`: pass to `new THREE.WebGLRenderer({ canvas: viji.canvas })`), `viji.width`, `viji.height`. `viji.useContext()` is available for non-Three.js Native scenes, but with Three.js you let it manage the GL context.\n\n**Timing**: `viji.time` (seconds since scene start), `viji.deltaTime` (seconds since last frame), `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) + each `*Smoothed` sibling (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves) + each `*Smoothed` (500ms); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked`; `spectral.{brightness, flatness}`; `getFrequencyData(): Uint8Array` (1024 bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1). Apply audio reactivity to Three.js objects via `material.uniforms` (for `ShaderMaterial`), `material.emissiveIntensity`, mesh `scale`, `rotation`, position, etc.\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. `currentFrame` (`OffscreenCanvas | ImageBitmap`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv: VideoCVAPI`. CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs all live on `viji.video.cv`, not on `viji.video` directly. Wrap `viji.video.currentFrame` in `THREE.CanvasTexture(viji.video.currentFrame)` and set `texture.needsUpdate = true` each frame the frame changes.\n\n**Computer Vision: `viji.video.cv`**: enable explicitly (never by default). All CV state lives on `viji.video.cv`:\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (paired with the current CV results; `null` until first inference), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures()`, `isProcessing()`. Common fallback pattern when texturing from a CV-paired frame: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes (52 ARKit coefficients)}`), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`). Unified across mouse and touch: preferred for OrbitControls replacements.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`.\n\n**Sensors / streams**: `viji.device.{motion, orientation}` (for tilt-driven cameras), `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]`.\n\n## Three.js setup pattern\n\nThe canonical Viji-Native + Three.js scaffold:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst meshColor = viji.color('#049ef4', { label: 'Color' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\nrenderer.setPixelRatio(1);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(50, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 0, 5);\nscene.add(new THREE.AmbientLight(0xffffff, 0.4));\nconst dir = new THREE.DirectionalLight(0xffffff, 0.9);\ndir.position.set(2, 3, 4);\nscene.add(dir);\n\nconst geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);\nconst material = new THREE.MeshStandardMaterial({ color: meshColor.value, roughness: 0.5 });\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n material.color.set(meshColor.value);\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime * 0.7;\n\n renderer.render(scene, camera);\n}\n```\n\n## Worked example\n\nSource Three.js app:\n```javascript\nimport * as THREE from 'three';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(window.innerWidth, window.innerHeight);\ndocument.body.appendChild(renderer.domElement);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);\ncamera.position.z = 5;\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nconst clock = new THREE.Clock();\nfunction animate() {\n requestAnimationFrame(animate);\n const dt = clock.getDelta();\n cube.rotation.x += dt;\n cube.rotation.y += dt;\n controls.update();\n renderer.render(scene, camera);\n}\nanimate();\n\nwindow.addEventListener('resize', () => {\n renderer.setSize(window.innerWidth, window.innerHeight);\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n});\n```\n\nConverted Viji Native scene:\n```javascript\nconst THREE = await import('https://esm.sh/three@0.160.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Rotation Speed' });\nconst orbitSensitivity = viji.slider(0.005, { min: 0.001, max: 0.02, step: 0.001, label: 'Orbit Sensitivity', category: 'interaction' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, viji.width / viji.height, 0.1, 1000);\n\nlet cameraYaw = 0;\nlet cameraPitch = 0;\nlet cameraRadius = 5;\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n // Manual orbit replacement (OrbitControls cannot run in the worker).\n if (viji.pointer.isDown) {\n cameraYaw -= viji.pointer.deltaX * orbitSensitivity.value;\n cameraPitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01,\n cameraPitch - viji.pointer.deltaY * orbitSensitivity.value));\n }\n cameraRadius = Math.max(1.5, Math.min(20, cameraRadius - viji.mouse.wheelDelta * 0.002));\n camera.position.set(\n cameraRadius * Math.cos(cameraPitch) * Math.sin(cameraYaw),\n cameraRadius * Math.sin(cameraPitch),\n cameraRadius * Math.cos(cameraPitch) * Math.cos(cameraYaw)\n );\n camera.lookAt(0, 0, 0);\n\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime;\n\n renderer.render(scene, camera);\n}\n```\n\nKey changes: replaced static `import` with dynamic `await import('https://esm.sh/three@0.160.0')` pinned to a version; passed `viji.canvas` to the renderer and added the mandatory `false` third argument to `setSize`; removed `document.body.appendChild`; removed `requestAnimationFrame` (Viji owns the loop) and moved per-frame logic into `render(viji)`; replaced `THREE.Clock` / `clock.getDelta()` with `viji.deltaTime`; replaced the `OrbitControls` instance with a manual orbit driven by `viji.pointer.deltaX/Y` and `viji.mouse.wheelDelta` (with an `orbitSensitivity` slider tagged `category: 'interaction'`); moved resize logic from a `window.addEventListener('resize', ...)` callback into a `prevWidth`/`prevHeight` check inside `render`; lifted the implicit rotation speed to a `speed` slider so the artist can adjust it.\n"
|
|
10244
|
+
"threejs": "# Converting Three.js Applications to Viji Native\n\nThis guide is loaded by the Viji-Backend AI when the user wants to convert a standalone Three.js application into a Viji scene. The target renderer is Viji **Native** (Three.js loads as an ESM dynamic import). This guide is self-contained: it includes both the source-to-target mapping and the Viji Native target reference needed to produce correct converted output, without requiring the Viji Native system prompt to also be loaded.\n\n## Source platform: standalone Three.js\n\nStandalone Three.js apps typically:\n- Create their own `<canvas>` element or accept one from the DOM.\n- Construct a `THREE.Scene`, `THREE.PerspectiveCamera`, and `THREE.WebGLRenderer`.\n- Use `requestAnimationFrame()` as the per-frame loop.\n- Use `THREE.Clock` / `clock.getDelta()` for timing.\n- Attach `window.addEventListener('resize', ...)` and use `window.innerWidth` / `window.innerHeight`.\n- Listen to DOM mouse / keyboard events, or use `OrbitControls`.\n- Load assets via `THREE.TextureLoader`, `GLTFLoader`, etc.\n\n## Target platform: Viji Native + Three.js\n\nViji Native scenes run on an `OffscreenCanvas` inside a Web Worker. The DOM is unavailable. Three.js loads as an ESM dynamic import. The canvas is created by Viji and passed to the Three.js renderer. Viji controls the render loop; the artist's `render(viji)` function runs each frame.\n\n## Conversion rules\n\n1. **Always** import Three.js dynamically at the top level with a pinned version:\n ```javascript\n const THREE = await import('https://esm.sh/three@0.180.0');\n ```\n Pin 0.170 or newer: versions below 0.170 cannot load glb textures inside a worker on Safari and iOS.\n Never use `<script>` tags, `require()`, or static `import` statements.\n\n2. **Always** use `viji.canvas` as the renderer's canvas:\n ```javascript\n const renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\n renderer.setSize(viji.width, viji.height, false);\n ```\n **Always** pass `false` as the third argument to `setSize()`. The Worker has no DOM, so any attempt to update CSS styles (Three.js's default) will throw.\n\n3. **Never** use `requestAnimationFrame()`. Viji controls the render loop. Put all per-frame logic inside `function render(viji) { ... }` and call `renderer.render(scene, camera)` at the end.\n\n4. **Always** handle resize by checking `viji.width` / `viji.height` inside `render()`:\n ```javascript\n let prevWidth = viji.width;\n let prevHeight = viji.height;\n\n function render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n // ...\n renderer.render(scene, camera);\n }\n ```\n Remove any `window.addEventListener('resize', ...)`. Resize is handled here.\n\n5. **Never** access `window`, `document`, `Image()`, or `localStorage`. `fetch()` and `await import()` are available.\n\n6. **Never** use `window.innerWidth` / `window.innerHeight`. Use `viji.width` / `viji.height`.\n\n7. **Lift hardcoded values to Viji parameters** at the top level:\n ```javascript\n const speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\n const meshColor = viji.color('#049ef4', { label: 'Color' });\n ```\n Read via `.value` inside `render()`. Three.js colors accept the hex string: `material.color.set(meshColor.value)`. For more control, color parameters also expose `.rgb` (0..255) and `.hsb`.\n\n8. **Always** use `viji.deltaTime` for animation timing:\n ```javascript\n cube.rotation.y += speed.value * viji.deltaTime;\n ```\n Remove `THREE.Clock` and `clock.getDelta()`. Never use `Date.now()` or `performance.now()` directly.\n\n **Never** multiply `viji.time` by a parameter; it causes visible jumps when the parameter changes. Same rule for nested multiplications: each independent speed needs its own accumulator at the top level.\n\n9. **Replace mouse / keyboard event listeners with Viji input APIs**:\n - `event.clientX` -> `viji.pointer.x` (works for both mouse and touch), or `viji.mouse.x`.\n - `event.clientY` -> `viji.pointer.y`, or `viji.mouse.y`.\n - Mouse buttons -> `viji.mouse.leftButton`, `viji.mouse.rightButton`, `viji.mouse.middleButton`.\n - Key presses -> `viji.keyboard.isPressed('keyName')`, `viji.keyboard.wasPressed('keyName')` for one-frame edges.\n\n10. **`OrbitControls` and other DOM-event-based controls do not work** in the Worker. For camera interaction, read `viji.pointer` and `viji.mouse.wheelDelta` directly and update the camera manually. (You can typically build a working orbit with: pointer delta when down -> yaw / pitch; wheel delta -> radius.)\n\n11. **Three.js addons** import from the examples directory with the same pinned version:\n ```javascript\n const { GLTFLoader } = await import('https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js');\n const { EffectComposer } = await import('https://esm.sh/three@0.180.0/examples/jsm/postprocessing/EffectComposer.js');\n ```\n Always pin the same version for addons as for the main library. When the model is a user asset, read it from `viji.model()` instead of a hardcoded URL:\n ```javascript\n const asset = viji.model(null, { label: 'Model', formats: ['glb'] });\n const loader = new GLTFLoader();\n let loaded = null;\n\n function render(viji) {\n if (asset.value !== loaded) {\n loaded = asset.value;\n if (loaded) loader.parse(loaded.bytes, '', (gltf) => scene.add(gltf.scene), (err) => console.error(err));\n }\n renderer.render(scene, camera);\n }\n ```\n `asset.value` is `{ bytes, url, format }` or `null`; `loader.load(asset.value.url, ...)` also works. Parse once per new value, never per frame.\n\n12. **File textures** become `viji.image` parameters:\n ```javascript\n const photo = viji.image(null, { label: 'Texture' });\n let texture = null;\n\n function render(viji) {\n if (photo.value && !texture) {\n texture = new THREE.CanvasTexture(photo.value);\n material.map = texture;\n material.needsUpdate = true;\n }\n // ...\n }\n ```\n\n13. **Video textures** use `viji.video`:\n ```javascript\n let videoTexture = null;\n\n function render(viji) {\n if (viji.video.isConnected && viji.video.currentFrame) {\n if (!videoTexture) {\n videoTexture = new THREE.CanvasTexture(viji.video.currentFrame);\n material.map = videoTexture;\n }\n videoTexture.needsUpdate = true;\n }\n // ...\n }\n ```\n\n14. **Never** allocate new objects (vectors, colors, materials, geometries) inside `render()`. Pre-create at the top level and mutate in place. Three.js reuses `Vector3` and `Color` mutably; this is the idiomatic pattern.\n\n15. **Always** set `category` on parameters that depend on an external input: `category: 'audio'`, `category: 'video'`, `category: 'interaction'`.\n\n16. **Remove** any CSS, HTML, or DOM manipulation code. Viji scenes produce canvas output only.\n\n17. If the source uses a framework on top of Three.js (React Three Fiber, Drei, Theatre.js): flag this to the artist. These cannot be converted directly; they would need to be rewritten as plain Three.js first.\n\n18. **Always** type the `viji` parameter on `render` and any helpers — `function render(viji: VijiAPI) { ... }`. Monaco's TS checker flags wrong API paths at edit time; sucrase strips the annotations at runtime, so they are safe to include.\n\n## Target API reference (Viji Native + Three.js patterns)\n\n**Canvas and context**: `viji.canvas` (`OffscreenCanvas`: pass to `new THREE.WebGLRenderer({ canvas: viji.canvas })`), `viji.width`, `viji.height`. `viji.useContext()` is available for non-Three.js Native scenes, but with Three.js you let it manage the GL context.\n\n**Timing**: `viji.time` (seconds since scene start), `viji.deltaTime` (seconds since last frame), `viji.frameCount`, `viji.fps`.\n\n**Parameters** (top-level only, read via `.value`):\n```javascript\nviji.slider(default, { min?, max?, step?, label, group?, category? }) // .value: number; `step` defaults to power-of-10 giving ~100 positions across min..max\nviji.color(default, { label, group?, category?, alpha? }) // .value: '#rrggbb' (or '#rrggbbaa' with alpha: true), .rgb: {r,g,b} 0..255, .hsb: {h:0..360, s/b:0..100}, .alpha: 0..1; alpha-bearing inputs (rgba(), #rrggbbaa, {..., a}) ONLY with alpha: true\nviji.gradient(default, { label, minStops?, maxStops?, group?, category?, alpha? }) // default: [{ color, position: 0..1 }, ...] (required, 2..8 stops); .value: sorted stops, .at(t): hex color at t (same math as canvas gradients)\nviji.toggle(default, { label, group?, category? }) // .value: boolean\nviji.select(default, { options, label, group?, category? }) // .value: string | number\nviji.number(default, { min?, max?, step?, label, group?, category? }) // .value: number\nviji.text(default, { label, group?, category?, maxLength? }) // .value: string\nviji.image(null, { label, group?, category? }) // .value: ImageBitmap | null\nviji.model(null, { label, formats, group?, category? }) // .value: ModelValue | null: { bytes: ArrayBuffer, url: blob URL, format: 'glb'|'obj'|'stl'|'ply'|'las'|'laz'|'splat' }; formats is required: list only the formats your loader parses; parse with your own loader once per new value, never per frame\nviji.button({ label, description?, group?, category? }) // .value: boolean (1 frame)\nviji.coordinate(default, { step?, label, group?, category? }) // .value: { x, y }, range -1..1\n```\n\n**Audio: `viji.audio`**: check `isConnected` first. Members: `volume.{current, peak, smoothed}` (0..1; `smoothed` 200ms decay); `bands.{low, lowMid, mid, highMid, high}` (instant 0..1) + each `*Smoothed` sibling (150ms decay); `beat.{kick, snare, hat, any}` (300ms decay curves) + each `*Smoothed` (500ms); `beat.triggers.{kick, snare, hat, any}` (boolean, true for one frame, OR-accumulated); `beat.events: Array<{type, time, strength}>` (`type` is `'kick' | 'snare' | 'hat'`); `beat.bpm` (`0` when no audio is connected; once audio connects it tracks the detected tempo clamped to 60..240, with `120` as a fallback before lock-on), `beat.confidence` (0..1), `beat.isLocked`; `spectral.{brightness, flatness}`; `getFrequencyData(): Uint8Array` (1024 bins 0..255); `getWaveform(): Float32Array` (2048 samples -1..1). Apply audio reactivity to Three.js objects via `material.uniforms` (for `ShaderMaterial`), `material.emissiveIntensity`, mesh `scale`, `rotation`, position, etc.\n\n**Video: `viji.video`**: check `isConnected && currentFrame` first. `currentFrame` (`OffscreenCanvas | ImageBitmap`), `frameWidth`, `frameHeight`, `frameRate`, `getFrameData()`, `cv: VideoCVAPI`. CV outputs (`analysedFrame`, `getAnalysedFrameData`, `faces`, `hands`, `pose`, `segmentation`) and CV verbs all live on `viji.video.cv`, not on `viji.video` directly. Wrap `viji.video.currentFrame` in `THREE.CanvasTexture(viji.video.currentFrame)` and set `texture.needsUpdate = true` each frame the frame changes.\n\n**Computer Vision: `viji.video.cv`**: enable explicitly (never by default). All CV state lives on `viji.video.cv`:\n```javascript\nawait viji.video.cv.enableFaceDetection(true);\nawait viji.video.cv.enableFaceMesh(true); // populates face.landmarks + face.headPose\nawait viji.video.cv.enableEmotionDetection(true); // populates face.blendshapes + face.expressions; loads landmarker\nawait viji.video.cv.enableHandTracking(true);\nawait viji.video.cv.enablePoseDetection(true);\nawait viji.video.cv.enableBodySegmentation(true);\n```\nAlso on `viji.video.cv`: `analysedFrame: OffscreenCanvas | null` (paired with the current CV results; `null` until first inference), `getAnalysedFrameData(): ImageData | null`, `getActiveFeatures()`, `isProcessing()`. Common fallback pattern when texturing from a CV-paired frame: `viji.video.cv.analysedFrame ?? viji.video.currentFrame`.\n\nData shapes on `viji.video.cv`: `faces: FaceData[]` (`{id, bounds, center, confidence, landmarks, expressions, headPose, blendshapes (52 ARKit coefficients)}`), `hands: HandData[]` (`{id, handedness, confidence, bounds, landmarks (21 pts), palm, gestures}`), `pose: PoseData | null` (`{confidence, landmarks (33 pts), face, torso, leftArm, rightArm, leftLeg, rightLeg}`), `segmentation: SegmentationData | null` (`{mask: Uint8Array (byte values 0 or 1), width, height}`).\n\n**Input**:\n- `viji.pointer`: `x`, `y`, `deltaX`, `deltaY`, `isDown`, `wasPressed`, `wasReleased`, `isInCanvas`, `type` (`'mouse' | 'touch' | 'none'`). Unified across mouse and touch: preferred for OrbitControls replacements.\n- `viji.mouse`: `x`, `y`, `isInCanvas`, `isPressed`, `leftButton`, `rightButton`, `middleButton`, `deltaX`, `deltaY`, `wheelDelta`, `wheelX`, `wheelY`, `wasPressed`, `wasReleased`, `wasMoved`.\n- `viji.keyboard`: `isPressed(key)`, `wasPressed(key)`, `wasReleased(key)`, `activeKeys`, `lastKeyPressed`, `lastKeyReleased`, `shift`, `ctrl`, `alt`, `meta`.\n- `viji.touches`: `count`, `points`, `started`, `moved`, `ended`, `primary`.\n\n**Sensors / streams**: `viji.device.{motion, orientation}` (for tilt-driven cameras), `viji.devices[]`, `viji.videoStreams[]`, `viji.audioStreams[]`.\n\n## Three.js setup pattern\n\nThe canonical Viji-Native + Three.js scaffold:\n\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Speed' });\nconst meshColor = viji.color('#049ef4', { label: 'Color' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\nrenderer.setPixelRatio(1);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(50, viji.width / viji.height, 0.1, 100);\ncamera.position.set(0, 0, 5);\nscene.add(new THREE.AmbientLight(0xffffff, 0.4));\nconst dir = new THREE.DirectionalLight(0xffffff, 0.9);\ndir.position.set(2, 3, 4);\nscene.add(dir);\n\nconst geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);\nconst material = new THREE.MeshStandardMaterial({ color: meshColor.value, roughness: 0.5 });\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n material.color.set(meshColor.value);\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime * 0.7;\n\n renderer.render(scene, camera);\n}\n```\n\n## Worked example\n\nSource Three.js app:\n```javascript\nimport * as THREE from 'three';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(window.innerWidth, window.innerHeight);\ndocument.body.appendChild(renderer.domElement);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);\ncamera.position.z = 5;\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nconst clock = new THREE.Clock();\nfunction animate() {\n requestAnimationFrame(animate);\n const dt = clock.getDelta();\n cube.rotation.x += dt;\n cube.rotation.y += dt;\n controls.update();\n renderer.render(scene, camera);\n}\nanimate();\n\nwindow.addEventListener('resize', () => {\n renderer.setSize(window.innerWidth, window.innerHeight);\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n});\n```\n\nConverted Viji Native scene:\n```javascript\nconst THREE = await import('https://esm.sh/three@0.180.0');\n\nconst speed = viji.slider(1, { min: 0.1, max: 5, label: 'Rotation Speed' });\nconst orbitSensitivity = viji.slider(0.005, { min: 0.001, max: 0.02, step: 0.001, label: 'Orbit Sensitivity', category: 'interaction' });\n\nconst renderer = new THREE.WebGLRenderer({ canvas: viji.canvas, antialias: true });\nrenderer.setSize(viji.width, viji.height, false);\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, viji.width / viji.height, 0.1, 1000);\n\nlet cameraYaw = 0;\nlet cameraPitch = 0;\nlet cameraRadius = 5;\n\nconst cube = new THREE.Mesh(\n new THREE.BoxGeometry(),\n new THREE.MeshNormalMaterial()\n);\nscene.add(cube);\n\nlet prevWidth = viji.width;\nlet prevHeight = viji.height;\n\nfunction render(viji) {\n if (viji.width !== prevWidth || viji.height !== prevHeight) {\n renderer.setSize(viji.width, viji.height, false);\n camera.aspect = viji.width / viji.height;\n camera.updateProjectionMatrix();\n prevWidth = viji.width;\n prevHeight = viji.height;\n }\n\n // Manual orbit replacement (OrbitControls cannot run in the worker).\n if (viji.pointer.isDown) {\n cameraYaw -= viji.pointer.deltaX * orbitSensitivity.value;\n cameraPitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01,\n cameraPitch - viji.pointer.deltaY * orbitSensitivity.value));\n }\n cameraRadius = Math.max(1.5, Math.min(20, cameraRadius - viji.mouse.wheelDelta * 0.002));\n camera.position.set(\n cameraRadius * Math.cos(cameraPitch) * Math.sin(cameraYaw),\n cameraRadius * Math.sin(cameraPitch),\n cameraRadius * Math.cos(cameraPitch) * Math.cos(cameraYaw)\n );\n camera.lookAt(0, 0, 0);\n\n cube.rotation.x += speed.value * viji.deltaTime;\n cube.rotation.y += speed.value * viji.deltaTime;\n\n renderer.render(scene, camera);\n}\n```\n\nKey changes: replaced static `import` with dynamic `await import('https://esm.sh/three@0.180.0')` pinned to a version; passed `viji.canvas` to the renderer and added the mandatory `false` third argument to `setSize`; removed `document.body.appendChild`; removed `requestAnimationFrame` (Viji owns the loop) and moved per-frame logic into `render(viji)`; replaced `THREE.Clock` / `clock.getDelta()` with `viji.deltaTime`; replaced the `OrbitControls` instance with a manual orbit driven by `viji.pointer.deltaX/Y` and `viji.mouse.wheelDelta` (with an `orbitSensitivity` slider tagged `category: 'interaction'`); moved resize logic from a `window.addEventListener('resize', ...)` callback into a `prevWidth`/`prevHeight` check inside `render`; lifted the implicit rotation speed to a `speed` slider so the artist can adjust it.\n"
|
|
10099
10245
|
}
|
|
10100
10246
|
}
|
|
10101
10247
|
};
|