@fusion.dev/helix-graph 0.185.0 → 0.186.1

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.
@@ -3,14 +3,18 @@ import { type NodeLook } from '../connect-graph/looks.js';
3
3
  /** How the three.js driver behaves where the page does not say. */
4
4
  export interface ThreeDriverOptions {
5
5
  /**
6
- * How many pixels the drawing has for each of the page's. By default half
7
- * as many again as the screen has, at least two and at most three, which
8
- * smooths every edge. Fewer is faster on a slow GPU.
6
+ * How many pixels the drawing has for each of the page's. By default, on
7
+ * a screen worked with a mouse, half as many again as the screen has, at
8
+ * least two and at most three, which smooths every edge; on a touch
9
+ * screen, as many as the screen has, at most two. Fewer is faster on a
10
+ * slow GPU.
9
11
  */
10
12
  density?: number | undefined;
11
13
  /**
12
- * Whether the graph turns slowly while nobody is handling it. On by
13
- * default, and never for a reader who asks for less motion.
14
+ * Whether the graph turns slowly for its first five seconds, until the
15
+ * reader handles it. On by default, and never for a reader who asks for
16
+ * less motion. It stops for good after five seconds, since motion that
17
+ * starts by itself and runs longer needs a way to pause it.
14
18
  */
15
19
  autoRotate?: boolean | undefined;
16
20
  /** Whether each group's name is drawn over its notes. On by default. */
@@ -32,10 +36,13 @@ export interface ThreeDriverOptions {
32
36
  * **A layout runs in three dimensions**, with d3-force-3d, where notes have no
33
37
  * place; with a solid `stencil`, the notes are drawn in its shape and none
34
38
  * runs. Dragging turns the graph, the wheel and a pinch bring it nearer, and
35
- * it turns slowly by itself while nobody is handling it, unless the reader
36
- * asks for less motion. It draws only while something moves, and not at all
37
- * while the box is out of view or off the page.
39
+ * it turns slowly by itself for its first five seconds, until the reader
40
+ * handles it, unless the reader asks for less motion. It draws only while
41
+ * something moves, and not at all while the box is out of view or off the
42
+ * page.
38
43
  *
39
- * The renderer is the driver's own, and `disconnect()` frees it.
44
+ * The renderer is the driver's own, and `disconnect()` frees it. Every
45
+ * three.js graph on a page shares six WebGL contexts: past six, one far off
46
+ * screen gives its context back and takes it again as it comes near.
40
47
  */
41
48
  export declare const threeDriver: (container: HTMLElement, options?: ThreeDriverOptions) => GraphDriver;
@@ -9,6 +9,7 @@ import { linkKey } from '../connect-graph/diff.js';
9
9
  import { rgbOf } from '../connect-graph/looks.js';
10
10
  import { ease, gesturesHeld, groupsOver, isLight, namesOver, signalsOver, } from '../overlay/overlay.js';
11
11
  import { surfaceOf } from '../shapes/surface.js';
12
+ import { pageBudget } from './budget.js';
12
13
  const SHAPES = { circle: 0, diamond: 2, ring: 0, square: 1 };
13
14
  const REACH = 100;
14
15
  const STEP = 250;
@@ -16,7 +17,7 @@ const MORPH = 1400;
16
17
  const ZOOM = 1.5;
17
18
  const SAMPLE = 96;
18
19
  const LINKS = { dark: { close: 0.16, far: 0.025 }, light: { close: 0.36, far: 0.07 } };
19
- const REST = 8000;
20
+ const TURNING = 5000;
20
21
  const WASH = { dark: 0.14, full: 1.5, light: 0.18, notes: 6, reach: 3.5 };
21
22
  const BLOOM = { dark: 0.28, light: 0 };
22
23
  const LIGHT = new Vector3(-0.4, 0.8, 0.45).normalize();
@@ -292,7 +293,9 @@ const depthOf = (id) => {
292
293
  export const threeDriver = (container, options = {}) => {
293
294
  const still = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
294
295
  const renderer = new WebGLRenderer({ alpha: true, antialias: true });
295
- renderer.setPixelRatio(options.density ?? Math.min(Math.max((window.devicePixelRatio || 1) * 1.5, 2), 3));
296
+ const screen = window.devicePixelRatio || 1;
297
+ const touch = window.matchMedia?.('(pointer: coarse)').matches ?? false;
298
+ renderer.setPixelRatio(options.density ?? (touch ? Math.min(screen, 2) : Math.min(Math.max(screen * 1.5, 2), 3)));
296
299
  renderer.setClearColor(0x000000, 0);
297
300
  const canvas = renderer.domElement;
298
301
  canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block';
@@ -317,7 +320,8 @@ export const threeDriver = (container, options = {}) => {
317
320
  let near = new Set();
318
321
  let stopped = false;
319
322
  let moved = false;
320
- let handled = 0;
323
+ const turnUntil = performance.now() + TURNING;
324
+ let touched = false;
321
325
  let scale = 1;
322
326
  let positions = new Float32Array();
323
327
  const uniforms = {
@@ -647,6 +651,7 @@ export const threeDriver = (container, options = {}) => {
647
651
  return;
648
652
  }
649
653
  glide = { duration, from, fromDistance, start: performance.now(), to, toDistance: distance };
654
+ invalidate();
650
655
  };
651
656
  const sphereOf = (spots) => {
652
657
  const center = new Vector3();
@@ -977,18 +982,20 @@ export const threeDriver = (container, options = {}) => {
977
982
  const shade = LINKS[dark ? 'dark' : 'light'];
978
983
  quiet.material.opacity = shade.far + (shade.close - shade.far) * closeness;
979
984
  };
985
+ let visible = true;
986
+ let pending = 0;
980
987
  let dirty = true;
981
988
  const invalidate = () => {
982
989
  dirty = true;
990
+ if (!pending && !stopped && visible && container.isConnected) {
991
+ pending = requestAnimationFrame(tick);
992
+ }
983
993
  };
984
- let visible = true;
985
- let pending = 0;
986
994
  const tick = (now) => {
987
995
  pending = 0;
988
996
  if (stopped || !visible || !container.isConnected) {
989
997
  return;
990
998
  }
991
- pending = requestAnimationFrame(tick);
992
999
  const moving = glide !== undefined || morph !== undefined || layout !== undefined;
993
1000
  if (glide) {
994
1001
  const through = Math.min((now - glide.start) / glide.duration, 1);
@@ -1042,14 +1049,18 @@ export const threeDriver = (container, options = {}) => {
1042
1049
  controls.autoRotate =
1043
1050
  !still() &&
1044
1051
  options.autoRotate !== false &&
1052
+ !touched &&
1045
1053
  hovered === undefined &&
1046
1054
  selected === undefined &&
1047
- now - handled > REST;
1048
- controls.enableDamping = !still();
1055
+ now < turnUntil;
1056
+ controls.enableDamping = !still() && (touched || now < turnUntil);
1049
1057
  const turned = controls.update();
1050
1058
  if (!(moving || turned || dirty)) {
1051
1059
  return;
1052
1060
  }
1061
+ if (!pending) {
1062
+ pending = requestAnimationFrame(tick);
1063
+ }
1053
1064
  dirty = false;
1054
1065
  cue();
1055
1066
  if (solids.cube.mesh || solids.octahedron.mesh) {
@@ -1061,9 +1072,6 @@ export const threeDriver = (container, options = {}) => {
1061
1072
  };
1062
1073
  const wake = () => {
1063
1074
  invalidate();
1064
- if (!pending && !stopped && visible && container.isConnected) {
1065
- pending = requestAnimationFrame(tick);
1066
- }
1067
1075
  };
1068
1076
  const seen = typeof IntersectionObserver === 'function'
1069
1077
  ? new IntersectionObserver((entries) => {
@@ -1083,6 +1091,26 @@ export const threeDriver = (container, options = {}) => {
1083
1091
  invalidate();
1084
1092
  };
1085
1093
  let resizing = 0;
1094
+ const restored = () => {
1095
+ invalidate();
1096
+ };
1097
+ canvas.addEventListener('webglcontextrestored', restored);
1098
+ const holder = {
1099
+ release: () => renderer.forceContextLoss(),
1100
+ restore: () => renderer.forceContextRestore(),
1101
+ };
1102
+ pageBudget.join(holder);
1103
+ const nearby = typeof IntersectionObserver === 'function'
1104
+ ? new IntersectionObserver((entries) => {
1105
+ if (entries.at(-1)?.isIntersecting ?? true) {
1106
+ pageBudget.near(holder);
1107
+ }
1108
+ else {
1109
+ pageBudget.far(holder, performance.now());
1110
+ }
1111
+ }, { rootMargin: '50%' })
1112
+ : undefined;
1113
+ nearby?.observe(container);
1086
1114
  const sized = new ResizeObserver(() => {
1087
1115
  cancelAnimationFrame(resizing);
1088
1116
  resizing = requestAnimationFrame(resize);
@@ -1092,12 +1120,12 @@ export const threeDriver = (container, options = {}) => {
1092
1120
  pending = requestAnimationFrame(tick);
1093
1121
  controls.addEventListener('start', () => {
1094
1122
  moved = true;
1095
- handled = Number.POSITIVE_INFINITY;
1123
+ touched = true;
1096
1124
  glide = undefined;
1097
1125
  wake();
1098
1126
  });
1099
- controls.addEventListener('end', () => {
1100
- handled = performance.now();
1127
+ controls.addEventListener('change', () => {
1128
+ invalidate();
1101
1129
  });
1102
1130
  const frontOf = (ray) => {
1103
1131
  const frame = shapeFrame;
@@ -1270,6 +1298,7 @@ export const threeDriver = (container, options = {}) => {
1270
1298
  const step = (now) => {
1271
1299
  const through = Math.min((now - start) / STEP, 1);
1272
1300
  camera.position.copy(from.clone().lerp(to, ease(through)));
1301
+ invalidate();
1273
1302
  if (through < 1 && !stopped) {
1274
1303
  requestAnimationFrame(step);
1275
1304
  }
@@ -1350,6 +1379,9 @@ export const threeDriver = (container, options = {}) => {
1350
1379
  },
1351
1380
  stop: () => {
1352
1381
  stopped = true;
1382
+ canvas.removeEventListener('webglcontextrestored', restored);
1383
+ nearby?.disconnect();
1384
+ pageBudget.leave(holder);
1353
1385
  cancelAnimationFrame(pending);
1354
1386
  cancelAnimationFrame(hovering);
1355
1387
  cancelAnimationFrame(resizing);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-graph",
3
3
  "description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, cosmos.gl, three.js, or a driver of your own.",
4
- "version": "0.185.0",
4
+ "version": "0.186.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -21,6 +21,10 @@
21
21
  "types": "./dist/cosmos/index.d.ts",
22
22
  "default": "./dist/cosmos/index.js"
23
23
  },
24
+ "./isometric": {
25
+ "types": "./dist/isometric/index.d.ts",
26
+ "default": "./dist/isometric/index.js"
27
+ },
24
28
  "./model": {
25
29
  "types": "./dist/model/index.d.ts",
26
30
  "default": "./dist/model/index.js"
@@ -55,12 +59,12 @@
55
59
  },
56
60
  "bugs": "https://github.com/project-fusion/fusion/issues",
57
61
  "dependencies": {
58
- "@fusion.dev/helix-core": "0.185.0"
62
+ "@fusion.dev/helix-core": "0.186.1"
59
63
  },
60
64
  "devDependencies": {
61
65
  "@cosmos.gl/graph": "^3.4.2",
62
- "@fusion.dev/helix-headless": "0.185.0",
63
- "@fusion.dev/helix-testing": "0.185.0",
66
+ "@fusion.dev/helix-headless": "0.186.1",
67
+ "@fusion.dev/helix-testing": "0.186.1",
64
68
  "@types/bun": "^1.4.2",
65
69
  "@types/three": "^0.186.0",
66
70
  "d3-force-3d": "^3.0.6",