@opsydyn/effect-three 0.3.0 → 0.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 (125) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +12 -0
  3. package/dist/{index-8cf8t6by.js → index-h3vf50fn.js} +2113 -83
  4. package/dist/index-h3vf50fn.js.map +53 -0
  5. package/dist/index-ttwy0znv.js +145 -0
  6. package/dist/index-ttwy0znv.js.map +12 -0
  7. package/dist/index.js +488 -147
  8. package/dist/index.js.map +8 -6
  9. package/dist/kernel/boxSelect.d.ts +22 -0
  10. package/dist/kernel/boxSelect.d.ts.map +1 -0
  11. package/dist/kernel/build.d.ts +142 -6
  12. package/dist/kernel/build.d.ts.map +1 -1
  13. package/dist/kernel/cameraShake.d.ts.map +1 -1
  14. package/dist/kernel/describeScene.d.ts +50 -0
  15. package/dist/kernel/describeScene.d.ts.map +1 -0
  16. package/dist/kernel/description.d.ts +155 -1
  17. package/dist/kernel/description.d.ts.map +1 -1
  18. package/dist/kernel/diff.d.ts +15 -0
  19. package/dist/kernel/diff.d.ts.map +1 -1
  20. package/dist/kernel/equality.d.ts +9 -1
  21. package/dist/kernel/equality.d.ts.map +1 -1
  22. package/dist/kernel/gizmo.d.ts +69 -0
  23. package/dist/kernel/gizmo.d.ts.map +1 -0
  24. package/dist/kernel/history.d.ts +61 -0
  25. package/dist/kernel/history.d.ts.map +1 -0
  26. package/dist/kernel/index.d.ts +13 -2
  27. package/dist/kernel/index.d.ts.map +1 -1
  28. package/dist/kernel/index.js +106 -3
  29. package/dist/kernel/index.js.map +1 -1
  30. package/dist/kernel/inspect.d.ts +85 -0
  31. package/dist/kernel/inspect.d.ts.map +1 -0
  32. package/dist/kernel/lineGeometry.d.ts +25 -0
  33. package/dist/kernel/lineGeometry.d.ts.map +1 -0
  34. package/dist/kernel/lockstep.d.ts +114 -0
  35. package/dist/kernel/lockstep.d.ts.map +1 -0
  36. package/dist/kernel/loop.d.ts.map +1 -1
  37. package/dist/kernel/materials.d.ts.map +1 -1
  38. package/dist/kernel/pivot.d.ts +54 -0
  39. package/dist/kernel/pivot.d.ts.map +1 -0
  40. package/dist/kernel/pointBuffers.d.ts +17 -0
  41. package/dist/kernel/pointBuffers.d.ts.map +1 -0
  42. package/dist/kernel/pointer.d.ts +12 -0
  43. package/dist/kernel/pointer.d.ts.map +1 -1
  44. package/dist/kernel/renderKit.d.ts +21 -1
  45. package/dist/kernel/renderKit.d.ts.map +1 -1
  46. package/dist/kernel/renderer.d.ts +6 -0
  47. package/dist/kernel/renderer.d.ts.map +1 -1
  48. package/dist/kernel/sceneChanges.d.ts +87 -0
  49. package/dist/kernel/sceneChanges.d.ts.map +1 -0
  50. package/dist/kernel/sceneGraph.d.ts +6 -0
  51. package/dist/kernel/sceneGraph.d.ts.map +1 -1
  52. package/dist/kernel/seeded.d.ts +9 -0
  53. package/dist/kernel/seeded.d.ts.map +1 -0
  54. package/dist/kernel/shots.d.ts +26 -0
  55. package/dist/kernel/shots.d.ts.map +1 -0
  56. package/dist/kernel/tape.d.ts +38 -0
  57. package/dist/kernel/tape.d.ts.map +1 -0
  58. package/dist/kernel/trail.d.ts +31 -0
  59. package/dist/kernel/trail.d.ts.map +1 -0
  60. package/dist/kernel/viewportGizmo.d.ts +25 -0
  61. package/dist/kernel/viewportGizmo.d.ts.map +1 -0
  62. package/dist/runtime/Inspector.d.ts +20 -0
  63. package/dist/runtime/Inspector.d.ts.map +1 -0
  64. package/dist/runtime/SceneFile.d.ts +448 -0
  65. package/dist/runtime/SceneFile.d.ts.map +1 -0
  66. package/dist/runtime/SceneSchema.d.ts +318 -0
  67. package/dist/runtime/SceneSchema.d.ts.map +1 -1
  68. package/dist/runtime/errors.d.ts +9 -0
  69. package/dist/runtime/errors.d.ts.map +1 -1
  70. package/dist/runtime/index.d.ts +3 -1
  71. package/dist/runtime/index.d.ts.map +1 -1
  72. package/dist/webgl/index.d.ts.map +1 -1
  73. package/dist/webgl/index.js +228 -3
  74. package/dist/webgl/index.js.map +6 -4
  75. package/dist/webgl/line.d.ts +4 -0
  76. package/dist/webgl/line.d.ts.map +1 -0
  77. package/dist/webgl/points.d.ts +4 -0
  78. package/dist/webgl/points.d.ts.map +1 -0
  79. package/dist/webgpu/index.d.ts.map +1 -1
  80. package/dist/webgpu/index.js +206 -26
  81. package/dist/webgpu/index.js.map +6 -4
  82. package/dist/webgpu/line.d.ts +4 -0
  83. package/dist/webgpu/line.d.ts.map +1 -0
  84. package/dist/webgpu/points.d.ts +4 -0
  85. package/dist/webgpu/points.d.ts.map +1 -0
  86. package/package.json +2 -1
  87. package/src/kernel/boxSelect.ts +73 -0
  88. package/src/kernel/build.ts +332 -0
  89. package/src/kernel/cameraShake.ts +3 -13
  90. package/src/kernel/describeScene.ts +224 -0
  91. package/src/kernel/description.ts +175 -0
  92. package/src/kernel/diff.ts +39 -0
  93. package/src/kernel/equality.ts +101 -0
  94. package/src/kernel/gizmo.ts +478 -0
  95. package/src/kernel/history.ts +118 -0
  96. package/src/kernel/index.ts +97 -0
  97. package/src/kernel/inspect.ts +214 -0
  98. package/src/kernel/lineGeometry.ts +75 -0
  99. package/src/kernel/lockstep.ts +237 -0
  100. package/src/kernel/loop.ts +5 -0
  101. package/src/kernel/materials.ts +3 -0
  102. package/src/kernel/pivot.ts +418 -0
  103. package/src/kernel/pointBuffers.ts +115 -0
  104. package/src/kernel/pointer.ts +55 -12
  105. package/src/kernel/renderKit.ts +23 -1
  106. package/src/kernel/renderer.ts +6 -0
  107. package/src/kernel/sceneChanges.ts +265 -0
  108. package/src/kernel/sceneGraph.ts +319 -8
  109. package/src/kernel/seeded.ts +18 -0
  110. package/src/kernel/shots.ts +167 -0
  111. package/src/kernel/tape.ts +124 -0
  112. package/src/kernel/trail.ts +170 -0
  113. package/src/kernel/viewportGizmo.ts +194 -0
  114. package/src/runtime/Inspector.ts +43 -0
  115. package/src/runtime/SceneFile.ts +46 -0
  116. package/src/runtime/SceneSchema.ts +247 -0
  117. package/src/runtime/errors.ts +6 -0
  118. package/src/runtime/index.ts +3 -1
  119. package/src/webgl/index.ts +4 -0
  120. package/src/webgl/line.ts +66 -0
  121. package/src/webgl/points.ts +205 -0
  122. package/src/webgpu/index.ts +4 -0
  123. package/src/webgpu/line.ts +67 -0
  124. package/src/webgpu/points.ts +193 -0
  125. package/dist/index-8cf8t6by.js.map +0 -41
@@ -32,12 +32,14 @@ import { clipRunning, clipTime } from './animation.ts';
32
32
  import { writeRotation } from './behaviours.ts';
33
33
  import { type BodyCollision, type BodyContactForce, createBodyHost } from './bodies.ts';
34
34
  import { type BodySource, instanceSources, nodeSource, placeObject } from './bodySources.ts';
35
- import { renderSettings, scene as describeScene } from './build.ts';
35
+ import { orthographicCamera, renderSettings, scene as describeScene } from './build.ts';
36
36
  import { type CameraMotion, createCameraMotion } from './cameraMotion.ts';
37
37
  import { type CameraShaker, createCameraShaker } from './cameraShake.ts';
38
38
  import type {
39
+ Hud,
39
40
  AmbientLightNode,
40
41
  BorrowedNode,
42
+ Edges,
41
43
  DirectionalLightNode,
42
44
  GridNode,
43
45
  HemisphereLightNode,
@@ -45,6 +47,8 @@ import type {
45
47
  Instance,
46
48
  InstancesNode,
47
49
  LabelNode,
50
+ LineNode,
51
+ PointsNode,
48
52
  MaterialRef,
49
53
  ShaderGridNode,
50
54
  MeshNode,
@@ -68,9 +72,16 @@ import type {
68
72
  ViewRect,
69
73
  } from './description.ts';
70
74
  import { diff, type Field, type Op } from './diff.ts';
71
- import { contactShadowsEquals, environmentEquals, overlaysEqual, skyEquals } from './equality.ts';
75
+ import {
76
+ contactShadowsEquals,
77
+ edgesEquals,
78
+ environmentEquals,
79
+ overlaysEqual,
80
+ skyEquals,
81
+ } from './equality.ts';
72
82
  import { createInstances, fits, type HostInstances, writeInstances } from './instances.ts';
73
83
  import type { HostedLabel, LabelEngine, LabelLayout } from './labels.ts';
84
+ import { edgeLine, edgePoints } from './lineGeometry.ts';
74
85
  import type { HostMaterial } from './materials.ts';
75
86
  import {
76
87
  type Behaviour,
@@ -80,7 +91,14 @@ import {
80
91
  resetPose,
81
92
  } from './motion.ts';
82
93
  import type { BodyPose, PhysicsEngine } from './physics.ts';
83
- import type { HostedShaderGrid, HostedSky, RenderKit } from './renderKit.ts';
94
+ import { animatedLook } from './pointBuffers.ts';
95
+ import type {
96
+ HostedLine,
97
+ HostedPoints,
98
+ HostedShaderGrid,
99
+ HostedSky,
100
+ RenderKit,
101
+ } from './renderKit.ts';
84
102
  import { createResourceStore, type ResourceStore } from './resources.ts';
85
103
  import {
86
104
  type DescriptionError,
@@ -89,6 +107,7 @@ import {
89
107
  type ParentKey,
90
108
  type SceneIndex,
91
109
  } from './sceneIndex.ts';
110
+ import { createTrailRibbon, type TrailRibbon } from './trail.ts';
92
111
  import { applyTween, startTween, type Tween } from './tween.ts';
93
112
 
94
113
  /** The mesh type the kernel hosts. */
@@ -142,6 +161,20 @@ type Entry =
142
161
  readonly _tag: 'HemisphereLight';
143
162
  node: HemisphereLightNode;
144
163
  readonly object: HemisphereLight;
164
+ }
165
+ // `hosted` is the points the render kit drew, inside the wrapper; none without a kit.
166
+ | {
167
+ readonly _tag: 'Points';
168
+ node: PointsNode;
169
+ readonly object: Group;
170
+ readonly hosted: HostedPoints | undefined;
171
+ }
172
+ // `hosted` is the line the render kit drew, inside the wrapper; none without a kit.
173
+ | {
174
+ readonly _tag: 'Line';
175
+ node: LineNode;
176
+ readonly object: Group;
177
+ readonly hosted: HostedLine | undefined;
145
178
  };
146
179
 
147
180
  /** Entries with a transform: position, rotation, scale and transitions. */
@@ -154,12 +187,26 @@ type Player = {
154
187
  readonly startedAt: number;
155
188
  };
156
189
 
190
+ /** A mesh's edge lines: the kit's line, and the geometry and look it was built from. */
191
+ type EdgeLines = {
192
+ readonly hosted: HostedLine;
193
+ geometry: BufferGeometry | undefined;
194
+ edges: Edges | undefined;
195
+ };
196
+
157
197
  /** Descriptions with a transform. */
158
- type PlacedNode = GroupNode | MeshNode | InstancesNode | BorrowedNode | LabelNode;
198
+ type PlacedNode =
199
+ | GroupNode
200
+ | MeshNode
201
+ | InstancesNode
202
+ | BorrowedNode
203
+ | LabelNode
204
+ | LineNode
205
+ | PointsNode;
159
206
 
160
207
  type Placed = Extract<
161
208
  Entry,
162
- { readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' }
209
+ { readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' | 'Line' | 'Points' }
163
210
  >;
164
211
 
165
212
  /** Entries drawing a geometry with a material. */
@@ -170,12 +217,17 @@ const isPlaced = (entry: Entry): entry is Placed =>
170
217
  entry._tag === 'Mesh' ||
171
218
  entry._tag === 'Instances' ||
172
219
  entry._tag === 'Borrowed' ||
173
- entry._tag === 'Label';
220
+ entry._tag === 'Label' ||
221
+ entry._tag === 'Line' ||
222
+ entry._tag === 'Points';
174
223
 
175
224
  const isSurfaced = (entry: Entry): entry is Surfaced =>
176
225
  entry._tag === 'Mesh' || entry._tag === 'Instances';
177
226
 
178
227
  /** A render texture as hosted: its id, size and the nested graph that draws it. */
228
+ /** The HUD as hosted: its nested graph, drawn over the scene by the frame loop. */
229
+ export type HostedHud = { readonly graph: SceneGraph };
230
+
179
231
  export type HostedTexture = {
180
232
  readonly id: string;
181
233
  readonly graph: SceneGraph;
@@ -232,6 +284,8 @@ export type SceneGraph = {
232
284
  readonly turned: (view: string | undefined) => Vec3 | undefined;
233
285
  /** Render textures, each a nested graph to draw into its texture (by the frame loop). */
234
286
  readonly renderTextures: () => ReadonlyArray<HostedTexture>;
287
+ /** The HUD, a nested graph drawn over the scene (by the frame loop), if there is one. */
288
+ readonly hud: () => HostedHud | undefined;
235
289
  /** Whether the scene claims the pointer's gestures from the page. */
236
290
  readonly gestures: () => Gestures;
237
291
  /** Points to show HTML at; the frame loop places them after drawing. */
@@ -448,6 +502,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
448
502
  { readonly graph: SceneGraph; description: RenderTexture }
449
503
  >();
450
504
  let textureList: ReadonlyArray<HostedTexture> = [];
505
+ let hudHosted:
506
+ | { readonly graph: SceneGraph; description: Hud; parent: SceneDescription }
507
+ | undefined;
451
508
  let viewDescriptions: ReadonlyArray<View> = [];
452
509
  let gestures: Gestures = emptyScene.gestures;
453
510
  let overlayList: ReadonlyArray<Overlay> = emptyScene.overlays;
@@ -559,6 +616,26 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
559
616
  const facing = new Set<Entry>();
560
617
  const cameraTurn = new Quaternion();
561
618
  const parentTurn = new Quaternion();
619
+ // Points that move by themselves (sparkles, stars), ticked in scene time while animating.
620
+ const sparkling = new Set<Entry>();
621
+ const trackSparkle = (entry: Entry, node: PointsNode): void => {
622
+ if (animatedLook(node.look)) sparkling.add(entry);
623
+ else sparkling.delete(entry);
624
+ if (entry._tag === 'Points') entry.hosted?.tick(animating ? elapsed : 0);
625
+ };
626
+ const sparkle = (entry: Entry): void => {
627
+ if (entry._tag === 'Points') entry.hosted?.tick(elapsed);
628
+ };
629
+ // Groups and meshes with trails: each ribbon, in the scene's space.
630
+ const trailing = new Map<Entry, TrailRibbon>();
631
+ const trailPlace = new Vector3();
632
+ const eye = new Vector3();
633
+ const trailsMoving = (): boolean => {
634
+ for (const ribbon of trailing.values()) if (!ribbon.settled()) return true;
635
+ return false;
636
+ };
637
+ // Meshes with edge lines: rebuilt when the mesh's geometry or the edges' look changes.
638
+ const edged = new Map<Entry, EdgeLines>();
562
639
  // Groups with levels of detail: one child shown at a time, by the camera's distance.
563
640
  const detailing = new Set<Entry>();
564
641
  const cameraPlace = new Vector3();
@@ -639,6 +716,81 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
639
716
  if (lockZ) object.rotation.z = 0;
640
717
  };
641
718
 
719
+ /** Where a trail follows its node: the world position, or with `local` its own. */
720
+ const trailTarget = (object: Object3D, local: boolean): Vector3 =>
721
+ local ? trailPlace.copy(object.position) : object.getWorldPosition(trailPlace);
722
+
723
+ /** Adds, restyles or removes a node's trail as its description says. */
724
+ const trackTrail = (entry: Entry, node: GroupNode | MeshNode): void => {
725
+ const current = trailing.get(entry);
726
+ if (node.trail === null) {
727
+ if (current === undefined) return;
728
+ current.object.removeFromParent();
729
+ current.dispose();
730
+ trailing.delete(entry);
731
+ return;
732
+ }
733
+ const start = trailTarget(entry.object, node.trail.local);
734
+ if (current !== undefined) {
735
+ current.restyle(node.trail, start, elapsed);
736
+ return;
737
+ }
738
+ const ribbon = createTrailRibbon(node.trail, start, elapsed);
739
+ scene.add(ribbon.object);
740
+ trailing.set(entry, ribbon);
741
+ };
742
+
743
+ /** Records where a trailing node is now, and turns its ribbon to the camera. */
744
+ const followTrail = (ribbon: TrailRibbon, entry: Entry): void => {
745
+ if (entry._tag !== 'Group' && entry._tag !== 'Mesh') return;
746
+ if (entry.node.trail === null) return;
747
+ ribbon.record(trailTarget(entry.object, entry.node.trail.local), elapsed);
748
+ ribbon.face(eye);
749
+ };
750
+
751
+ /** Drei's `Edges` turns off picking on its line: hits belong to the mesh. */
752
+ const unpickable = (part: Object3D): void => {
753
+ part.raycast = () => undefined;
754
+ };
755
+
756
+ /** Adds, keeps or removes a mesh's edge lines as its description says. */
757
+ const trackEdges = (entry: Entry, node: MeshNode): void => {
758
+ const current = edged.get(entry);
759
+ const draw = options.kit?.line;
760
+ if (node.edges === null || draw === undefined) {
761
+ if (current === undefined) return;
762
+ current.hosted.object.removeFromParent();
763
+ current.hosted.dispose();
764
+ edged.delete(entry);
765
+ return;
766
+ }
767
+ if (current !== undefined) return;
768
+ const hosted = draw(edgeLine(node.key, node.edges, []));
769
+ entry.object.add(hosted.object);
770
+ edged.set(entry, { hosted, geometry: undefined, edges: undefined });
771
+ };
772
+
773
+ /**
774
+ * Builds the edge lines again when the mesh shows another geometry (its own changed, a
775
+ * shared one was replaced, a lent one arrived) or the edges' look changed.
776
+ */
777
+ const refreshEdges = (lines: EdgeLines, entry: Entry): void => {
778
+ if (entry._tag !== 'Mesh' || entry.node.edges === null) return;
779
+ const { geometry } = entry.object;
780
+ const look = entry.node.edges;
781
+ if (
782
+ geometry === lines.geometry &&
783
+ lines.edges !== undefined &&
784
+ edgesEquals(look, lines.edges)
785
+ ) {
786
+ return;
787
+ }
788
+ lines.hosted.update(edgeLine(entry.node.key, look, edgePoints(geometry, look.threshold)));
789
+ lines.hosted.object.traverse(unpickable);
790
+ lines.geometry = geometry;
791
+ lines.edges = look;
792
+ };
793
+
642
794
  /** The object of a group's `index`th child, as described. */
643
795
  const childObject = (node: GroupNode, index: number): Object3D | undefined => {
644
796
  const child = node.children[index];
@@ -704,6 +856,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
704
856
  // A group following a tracked space shows only once the space is found.
705
857
  if (node.follow !== null) entry.object.visible = false;
706
858
  trackDetail(entry, node);
859
+ trackTrail(entry, node);
707
860
  return entry;
708
861
  }
709
862
  case 'Mesh': {
@@ -719,6 +872,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
719
872
  trackInteractive(object, node.interactive);
720
873
  object.castShadow = node.castShadow;
721
874
  object.receiveShadow = node.receiveShadow;
875
+ trackEdges(entry, node);
876
+ trackTrail(entry, node);
722
877
  return entry;
723
878
  }
724
879
  case 'Instances': {
@@ -799,6 +954,28 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
799
954
  const hosted = options.kit?.shaderGrid(node);
800
955
  return { _tag: 'ShaderGrid', node, object: hosted?.object ?? new Group(), hosted };
801
956
  }
957
+ case 'Points': {
958
+ const hosted = options.kit?.points?.(node);
959
+ const entry: Entry = { _tag: 'Points', node, object: new Group(), hosted };
960
+ if (hosted !== undefined) entry.object.add(hosted.object);
961
+ writeVec3(entry.object.position, node.position);
962
+ writeVec3(entry.object.scale, node.scale);
963
+ trackRotation(entry, node);
964
+ trackBehaviour(entry, node);
965
+ trackSparkle(entry, node);
966
+ return entry;
967
+ }
968
+ case 'Line': {
969
+ const hosted = options.kit?.line?.(node);
970
+ const entry: Entry = { _tag: 'Line', node, object: new Group(), hosted };
971
+ if (hosted !== undefined) entry.object.add(hosted.object);
972
+ writeVec3(entry.object.position, node.position);
973
+ writeVec3(entry.object.scale, node.scale);
974
+ trackRotation(entry, node);
975
+ trackBehaviour(entry, node);
976
+ trackInteractive(entry.object, node.interactive);
977
+ return entry;
978
+ }
802
979
  case 'HemisphereLight': {
803
980
  const object = new HemisphereLight();
804
981
  writeHemisphereLight(object, node);
@@ -973,6 +1150,17 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
973
1150
  spinning.delete(entry);
974
1151
  facing.delete(entry);
975
1152
  detailing.delete(entry);
1153
+ const lines = edged.get(entry);
1154
+ if (lines !== undefined) {
1155
+ lines.hosted.dispose();
1156
+ edged.delete(entry);
1157
+ }
1158
+ const ribbon = trailing.get(entry);
1159
+ if (ribbon !== undefined) {
1160
+ ribbon.object.removeFromParent();
1161
+ ribbon.dispose();
1162
+ trailing.delete(entry);
1163
+ }
976
1164
  moving.delete(entry);
977
1165
  reparented.delete(entry);
978
1166
  trackInteractive(entry.object, false);
@@ -984,7 +1172,10 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
984
1172
  if (entry._tag === 'Instances') entry.object.dispose();
985
1173
  // A grid's lines are its own.
986
1174
  if (entry._tag === 'Grid') disposeGridLines(entry.helper);
987
- if (entry._tag === 'ShaderGrid') entry.hosted?.dispose();
1175
+ if (entry._tag === 'ShaderGrid' || entry._tag === 'Line' || entry._tag === 'Points') {
1176
+ entry.hosted?.dispose();
1177
+ }
1178
+ sparkling.delete(entry);
988
1179
  if (entry._tag === 'Label') {
989
1180
  entry.hosted?.dispose();
990
1181
  if (entry.node.material !== null && entry.material !== undefined) {
@@ -1114,6 +1305,12 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1114
1305
  entry.node = next;
1115
1306
  if (movesTransform(entry, fields)) retarget(entry, next);
1116
1307
  if (fields.includes('rotation')) trackRotation(entry, next);
1308
+ if (entry._tag === 'Mesh' && next._tag === 'Mesh' && fields.includes('edges')) {
1309
+ trackEdges(entry, next);
1310
+ }
1311
+ if (entry._tag === 'Mesh' && next._tag === 'Mesh' && fields.includes('trail')) {
1312
+ trackTrail(entry, next);
1313
+ }
1117
1314
  if (fields.includes('behaviour')) trackBehaviour(entry, next);
1118
1315
  if (fields.includes('interactive')) trackInteractive(mesh, next.interactive);
1119
1316
  mesh.castShadow = next.castShadow;
@@ -1150,6 +1347,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1150
1347
  if (fields.includes('behaviour')) trackBehaviour(entry, next);
1151
1348
  if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
1152
1349
  if (fields.includes('detail')) trackDetail(entry, next);
1350
+ if (fields.includes('trail')) trackTrail(entry, next);
1153
1351
  if (fields.includes('follow')) {
1154
1352
  // Back to its own transform, or hidden until the new space is found.
1155
1353
  entry.object.visible = next.follow === null;
@@ -1186,6 +1384,26 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1186
1384
  entry.node = next;
1187
1385
  return;
1188
1386
  }
1387
+ if (entry._tag === 'Points' && next._tag === 'Points') {
1388
+ entry.node = next;
1389
+ if (movesTransform(entry, fields)) retarget(entry, next);
1390
+ if (fields.includes('rotation')) trackRotation(entry, next);
1391
+ if (fields.includes('behaviour')) trackBehaviour(entry, next);
1392
+ if (fields.includes('look')) {
1393
+ entry.hosted?.update(next);
1394
+ trackSparkle(entry, next);
1395
+ }
1396
+ return;
1397
+ }
1398
+ if (entry._tag === 'Line' && next._tag === 'Line') {
1399
+ entry.node = next;
1400
+ if (movesTransform(entry, fields)) retarget(entry, next);
1401
+ if (fields.includes('rotation')) trackRotation(entry, next);
1402
+ if (fields.includes('behaviour')) trackBehaviour(entry, next);
1403
+ if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
1404
+ if (fields.includes('line')) entry.hosted?.update(next);
1405
+ return;
1406
+ }
1189
1407
  if (entry._tag === 'ShaderGrid' && next._tag === 'ShaderGrid') {
1190
1408
  entry.hosted?.update(entry.node, next);
1191
1409
  entry.node = next;
@@ -1317,6 +1535,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1317
1535
  gestures: 'page',
1318
1536
  views: [],
1319
1537
  renderTextures: [],
1538
+ hud: null,
1320
1539
  overlays: [],
1321
1540
  environment: null,
1322
1541
  sky: null,
@@ -1329,6 +1548,78 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1329
1548
  children: texture.children,
1330
1549
  });
1331
1550
 
1551
+ /**
1552
+ * The HUD's camera: orthographic in CSS pixels, from in front, aimed so that the HUD's
1553
+ * origin falls `margin` in from the aligned edges (as Drei's `GizmoHelper` places its
1554
+ * group). The camera moves rather than the nodes, so their positions stay the Model's.
1555
+ */
1556
+ const hudCamera = (hud: Hud): CameraDescription => {
1557
+ const [marginX, marginY] = hud.margin;
1558
+ const [vertical, horizontal] = hud.alignment.split('-');
1559
+ const across = { left: -viewport.width / 2 + marginX, right: viewport.width / 2 - marginX };
1560
+ const up = { top: viewport.height / 2 - marginY, bottom: -viewport.height / 2 + marginY };
1561
+ const x = across[horizontal as keyof typeof across] ?? 0;
1562
+ const y = up[vertical as keyof typeof up] ?? 0;
1563
+ return orthographicCamera({
1564
+ zoom: 1,
1565
+ near: 0.1,
1566
+ far: 2000,
1567
+ position: [-x, -y, 200],
1568
+ lookAt: [-x, -y, 0],
1569
+ });
1570
+ };
1571
+
1572
+ /** The HUD as a scene of its own, sharing the parent's resources. */
1573
+ const hudScene = (hud: Hud, parent: SceneDescription): SceneDescription =>
1574
+ textureScene(
1575
+ {
1576
+ id: 'hud',
1577
+ width: viewport.width,
1578
+ height: viewport.height,
1579
+ camera: hudCamera(hud),
1580
+ background: null,
1581
+ children: hud.children,
1582
+ },
1583
+ parent,
1584
+ );
1585
+
1586
+ /**
1587
+ * Brings the HUD in line with the description, `parent` the scene it is over: a nested
1588
+ * graph on the same store, created, reconciled, re-aimed or disposed.
1589
+ */
1590
+ const syncHud = (next: SceneDescription): ReadonlyArray<Op> => {
1591
+ if (next.hud === null) {
1592
+ if (hudHosted === undefined) return [];
1593
+ hudHosted.graph.dispose();
1594
+ hudHosted = undefined;
1595
+ return [{ _tag: 'RemoveHud' }];
1596
+ }
1597
+ const created = hudHosted === undefined;
1598
+ const hosted = hudHosted ?? {
1599
+ graph: createSceneGraph({
1600
+ store,
1601
+ ...(options.behaviours === undefined ? {} : { behaviours: options.behaviours }),
1602
+ ...(options.kit === undefined ? {} : { kit: options.kit }),
1603
+ }),
1604
+ description: next.hud,
1605
+ parent: next,
1606
+ };
1607
+ if (created) hosted.graph.resize(viewport.width, viewport.height);
1608
+ const result = hosted.graph.reconcile(hudScene(next.hud, next));
1609
+ hosted.description = next.hud;
1610
+ hosted.parent = next;
1611
+ hudHosted = hosted;
1612
+ const ops = result._tag === 'Applied' ? result.ops : [];
1613
+ return created || ops.length > 0 ? [{ _tag: 'UpdateHud', ops }] : [];
1614
+ };
1615
+
1616
+ /** The HUD fitted to a resized canvas: its camera re-aimed at the aligned point. */
1617
+ const refitHud = (): void => {
1618
+ if (hudHosted === undefined) return;
1619
+ hudHosted.graph.resize(viewport.width, viewport.height);
1620
+ hudHosted.graph.reconcile(hudScene(hudHosted.description, hudHosted.parent));
1621
+ };
1622
+
1332
1623
  /** A hosted render texture as the loop sees it; none if it is not hosted. */
1333
1624
  function hostedTexture(texture: RenderTexture): ReadonlyArray<HostedTexture> {
1334
1625
  const hosted = textureGraphs.get(texture.id);
@@ -1389,6 +1680,10 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1389
1680
  const checked = indexScene(textureScene(texture, next), knownBehaviours);
1390
1681
  if (checked._tag === 'Invalid') return checked;
1391
1682
  }
1683
+ if (next.hud !== null) {
1684
+ const checked = indexScene(hudScene(next.hud, next), knownBehaviours);
1685
+ if (checked._tag === 'Invalid') return checked;
1686
+ }
1392
1687
  const result = diff(index, next, knownBehaviours);
1393
1688
  if (result._tag === 'Invalid') return result;
1394
1689
  for (const op of result.ops) apply(op);
@@ -1422,7 +1717,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1422
1717
  // Measured after the next drawn frame; a new list needs one.
1423
1718
  if (next.measure.join('\u0000') !== measured.join('\u0000')) invalidate();
1424
1719
  measured = next.measure;
1425
- const ops = [...result.ops, ...syncTextures(next)];
1720
+ const ops = [...result.ops, ...syncTextures(next), ...syncHud(next)];
1426
1721
  if (ops.length > 0) invalidate();
1427
1722
  return { _tag: 'Applied', ops };
1428
1723
  };
@@ -1435,6 +1730,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1435
1730
  const tick = (elapsedSeconds: number): void => {
1436
1731
  elapsed = elapsedSeconds;
1437
1732
  for (const texture of textureList) texture.graph.tick(elapsedSeconds);
1733
+ hudHosted?.graph.tick(elapsedSeconds);
1438
1734
  for (const entry of spinning) {
1439
1735
  if (isPlaced(entry)) {
1440
1736
  writeRotation(entry.object.rotation, entry.node.rotation, elapsed);
@@ -1455,6 +1751,13 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1455
1751
  camera.getWorldQuaternion(cameraTurn);
1456
1752
  facing.forEach(faceCamera);
1457
1753
  }
1754
+ edged.forEach(refreshEdges);
1755
+ // Reduced motion holds sparkles and stars where they are.
1756
+ if (animating) sparkling.forEach(sparkle);
1757
+ if (trailing.size > 0) {
1758
+ camera.getWorldPosition(eye);
1759
+ trailing.forEach(followTrail);
1760
+ }
1458
1761
  if (detailing.size > 0) {
1459
1762
  camera.getWorldPosition(cameraPlace);
1460
1763
  detailing.forEach(showLevel);
@@ -1468,6 +1771,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1468
1771
  const setAnimating = (next: boolean): void => {
1469
1772
  if (next !== animating) invalidate();
1470
1773
  for (const texture of textureList) texture.graph.setAnimating(next);
1774
+ hudHosted?.graph.setAnimating(next);
1471
1775
  animating = next;
1472
1776
  if (next) return;
1473
1777
  moving.forEach(finishTween);
@@ -1482,6 +1786,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1482
1786
  for (const texture of textureList) texture.graph.dispose();
1483
1787
  textureGraphs.clear();
1484
1788
  textureList = [];
1789
+ hudHosted?.graph.dispose();
1790
+ hudHosted = undefined;
1485
1791
  for (const entry of entries.values()) {
1486
1792
  entry.object.removeFromParent();
1487
1793
  release(entry);
@@ -1512,6 +1818,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1512
1818
  return viewMotions.get(view)?.turned();
1513
1819
  },
1514
1820
  renderTextures: () => textureList,
1821
+ hud: () => hudHosted,
1515
1822
  gestures: () => gestures,
1516
1823
  overlays: () => overlayList,
1517
1824
  environment: () => environmentDescription,
@@ -1540,7 +1847,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1540
1847
  renderSettings: () => settings,
1541
1848
  needsFrame: () =>
1542
1849
  dirty ||
1850
+ trailsMoving() ||
1543
1851
  textureList.some((texture) => texture.graph.needsFrame()) ||
1852
+ (hudHosted?.graph.needsFrame() ?? false) ||
1544
1853
  (animating &&
1545
1854
  (spinning.size > 0 ||
1546
1855
  moving.size > 0 ||
@@ -1549,6 +1858,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1549
1858
  cameraMotion.moving() ||
1550
1859
  viewsMoving() ||
1551
1860
  shaking() ||
1861
+ sparkling.size > 0 ||
1552
1862
  bodyHost.needsFrame())),
1553
1863
  drawn: () => {
1554
1864
  dirty = false;
@@ -1576,6 +1886,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
1576
1886
  viewport = { width, height };
1577
1887
  fitCamera(camera, viewport);
1578
1888
  hostViews(viewDescriptions);
1889
+ refitHud();
1579
1890
  invalidate();
1580
1891
  },
1581
1892
  dispose,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A small seeded random generator (mulberry32): the same numbers for the same seed on
3
+ * every page load, so randomly scattered scene parts are reproducible.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ /** A generator of numbers in [0, 1) from `seed`, as `Math.random` would give them. */
9
+ export const seededRandom = (seed: number): (() => number) => {
10
+ let state = seed >>> 0;
11
+ return () => {
12
+ state = (state + 0x6d2b79f5) >>> 0;
13
+ let t = state;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ };