@fusion.dev/helix-headless 0.194.0 → 0.195.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.
@@ -1,4 +1,4 @@
1
- import { pathData, projectionOf, shortNumber, } from '@fusion.dev/helix-core';
1
+ import { boxLabels, pathData, projectionOf, shortNumber, } from '@fusion.dev/helix-core';
2
2
  import { svg } from 'lit';
3
3
  export const SPATIAL_VIEW = { tilt: 25, turn: 42 };
4
4
  export const VIEW_STEP = 15;
@@ -10,6 +10,7 @@ export const tickText = (value) => String(Number(value.toPrecision(6)));
10
10
  const WIDE = 0.58;
11
11
  export const TICK_SIZE = 0.13;
12
12
  export const NAME_SIZE = 0.15;
13
+ const LABEL_GAP = 0.06;
13
14
  export const boxView = (axes, camera, extra = []) => {
14
15
  const projection = projectionOf(camera);
15
16
  const on = ([x, y, z]) => projection.at(x, -z, y);
@@ -41,22 +42,11 @@ export const boxView = (axes, camera, extra = []) => {
41
42
  line([-1, y, -1], [1, y, -1]);
42
43
  line([-1, y, -1], [-1, y, 1]);
43
44
  }
44
- const labels = [];
45
- const label = (at, text, name = false) => {
46
- labels.push({ name, text, ...on(at) });
47
- };
48
- for (const tick of axes.x.ticks) {
49
- label([inBox(axes.x, tick), -1, 1.14], tickText(tick));
50
- }
51
- for (const tick of axes.z.ticks) {
52
- label([1.14, -1, inBox(axes.z, tick)], tickText(tick));
53
- }
54
- for (const tick of axes.y.ticks.slice(1)) {
55
- label([-1.14, inBox(axes.y, tick), 1], tickText(tick));
56
- }
57
- label([0, -1, 1.38], axes.x.name, true);
58
- label([1.6, -1, 0], axes.z.name, true);
59
- label([-1, 1.28, 1], axes.y.name, true);
45
+ const text = (axis) => ({
46
+ name: axis.name,
47
+ ticks: axis.ticks.map((tick) => ({ at: inBox(axis, tick), text: tickText(tick) })),
48
+ });
49
+ const labels = boxLabels((point) => ({ ...on(point), depth: depth(point) }), { x: text(axes.x), y: text(axes.y), z: text(axes.z) }, { gap: LABEL_GAP, name: NAME_SIZE, tick: TICK_SIZE, wide: WIDE });
60
50
  const corners = [];
61
51
  for (const x of [-1, 1]) {
62
52
  for (const y of [-1, 1]) {
@@ -113,3 +103,110 @@ export const nearestTo = (drawing, event, places, reach = 14) => {
113
103
  }
114
104
  return found;
115
105
  };
106
+ const TURN_PER_PIXEL = 0.5;
107
+ const TILT_PER_PIXEL = 0.35;
108
+ const DRAG = 4;
109
+ const SETTLE = 250;
110
+ const now = () => globalThis.performance?.now() ?? Date.now();
111
+ const easeOut = (t) => 1 - (1 - t) ** 3;
112
+ export class DragTurn {
113
+ constructor(turn, redraw) {
114
+ this.turn = turn;
115
+ this.redraw = redraw;
116
+ this.dragged = false;
117
+ this.click = (event) => {
118
+ if (this.dragged) {
119
+ this.dragged = false;
120
+ event.stopPropagation();
121
+ event.preventDefault();
122
+ }
123
+ };
124
+ }
125
+ get moving() {
126
+ return this.held?.moved === true;
127
+ }
128
+ down(event) {
129
+ this.dragged = false;
130
+ this.held =
131
+ event.button === 0
132
+ ? { id: event.pointerId, moved: false, x: event.clientX, y: event.clientY }
133
+ : undefined;
134
+ }
135
+ move(event) {
136
+ const held = this.held;
137
+ if (!held || held.id !== event.pointerId) {
138
+ return false;
139
+ }
140
+ const dx = event.clientX - held.x;
141
+ const dy = event.clientY - held.y;
142
+ if (!held.moved && Math.hypot(dx, dy) < DRAG) {
143
+ return false;
144
+ }
145
+ if (!held.moved) {
146
+ held.moved = true;
147
+ try {
148
+ ;
149
+ event.currentTarget?.setPointerCapture?.(event.pointerId);
150
+ }
151
+ catch {
152
+ }
153
+ }
154
+ held.x = event.clientX;
155
+ held.y = event.clientY;
156
+ this.turn((camera) => ({
157
+ tilt: Math.min(Math.max(camera.tilt + dy * TILT_PER_PIXEL, 15), 90),
158
+ turn: camera.turn - dx * TURN_PER_PIXEL,
159
+ }));
160
+ return true;
161
+ }
162
+ up(event) {
163
+ const held = this.held;
164
+ if (!held || held.id !== event.pointerId) {
165
+ return false;
166
+ }
167
+ this.held = undefined;
168
+ this.dragged = held.moved;
169
+ if (held.moved) {
170
+ this.redraw();
171
+ }
172
+ return held.moved;
173
+ }
174
+ frame(fits, middle) {
175
+ let shown = fits;
176
+ if (this.moving) {
177
+ this.settling = undefined;
178
+ const from = this.last ?? { frame: fits, middle };
179
+ this.kept ??= {
180
+ frame: from.frame,
181
+ middle: { x: from.middle.x - from.frame.x, y: from.middle.y - from.frame.y },
182
+ };
183
+ const { frame, middle: at } = this.kept;
184
+ shown = { height: frame.height, width: frame.width, x: middle.x - at.x, y: middle.y - at.y };
185
+ }
186
+ else if (this.kept) {
187
+ this.kept = undefined;
188
+ const still = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
189
+ this.settling = still || !this.last ? undefined : { from: this.last.frame, start: now() };
190
+ }
191
+ if (this.settling && !this.moving) {
192
+ const { from, start } = this.settling;
193
+ const t = (now() - start) / SETTLE;
194
+ if (t >= 1) {
195
+ this.settling = undefined;
196
+ }
197
+ else {
198
+ const k = easeOut(Math.max(0, t));
199
+ const toward = (a, b) => a + (b - a) * k;
200
+ shown = {
201
+ height: toward(from.height, fits.height),
202
+ width: toward(from.width, fits.width),
203
+ x: toward(from.x, fits.x),
204
+ y: toward(from.y, fits.y),
205
+ };
206
+ globalThis.requestAnimationFrame?.(() => this.redraw());
207
+ }
208
+ }
209
+ this.last = { frame: shown, middle };
210
+ return shown;
211
+ }
212
+ }
@@ -37,6 +37,9 @@ export interface SurfaceField {
37
37
  depths: number[];
38
38
  /** How many cells hold a reading. */
39
39
  readings: number;
40
+ /** The lowest and the highest reading, which every level is measured between. */
41
+ lowest: number;
42
+ highest: number;
40
43
  }
41
44
  /**
42
45
  * Reads the table: **the first numeric column is across, the second is in
@@ -33,6 +33,8 @@ export const surfaceOf = (rows, columns) => {
33
33
  },
34
34
  cells,
35
35
  depths,
36
+ highest: read.length > 0 ? Math.max(...read.map((one) => one.value)) : 0,
37
+ lowest: read.length > 0 ? Math.min(...read.map((one) => one.value)) : 0,
36
38
  readings: read.length,
37
39
  };
38
40
  };
@@ -40,7 +42,6 @@ export const levelOf = (field, cell) => {
40
42
  if (cell.value === undefined) {
41
43
  return undefined;
42
44
  }
43
- const values = field.cells.flatMap((one) => (one.value === undefined ? [] : [one.value]));
44
- const [low, high] = [Math.min(...values), Math.max(...values)];
45
+ const { highest: high, lowest: low } = field;
45
46
  return high === low ? 0.5 : (cell.value - low) / (high - low);
46
47
  };
@@ -15,11 +15,12 @@ declare const HelixHeadlessSpatialSurfaceChart_base: typeof HelixElement & (new
15
15
  *
16
16
  * **Helix draws the frame, and a renderer draws the surface in 3D.** Put a
17
17
  * box for the renderer inside the chart, and `connectSpatialSurfaceChart` from
18
- * `@fusion.dev/helix-plot` draws the surface in it with three.js, shaded from
19
- * low to high in the theme's colors. Until a renderer has drawn, and where
18
+ * `@fusion.dev/helix-plot`, given `threeSurfaceDriver` from
19
+ * `@fusion.dev/helix-plot/three`, draws the surface in it with three.js,
20
+ * shaded from low to high in the theme's colors. Until a renderer has drawn, and where
20
21
  * none is connected, the chart draws the surface in 3D itself, in SVG, from
21
- * the same place the renderer's camera starts: a still picture the server, a
22
- * print, and a page without WebGL all show, which Shift with an arrow turns.
22
+ * the same place the renderer's camera starts: a picture a print and a page
23
+ * without WebGL show too, which a drag or Shift with an arrow turns.
23
24
  *
24
25
  * **The table is the reading.** Every number is in it, and screen readers
25
26
  * read it. The `summary` part names the measures, the number of readings,
@@ -131,13 +132,15 @@ export declare class HelixHeadlessSpatialSurfaceChart extends HelixHeadlessSpati
131
132
  summaryLabel: string;
132
133
  /** Whether a renderer has drawn the surface, which hides the chart's own drawing. */
133
134
  private rendered;
134
- /** Where the chart's own drawing is seen from, which Shift with an arrow turns. */
135
+ /** Where the chart's own drawing is seen from, which a drag or Shift with an arrow turns. */
135
136
  private camera;
137
+ /** A drag across the chart's own drawing, which turns its view. */
138
+ private readonly drag;
136
139
  /** Each reading where the chart's own drawing put it, which the pointer finds. */
137
140
  private placed;
138
141
  /** Where the renderer draws the reading, which the tooltip points at. */
139
142
  private place;
140
- /** The field last read, kept so a change of it can be told. */
143
+ /** The field last told, kept so a change of it can be told. */
141
144
  private last;
142
145
  /** The place the keys rested on last, so a change of it can be told. */
143
146
  private told;
@@ -149,6 +152,8 @@ export declare class HelixHeadlessSpatialSurfaceChart extends HelixHeadlessSpati
149
152
  * renderer reads it.
150
153
  */
151
154
  get field(): SurfaceField;
155
+ /** The field last read, and the rows it was read from. */
156
+ private memo;
152
157
  /** The place the keys or the pointer rest on, as its place in `field.cells`, or `-1`. */
153
158
  get active(): number;
154
159
  /** The number of places on the grid, which the keys walk. */
@@ -197,8 +202,14 @@ export declare class HelixHeadlessSpatialSurfaceChart extends HelixHeadlessSpati
197
202
  * faces are painted back to front.
198
203
  */
199
204
  private readonly onDrawingMove;
205
+ private readonly onDrawingDown;
206
+ /** A press the browser took back, such as for a scroll, which ends a drag as a release does. */
207
+ private readonly onDrawingCancel;
200
208
  private readonly onDrawingLeave;
201
- /** A tap is a touch reader's hover: it shows the place nearest it until a tap lands elsewhere. */
209
+ /**
210
+ * A tap is a touch reader's hover, unless it was a drag, which turned the
211
+ * view: it shows the place nearest it until a tap lands elsewhere.
212
+ */
202
213
  private readonly onDrawingTap;
203
214
  protected render(): TemplateResult;
204
215
  /**
@@ -3,7 +3,7 @@ import { AriaForwardController, CHART_PLOT, customElement, drawTooltip, formatWo
3
3
  import { html, nothing, svg } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import '../../tooltip/index.js';
6
- import { boxView, drawBox, inBox, nearestTo, SPATIAL_VIEW, shape, turned, } from '../../spatial-scatter-chart/view.js';
6
+ import { boxView, DragTurn, drawBox, inBox, nearestTo, SPATIAL_VIEW, shape, turned, } from '../../spatial-scatter-chart/view.js';
7
7
  import { levelOf, surfaceOf } from '../grid.js';
8
8
  const LIGHT = (() => {
9
9
  const [x, y, z] = [-0.45, 1, 0.35];
@@ -25,8 +25,10 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
25
25
  this.summaryLabel = '{count, plural, one {# reading} other {# readings}} of {y} over {x} and {z}, from {low} to {high}.';
26
26
  this.rendered = false;
27
27
  this.camera = SPATIAL_VIEW;
28
+ this.drag = new DragTurn((next) => {
29
+ this.camera = next(this.camera);
30
+ }, () => this.requestUpdate());
28
31
  this.placed = [];
29
- this.last = '';
30
32
  this.told = -1;
31
33
  this.aria = new AriaForwardController(this, {
32
34
  target: () => this.partNamed('plot'),
@@ -42,17 +44,29 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
42
44
  this.dispatchEvent(new HelixPlotTurnEvent(turn));
43
45
  };
44
46
  this.onDrawingMove = (event) => {
47
+ if (this.drag.move(event)) {
48
+ return;
49
+ }
45
50
  if (event.pointerType === 'touch') {
46
51
  return;
47
52
  }
48
53
  this.requestActive(this.placeAt(event), event.pointerType === 'pen' ? 'pen' : 'mouse');
49
54
  };
55
+ this.onDrawingDown = (event) => {
56
+ this.drag.down(event);
57
+ };
58
+ this.onDrawingCancel = (event) => {
59
+ this.drag.up(event);
60
+ };
50
61
  this.onDrawingLeave = (event) => {
51
62
  if (event.pointerType !== 'touch') {
52
63
  this.requestActive(-1, 'mouse');
53
64
  }
54
65
  };
55
66
  this.onDrawingTap = (event) => {
67
+ if (this.drag.up(event)) {
68
+ return;
69
+ }
56
70
  if (event.pointerType === 'touch') {
57
71
  this.requestActive(this.placeAt(event, 24), 'touch');
58
72
  }
@@ -60,7 +74,11 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
60
74
  }
61
75
  static { this.words = 'helix-spatial-surface-chart'; }
62
76
  get field() {
63
- return surfaceOf(this.sourceRows, this.plottedColumns);
77
+ const rows = this.sourceRows;
78
+ if (this.memo?.rows !== rows) {
79
+ this.memo = { field: surfaceOf(rows, this.plottedColumns), rows };
80
+ }
81
+ return this.memo.field;
64
82
  }
65
83
  get active() {
66
84
  return this.pointer.at;
@@ -98,7 +116,7 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
98
116
  }
99
117
  updated(changed) {
100
118
  super.updated(changed);
101
- const field = JSON.stringify(this.field);
119
+ const field = this.field;
102
120
  if (field !== this.last) {
103
121
  this.last = field;
104
122
  this.dispatchEvent(new HelixPlotChangeEvent({ kind: 'data' }));
@@ -174,7 +192,7 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
174
192
  faces.sort((one, other) => one.depth - other.depth);
175
193
  const active = places.find((place) => place.index === at);
176
194
  const row = field.cells[at]?.row;
177
- const { bounds } = view;
195
+ const bounds = this.drag.frame(view.bounds, view.on([0, 0, 0]));
178
196
  const short = (value) => Math.round(value * 1000) / 1000;
179
197
  return html `<div aria-hidden="true" dir="ltr" ?hidden=${this.rendered} part="fallback">
180
198
  <svg
@@ -182,6 +200,9 @@ let HelixHeadlessSpatialSurfaceChart = class HelixHeadlessSpatialSurfaceChart ex
182
200
  preserveAspectRatio="xMidYMid meet"
183
201
  viewBox="${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}"
184
202
  xmlns="http://www.w3.org/2000/svg"
203
+ @click=${this.drag.click}
204
+ @pointercancel=${this.onDrawingCancel}
205
+ @pointerdown=${this.onDrawingDown}
185
206
  @pointerleave=${this.onDrawingLeave}
186
207
  @pointermove=${this.onDrawingMove}
187
208
  @pointerup=${this.onDrawingTap}