phoria2 1.2.0 → 1.4.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.
Files changed (69) hide show
  1. package/README.md +11 -9
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +527 -510
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1760 -1170
  7. package/dist/core.js.map +1 -1
  8. package/dist/debug.js +297 -0
  9. package/dist/debug.js.map +1 -0
  10. package/dist/edit.js +60 -60
  11. package/dist/edit.js.map +1 -1
  12. package/dist/explain.js +32 -13
  13. package/dist/explain.js.map +1 -1
  14. package/dist/gltf.js +155 -155
  15. package/dist/gltf.js.map +1 -1
  16. package/dist/keyboard.js +188 -0
  17. package/dist/keyboard.js.map +1 -0
  18. package/dist/motion.js +35 -35
  19. package/dist/motion.js.map +1 -1
  20. package/dist/particles.js +187 -114
  21. package/dist/particles.js.map +1 -1
  22. package/dist/passes.js +116 -104
  23. package/dist/passes.js.map +1 -1
  24. package/dist/phoria2.js +1 -1
  25. package/dist/picking.js +95 -62
  26. package/dist/picking.js.map +1 -1
  27. package/dist/snapshot.js +1 -1
  28. package/dist/tooltip.js +1 -1
  29. package/dist/types/View.d.ts +27 -0
  30. package/dist/types/area.d.ts +18 -0
  31. package/dist/types/debug.d.ts +74 -0
  32. package/dist/types/explain.d.ts +4 -0
  33. package/dist/types/frame.d.ts +5 -0
  34. package/dist/types/geometry/Geometry.d.ts +16 -0
  35. package/dist/types/index.d.ts +2 -0
  36. package/dist/types/material/Material.d.ts +67 -0
  37. package/dist/types/material/Texture.d.ts +18 -2
  38. package/dist/types/particles/Emitter.d.ts +39 -2
  39. package/dist/types/scene/drawables.d.ts +23 -2
  40. package/dist/types/watch.d.ts +8 -1
  41. package/dist/types/webgl/hooks.d.ts +48 -0
  42. package/dist/types/webgl/passes.d.ts +4 -0
  43. package/dist/types/webgl/programs.d.ts +15 -5
  44. package/dist/types/webgl/resources.d.ts +12 -2
  45. package/dist/types/webgl/shaders.d.ts +41 -5
  46. package/llms-full.txt +275 -1
  47. package/llms.txt +1 -1
  48. package/package.json +1 -1
  49. package/schema/scene.schema.json +75 -0
  50. package/src/View.js +121 -7
  51. package/src/area.js +131 -0
  52. package/src/charts/marks.js +29 -2
  53. package/src/debug.js +283 -0
  54. package/src/explain.js +19 -0
  55. package/src/frame.js +7 -2
  56. package/src/geometry/Geometry.js +16 -1
  57. package/src/index.js +1 -0
  58. package/src/material/Material.js +130 -4
  59. package/src/material/Texture.js +55 -2
  60. package/src/particles/Emitter.js +121 -10
  61. package/src/scene/drawables.js +89 -8
  62. package/src/watch.js +26 -3
  63. package/src/webgl/WebGLRenderer.js +55 -7
  64. package/src/webgl/hooks.js +121 -0
  65. package/src/webgl/passes.js +17 -2
  66. package/src/webgl/picking.js +45 -3
  67. package/src/webgl/programs.js +20 -9
  68. package/src/webgl/resources.js +169 -15
  69. package/src/webgl/shaders.js +545 -38
package/llms-full.txt CHANGED
@@ -139,12 +139,37 @@ Methods:
139
139
  - `on('click' | 'hover', (hit, event) => ...)` - see Interaction. Returns an unsubscribe function.
140
140
  - `tooltip(fn)` - set the tooltip function later.
141
141
  - `pick(x, y)` / `pick(event)` - the hit under a canvas position; `pickAll` for every hit.
142
+ - `pickArea(area)` - every hit inside a box `{ x, y, width, height }` or a lasso of `[x, y]` points
143
+ (CSS pixels), nearest first - see Interaction.
142
144
  - `rayAt(x, y)` / `rayAt(event)` - the world `Ray` through a canvas position, for placing things:
143
145
  `ray.at(ray.intersectPlane([0, 1, 0], [0, 0, 0]))` is the point on the floor under the pointer
144
146
  (`intersectPlane` returns null when the ray runs along the plane or away from it).
145
147
  - `await view.toImage()` - a PNG `Blob` of the picture, labels included.
146
148
  - `await view.explain()` - a plain-text report of what is drawn, with warnings ("nothing in view",
147
149
  "no lights", labels hidden...). Use it to debug a blank or wrong picture.
150
+ - `view.debug` (or the `debug` option) - pictures for a person checking a scene, drawn over it:
151
+ `true` for all but normals, or `{ lights, shadows, bounds, normals, axes, camera, stats }`.
152
+ - `lights`: an arrow for each directional light (over the scene's middle, the way it shines), a
153
+ small sphere and a ring at its range for point lights, a cone for spot lights - in the light's colour.
154
+ - `shadows`: the box (directional) or cone (spot) each shadow map is drawn from, in yellow - the
155
+ usual reason a shadow is cut off, blocky or soft.
156
+ - `bounds`: each drawable's world box, in cyan - what culling, `view.frame()` and shadow fitting
157
+ see. `'hovered'`: only what is under the pointer (an instance's own box).
158
+ - `normals`: a short magenta line along each vertex normal - for lighting that looks wrong on
159
+ imported or generated geometry. Meshes up to 5,000 vertices when all are shown; `'hovered'` shows
160
+ the mesh under the pointer, whatever its size.
161
+ - `axes`: x, y and z at the origin, red, green and blue. `camera`: the point the orbit turns round.
162
+ - `stats`: a corner readout - fps and frame time, the CPU time of a frame, draw calls, triangles,
163
+ programs, textures and geometries; `true` is top left, or `'top-right'`, `'bottom-left'`,
164
+ `'bottom-right'`.
165
+
166
+ The helpers are lines in the scene's depth that also show faintly through what is in front of
167
+ them. They aren't part of the scene: never picked, framed, saved or counted, and `explain()` says
168
+ when debug drawing is on. Their code loads the first time debug is turned on.
169
+
170
+ ```js
171
+ view.debug = { lights: true, shadows: true, bounds: 'hovered', stats: 'bottom-right' }
172
+ ```
148
173
  - `render()`, `invalidate()` (redraw after changing data the view can't see), `dispose()`.
149
174
 
150
175
  If a page shows nothing or the wrong thing, `console.log(await view.explain())` before anything else.
@@ -222,6 +247,24 @@ new Lines({ geometry: edges(box(2)), material: { color: '#333', width: 2 } })
222
247
  new Label({ text: 'Rack A1', position: [0, 2, 0], className: 'my-label', place: 'above' })
223
248
  ```
224
249
 
250
+ **Lines** dashed and with arrowheads - flow and direction in network diagrams. Dashes are material
251
+ options in the model's units: `dash` (0 is solid), `gap` (0 is the dash's length) and `dashOffset`,
252
+ which moves them along - animate it for traffic. Arrowheads are Lines options: `arrows: 'start' |
253
+ 'end' | 'both'` puts them at the ends of each chain (segments each starting where the one before
254
+ ended - a polyline is one chain, separate links are one each), `arrowSize` in pixels (0, the default,
255
+ sizes them from the width). The dashes run on along a chain, and arrowheads are never dashed.
256
+
257
+ ```js
258
+ const flow = view.scene.add(new Lines({ geometry: links, arrows: 'end', material: { color: '#4cc9f0', width: 2, dash: 0.3, gap: 0.15 } }))
259
+ view.start((dt) => { flow.material.dashOffset += dt * 0.8 }) // traffic moving along each link
260
+ ```
261
+
262
+ **Points** each their own size, opacity or angle: give the geometry `sizes` (multiples of the material's
263
+ `size`), `opacities` (0-1) or `angles` (radians, turning the point's `map`), one per vertex. Such points
264
+ are drawn as camera-facing squares, so any size works and they stay whole at the edge of the view.
265
+ `sort: true` draws them furthest first each frame - for soft points with `blending: 'normal'`
266
+ (smoke, cloud) that would otherwise layer wrongly; with additive blending order doesn't matter.
267
+
225
268
  **InstancedMesh** - many copies of one geometry in one draw call, for bars, racks, nodes:
226
269
 
227
270
  ```js
@@ -361,7 +404,10 @@ unless its `colorSpace` was set on purpose - `normalMap: await loadTexture('n.pn
361
404
 
362
405
  **Canvas and video textures**: a texture's `image` can be a canvas - draw on it, then call
363
406
  `texture.needsUpdate()` - or a `<video>`, whose frames upload by themselves while it plays, with
364
- nothing to call. `new Texture({ url: 'promo.mp4' })` (or .webm, .ogv, .mov, .m4v) makes the video
407
+ nothing to call. For a big canvas drawn on a little at a time (paint, a map being revealed), say what
408
+ changed - `texture.needsUpdate({ x, y, width, height })`, in the canvas's pixels from its top left -
409
+ and only that rectangle is sent, rather than the whole canvas every frame (several calls before a
410
+ frame add up). `new Texture({ url: 'promo.mp4' })` (or .webm, .ogv, .mov, .m4v) makes the video
365
411
  for you, muted and looping so browsers let it start by itself; `texture.image` is that `<video>` -
366
412
  pause, seek or unmute it as any other. A browser that blocks autoplay leaves it on its first frame
367
413
  until `texture.image.play()` from a click. Video textures make no mipmaps (they would be remade
@@ -445,6 +491,210 @@ on top, as with UVs (animate `offset` to scroll a pattern). 0 (the default) uses
445
491
  { bumpMap: pores, bumpScale: 0.0005, roughnessMap: sheen, triplanar: 45 } // bone texture on a scanned skull
446
492
  ```
447
493
 
494
+ **Shader hooks** - a few lines of GLSL of your own, run at fixed points inside phoria2's shader, for
495
+ what the material options can't say: waves, wind, dissolves, scanlines, glows, colour by height. You
496
+ write statements, not a shader: lighting, shadows, fog, clipping, picking and outlines keep working -
497
+ a waving flag casts a waving shadow and is picked where it is drawn. Meshes, instanced meshes, points
498
+ and lines all take them, as `material.shader`:
499
+
500
+ ```js
501
+ new Mesh({ geometry: plane({ width: 3, depth: 2, segments: 40 }), material: {
502
+ color: '#3a7bd5', doubleSided: true,
503
+ shader: {
504
+ uniforms: { uAmp: 0.25 },
505
+ vertex: 'local.y += sin(local.x * 3.0 + uTime * 3.0) * uAmp;', // a wave, in the geometry's own space
506
+ fragment: 'base.rgb *= 0.8 + 0.2 * sin(vWorldPosition.y * 20.0);', // stripes, before lighting
507
+ lit: 'color += vec3(0.0, 0.1, 0.2) * (0.5 + 0.5 * sin(uTime));', // a glow the light doesn't touch
508
+ },
509
+ } })
510
+ ```
511
+
512
+ Three hooks, each optional. What each can use (✎ = change it; everything else is read only):
513
+
514
+ | | Mesh / InstancedMesh | Points | Lines |
515
+ |---|---|---|---|
516
+ | `vertex` - each vertex, before it is placed | ✎ `local` (vec3, own space), ✎ `n` (vec3 normal), `uv` (vec2), `index` (int: the instance) | ✎ `local`, ✎ `size` (float, times the material's), `index` (int: the point) | ✎ `local`, `along` (float: 0 at a segment's start, 1 at its end), `index` (int: the segment) - runs once per end, so a segment moves but stays straight |
517
+ | `fragment` - each pixel, before lighting | ✎ `base` (vec4: linear colour, alpha), ✎ `emissive` (vec3), ✎ `normal` (vec3: the shading normal, world space, facing the viewer - bend it for relief lit per pixel), `vUv`, `vWorldPosition`, `vNormal`, `vColor`; `discard` | ✎ `base`, `pointUv` (vec2, 0-1 across the point), `index` (int); `discard` | ✎ `base`, `distance` (float: along the line, in the geometry's units), `index` (int: the segment); `discard` |
518
+ | `lit` - each pixel, after lighting | ✎ `color` (vec3, linear, lit), ✎ `base.a` | ✎ `color`, ✎ `base.a` | ✎ `color`, ✎ `base.a` |
519
+
520
+ Every hook can also read `uTime` - the View's clock in seconds - and the uniforms you give, and
521
+ call functions for randomness and texture without a texture (hooks can't define their own). `p`
522
+ is a vec2 or vec3, and every result is 0 to 1:
523
+
524
+ - `hash(x)` - from a float, vec2 or vec3's exact bits. The same on every GPU for the same bits, so hash
525
+ exact things - `hash(float(index))` gives each point, instance or segment its own random number,
526
+ `hash(floor(vUv * 20.0))` each cell of a grid. An interpolated value such as `vWorldPosition` can
527
+ differ in its last bits from GPU to GPU, and so can its hash.
528
+ - `vnoise(p)` - value noise: random values at whole-number points, eased between. Cheapest; looks
529
+ blockier, its features lined up with the axes.
530
+ - `snoise(p)` - simplex noise: smoother, rounder, no grid to see - the better choice for anything
531
+ natural. About one feature per unit of `p`: scale `p` up for finer detail, `snoise(vUv * 10.0)`,
532
+ `snoise(vec3(local.xz, uTime))` to change over time.
533
+ - `snoised(p)` - simplex noise with its gradient: a vec3 (value, d/dx, d/dy) from a vec2, a vec4
534
+ from a vec3. The slope for a normal, with no extra samples.
535
+ - `vfbm(p)`, `sfbm(p)` - fractal noise: octaves of value or simplex noise added up, each finer and
536
+ half as strong, each turned so their grids don't line up. Five octaves; `sfbm(p, 8)` for more
537
+ detail, fewer for softer and cheaper (1-12). Mostly 0.25-0.75 - the octaves average out.
538
+ - `sfbmd(p)`, `sfbmd(p, octaves)` - simplex fbm with its gradient, as `snoised`: terrain normals.
539
+ - `voronoi(p)` - cellular (Worley) noise: a random point in each unit cell of `p`. Returns a vec3 -
540
+ `x` the distance to the nearest point (F1), `y` to the second nearest (F2), both in cells, and `z` a
541
+ random 0-1 for the nearest point's cell. `y - x` is 0 on the edges between cells: cracks, cells,
542
+ scales, stone, crystals; `z` gives each cell its own colour or height. A vec3 `p` with time as one
543
+ axis makes the cells drift and re-form. Costs 9 cells (vec2) or 27 (vec3) - more than `snoise`.
544
+
545
+ And the everyday ones, with `cameraPosition` (vec3, the camera in world space) to read too:
546
+
547
+ - `srgb(c)` - a colour as picked (sRGB, as in a colour picker or CSS) to the linear colour hooks work
548
+ in: `base.rgb = srgb(vec3(0.75, 0.12, 0.16))` is the red you picked, not a pale pink.
549
+ - `hsv2rgb(h, s, v)` (or a vec3) - hue, saturation and value 0-1 to a linear colour. Hue 0 red, 1/3
550
+ green, 2/3 blue, wrapping: `hsv2rgb(fract(vWorldPosition.y * 0.2 - uTime * 0.1), 0.8, 1.0)`.
551
+ - `palette(t, a, b, c, d)` - Inigo Quilez's cosine palette, `a + b cos(2pi(c t + d))`, its four
552
+ vec3s as picked, the result linear. `palette(t, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.0, 0.33,
553
+ 0.67))` is a rainbow; other a-d give heat, ocean, pastel ramps.
554
+ - `fresnel(normal, position, power)` - how side-on a surface is to the camera, 0 facing it, 1 edge-on,
555
+ sharpened by the power: rim light, holograms, force fields, `fresnel(vNormal, vWorldPosition, 3.0)`.
556
+ - `rotate2(angle)` - a mat2 turning anticlockwise: `rotate2(a) * v`. `rotate3(axis, angle)` - a mat3
557
+ turning about an axis. Twists, spins, turned uvs or noise.
558
+ - `remap(x, inMin, inMax, outMin, outMax)` - from one range to another (not clamped; float, vec2 or
559
+ vec3 x). `saturate(x)` - clamped to 0-1, as HLSL has it.
560
+
561
+ - **`uniforms`**, by GLSL name: a number (float), two to four numbers (vec2-vec4), a colour string or
562
+ Color (vec3, sent linear) or a Texture (sampler2D, read with `texture(uName, uv)`). Change one on
563
+ `material.shader.uniforms` (`mesh.material.shader.uniforms.uAmp = 0.5`) and the View redraws; assign
564
+ a new `material.shader` to change the code. Names starting `gl_` or `phoria`, `uTime`, and phoria2's
565
+ own uniforms (`uColor`, `uModel`, `uOpacity`...) are taken.
566
+ - **`uTime`**: a hook that reads it keeps the View drawing while time runs - no frame loop of your own.
567
+ `view.stop()` freezes it.
568
+ - **Every pass runs the vertex hook** - shadow maps, outlines, GPU picking. A mesh's `fragment`
569
+ `discard` cuts its shadow and its picking too, so a dissolve leaves no shadow of what has gone.
570
+ - **`bounds`**: how far, in world units, the vertex hook can move a vertex. Bounds are padded by it for
571
+ culling, `view.frame()` and shadows; without it a vertex hook turns frustum culling off for the node.
572
+ - **Picking** a node a vertex hook moves is always done on the GPU, so clicks, hovers and tooltips find
573
+ it where it is drawn (the hit has the point, not a normal or uv). `pickAll`, `pickArea` and
574
+ `Raycaster` work from the geometry as given, not as moved.
575
+ - **A hook that doesn't compile** is reported once, by its own line numbers - `shader.vertex line 2:
576
+ 'wobble' : undeclared identifier` - with the names it can use, and the material is drawn without its
577
+ hooks. `view.explain()` lists the hooked materials.
578
+
579
+ Recipes - each a material option to copy:
580
+
581
+ ```js
582
+ // bumps lit per pixel from any height: bend the shading normal by its slope - crisp at any mesh
583
+ // resolution, no vertices moved (here a surface facing up, its uvs along x and z)
584
+ shader: { fragment: `
585
+ vec2 q = vUv * 20.0;
586
+ float e = 0.01, h = sfbm(q, 4);
587
+ vec2 slope = vec2(sfbm(q + vec2(e, 0.0), 4) - h, sfbm(q + vec2(0.0, e), 4) - h) / e;
588
+ normal = normalize(normal - vec3(slope.x, 0.0, slope.y) * 0.05);` }
589
+
590
+ // a wave with its normal bent to match, so the light follows the folds (a plane, a flag, a sheet)
591
+ shader: { uniforms: { uAmp: 0.2 }, vertex: `
592
+ float phase = local.x * 3.0 - uTime * 4.0;
593
+ local.y += sin(phase) * uAmp;
594
+ n = normalize(vec3(-cos(phase) * 3.0 * uAmp, 1.0, 0.0));` }
595
+
596
+ // ripples spreading from the middle of a plane (give it segments: 64 or so)
597
+ shader: { vertex: `
598
+ float r = length(local.xz), phase = r * 6.0 - uTime * 3.0, h = 0.04 * exp(-r * 0.4);
599
+ local.y += sin(phase) * h;
600
+ vec2 slope = local.xz / max(r, 1e-3) * cos(phase) * 6.0 * h;
601
+ n = normalize(vec3(-slope.x, 1.0, -slope.y));` }
602
+
603
+ // breathing: grow and shrink about the middle
604
+ shader: { vertex: 'local *= 1.0 + 0.06 * sin(uTime * 2.0);' }
605
+
606
+ // dissolve through fractal noise, its edge glowing (its shadow dissolves too)
607
+ shader: { fragment: `
608
+ float level = 0.5 - 0.55 * cos(uTime * 0.6);
609
+ float v = sfbm(vUv * vec2(10.0, 3.0));
610
+ if (v < level) discard;
611
+ emissive += vec3(1.0, 0.2, 0.02) * smoothstep(level + 0.05, level, v) * 5.0;` }
612
+
613
+ // terrain from simplex fbm, its normal from the same call's gradient (a plane with segments: 128
614
+ // or so); the octaves set how rugged - 3 rolling hills, 8 craggy
615
+ shader: { uniforms: { uOctaves: 7 }, vertex: `
616
+ vec3 h = sfbmd(local.xz * 0.4, int(uOctaves));
617
+ local.y += h.x * 2.0;
618
+ n = normalize(vec3(-h.y * 2.0 * 0.4, 1.0, -h.z * 2.0 * 0.4));` }
619
+
620
+ // a texture uniform: scroll a noise or pattern image across a surface
621
+ shader: { uniforms: { uPattern: patternTexture }, fragment: 'base.rgb *= texture(uPattern, vUv * 4.0 + vec2(uTime * 0.1, 0.0)).rgb;' }
622
+
623
+ // colour by height - a ramp from deep blue at y = 0 to orange at y = 2
624
+ shader: { fragment: 'base.rgb = mix(vec3(0.01, 0.05, 0.5), vec3(1.0, 0.25, 0.0), clamp(vWorldPosition.y / 2.0, 0.0, 1.0));' }
625
+
626
+ // rim glow: a hologram or force field - a faint body, bright where it turns away (with
627
+ // transparent: true)
628
+ shader: {
629
+ fragment: 'base.rgb = srgb(vec3(0.25, 0.9, 1.0)) * 0.15; emissive += srgb(vec3(0.25, 0.9, 1.0)) * fresnel(vNormal, vWorldPosition, 2.5) * 2.0;',
630
+ lit: 'base.a *= 0.3 + 0.7 * fresnel(vNormal, vWorldPosition, 1.5);',
631
+ }
632
+
633
+ // a twist: each height turned further round the y axis, its normal turned with it (give the mesh segments)
634
+ shader: { vertex: 'mat2 turn = rotate2(local.y * 1.2 * sin(uTime)); local.xz = turn * local.xz; n.xz = turn * n.xz;' }
635
+
636
+ // a hue climbing an object, and a cosine palette across another
637
+ shader: { fragment: 'base.rgb = hsv2rgb(fract(vWorldPosition.y * 0.2 - uTime * 0.1), 0.75, 1.0);' }
638
+ shader: { fragment: 'base.rgb = palette(vUv.x + uTime * 0.05, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.0, 0.33, 0.67));' }
639
+
640
+ // instances as a meter: each bar's height from one sine, a phase apart by index, remapped and saturated
641
+ shader: { vertex: 'float h = saturate(remap(sin(uTime * 2.0 + float(index) * 0.5), -1.0, 1.0, 0.05, 1.0)); local.y = (local.y + 0.5) * h - 0.5;' }
642
+
643
+ // cracked lava: dark crust plates, each its own shade, molten in the cracks between them (time as
644
+ // the third axis, so the plates drift and re-form)
645
+ shader: { fragment: `
646
+ vec3 v = voronoi(vec3(vUv * 8.0, uTime * 0.2));
647
+ float crack = 1.0 - smoothstep(0.0, 0.12, v.y - v.x);
648
+ base.rgb = srgb(vec3(0.16, 0.12, 0.11)) * (0.5 + 0.9 * v.z);
649
+ emissive += vec3(1.0, 0.25, 0.0) * crack * 4.0;` }
650
+
651
+ // stained glass: each cell a random hue, dark leading between them
652
+ shader: { fragment: 'vec3 v = voronoi(vUv * 6.0); base.rgb = hsv2rgb(v.z, 0.7, 1.0) * smoothstep(0.02, 0.06, v.y - v.x);' }
653
+
654
+ // scanlines over the lit picture
655
+ shader: { lit: 'color *= 0.75 + 0.25 * step(0.5, fract(vWorldPosition.y * 20.0 - uTime));' }
656
+
657
+ // fade with distance from a point (with transparent: true on the material)
658
+ shader: { uniforms: { uCentre: [0, 0, 0] }, lit: 'base.a *= 1.0 - smoothstep(2.0, 4.0, distance(vWorldPosition, uCentre));' }
659
+
660
+ // points: twinkling, each at its own rate, and soft and round
661
+ shader: {
662
+ vertex: 'size *= 1.0 + 0.6 * sin(uTime * (1.5 + hash(float(index)) * 3.0) + float(index));',
663
+ fragment: 'base.a *= smoothstep(1.0, 0.0, length(pointUv * 2.0 - 1.0));',
664
+ }
665
+
666
+ // lines: pulses running along them (distance is along each run of joined segments), each line's
667
+ // at its own time
668
+ shader: { fragment: 'base.rgb += vec3(0.3, 0.9, 1.0) * smoothstep(0.85, 1.0, fract(distance * 0.6 - uTime + hash(float(index)))) * 4.0;' }
669
+ ```
670
+
671
+ Mistakes to avoid:
672
+
673
+ - **Colours are linear**, as the shader works, so an sRGB-looking value comes out pale. Wrap a colour
674
+ as picked in `srgb()` - `srgb(vec3(0.75, 0.12, 0.16))` - or write linear values, roughly the sRGB
675
+ value squared: orange `vec3(1.0, 0.22, 0.0)`. `hsv2rgb` and `palette` give linear colour already,
676
+ and colour uniforms given as strings are converted for you.
677
+ - **Hooks are statements**, run inside phoria2's `main()`: no functions of your own, no `uniform`
678
+ declarations (use `uniforms`), no `return` in a mesh or points hook. Declare locals as you like.
679
+ `hash`, the noises and the fbms are there for what you'd otherwise write a function for.
680
+ - **Randomness from `hash()`, not `fract(sin(x) * 43758.5453)`**: the sin trick multiplies a GPU's
681
+ last bits of `sin` into the answer, so the same scene comes out differently on different GPUs.
682
+ `hash` is integer arithmetic, exact everywhere.
683
+ - **`index` is an int**: `float(index)` to do sums with it.
684
+ - **Bend the normal when you move vertices**, or the light shades the shape as it was: change `n`
685
+ (meshes) to match, and keep it normalised. Waves on a flat plane need `segments` to bend at all.
686
+ - **Fine relief belongs in the fragment hook**: moving vertices can only be as sharp as the mesh's
687
+ resolution - a steep step (cracks, tiles, grooves) becomes a staircase between them. Bend `normal`
688
+ in the fragment hook instead, lit per pixel, and keep any vertex movement gentle.
689
+ - **Fades need `transparent: true`**: changing `base.a` in an opaque material changes nothing.
690
+ `discard` works either way.
691
+ - **Move in the geometry's own space**: `local` is before the node's position, rotation and scale (and
692
+ an instance's), so a wave along `local.y` turns with the mesh.
693
+ - **Use `vWorldPosition`** for effects fixed in the world - a band at a height, a fade round a point -
694
+ and `vUv` for ones fixed to the surface.
695
+ - **Lines move by their ends**: a straight segment stays straight - give the geometry more segments to
696
+ bend a line.
697
+
448
698
  ## Lights and cameras
449
699
 
450
700
  | | Options |
@@ -537,6 +787,16 @@ view.on('hover', (hit) => show(hit?.datum)) // fires when the th
537
787
  - The **nearest surface** under the pointer is the hit - a link drawn over a site's top wins the
538
788
  hover there. To let one kind of thing through, give it `pickable: false`; to choose among
539
789
  everything under the pointer, `view.pickAll(event)` lists every hit, nearest first.
790
+ - **Selecting an area** - `view.pickArea({ x, y, width, height })` (width and height may be negative,
791
+ as a drag up or left makes them) or `view.pickArea([[x, y], ...])` for a lasso: a hit for each point,
792
+ instance and line segment whose place on screen is inside, and each mesh whose middle is. Places, not
793
+ surfaces: what is hidden behind other things is found too, so a box over a scatter takes that whole
794
+ region. Hidden, unpickable and zero-scale instances are left out. Turn `view.controls.enabled` off
795
+ while the user drags out the area.
796
+
797
+ ```js
798
+ const chosen = view.pickArea({ x: 40, y: 30, width: 200, height: 120 }).map((h) => h.datum)
799
+ ```
540
800
  - **Rays of your own** - line of sight, what's below a dropped object, picking in tests: a
541
801
  `Raycaster` finds what any ray hits, on the CPU. Set `raycaster.ray.origin` and `.direction`, or aim
542
802
  it with `setFromCamera([x, y], camera)` (-1 to 1, y up); `intersect(nodeOrNodes)` returns hits,
@@ -807,6 +1067,20 @@ second), `follow` (a node whose position is the origin each frame - a trail), `o
807
1067
  default material is small additive points. `burst(count, spawnOptions)`, `clear()`, `set(options)`,
808
1068
  `count`.
809
1069
 
1070
+ How particles look over their lives: `size` (a multiple of the material's size, or a list each particle
1071
+ goes through - `[0.5, 3]` billows), `sizeSpread` (each one's size varies by up to this share, 0-1),
1072
+ `opacity` (0-1, or a list - `[0, 1, 0]` fades in and out) and `spin` (radians per second: a number
1073
+ either way, or `[min, max]`; spinning particles start at a random angle). For smoke, a soft `map`,
1074
+ `blending: 'normal'` and `sort: true`:
1075
+
1076
+ ```js
1077
+ const smoke = view.scene.add(new Emitter({
1078
+ rate: 40, velocity: [0, 1.5, 0], velocitySpread: 0.4, lifetime: [3, 5], color: ['#777', '#333'],
1079
+ size: [0.6, 4], sizeSpread: 0.3, opacity: [0, 0.6, 0], spin: 0.5, sort: true,
1080
+ material: { map: puff, size: 1, blending: 'normal' },
1081
+ }))
1082
+ ```
1083
+
810
1084
  `Body` moves any node: `node.onUpdate(new Body({ velocity, gravity, drag }).update)`;
811
1085
  `body.impulse([x, y, z])` kicks it.
812
1086
 
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ why a page might look wrong. Coordinates are +Y up, in metres; angles in radians
11
11
 
12
12
  ## Docs
13
13
 
14
- - [Full reference](https://kevs3d.co.uk/phoria2/llms-full.txt): the whole API in one page - View, scene graph, drawables, geometry, materials, lights, interaction and dragging, charts, editing, models, motion, particles, plain-data scenes
14
+ - [Full reference](https://kevs3d.co.uk/phoria2/llms-full.txt): the whole API in one page - View, scene graph, drawables, geometry, materials (and shader hooks: GLSL of your own inside the material - waves, dissolves, glows), lights, interaction and dragging, charts, editing, models, motion, particles, plain-data scenes
15
15
  - [README](https://github.com/kevinroast/phoria2#readme): install, CDN use, the entry points
16
16
  - Source: the package ships its readable, commented source in `node_modules/phoria2/src` - read that, not the minified `dist/`, for anything the reference doesn't settle
17
17
  - [Chart spec JSON Schema](https://cdn.jsdelivr.net/npm/phoria2/schema/chart.schema.json): the spec `chart()` takes, as `phoria2/charts/schema.json` in the package
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoria2",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Small, friendly 3D for the web - a WebGL 2 renderer with charts, labels, picking, particles and glTF",
5
5
  "keywords": [
6
6
  "3d",
@@ -102,6 +102,10 @@
102
102
  "properties": {
103
103
  "type": {
104
104
  "const": "points"
105
+ },
106
+ "sort": {
107
+ "type": "boolean",
108
+ "description": "Draw the points furthest first each frame, for soft points that cover what is behind them."
105
109
  }
106
110
  },
107
111
  "unevaluatedProperties": false
@@ -113,6 +117,15 @@
113
117
  "properties": {
114
118
  "type": {
115
119
  "const": "lines"
120
+ },
121
+ "arrows": {
122
+ "enum": ["none", "start", "end", "both"],
123
+ "description": "Arrowheads at the ends of each chain of joined segments."
124
+ },
125
+ "arrowSize": {
126
+ "type": "number",
127
+ "description": "Arrowhead length in pixels; 0 sizes them from the material's width.",
128
+ "minimum": 0
116
129
  }
117
130
  },
118
131
  "unevaluatedProperties": false
@@ -916,6 +929,47 @@
916
929
  "description": "Lines: width in pixels.",
917
930
  "minimum": 0
918
931
  },
932
+ "dash": {
933
+ "type": "number",
934
+ "description": "Lines: dashed, with dashes this long in the model's units; 0 is solid.",
935
+ "minimum": 0
936
+ },
937
+ "gap": {
938
+ "type": "number",
939
+ "description": "Lines: the gaps between dashes; 0 is as long as the dashes.",
940
+ "minimum": 0
941
+ },
942
+ "dashOffset": {
943
+ "type": "number",
944
+ "description": "Lines: moves the dashes along the lines - animate it for flow."
945
+ },
946
+ "shader": {
947
+ "type": "object",
948
+ "description": "GLSL hooks run inside phoria2's own shader: vertex (change local, and n on meshes), fragment (change base and emissive before lighting; discard cuts) and lit (change color after lighting). Every hook can read uTime.",
949
+ "additionalProperties": false,
950
+ "properties": {
951
+ "vertex": { "type": "string" },
952
+ "fragment": { "type": "string" },
953
+ "lit": { "type": "string" },
954
+ "uniforms": {
955
+ "type": "object",
956
+ "description": "Values the hooks read, by GLSL name: a number, two to four numbers, a colour string, or a texture description.",
957
+ "additionalProperties": {
958
+ "anyOf": [
959
+ { "type": "number" },
960
+ { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 4 },
961
+ { "type": "string" },
962
+ { "type": "object", "required": ["url"] }
963
+ ]
964
+ }
965
+ },
966
+ "bounds": {
967
+ "type": "number",
968
+ "minimum": 0,
969
+ "description": "How far, in world units, the vertex hook can move a vertex; without it a vertex hook turns frustum culling off."
970
+ }
971
+ }
972
+ },
919
973
  "alphaTest": {
920
974
  "type": "number",
921
975
  "description": "Cut-outs: drop pixels with less alpha.",
@@ -1280,6 +1334,27 @@
1280
1334
  "type": "number"
1281
1335
  }
1282
1336
  },
1337
+ "sizes": {
1338
+ "type": "array",
1339
+ "description": "Points: one per vertex, a multiple of the material's size.",
1340
+ "items": {
1341
+ "type": "number"
1342
+ }
1343
+ },
1344
+ "opacities": {
1345
+ "type": "array",
1346
+ "description": "Points: one per vertex, 0-1, times the material's opacity.",
1347
+ "items": {
1348
+ "type": "number"
1349
+ }
1350
+ },
1351
+ "angles": {
1352
+ "type": "array",
1353
+ "description": "Points: one per vertex, radians anticlockwise on screen.",
1354
+ "items": {
1355
+ "type": "number"
1356
+ }
1357
+ },
1283
1358
  "indices": {
1284
1359
  "type": "array",
1285
1360
  "items": {