phoria2 1.0.0 → 1.2.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 +71 -13
- package/dist/bloom.js +1 -1
- package/dist/charts.js +397 -384
- package/dist/charts.js.map +1 -1
- package/dist/core.js +4451 -3568
- package/dist/core.js.map +1 -1
- package/dist/edit.js +67 -67
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +6 -2
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +211 -166
- package/dist/gltf.js.map +1 -1
- package/dist/motion.js +26 -26
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +49 -49
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +188 -86
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +8 -13
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +3 -3
- package/dist/tooltip.js.map +1 -1
- package/dist/types/OrbitControl.d.ts +6 -2
- package/dist/types/Raycaster.d.ts +10 -0
- package/dist/types/View.d.ts +64 -1
- package/dist/types/charts/Chart.d.ts +7 -0
- package/dist/types/explain.d.ts +3 -0
- package/dist/types/frame.d.ts +7 -0
- package/dist/types/geometry/Geometry.d.ts +14 -2
- package/dist/types/keyboard.d.ts +48 -0
- package/dist/types/labels.d.ts +4 -0
- package/dist/types/material/Material.d.ts +125 -0
- package/dist/types/material/Texture.d.ts +37 -5
- package/dist/types/scene/Node.d.ts +19 -14
- package/dist/types/scene/drawables.d.ts +32 -14
- package/dist/types/scene/lights.d.ts +16 -2
- package/dist/types/shadow.d.ts +2 -1
- package/dist/types/types.d.ts +14 -0
- package/dist/types/value/Color.d.ts +0 -4
- package/dist/types/value/Quat.d.ts +4 -2
- package/dist/types/webgl/WebGLRenderer.d.ts +6 -0
- package/dist/types/webgl/passes.d.ts +37 -13
- package/dist/types/webgl/resources.d.ts +24 -2
- package/dist/types/webgl/shaders.d.ts +4 -2
- package/llms-full.txt +153 -15
- package/package.json +4 -1
- package/schema/scene.schema.json +146 -1
- package/src/OrbitControl.js +7 -3
- package/src/Raycaster.js +58 -0
- package/src/View.js +207 -42
- package/src/charts/Chart.js +20 -2
- package/src/explain.js +6 -2
- package/src/frame.js +29 -9
- package/src/geometry/Geometry.js +24 -4
- package/src/gltf/load.js +50 -6
- package/src/keyboard.js +298 -0
- package/src/labels.js +4 -1
- package/src/material/Material.js +104 -19
- package/src/material/Texture.js +116 -11
- package/src/scene/Node.js +20 -9
- package/src/scene/Scene.js +2 -0
- package/src/scene/cameras.js +4 -1
- package/src/scene/drawables.js +48 -12
- package/src/scene/lights.js +40 -6
- package/src/shadow.js +27 -4
- package/src/types.js +6 -0
- package/src/value/Color.js +21 -0
- package/src/value/Quat.js +15 -6
- package/src/watch.js +25 -1
- package/src/webgl/WebGLRenderer.js +51 -12
- package/src/webgl/passes.js +190 -32
- package/src/webgl/picking.js +0 -1
- package/src/webgl/resources.js +89 -8
- package/src/webgl/shaders.js +350 -48
package/llms-full.txt
CHANGED
|
@@ -15,8 +15,8 @@ editors that drag, snap and undo. Picking, tooltips, hover highlights, HTML labe
|
|
|
15
15
|
image-based light are built in, and a view draws only when something changes, so idle pages cost
|
|
16
16
|
nothing. A dashboard of many small views just works - each tile its own `new View({ canvas })`,
|
|
17
17
|
with no limit on WebGL contexts to manage (after the first four, views share one). Apps come out short: typically a few dozen lines. For games, custom shaders or
|
|
18
|
-
post-processing pipelines, three.js is the better fit: phoria2 has no shader hook (no
|
|
19
|
-
|
|
18
|
+
post-processing pipelines, three.js is the better fit: phoria2 has no shader hook (no custom
|
|
19
|
+
effects), no refraction, and no shadow-softness setting.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -114,9 +114,13 @@ const view = new View({ canvas: '#c', orbit: true })
|
|
|
114
114
|
| `autoLight` | `true` | light a scene that has no lights (a chart needs none of its own); `false` to leave it dark |
|
|
115
115
|
| `pixelRatio` | `min(devicePixelRatio, 2)` | |
|
|
116
116
|
| `context` | `'auto'` | WebGL context: own for the first four views on a page, then one shared context - so a page can hold any number of views |
|
|
117
|
+
| `keyboard` | `false` | `true` or `{ label: (hit) => text }`: use without a pointer, with a screen reader - see Interaction |
|
|
118
|
+
| `description` | none | what the view shows, for screen readers (the canvas's label); made from the scene when `keyboard` is on |
|
|
119
|
+
| `reducedMotion` | `'auto'` | as the system's reduced-motion setting: `frame()` jumps rather than glides, auto-rotation holds, charts change at once. `true` or `false` to choose |
|
|
117
120
|
|
|
118
121
|
Members: `view.scene`, `view.camera` (assign a camera or a preset name to switch), `view.controls`
|
|
119
|
-
(the orbit control), `view.renderer`, `view.width`, `view.height
|
|
122
|
+
(the orbit control), `view.renderer`, `view.width`, `view.height`, `view.description`, `view.reducedMotion`
|
|
123
|
+
(true or false as it stands - check it for motion of your own, such as a timeline; set `'auto'`, true or false).
|
|
120
124
|
|
|
121
125
|
Methods:
|
|
122
126
|
|
|
@@ -164,7 +168,7 @@ something changes):
|
|
|
164
168
|
## Scene graph
|
|
165
169
|
|
|
166
170
|
Everything in a scene is a node. Node options (all optional): `name`, `position`, `rotation`
|
|
167
|
-
(Euler XYZ radians, `[x, y, z]`), `quaternion
|
|
171
|
+
(Euler XYZ radians, `[x, y, z]`), `quaternion` (`[x, y, z, w]` or `{ x, y, z, w }`), `scale` (number or `[x, y, z]`), `visible`,
|
|
168
172
|
`pickable` (default true), `userData`, `children`, `onUpdate`, `clip` (a section plane - see
|
|
169
173
|
Cutaways), plus the interaction options below.
|
|
170
174
|
|
|
@@ -229,7 +233,11 @@ bars.setColor(3, 'red')
|
|
|
229
233
|
`setInstance(i, { position, rotation, quaternion, scale, color })`. An instance's `color` multiplies the
|
|
230
234
|
material's colour, which is white unless you set one - so the instance colour shows as given. `count` defaults to the number
|
|
231
235
|
of rows in `data`. For very large data, write `bars.instanceMatrices` (16 numbers per instance,
|
|
232
|
-
column-major) directly and call `bars.needsUpdate()`.
|
|
236
|
+
column-major) directly and call `bars.needsUpdate()`. An instance scaled to 0 is hidden: it draws
|
|
237
|
+
nothing, and bounds, framing, shadows and labels leave it out - so a pool can keep spare instances.
|
|
238
|
+
Positions and quaternions also take `{ x, y, z }` and `{ x, y, z, w }`, as physics engines return
|
|
239
|
+
them, so a physics bridge is one line a body:
|
|
240
|
+
`bricks.setInstance(i, { position: body.translation(), quaternion: body.rotation() })`.
|
|
233
241
|
|
|
234
242
|
**Labels** are HTML elements pinned to 3D points (style them with CSS). Options: `text`, `position`,
|
|
235
243
|
`className`, `place` (`'center'` | `'above'` | `'below'` | `'left'` | `'right'`), `offset` ([x, y]
|
|
@@ -237,7 +245,8 @@ pixels), `priority` (higher wins when labels overlap), `overlap` (`'hide'` | `'d
|
|
|
237
245
|
a label does when covered), `occlude` (true: counts as covered when geometry is in front), `leader`
|
|
238
246
|
(true: a line from the point to the label, with a dot on the point - for callouts), `edge` (`'left'` |
|
|
239
247
|
`'right'`: set the label at that edge of the view, level with its point - callouts in a column, with
|
|
240
|
-
`leader: true`; `offset` then moves it in and up or down)
|
|
248
|
+
`leader: true`; `offset` then moves it in and up or down), `minScale` (show it only when zoomed in this
|
|
249
|
+
far - below).
|
|
241
250
|
|
|
242
251
|
A label answers the pointer when it has handlers: a Label with its own `onClick` or `onHover` is
|
|
243
252
|
clicked and hovered like a mesh (`new Label({ text: 'Mantle', onClick: () => show('mantle') })` - a
|
|
@@ -250,6 +259,21 @@ clicking that bar, with the same hit. Other labels let the pointer through to th
|
|
|
250
259
|
new InstancedMesh({ geometry: box(1), data: rows, labels: { text: (row) => row.name, place: 'above', priority: (row) => row.alert ? 2 : 0 } })
|
|
251
260
|
```
|
|
252
261
|
|
|
262
|
+
**Labels by zoom level**: `minScale` on any label shows it only when the view is zoomed in at least that
|
|
263
|
+
far - that many CSS pixels to one world unit, at the label's point (perspective or orthographic alike).
|
|
264
|
+
Until then it takes no part in the layout, so it can't push other labels out. `labels` can also be a
|
|
265
|
+
list, several labels per instance - the short one always, the details as you come closer:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
new InstancedMesh({ geometry: box(1), data: elements, labels: [
|
|
269
|
+
{ text: (e) => e.symbol, place: 'center', className: 'symbol' },
|
|
270
|
+
{ text: (e) => e.name, place: 'below', className: 'name', minScale: 45 },
|
|
271
|
+
] })
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Pick `minScale` by looking: at the zoom where the details should appear, a world unit spans that many
|
|
275
|
+
pixels (a 1-unit tile drawn 60 px wide is 60). `view.explain()` counts the labels still waiting.
|
|
276
|
+
|
|
253
277
|
## Geometry
|
|
254
278
|
|
|
255
279
|
Generators return a `Geometry`; each takes one options object (a number is the main size):
|
|
@@ -285,8 +309,11 @@ extrude({
|
|
|
285
309
|
})
|
|
286
310
|
```
|
|
287
311
|
|
|
288
|
-
Your own: `new Geometry({ positions, normals?, uvs?, colors?, indices? })` - flat arrays, xyz per
|
|
289
|
-
vertex; `colors` are rgb 0-1 per vertex (use `material: { vertexColors: true }`)
|
|
312
|
+
Your own: `new Geometry({ positions, normals?, uvs?, uvs2?, colors?, indices? })` - flat arrays, xyz per
|
|
313
|
+
vertex; `colors` are rgb 0-1 per vertex (use `material: { vertexColors: true }`); `uvs2` is a second UV
|
|
314
|
+
set, for baked occlusion and light maps (below). To animate one - cloth, a wave, a morph - write
|
|
315
|
+
`geometry.positions` in place and call `geometry.needsUpdate()` each frame; normals you didn't give are
|
|
316
|
+
worked out again to follow the shape.
|
|
290
317
|
|
|
291
318
|
## Materials
|
|
292
319
|
|
|
@@ -296,7 +323,14 @@ One material type, given as a plain object:
|
|
|
296
323
|
{ color: 'white' } // lit, physically based
|
|
297
324
|
{ color: '#c33', roughness: 0.2, metalness: 1 } // metal
|
|
298
325
|
{ color: 'orange', unlit: true } // flat colour, ignores lights
|
|
299
|
-
{ color: '#8cf', opacity: 0.4 } // transparent
|
|
326
|
+
{ color: '#8cf', opacity: 0.4 } // transparent: fades the whole surface
|
|
327
|
+
{ transmission: 1, roughness: 0.05 } // clear glass
|
|
328
|
+
{ color: '#a3121f', metalness: 0.6, clearcoat: 1 } // car paint: a glossy coat over it
|
|
329
|
+
{ color: '#1f2d6e', roughness: 0.9, sheen: 1 } // velvet: cloth's bright rim
|
|
330
|
+
{ color: '#ccc', metalness: 1, anisotropy: 0.8 } // brushed metal
|
|
331
|
+
{ color: '#f2e4c4', translucency: 0.8 } // paper: lit from behind too
|
|
332
|
+
{ matcap: 'clay.png' } // the look from a picture of a lit sphere
|
|
333
|
+
{ color: '#e4572e', toon: 3 } // cartoon: three flat tones
|
|
300
334
|
{ emissive: '#ff0', emissiveIntensity: 3 } // glows
|
|
301
335
|
{ map: 'wood.jpg', normalMap: 'wood_n.png' } // textures from urls
|
|
302
336
|
```
|
|
@@ -313,7 +347,8 @@ Properties: `color`, `opacity`, `roughness` (0.5), `metalness` (0), `emissive`,
|
|
|
313
347
|
(`'normal'`, `'additive'` - adds light, for glows and sparks - or `'multiply'` - darkens what is behind, as a colour
|
|
314
348
|
filter does: cyan over magenta is blue), `alphaTest` (cut-outs: drop pixels with less alpha - leaves, fences),
|
|
315
349
|
`map`, `normalMap`/`normalScale`, `bumpMap`/`bumpScale`, `roughnessMap`, `metalnessMap`, `aoMap`,
|
|
316
|
-
`emissiveMap
|
|
350
|
+
`emissiveMap`, `lightMap`/`lightMapIntensity`, `environmentIntensity` (1), `transmission`, `clearcoat`/`clearcoatRoughness`, `sheen`/`sheenColor`/`sheenRoughness`, `ior`/
|
|
351
|
+
`specularIntensity`/`specularColor`, `anisotropy`/`anisotropyRotation`, `translucency`/`translucencyColor`, `matcap`, `toon` (below); `size`/`sizeAttenuation` for Points, `width` for Lines. Change later through
|
|
317
352
|
`mesh.material.color.set('#0f0')`, `mesh.material.opacity = 0.5`, or a whole new one: `mesh.material = { color: 'red' }`
|
|
318
353
|
(options become a Material, as in the constructor; `mesh.geometry = box(2)` likewise).
|
|
319
354
|
|
|
@@ -324,6 +359,83 @@ into the distance. A cut-out (`alphaTest`) casts the shadow of what is left of i
|
|
|
324
359
|
data map (`normalMap`, `bumpMap`, `roughnessMap`, `metalnessMap`, `aoMap`) is read as data, not colour,
|
|
325
360
|
unless its `colorSpace` was set on purpose - `normalMap: await loadTexture('n.png')` just works.
|
|
326
361
|
|
|
362
|
+
**Canvas and video textures**: a texture's `image` can be a canvas - draw on it, then call
|
|
363
|
+
`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
|
|
365
|
+
for you, muted and looping so browsers let it start by itself; `texture.image` is that `<video>` -
|
|
366
|
+
pause, seek or unmute it as any other. A browser that blocks autoplay leaves it on its first frame
|
|
367
|
+
until `texture.image.play()` from a click. Video textures make no mipmaps (they would be remade
|
|
368
|
+
every frame) unless `mipmaps: true`. Put one on an unlit plane for a screen:
|
|
369
|
+
`{ map: new Texture({ url: 'promo.mp4', wrap: 'clamp' }), unlit: true }`.
|
|
370
|
+
|
|
371
|
+
**Placing a texture**: `repeat` ([u, v] times across), `offset`, and `rotation` (radians, about
|
|
372
|
+
`center`: [0, 0], the corner, by default - [0.5, 0.5] for the middle) - applied in that order, as glTF's
|
|
373
|
+
`KHR_texture_transform` does, and each map keeps its own: a fabric's colour can repeat 7 times while
|
|
374
|
+
its normal map repeats twice. Animate them to scroll or spin a pattern.
|
|
375
|
+
|
|
376
|
+
**Baked occlusion and light maps**: occlusion and lighting baked in Blender, Unreal or Unity need
|
|
377
|
+
every surface on its own patch of texture, so they come on a second UV set while the first tiles
|
|
378
|
+
colour and detail. Give the geometry `uvs2` and the texture `uvSet: 1` - glTF models do both
|
|
379
|
+
for you (`TEXCOORD_1`, `texCoord: 1`). Read through the first set, baked occlusion lands in the
|
|
380
|
+
wrong places as dark smudges. `lightMap` is light baked into a texture, added as light from all
|
|
381
|
+
round (`lightMapIntensity`, 1) - the soft, cheap lighting of architectural scenes.
|
|
382
|
+
|
|
383
|
+
**Environment strength per material**: `environmentIntensity` (1) scales how much of the
|
|
384
|
+
scene's environment - its light and its reflections - one material takes: lower for a matte floor in
|
|
385
|
+
a shiny showroom, higher to make one product catch the light, without changing the scene's.
|
|
386
|
+
|
|
387
|
+
**Glass**: `transmission` (0-1) lets light through as glass does. Unlike `opacity`, which fades
|
|
388
|
+
everything - reflections too, so faded things look washed out, as dimmed chart bars should - glass
|
|
389
|
+
keeps its reflections whole, and shows less of what is behind it towards its edges, where it
|
|
390
|
+
reflects nearly everything. Its colour shows in proportion to `1 - transmission`: `transmission: 0.6`
|
|
391
|
+
with a colour is tinted glass. Keep `roughness` low for clear glass, higher for frosted. There is
|
|
392
|
+
no refraction - what is behind isn't bent. It shines with an `environment` to reflect
|
|
393
|
+
(`'studio'`, or better an `.hdr`).
|
|
394
|
+
|
|
395
|
+
**Clearcoat**: `clearcoat` (0-1) puts a clear glossy layer over the surface - car paint, lacquered
|
|
396
|
+
wood, varnish - with its own sharp reflection over the base's softer one; `clearcoatRoughness` (0,
|
|
397
|
+
a mirror) makes it satin. Normal and bump maps show under a smooth coat.
|
|
398
|
+
|
|
399
|
+
**Sheen**: `sheen` (0-1) is the soft bright rim of cloth at grazing angles - velvet, felt,
|
|
400
|
+
upholstery - which plain roughness can't give: without it fabric looks like matte plastic.
|
|
401
|
+
`sheenColor` (white) tints it, often lighter than the cloth or another colour entirely;
|
|
402
|
+
`sheenRoughness` (0.5) runs from a thin bright rim (0) to a broad soft glow (1).
|
|
403
|
+
|
|
404
|
+
**Reflectance**: `ior` (1.5) sets how strongly a non-metal reflects - water 1.33, glass and plastic
|
|
405
|
+
1.5, diamond 2.4 - and glass with `transmission` reflects more the higher it is.
|
|
406
|
+
`specularIntensity` (1) scales a non-metal's reflections, 0 for none (chalk, clay, unglazed
|
|
407
|
+
pottery); `specularColor` tints them face on. Metals take their reflections from `color`, so
|
|
408
|
+
these leave them alone.
|
|
409
|
+
|
|
410
|
+
**Brushed metal**: `anisotropy` (0-1) - the material's, not a texture's filtering - draws highlights and reflections out along the brushing -
|
|
411
|
+
brushed aluminium, steel appliances, the bottom of a pan. The brushing runs along the UVs' u (round
|
|
412
|
+
a sphere or cylinder, say; round the up axis on a mesh with no UVs); `anisotropyRotation`
|
|
413
|
+
(radians) turns it - `Math.PI / 2` runs it along v. Use it with `metalness: 1` and a middling
|
|
414
|
+
`roughness` (0.2-0.4).
|
|
415
|
+
|
|
416
|
+
**Translucency**: `translucency` (0-1) lets light through thin surfaces, so they are lit from
|
|
417
|
+
behind as well as in front - leaves against the sun, paper lanterns and lampshades with a light
|
|
418
|
+
inside, curtains. What passes through is taken from what is scattered back, so at 1 a surface is lit
|
|
419
|
+
only through it. `translucencyColor` (white) tints the light that comes through. It is light
|
|
420
|
+
through a thin sheet, not light scattering inside a solid: a lantern glows, marble stays marble.
|
|
421
|
+
|
|
422
|
+
**Matcap**: `matcap` is a picture of a lit sphere - clay, wax, chrome, a painted ball - that gives the
|
|
423
|
+
whole look: each point takes the colour from where a sphere facing the same way would show it. It is
|
|
424
|
+
the clay look of sculpting tools and CAD viewers, good for reading a model's shape, and costs one
|
|
425
|
+
texture lookup - no lights, shadows or environment, which it ignores. Its shine stays put as the camera
|
|
426
|
+
turns, since the light is in the picture, so it looks out of place beside lit things. `color` and
|
|
427
|
+
`map` multiply it, so one grey matcap serves any colour; normal and bump maps and `flatShading`
|
|
428
|
+
still shape it. Any matcap image works, or draw one on a canvas (`new Texture({ image: canvas })`).
|
|
429
|
+
|
|
430
|
+
**Toon**: `toon: 3` shades in that many flat tones (2 or more; 0, the default, is smooth), with one
|
|
431
|
+
hard-edged highlight - a cartoon. Lights and shadows work as usual; the shaded tone is the scene's
|
|
432
|
+
ambient and hemisphere light, so add some for a lighter shade. Metals keep their colour, with a
|
|
433
|
+
highlight of it. Pair it with `outline` on the nodes for inked edges.
|
|
434
|
+
|
|
435
|
+
glTF models bring glass, clearcoat, sheen, reflectance, brushed metal and translucency from `KHR_materials_transmission`, `KHR_materials_clearcoat`,
|
|
436
|
+
`KHR_materials_sheen`, `KHR_materials_ior`, `KHR_materials_specular`, `KHR_materials_anisotropy`
|
|
437
|
+
and `KHR_materials_diffuse_transmission` (by their factors; their textures aren't read).
|
|
438
|
+
|
|
327
439
|
**Models without UVs** (scans, CAD exports) can still take maps: `triplanar: 40` projects them
|
|
328
440
|
from the mesh's three axes instead of using UVs, repeating 40 times per unit of the mesh's own space.
|
|
329
441
|
The maps stay on the mesh as it moves; normal maps follow the surface; a map's `repeat` and `offset` apply
|
|
@@ -339,7 +451,7 @@ on top, as with UVs (animate `offset` to scroll a pattern). 0 (the default) uses
|
|
|
339
451
|
|---|---|
|
|
340
452
|
| `AmbientLight` | `color`, `intensity` |
|
|
341
453
|
| `HemisphereLight` | `sky`, `ground`, `intensity` |
|
|
342
|
-
| `DirectionalLight` | `color`, `intensity`, `direction` (the way it travels, e.g. `[-1, -2, -1]`), `castShadow` |
|
|
454
|
+
| `DirectionalLight` | `color`, `intensity`, `direction` (the way it travels, e.g. `[-1, -2, -1]`), `castShadow`, `shadowBounds` |
|
|
343
455
|
| `PointLight` | `color`, `intensity`, `position`, `range` - inverse-square falloff, so intensity ~ distance squared (30 lights things ~5 m away); `castShadow` |
|
|
344
456
|
| `SpotLight` | point light options plus `direction`, `angle`, `penumbra`, `castShadow` |
|
|
345
457
|
|
|
@@ -357,7 +469,9 @@ meshes opt in with `castShadow` and `receiveShadow`. No shadow camera to set up.
|
|
|
357
469
|
lamp - anything whose bounds hold the light - doesn't block it, so a lamp's shade can cast shadows of
|
|
358
470
|
its own. A point light casts all round, and everything that casts blocks it, around it too: a candle in
|
|
359
471
|
a lantern or a carved pumpkin lights the room only through the openings. Give a bulb or flame mesh
|
|
360
|
-
drawn at the light `castShadow: false`.
|
|
472
|
+
drawn at the light `castShadow: false`. The directional shadow is fitted to whatever casts, every
|
|
473
|
+
frame; when things fly far off, give the light `shadowBounds: { min: [x, y, z], max: [x, y, z] }` -
|
|
474
|
+
the world area that gets shadows - so the map stays sharp there.
|
|
361
475
|
|
|
362
476
|
```js
|
|
363
477
|
scene.add(new PointLight({ position: [0, 0.1, 0], intensity: 0.5, range: 4, castShadow: true })) // a candle in a jack-o'-lantern
|
|
@@ -383,6 +497,9 @@ and `target` yourself; the orbit always turns about wherever the target is.
|
|
|
383
497
|
press or the wheel takes over.
|
|
384
498
|
- **Back to the start**: `view.frame(undefined, { direction: 'perspective', duration: 0.6 })`
|
|
385
499
|
(or the preset the view started with).
|
|
500
|
+
- **Where framing would go**: `view.framing(target, { padding, direction })` returns
|
|
501
|
+
`{ position, target, height? }` without moving the camera - for a camera that eases after
|
|
502
|
+
something that keeps changing size (`camera.position.lerp(aim.position, 0.1)`, the same for target).
|
|
386
503
|
- **Follow something that moves** - move the camera and its target by as much as it moved, so the
|
|
387
504
|
view keeps its angle and the user can still orbit round it:
|
|
388
505
|
|
|
@@ -438,7 +555,8 @@ view.on('hover', (hit) => show(hit?.datum)) // fires when the th
|
|
|
438
555
|
children. A node's `highlight` is the colour for its subtree; it tints the mesh, instance or point
|
|
439
556
|
under the pointer. Add `highlightWhole: true` to tint the whole subtree as one thing (a piece of
|
|
440
557
|
furniture, a machine) whichever part is hovered. Without the View's `highlight`, only nodes with
|
|
441
|
-
their own highlight light up - leave it off to keep floors and walls plain
|
|
558
|
+
their own highlight light up - leave it off to keep floors and walls plain, or keep it and give
|
|
559
|
+
those `highlight: false` (`null` means "the View's").
|
|
442
560
|
- `onHover` and `onClick` are the app's own handler for a node; to add more without replacing it
|
|
443
561
|
(an add-on, a second concern), `node.on('hover' | 'click', fn)` - it returns a function that stops
|
|
444
562
|
listening. Both run. A drag has one handler, `onDrag`: one thing moves a node at a time.
|
|
@@ -458,8 +576,27 @@ chair.onDrag = (drag) => {
|
|
|
458
576
|
```
|
|
459
577
|
|
|
460
578
|
- **Outline**: `outline: '#ffcc00'` or `{ color, width }` (pixels, up to 8; default 2) on a node draws
|
|
461
|
-
a line round its silhouette, over everything - for what is selected
|
|
579
|
+
a line round its silhouette, over everything - for what is selected, or as an inked edge for a
|
|
580
|
+
drawn look. Set it any time; `null` removes it. Outlines are antialiased - soft edges and round
|
|
581
|
+
corners - as the canvas is; `outlineAntialias: false` on the View (or `view.renderer.outlineAntialias =
|
|
582
|
+
false`, any time) draws them in hard pixel steps instead.
|
|
462
583
|
- `view.controls.cancel()` lets go of the orbit gesture in progress, for apps that take over a press.
|
|
584
|
+
- **Keyboard and screen readers**: `keyboard: true` on the View makes the canvas a stop in the page's
|
|
585
|
+
tab order. The arrow keys (and Home, End, PageUp, PageDown) step through what can be hovered or
|
|
586
|
+
clicked, in scene order and each mesh's instances in order: each instance, point, surface grid
|
|
587
|
+
point (a mesh with a row per vertex) or row of data under a node with its own `tooltip`, `onClick`,
|
|
588
|
+
`onHover`, `onDrag` or `highlight` - and, when the View has a tooltip or handlers itself, each one
|
|
589
|
+
with data. A mesh with no rows is a stop when it has a name or a handler of its own. A
|
|
590
|
+
`highlightWhole` subtree is one stop. Lines with a row per segment (a network's links) are a stop
|
|
591
|
+
per segment, after everything else. The one stepped to is shown as hovering would show it - tint (`#ffcc00` when no highlight
|
|
592
|
+
is set), tooltip, hover handlers - and Enter or Space clicks it (the handler's event is null);
|
|
593
|
+
Escape leaves it, and moving the pointer takes over. A screen reader hears each one: the tooltip's
|
|
594
|
+
text, else its data, else its name, with "3 of 12" - or `keyboard: { label: (hit) => text }`. The
|
|
595
|
+
canvas is labelled with `description`, or from the scene: each chart's `summary()` ("Sales: bar
|
|
596
|
+
chart of 12 rows by month, sales"), else "3D view of 12 items".
|
|
597
|
+
- **Reduced motion**: `reducedMotion` follows the system's setting by default. When on, `frame()`
|
|
598
|
+
with a `duration` jumps, `autoRotate` holds, and chart transitions and flowing links settle at
|
|
599
|
+
once. Motion of your own (a `Timeline`, `onUpdate` spin) can check `view.reducedMotion`.
|
|
463
600
|
|
|
464
601
|
## Charts: `phoria2/charts`
|
|
465
602
|
|
|
@@ -511,7 +648,8 @@ A chart is a node with axes, ticks, grid walls, a legend and tooltips built in.
|
|
|
511
648
|
in it as a wrapping row, so give it no fixed height). Categories get swatches that click to hide
|
|
512
649
|
and show; a number colour gets a key - a ramp, highest at the top, with values marked up its side,
|
|
513
650
|
`ticks` as on an axis),
|
|
514
|
-
`theme: 'dark' | 'light'`, `title`, `
|
|
651
|
+
`theme: 'dark' | 'light'`, `title`, `description` (what a screen reader hears for it; else `chart.summary()`
|
|
652
|
+
makes one from the title and channels), `transition: false | { duration, ease }`,
|
|
515
653
|
`overlap: 'dim' | 'hide' | 'show'` (labels), `highlight` (a colour, false, or for networks
|
|
516
654
|
`'neighbours'` - the default, lighting the node's links and linked nodes - or `'node'`, the node alone), and node options (`position`, ...).
|
|
517
655
|
- **Surfaces**: one row per grid point - `x` and `z` are the grid (numbers or categories), `y` the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phoria2",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -85,10 +85,13 @@
|
|
|
85
85
|
"plugin": "node scripts/plugin.mjs"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
+
"@dimforge/rapier3d-compat": "^0.21.0",
|
|
88
89
|
"@eslint/js": "^10.0.1",
|
|
89
90
|
"eslint": "^10.3.0",
|
|
90
91
|
"gl-matrix": "^3.4.4",
|
|
91
92
|
"globals": "^17.6.0",
|
|
93
|
+
"manifold-3d": "^3.5.4",
|
|
94
|
+
"recast-navigation": "^0.44.0",
|
|
92
95
|
"three": "0.186.1",
|
|
93
96
|
"typescript": "^7.0.2",
|
|
94
97
|
"vite": "^8.0.12",
|
package/schema/scene.schema.json
CHANGED
|
@@ -202,6 +202,11 @@
|
|
|
202
202
|
"type": "null"
|
|
203
203
|
}
|
|
204
204
|
]
|
|
205
|
+
},
|
|
206
|
+
"minScale": {
|
|
207
|
+
"type": "number",
|
|
208
|
+
"description": "Show the label only when zoomed in this far: CSS pixels to one world unit at its point (0, always).",
|
|
209
|
+
"minimum": 0
|
|
205
210
|
}
|
|
206
211
|
},
|
|
207
212
|
"unevaluatedProperties": false
|
|
@@ -327,6 +332,16 @@
|
|
|
327
332
|
},
|
|
328
333
|
"castShadow": {
|
|
329
334
|
"type": "boolean"
|
|
335
|
+
},
|
|
336
|
+
"shadowBounds": {
|
|
337
|
+
"type": ["object", "null"],
|
|
338
|
+
"description": "The part of the world, in world units, that has shadows; null (the default) follows what casts.",
|
|
339
|
+
"required": ["min", "max"],
|
|
340
|
+
"properties": {
|
|
341
|
+
"min": { "$ref": "#/$defs/vec3" },
|
|
342
|
+
"max": { "$ref": "#/$defs/vec3" }
|
|
343
|
+
},
|
|
344
|
+
"additionalProperties": false
|
|
330
345
|
}
|
|
331
346
|
},
|
|
332
347
|
"unevaluatedProperties": false
|
|
@@ -462,13 +477,16 @@
|
|
|
462
477
|
"description": "Anything of your own."
|
|
463
478
|
},
|
|
464
479
|
"highlight": {
|
|
465
|
-
"description": "Hover tint for this subtree,
|
|
480
|
+
"description": "Hover tint for this subtree; false for none, null to use the View's.",
|
|
466
481
|
"anyOf": [
|
|
467
482
|
{
|
|
468
483
|
"$ref": "#/$defs/color"
|
|
469
484
|
},
|
|
470
485
|
{
|
|
471
486
|
"type": "null"
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"const": false
|
|
472
490
|
}
|
|
473
491
|
]
|
|
474
492
|
},
|
|
@@ -703,6 +721,23 @@
|
|
|
703
721
|
"minItems": 2,
|
|
704
722
|
"maxItems": 2
|
|
705
723
|
},
|
|
724
|
+
"rotation": {
|
|
725
|
+
"type": "number",
|
|
726
|
+
"description": "Turns the image on the surface, in radians, about center."
|
|
727
|
+
},
|
|
728
|
+
"center": {
|
|
729
|
+
"type": "array",
|
|
730
|
+
"description": "The point rotation turns about, in repeated UVs ([0, 0], the corner, by default).",
|
|
731
|
+
"items": {
|
|
732
|
+
"type": "number"
|
|
733
|
+
},
|
|
734
|
+
"minItems": 2,
|
|
735
|
+
"maxItems": 2
|
|
736
|
+
},
|
|
737
|
+
"uvSet": {
|
|
738
|
+
"enum": [0, 1],
|
|
739
|
+
"description": "Which UV set to read: 0 the first, 1 the second (uvs2) - baked occlusion and light maps usually come on the second."
|
|
740
|
+
},
|
|
706
741
|
"filter": {
|
|
707
742
|
"enum": ["linear", "nearest"]
|
|
708
743
|
},
|
|
@@ -801,6 +836,28 @@
|
|
|
801
836
|
}
|
|
802
837
|
]
|
|
803
838
|
},
|
|
839
|
+
"lightMap": {
|
|
840
|
+
"description": "Light baked into a texture, added as light from all round - usually on the second UV set.",
|
|
841
|
+
"anyOf": [
|
|
842
|
+
{
|
|
843
|
+
"$ref": "#/$defs/texture"
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
"type": "null"
|
|
847
|
+
}
|
|
848
|
+
]
|
|
849
|
+
},
|
|
850
|
+
"matcap": {
|
|
851
|
+
"description": "A picture of a lit sphere that gives the whole look - clay, wax, chrome - picked by which way the surface faces the camera; lights are ignored.",
|
|
852
|
+
"anyOf": [
|
|
853
|
+
{
|
|
854
|
+
"$ref": "#/$defs/texture"
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
"type": "null"
|
|
858
|
+
}
|
|
859
|
+
]
|
|
860
|
+
},
|
|
804
861
|
"unlit": {
|
|
805
862
|
"type": "boolean",
|
|
806
863
|
"description": "Ignore lights: draws exactly its colour."
|
|
@@ -873,6 +930,15 @@
|
|
|
873
930
|
"minimum": 0,
|
|
874
931
|
"maximum": 1
|
|
875
932
|
},
|
|
933
|
+
"lightMapIntensity": {
|
|
934
|
+
"type": "number",
|
|
935
|
+
"minimum": 0
|
|
936
|
+
},
|
|
937
|
+
"environmentIntensity": {
|
|
938
|
+
"type": "number",
|
|
939
|
+
"description": "This material's share of the environment's light and reflections (1).",
|
|
940
|
+
"minimum": 0
|
|
941
|
+
},
|
|
876
942
|
"triplanar": {
|
|
877
943
|
"type": "number",
|
|
878
944
|
"description": "Repeats per unit, projecting maps from three axes instead of using UVs; 0 uses the UVs.",
|
|
@@ -881,6 +947,78 @@
|
|
|
881
947
|
"bumpScale": {
|
|
882
948
|
"type": "number"
|
|
883
949
|
},
|
|
950
|
+
"transmission": {
|
|
951
|
+
"type": "number",
|
|
952
|
+
"description": "How much light passes through, as through glass: reflections stay whole, and less of what is behind shows towards the edges. Unlike opacity, which fades the whole surface.",
|
|
953
|
+
"minimum": 0,
|
|
954
|
+
"maximum": 1
|
|
955
|
+
},
|
|
956
|
+
"clearcoat": {
|
|
957
|
+
"type": "number",
|
|
958
|
+
"description": "A clear glossy layer over the surface - car paint, lacquer, varnish.",
|
|
959
|
+
"minimum": 0,
|
|
960
|
+
"maximum": 1
|
|
961
|
+
},
|
|
962
|
+
"clearcoatRoughness": {
|
|
963
|
+
"type": "number",
|
|
964
|
+
"description": "How glossy the coat is, 0 a mirror.",
|
|
965
|
+
"minimum": 0,
|
|
966
|
+
"maximum": 1
|
|
967
|
+
},
|
|
968
|
+
"sheen": {
|
|
969
|
+
"type": "number",
|
|
970
|
+
"description": "The soft bright rim of cloth at grazing angles - velvet, felt, upholstery.",
|
|
971
|
+
"minimum": 0,
|
|
972
|
+
"maximum": 1
|
|
973
|
+
},
|
|
974
|
+
"sheenColor": {
|
|
975
|
+
"$ref": "#/$defs/color"
|
|
976
|
+
},
|
|
977
|
+
"sheenRoughness": {
|
|
978
|
+
"type": "number",
|
|
979
|
+
"description": "A thin bright rim at 0, a broad soft glow at 1.",
|
|
980
|
+
"minimum": 0,
|
|
981
|
+
"maximum": 1
|
|
982
|
+
},
|
|
983
|
+
"ior": {
|
|
984
|
+
"type": "number",
|
|
985
|
+
"description": "Index of refraction: how strongly a non-metal reflects - water 1.33, glass and plastic 1.5 (the default), diamond 2.4.",
|
|
986
|
+
"minimum": 1
|
|
987
|
+
},
|
|
988
|
+
"specularIntensity": {
|
|
989
|
+
"type": "number",
|
|
990
|
+
"description": "Scales a non-metal's reflections, 0 for none.",
|
|
991
|
+
"minimum": 0,
|
|
992
|
+
"maximum": 1
|
|
993
|
+
},
|
|
994
|
+
"specularColor": {
|
|
995
|
+
"$ref": "#/$defs/color"
|
|
996
|
+
},
|
|
997
|
+
"anisotropy": {
|
|
998
|
+
"type": "number",
|
|
999
|
+
"description": "Brushed metal: highlights drawn out along the brushing, which runs along the UVs' u.",
|
|
1000
|
+
"minimum": 0,
|
|
1001
|
+
"maximum": 1
|
|
1002
|
+
},
|
|
1003
|
+
"anisotropyRotation": {
|
|
1004
|
+
"type": "number",
|
|
1005
|
+
"description": "Turns the brushing from the UVs' u, in radians."
|
|
1006
|
+
},
|
|
1007
|
+
"translucency": {
|
|
1008
|
+
"type": "number",
|
|
1009
|
+
"description": "Light through thin surfaces, lighting them from behind - leaves, paper, lampshades.",
|
|
1010
|
+
"minimum": 0,
|
|
1011
|
+
"maximum": 1
|
|
1012
|
+
},
|
|
1013
|
+
"translucencyColor": {
|
|
1014
|
+
"$ref": "#/$defs/color"
|
|
1015
|
+
},
|
|
1016
|
+
"toon": {
|
|
1017
|
+
"type": "number",
|
|
1018
|
+
"description": "Cartoon shading in this many flat tones (2 or more), with a hard-edged highlight; 0 is smooth.",
|
|
1019
|
+
"minimum": 0,
|
|
1020
|
+
"maximum": 16
|
|
1021
|
+
},
|
|
884
1022
|
"blending": {
|
|
885
1023
|
"enum": ["normal", "additive", "multiply"]
|
|
886
1024
|
}
|
|
@@ -1129,6 +1267,13 @@
|
|
|
1129
1267
|
"type": "number"
|
|
1130
1268
|
}
|
|
1131
1269
|
},
|
|
1270
|
+
"uvs2": {
|
|
1271
|
+
"type": "array",
|
|
1272
|
+
"description": "A second UV set, for baked occlusion and light maps.",
|
|
1273
|
+
"items": {
|
|
1274
|
+
"type": "number"
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1132
1277
|
"colors": {
|
|
1133
1278
|
"type": "array",
|
|
1134
1279
|
"items": {
|
package/src/OrbitControl.js
CHANGED
|
@@ -255,8 +255,12 @@ export class OrbitControl {
|
|
|
255
255
|
this.target.add(move)
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Apply inertia and auto-rotate; the View calls this each frame.
|
|
260
|
+
* @param {number} dt seconds
|
|
261
|
+
* @param {boolean} [still] hold auto-rotation - the View passes its reducedMotion
|
|
262
|
+
*/
|
|
263
|
+
update(dt, still = false) {
|
|
260
264
|
if (!this.enabled) return
|
|
261
265
|
if (this.#pointers.size === 0) {
|
|
262
266
|
const v = this.#velocity
|
|
@@ -267,7 +271,7 @@ export class OrbitControl {
|
|
|
267
271
|
v.yaw *= keep
|
|
268
272
|
v.pitch *= keep
|
|
269
273
|
}
|
|
270
|
-
if (this.autoRotate) this.rotate(-(this.autoRotate === true ? 0.5 : this.autoRotate) * dt, 0)
|
|
274
|
+
if (this.autoRotate && !still) this.rotate(-(this.autoRotate === true ? 0.5 : this.autoRotate) * dt, 0)
|
|
271
275
|
}
|
|
272
276
|
}
|
|
273
277
|
|
package/src/Raycaster.js
CHANGED
|
@@ -10,6 +10,7 @@ import { BVH_MIN_TRIANGLES, bvhOf, walk } from './geometry/bvh.js'
|
|
|
10
10
|
/** @import { InstancedMesh, Lines, Mesh, Points } from './scene/drawables.js' */
|
|
11
11
|
|
|
12
12
|
/** The section plane cutting what is being tested: world plane, and whether its cut faces are filled. @typedef {{ plane: Float32Array, cap: boolean }} Cut */
|
|
13
|
+
/** @internal A surface ready for many rays: a mesh or one instance, with its world matrix and bounding sphere. @typedef {{ node: any, world: Float32Array, instance: number | null, cut: Cut | null, x: number, y: number, z: number, radius: number }} Solid */
|
|
13
14
|
|
|
14
15
|
// Nodes are told apart by markers (drawMode, instanceMatrices) rather than instanceof, so picking
|
|
15
16
|
// doesn't pull every node class into an app's bundle.
|
|
@@ -119,6 +120,63 @@ export class Raycaster {
|
|
|
119
120
|
return hits.filter((h) => h.distance >= this.near && h.distance <= this.far).sort((x, y) => x.distance - y.distance)
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/** @internal
|
|
124
|
+
* The solid surfaces of a frame's draw list, ready for many rays: each mesh and each instance with its
|
|
125
|
+
* world matrix and bounding sphere worked out once, rather than once per ray - for label occlusion,
|
|
126
|
+
* which casts a ray per label.
|
|
127
|
+
* @param {{ node: Node, worldMatrix: Float32Array, instances: { count: number, instanceMatrices: Float32Array } | null, clip: { plane: Float32Array, cap: unknown } | null }[]} items
|
|
128
|
+
* @returns {Solid[]}
|
|
129
|
+
*/
|
|
130
|
+
static solids(items) {
|
|
131
|
+
/** @type {Solid[]} */
|
|
132
|
+
const out = []
|
|
133
|
+
for (const item of items) {
|
|
134
|
+
const n = /** @type {any} */ (item.node)
|
|
135
|
+
if (n.drawMode !== 'triangles' || !n.geometry.vertexCount) continue
|
|
136
|
+
const box = n.geometry.bounds, center = box.getCenter(), radius = box.getRadius()
|
|
137
|
+
const cut = item.clip ? { plane: item.clip.plane, cap: !!item.clip.cap } : null
|
|
138
|
+
/** @param {Float32Array} world @param {number | null} instance */
|
|
139
|
+
const add = (world, instance) => {
|
|
140
|
+
const c = center.clone().applyMatrix4(world)
|
|
141
|
+
const scale = Math.max(Math.hypot(world[0], world[1], world[2]), Math.hypot(world[4], world[5], world[6]), Math.hypot(world[8], world[9], world[10]))
|
|
142
|
+
out.push({ node: n, world, instance, cut, x: c[0], y: c[1], z: c[2], radius: radius * scale })
|
|
143
|
+
}
|
|
144
|
+
if (item.instances) {
|
|
145
|
+
for (let i = 0; i < item.instances.count; i++) {
|
|
146
|
+
add(mat4.multiply(new Float32Array(16), item.worldMatrix, item.instances.instanceMatrices.subarray(i * 16, i * 16 + 16)), i)
|
|
147
|
+
}
|
|
148
|
+
} else add(Float32Array.from(item.worldMatrix), null)
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @internal
|
|
154
|
+
* Whether the ray meets any of these solids nearer than `distance` - the first one found settles it,
|
|
155
|
+
* so nothing is sorted. One node, or one instance of it, can be left out: what a label is on.
|
|
156
|
+
* @param {Solid[]} solids from Raycaster.solids() @param {number} distance
|
|
157
|
+
* @param {Node | null} [skip] @param {number | null} [skipInstance] null for the whole node
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
blocked(solids, distance, skip = null, skipInstance = null) {
|
|
161
|
+
const o = this.ray.origin, d = this.ray.direction
|
|
162
|
+
/** @type {Hit[]} */
|
|
163
|
+
const hits = []
|
|
164
|
+
for (const s of solids) {
|
|
165
|
+
if (s.node === skip && (skipInstance === null || s.instance === skipInstance)) continue
|
|
166
|
+
// its bounding sphere first: missed, or wholly beyond the distance or behind the ray, it can't block
|
|
167
|
+
const ox = s.x - o[0], oy = s.y - o[1], oz = s.z - o[2]
|
|
168
|
+
const along = ox * d[0] + oy * d[1] + oz * d[2]
|
|
169
|
+
const off = ox * ox + oy * oy + oz * oz - along * along, r2 = s.radius * s.radius
|
|
170
|
+
if (off > r2) continue
|
|
171
|
+
const half = Math.sqrt(r2 - off)
|
|
172
|
+
if (along - half > distance || along + half < 0) continue
|
|
173
|
+
hits.length = 0
|
|
174
|
+
this.#mesh(s.node, s.world, s.instance, hits, s.cut)
|
|
175
|
+
if (hits.length && hits[0].distance < distance) return true
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
/** @internal
|
|
123
181
|
* Where the ray meets one instance, point or segment of a drawable - the CPU half of GPU picking,
|
|
124
182
|
* which finds what is under the pointer and leaves the exact point, face and uv to this.
|