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.
- package/README.md +11 -9
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +527 -510
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1760 -1170
- package/dist/core.js.map +1 -1
- package/dist/debug.js +297 -0
- package/dist/debug.js.map +1 -0
- package/dist/edit.js +60 -60
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +32 -13
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +155 -155
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +188 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +187 -114
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +116 -104
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +95 -62
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/types/View.d.ts +27 -0
- package/dist/types/area.d.ts +18 -0
- package/dist/types/debug.d.ts +74 -0
- package/dist/types/explain.d.ts +4 -0
- package/dist/types/frame.d.ts +5 -0
- package/dist/types/geometry/Geometry.d.ts +16 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/material/Material.d.ts +67 -0
- package/dist/types/material/Texture.d.ts +18 -2
- package/dist/types/particles/Emitter.d.ts +39 -2
- package/dist/types/scene/drawables.d.ts +23 -2
- package/dist/types/watch.d.ts +8 -1
- package/dist/types/webgl/hooks.d.ts +48 -0
- package/dist/types/webgl/passes.d.ts +4 -0
- package/dist/types/webgl/programs.d.ts +15 -5
- package/dist/types/webgl/resources.d.ts +12 -2
- package/dist/types/webgl/shaders.d.ts +41 -5
- package/llms-full.txt +275 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/schema/scene.schema.json +75 -0
- package/src/View.js +121 -7
- package/src/area.js +131 -0
- package/src/charts/marks.js +29 -2
- package/src/debug.js +283 -0
- package/src/explain.js +19 -0
- package/src/frame.js +7 -2
- package/src/geometry/Geometry.js +16 -1
- package/src/index.js +1 -0
- package/src/material/Material.js +130 -4
- package/src/material/Texture.js +55 -2
- package/src/particles/Emitter.js +121 -10
- package/src/scene/drawables.js +89 -8
- package/src/watch.js +26 -3
- package/src/webgl/WebGLRenderer.js +55 -7
- package/src/webgl/hooks.js +121 -0
- package/src/webgl/passes.js +17 -2
- package/src/webgl/picking.js +45 -3
- package/src/webgl/programs.js +20 -9
- package/src/webgl/resources.js +169 -15
- 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.
|
|
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
package/schema/scene.schema.json
CHANGED
|
@@ -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": {
|