multi-gauge 0.1.0 → 0.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 CHANGED
@@ -1,15 +1,41 @@
1
1
  # MultiGauge
2
2
 
3
- > A tiny, fast WebGPU instrument panel for realtime signals.
3
+ > A fast WebGPU instrument panel for realtime signals.
4
4
 
5
- MultiGauge renders modern, dark telemetry panels directly with WebGPU. It is plain JavaScript ESM, has no runtime dependencies, draws only when something changes, and does not care where values come from.
5
+ MultiGauge displays multiple telemetry gauges in a responsive grid. It is a JavaScript ESM package with no runtime dependencies and accepts data from any source, including WebSocket, SSE, browser sensors and simulations.
6
6
 
7
7
  ![MultiGauge vehicle telemetry dashboard](./docs/multigauge-demo.png)
8
8
 
9
+ ## Requirements
10
+
11
+ - A browser with WebGPU support.
12
+ - A canvas with a visible width and height.
13
+ - A secure context when required by the browser.
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ npm install multi-gauge
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```html
24
+ <canvas id="instruments"></canvas>
25
+
26
+ <style>
27
+ #instruments {
28
+ display: block;
29
+ width: 100%;
30
+ height: 600px;
31
+ }
32
+ </style>
33
+ ```
34
+
9
35
  ```js
10
36
  import { MultiGauge } from 'multi-gauge';
11
37
 
12
- const panel = await MultiGauge.create(document.querySelector('canvas'), {
38
+ const panel = await MultiGauge.create(document.querySelector('#instruments'), {
13
39
  grid: { rows: 2, columns: 3, gap: 8 },
14
40
  accent: '#00eaff',
15
41
  gauges: [
@@ -22,95 +48,289 @@ const panel = await MultiGauge.create(document.querySelector('canvas'), {
22
48
  unit: 'km/h',
23
49
  bands: [
24
50
  { from: 0, to: 160, kind: 'normal' },
25
- { from: 240, to: 300, kind: 'critical' }
51
+ { from: 240, to: 300, kind: 'critical', label: 'LIMIT' }
26
52
  ],
27
- markers: [{ id: 'target', value: 145 }]
53
+ markers: [
54
+ { id: 'target', value: 145, kind: 'target', label: 'TARGET' }
55
+ ]
28
56
  },
29
- { id: 'heading', type: 'compass', label: 'HDG' },
57
+ {
58
+ id: 'temperature',
59
+ type: 'linear',
60
+ label: 'TEMPERATURE',
61
+ min: -40,
62
+ max: 150,
63
+ unit: '°C'
64
+ },
65
+ { id: 'heading', type: 'compass', label: 'HEADING' },
30
66
  { id: 'radar', type: 'status', label: 'RADAR' }
31
67
  ]
32
68
  });
33
69
 
34
- panel.update({ speed: 127, heading: 182, radar: true });
70
+ panel.update({
71
+ speed: 127,
72
+ temperature: 86,
73
+ heading: 182,
74
+ radar: true
75
+ });
35
76
  ```
36
77
 
37
- ## What it renders
78
+ ## Gauge types
79
+
80
+ | Type | Use |
81
+ | --- | --- |
82
+ | `arc` | A scalar value displayed on a radial scale. |
83
+ | `linear` | A horizontal or vertical scalar gauge. |
84
+ | `compass` | A cyclic heading. Values wrap between `min` and `max`. |
85
+ | `status` | A boolean, numeric or string state. |
86
+
87
+ Every gauge requires a unique `id`. Common options include:
38
88
 
39
- - `arc`: scalar dial with configurable range, bands and markers.
40
- - `linear`: horizontal or vertical, with `fill`, `marker`, or `fill-marker` mode.
41
- - `compass`: cyclic dial; values wrap correctly around zero.
42
- - `status`: discrete boolean, number or string states with semantic colors.
89
+ | Option | Description |
90
+ | --- | --- |
91
+ | `label` | Primary gauge label. Defaults to `id`. |
92
+ | `info` | Optional secondary information. |
93
+ | `unit` | Unit displayed with the value. |
94
+ | `min`, `max` | Numeric range. Defaults to `0` and `100`; compass defaults to `0..360`. |
95
+ | `value` | Initial value. |
96
+ | `row`, `col` | Zero-based grid position. Omit both for automatic placement. |
97
+ | `rowSpan`, `colSpan` | Number of occupied grid cells. Defaults to `1`. |
98
+ | `bands` | Semantic ranges for arc, linear and compass gauges. |
99
+ | `markers` | Fixed reference values for arc, linear and compass gauges. |
43
100
 
44
- Bands are shared concepts across continuous gauge families. Arc, linear and compass bands can include a `label`, shown near the midpoint of the colored range when its measured text fits. Radial band labels sit close outside the arc with their baseline rotated along the midpoint tangent; labeled targets use a second, farther radial tier to avoid collisions. Status gauges use `states[].label` instead. `normal`, `warning`, `critical`, `inactive`, `target`, and custom theme keys remain separate from the panel accent.
101
+ ### Linear gauges
45
102
 
46
- Moving value indicators and progress tracks use the panel accent. Static key markers use `target` by default—or their configured semantic `kind`—and cross the gauge track with the same short-stroke language in linear and radial gauges. Numeric readouts use the regular text color outside bands and adopt the semantic color of the active band; units remain neutral.
103
+ ```js
104
+ {
105
+ id: 'fuel',
106
+ type: 'linear',
107
+ orientation: 'horizontal', // "horizontal" or "vertical"
108
+ mode: 'fill-marker', // "fill", "marker" or "fill-marker"
109
+ label: 'FUEL',
110
+ min: 0,
111
+ max: 100,
112
+ unit: '%'
113
+ }
114
+ ```
47
115
 
48
- Radial scale divisions use muted rounded strokes pointing toward the dial center. They scale with the dial and disappear below a useful radius. Arc endpoints and the midpoint are emphasized with slightly longer major ticks; compass cardinal positions remain reserved for `N`, `E`, `S`, and `W`.
116
+ ### Compass gauges
49
117
 
50
- Responsive radial layout prioritizes the dial itself. Band and target labels reserve an outer tier only when the complete text fits and the remaining dial keeps a useful radius; otherwise the label disappears without an ellipsis while the marker remains visible.
118
+ ```js
119
+ {
120
+ id: 'heading',
121
+ type: 'compass',
122
+ label: 'HEADING',
123
+ min: 0,
124
+ max: 360
125
+ }
126
+ ```
51
127
 
52
- ## Grid and header
128
+ Compass bands may cross the wrap point:
129
+
130
+ ```js
131
+ bands: [{ from: 330, to: 30, kind: 'warning', label: 'SECTOR' }]
132
+ ```
133
+
134
+ ### Status gauges
135
+
136
+ Status gauges use `states` to map incoming values to labels and semantic colors:
137
+
138
+ ```js
139
+ {
140
+ id: 'mode',
141
+ type: 'status',
142
+ label: 'MODE',
143
+ states: [
144
+ { value: 0, label: 'OFF', kind: 'inactive' },
145
+ { value: 1, label: 'READY', kind: 'normal' },
146
+ { value: 2, label: 'FAULT', kind: 'critical' }
147
+ ]
148
+ }
149
+ ```
150
+
151
+ Without an explicit `states` array, status gauges accept `false` as `OFF` and `true` as `ON`.
152
+
153
+ ## Colors and themes
154
+
155
+ The panel accent colors live values and progress indicators. Bands, markers and states use their `kind`:
156
+
157
+ ```js
158
+ import { MultiGauge, SEMANTIC_KINDS } from 'multi-gauge';
159
+
160
+ console.log(SEMANTIC_KINDS);
161
+ // ["normal", "warning", "critical", "inactive", "target"]
162
+
163
+ const panel = await MultiGauge.create(canvas, {
164
+ accent: '#00eaff',
165
+ theme: {
166
+ background: '#071014',
167
+ text: '#e7f7fa',
168
+ normal: '#41e6a1',
169
+ warning: '#ffc857',
170
+ critical: '#ff4d6d',
171
+ inactive: '#40515a',
172
+ target: '#f3f7a7'
173
+ },
174
+ gauges: []
175
+ });
176
+ ```
177
+
178
+ Additional theme keys can be used as custom `kind` values.
179
+
180
+ Use `setAccents()` when individual gauges need source-owned runtime colors:
181
+
182
+ ```js
183
+ panel.setAccents({
184
+ speed: '#4b9cff',
185
+ temperature: '#ff8c42'
186
+ });
187
+ ```
53
188
 
54
- Cells use `row`, `col`, `rowSpan`, and `colSpan`. Omit `row` and `col` to use deterministic auto-placement. A header is independent from the grid:
189
+ Per-gauge runtime accents are not included in serialized state.
190
+
191
+ ## Grid and header
55
192
 
56
193
  ```js
57
194
  const panel = await MultiGauge.create(canvas, {
58
- grid: { rows: 3, columns: 3 },
195
+ grid: { rows: 3, columns: 3, gap: 8 },
59
196
  header: {
60
197
  icon: new URL('./vehicle.svg', import.meta.url),
61
198
  title: 'VEHICLE 01',
62
- subtitle: 'Prototype',
199
+ subtitle: 'Prototype telemetry',
63
200
  badge: 'ONLINE'
64
201
  },
65
202
  gauges: []
66
203
  });
67
204
  ```
68
205
 
69
- SVG, PNG and WebP header icons are decoded by browser APIs, uploaded once, and cached by URL in the shared GPU runtime. Text uses one shared GPU texture containing a small set of fixed Inter Variable raster styles. Smaller cells automatically reduce labels, ticks and secondary information.
206
+ Header icons can be SVG, PNG or WebP URLs. Change the grid at runtime with:
70
207
 
71
- ## Runtime updates
208
+ ```js
209
+ panel.setGrid({ rows: 4, columns: 4, gap: 8 });
210
+ ```
211
+
212
+ ## Updating values and configuration
72
213
 
73
214
  ```js
74
215
  panel.set('speed', 130);
75
216
  panel.update({ speed: 132, heading: 184 });
76
217
  panel.setMarker('speed', 'target', 150);
77
218
 
78
- panel.add({ id: 'temperature', type: 'linear', min: -40, max: 150 });
79
- panel.remove('temperature');
219
+ panel.configure('speed', {
220
+ type: 'linear',
221
+ orientation: 'horizontal',
222
+ mode: 'fill-marker'
223
+ });
224
+
225
+ panel.add({
226
+ id: 'pressure',
227
+ type: 'linear',
228
+ label: 'PRESSURE',
229
+ min: 0,
230
+ max: 300,
231
+ unit: 'bar'
232
+ });
233
+
234
+ panel.remove('pressure');
80
235
  ```
81
236
 
82
- Multiple updates in one browser frame produce one render. There is no permanent animation loop and no scheduler per gauge. Dynamic values are written through reusable staging memory; glyph buffers are rebuilt only when a formatted readout or its semantic color changes. Text measurements use a bounded atlas cache. All panels on a page share one `GPUDevice`, pipelines, glyph atlas and icon cache.
237
+ `set(id, value)` reports an unknown gauge. `update(values)` ignores unknown keys, which allows a telemetry source to continue publishing while gauges are added or removed.
238
+
239
+ ## Editing and selection
83
240
 
84
- ## Layout editing
241
+ Editing is enabled by default. Users can:
242
+
243
+ - Drag a gauge to move it.
244
+ - Drag from an edge or corner to resize it.
245
+ - Select a gauge with a click.
246
+ - Remove a gauge with its `×` control.
247
+ - Press `Escape` to cancel a move or resize.
85
248
 
86
249
  ```js
250
+ panel.setEditing(false);
251
+ panel.setEditing(true);
252
+
87
253
  panel.move('speed', 0, 1);
88
254
  panel.resize('speed', 2, 2);
89
255
  panel.maximize('speed');
90
256
  panel.restoreGauge('speed');
91
257
  panel.minimize('speed');
258
+ panel.select('speed');
259
+ panel.select(null);
260
+ ```
261
+
262
+ ## Events
263
+
264
+ ```js
265
+ panel.addEventListener('configurationchange', (event) => {
266
+ const { reason, state } = event.detail;
267
+ saveDashboard(state);
268
+ });
269
+
270
+ panel.addEventListener('selectionchange', (event) => {
271
+ const { id, gauge } = event.detail;
272
+ openGaugeEditor(id, gauge);
273
+ });
274
+ ```
275
+
276
+ `configurationchange` is emitted after gauges are added, removed or configured and after grid or layout operations. Value updates, marker updates and runtime accents do not emit it. `selectionchange` is emitted when the selected gauge changes.
277
+
278
+ ## Persistence
92
279
 
93
- panel.setEditing(false); // optionally disable direct manipulation
94
- panel.setEditing(true); // enable it again
280
+ ```js
281
+ const saved = JSON.stringify(panel.serialize());
282
+
283
+ // Later
284
+ panel.restore(JSON.parse(saved));
95
285
  ```
96
286
 
97
- Direct manipulation is enabled by default: drag anywhere inside a gauge to move it, or drag from any edge or corner to resize it. There is no edit mode or visible resize handle. Move and resize run as transactions: an outline follows the pointer, a placeholder shows the resolved grid destination, and the committed layout changes only on pointer release. Press `Escape` to cancel. Exact compatible footprints swap; other collisions use deterministic push/reflow. Failed operations leave the previous layout intact. The occupancy map—not the editor DOM—is the source of truth.
287
+ Serialized state contains the grid, header, theme, gauge configuration and current gauge values. Runtime accents and the current selection are not persisted.
98
288
 
99
- Arc and compass gauges use their complete content region and place the numeric value and unit on separate centered lines inside the dial. Compass headings use a moving perimeter mark instead of a center needle, leaving the middle available for the heading value. Horizontal linear gauges keep their readout below the bar; vertical linear gauges place value and unit on one line to the right so the bar can use the full available height. Labels are measured against the glyph atlas, reduced through 9–12 px UI buckets, and truncated with an ellipsis only when necessary. The bundled Inter Variable font is loaded before atlas generation; glyph styles are rasterized at the active `devicePixelRatio`, use real font metrics and snap text quads to the physical pixel grid. Numeric values use stable tabular advances without introducing a second font family.
289
+ ## Fonts
100
290
 
101
- ## Persistence and lifecycle
291
+ MultiGauge uses the canvas' computed `font-family`. Define application fonts in CSS, or pass an explicit CSS font-family value:
102
292
 
103
293
  ```js
104
- const json = JSON.stringify(panel.serialize());
105
- panel.restore(JSON.parse(json));
294
+ await document.fonts.ready;
106
295
 
107
- console.log(panel.getStats());
108
- panel.destroy();
296
+ const panel = await MultiGauge.create(canvas, {
297
+ fontFamily: '"Inter Variable", Inter, sans-serif',
298
+ gauges: []
299
+ });
109
300
  ```
110
301
 
111
- Serialized state contains only public grid, layout, gauge, header and visual configuration. `destroy()` cancels pending work and releases canvas-specific GPU resources, observers, editor DOM and icon references; shared resources remain available to other panels.
302
+ MultiGauge does not include or download font files.
303
+
304
+ ## API
305
+
306
+ | Method | Result |
307
+ | --- | --- |
308
+ | `MultiGauge.create(canvas, options)` | Creates a panel asynchronously. |
309
+ | `add(configuration)` | Adds and returns a gauge definition. |
310
+ | `remove(id)` | Removes a gauge and returns whether it existed. |
311
+ | `set(id, value)` | Updates one gauge. |
312
+ | `update(values)` | Updates several gauges. |
313
+ | `setMarker(gaugeId, markerId, value, options?)` | Adds or updates a marker. |
314
+ | `configure(id, patch)` | Updates a gauge definition. |
315
+ | `setAccents(accents)` | Applies transient per-gauge accent colors. |
316
+ | `setGrid(configuration)` | Changes grid rows, columns or gap. |
317
+ | `setEditing(enabled)` | Enables or disables direct editing. |
318
+ | `select(id)` | Selects a gauge, or clears selection with `null`. |
319
+ | `move(id, row, col)` | Moves a gauge. |
320
+ | `resize(id, rowSpan, colSpan)` | Resizes a gauge. |
321
+ | `maximize(id)` | Maximizes a gauge inside its grid. |
322
+ | `minimize(id)` | Minimizes a maximized gauge. |
323
+ | `restoreGauge(id)` | Restores the previous gauge placement. |
324
+ | `serialize()` | Returns JSON-compatible panel state. |
325
+ | `restore(state)` | Replaces the panel configuration. |
326
+ | `getStats()` | Returns diagnostic counters. |
327
+ | `destroy()` | Releases the panel and its browser resources. |
328
+
329
+ Call `destroy()` when the canvas is permanently removed:
112
330
 
113
- WebGPU is required. MultiGauge fails with a clear `MultiGaugeError` rather than silently switching renderer.
331
+ ```js
332
+ panel.destroy();
333
+ ```
114
334
 
115
335
  ## Development
116
336
 
@@ -119,6 +339,4 @@ npm test
119
339
  npm run demo
120
340
  ```
121
341
 
122
- Open `http://localhost:8080/demo/` for two simultaneous panels or `/benchmark/` for 1, 16, 64 and 100-gauge workloads at 10, 30 and 60 Hz. The benchmark also compares sixteen 1×1 panels with one 4×4 panel.
123
-
124
- MultiGauge is source-agnostic: call `set()` or `update()` from WebSocket, SSE, sensors, shared memory, audio, simulations, or manual controls. Transport adapters belong outside this core package.
342
+ The demo is available at `http://localhost:8080/demo/` and the benchmark at `http://localhost:8080/benchmark/`.
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "multi-gauge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A tiny, fast WebGPU instrument panel for realtime signals.",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/index.js"
7
+ ".": {
8
+ "types": "./types/index.d.ts",
9
+ "default": "./src/index.js"
10
+ }
8
11
  },
12
+ "types": "./types/index.d.ts",
9
13
  "files": [
10
14
  "src",
15
+ "types",
11
16
  "docs/multigauge-demo.png",
12
17
  "README.md",
13
18
  "LICENSE"
package/src/MultiGauge.js CHANGED
@@ -8,7 +8,7 @@ import { GridEditor } from './interaction/GridEditor.js';
8
8
  import { resolveTheme } from './theme.js';
9
9
 
10
10
  /** A source-agnostic WebGPU panel containing multiple realtime gauges. */
11
- export class MultiGauge {
11
+ export class MultiGauge extends EventTarget {
12
12
  #canvas;
13
13
  #runtime;
14
14
  #renderer;
@@ -26,7 +26,10 @@ export class MultiGauge {
26
26
  #height = 1;
27
27
  #panelLayout = layoutPanel({ width: 1, height: 1 });
28
28
  #editor;
29
+ #editing = true;
29
30
  #previewEntries;
31
+ #selectedGaugeId = null;
32
+ #accents = {};
30
33
  #stats = { updatesReceived: 0, rendersCoalesced: 0 };
31
34
 
32
35
  /** Create a panel after the shared WebGPU runtime is ready. */
@@ -34,13 +37,17 @@ export class MultiGauge {
34
37
  if (!canvas || typeof canvas.getContext !== 'function') {
35
38
  throw new MultiGaugeError('MultiGauge.create() requires a canvas.');
36
39
  }
37
- const runtime = await SharedGpuRuntime.get();
40
+ const inheritedFontFamily = globalThis.getComputedStyle?.(canvas).fontFamily;
41
+ const runtime = await SharedGpuRuntime.get({
42
+ fontFamily: options.fontFamily || inheritedFontFamily
43
+ });
38
44
  const panel = new MultiGauge(canvas, options, runtime);
39
45
  panel.#initialize();
40
46
  return panel;
41
47
  }
42
48
 
43
49
  constructor(canvas, options, runtime) {
50
+ super();
44
51
  this.#canvas = canvas;
45
52
  this.#runtime = runtime;
46
53
  this.#layout = new GridLayout(options.grid);
@@ -58,6 +65,7 @@ export class MultiGauge {
58
65
  this.#assertAlive();
59
66
  const gauge = this.#addNow(configuration);
60
67
  this.#invalidate(true);
68
+ this.#emitConfigurationChange('add');
61
69
  return { ...gauge };
62
70
  }
63
71
 
@@ -68,7 +76,12 @@ export class MultiGauge {
68
76
  return false;
69
77
  }
70
78
  this.#layout.remove(id);
79
+ delete this.#accents[id];
80
+ if (this.#selectedGaugeId === id) {
81
+ this.#selectNow(null);
82
+ }
71
83
  this.#invalidate(true);
84
+ this.#emitConfigurationChange('remove');
72
85
  return true;
73
86
  }
74
87
 
@@ -89,11 +102,25 @@ export class MultiGauge {
89
102
  return this;
90
103
  }
91
104
 
92
- /** Set several signals; all changes are coalesced into one animation frame. */
105
+ /** Set known signals; unknown stream fields are ignored after gauges are removed. */
93
106
  update(values) {
94
107
  this.#assertAlive();
108
+ let changed = false;
95
109
  for (const [id, value] of Object.entries(values)) {
96
- this.set(id, value);
110
+ const gauge = this.#gauges.get(id);
111
+ if (!gauge) {
112
+ continue;
113
+ }
114
+ this.#stats.updatesReceived += 1;
115
+ const next = coerceValue(gauge, value);
116
+ if (!Object.is(next, gauge.value)) {
117
+ gauge.value = next;
118
+ this.#dynamicDirty = true;
119
+ changed = true;
120
+ }
121
+ }
122
+ if (changed) {
123
+ this.#schedule();
97
124
  }
98
125
  return this;
99
126
  }
@@ -110,11 +137,48 @@ export class MultiGauge {
110
137
  return this;
111
138
  }
112
139
 
140
+ /** Atomically update one gauge definition without changing its placement. */
141
+ configure(id, patch = {}) {
142
+ this.#assertAlive();
143
+ const current = this.#gauges.get(id);
144
+ if (!current) {
145
+ throw new MultiGaugeError(`Unknown gauge: ${id}.`);
146
+ }
147
+ const next = normalizeGauge({ ...current, ...patch, id });
148
+ this.#gauges.set(id, next);
149
+ this.#invalidate(true);
150
+ this.#emitConfigurationChange('configure');
151
+ return { ...next };
152
+ }
153
+
154
+ /** Apply source-owned runtime accent colors without serializing them. */
155
+ setAccents(accents = {}) {
156
+ this.#assertAlive();
157
+ const next = Object.fromEntries(Object.entries(accents)
158
+ .filter(([id, value]) => this.#gauges.has(id) && typeof value === 'string' && value));
159
+ if (JSON.stringify(next) !== JSON.stringify(this.#accents)) {
160
+ this.#accents = next;
161
+ this.#invalidate(true);
162
+ }
163
+ return this;
164
+ }
165
+
166
+ /** Select a gauge for an external editor. Selection is transient. */
167
+ select(id = null) {
168
+ this.#assertAlive();
169
+ if (id !== null && !this.#gauges.has(id)) {
170
+ throw new MultiGaugeError(`Unknown gauge: ${id}.`);
171
+ }
172
+ this.#selectNow(id);
173
+ return this;
174
+ }
175
+
113
176
  move(id, row, col) {
114
177
  this.#assertAlive();
115
178
  const result = this.#layout.move(id, row, col);
116
179
  if (result) {
117
180
  this.#invalidate(true);
181
+ this.#emitConfigurationChange('move');
118
182
  }
119
183
  return Boolean(result);
120
184
  }
@@ -124,6 +188,7 @@ export class MultiGauge {
124
188
  const result = this.#layout.resize(id, rowSpan, colSpan);
125
189
  if (result) {
126
190
  this.#invalidate(true);
191
+ this.#emitConfigurationChange('resize');
127
192
  }
128
193
  return Boolean(result);
129
194
  }
@@ -132,6 +197,7 @@ export class MultiGauge {
132
197
  this.#assertAlive();
133
198
  this.#layout.maximize(id);
134
199
  this.#invalidate(true);
200
+ this.#emitConfigurationChange('maximize');
135
201
  return this;
136
202
  }
137
203
 
@@ -140,6 +206,7 @@ export class MultiGauge {
140
206
  const result = this.#layout.minimize(id);
141
207
  if (result) {
142
208
  this.#invalidate(true);
209
+ this.#emitConfigurationChange('minimize');
143
210
  }
144
211
  return Boolean(result);
145
212
  }
@@ -149,23 +216,60 @@ export class MultiGauge {
149
216
  const result = this.#layout.restore(id);
150
217
  if (result) {
151
218
  this.#invalidate(true);
219
+ this.#emitConfigurationChange('restore-gauge');
152
220
  }
153
221
  return Boolean(result);
154
222
  }
155
223
 
224
+ /** Atomically resize the grid and deterministically reflow gauges that no longer fit. */
225
+ setGrid(configuration = {}) {
226
+ this.#assertAlive();
227
+ const current = this.#layout.config;
228
+ const next = new GridLayout({ ...current, ...configuration });
229
+ const entries = new Map(this.#layout.entries());
230
+ const ordered = [...this.#gauges.values()].sort((a, b) => {
231
+ const first = entries.get(a.id);
232
+ const second = entries.get(b.id);
233
+ return first.row - second.row || first.col - second.col || a.id.localeCompare(b.id);
234
+ });
235
+ for (const gauge of ordered) {
236
+ const placement = entries.get(gauge.id);
237
+ try {
238
+ next.add(gauge.id, placement);
239
+ } catch {
240
+ next.add(gauge.id, {
241
+ rowSpan: placement.rowSpan,
242
+ colSpan: placement.colSpan
243
+ });
244
+ }
245
+ }
246
+ this.#replaceLayout(next);
247
+ this.#invalidate(true);
248
+ this.#emitConfigurationChange('grid');
249
+ return this;
250
+ }
251
+
156
252
  /** Enable or disable direct layout manipulation. Enabled by default. */
157
253
  setEditing(enabled) {
158
254
  this.#assertAlive();
255
+ this.#editing = Boolean(enabled);
159
256
  if (enabled && !this.#editor) {
160
257
  this.#editor = new GridEditor(this.#canvas, this.#layout, {
161
258
  preview: (entries) => this.#setLayoutPreview(entries),
162
- commit: () => this.#finishLayoutPreview(),
163
- cancel: () => this.#finishLayoutPreview()
259
+ commit: () => {
260
+ this.#finishLayoutPreview();
261
+ this.#emitConfigurationChange('layout');
262
+ },
263
+ cancel: () => this.#finishLayoutPreview(),
264
+ select: (id) => this.#selectNow(id),
265
+ remove: (id) => this.remove(id)
164
266
  });
267
+ this.#editor.select(this.#selectedGaugeId);
165
268
  this.#refreshEditor();
166
269
  } else if (!enabled && this.#editor) {
167
270
  this.#editor.destroy();
168
271
  this.#editor = null;
272
+ this.#selectNow(null);
169
273
  }
170
274
  return this;
171
275
  }
@@ -202,18 +306,18 @@ export class MultiGauge {
202
306
  layout.add(gauge.id, gauge);
203
307
  gauges.set(gauge.id, gauge);
204
308
  }
205
- this.#layout = layout;
206
- this.#previewEntries = undefined;
207
309
  this.#gauges = gauges;
310
+ this.#accents = Object.fromEntries(Object.entries(this.#accents)
311
+ .filter(([id]) => gauges.has(id)));
312
+ if (this.#selectedGaugeId && !gauges.has(this.#selectedGaugeId)) {
313
+ this.#selectNow(null);
314
+ }
208
315
  this.#accent = state.accent ?? '#00eaff';
209
316
  this.#theme = resolveTheme(state.theme, this.#accent);
210
317
  this.#header = state.header ? { ...state.header } : null;
211
- if (this.#editor) {
212
- this.#editor.destroy();
213
- this.#editor = null;
214
- this.setEditing(true);
215
- }
318
+ this.#replaceLayout(layout);
216
319
  this.#invalidate(true);
320
+ this.#emitConfigurationChange('restore');
217
321
  return this;
218
322
  }
219
323
 
@@ -260,7 +364,7 @@ export class MultiGauge {
260
364
  this.#resizeObserver.observe(this.#canvas);
261
365
  }
262
366
  resize();
263
- this.setEditing(true);
367
+ this.setEditing(this.#editing);
264
368
  this.#invalidate(true);
265
369
  }
266
370
 
@@ -314,10 +418,17 @@ export class MultiGauge {
314
418
  gauges = [maximized];
315
419
  }
316
420
  if (this.#staticDirty) {
317
- this.#renderer.rebuildStatic(gauges, rectangles, this.#header, this.#theme, this.#panelLayout);
421
+ this.#renderer.rebuildStatic(
422
+ gauges,
423
+ rectangles,
424
+ this.#header,
425
+ this.#theme,
426
+ this.#panelLayout,
427
+ this.#accents
428
+ );
318
429
  }
319
430
  if (this.#dynamicDirty) {
320
- this.#renderer.updateDynamic(gauges, this.#theme);
431
+ this.#renderer.updateDynamic(gauges, this.#theme, this.#accents);
321
432
  }
322
433
  this.#renderer.render(this.#theme);
323
434
  this.#staticDirty = false;
@@ -337,6 +448,44 @@ export class MultiGauge {
337
448
  this.#invalidate(true);
338
449
  }
339
450
 
451
+ #replaceLayout(layout) {
452
+ this.#layout = layout;
453
+ this.#previewEntries = undefined;
454
+ if (!this.#editor) {
455
+ return;
456
+ }
457
+ this.#editor.destroy();
458
+ this.#editor = null;
459
+ if (this.#editing) {
460
+ this.setEditing(true);
461
+ }
462
+ }
463
+
464
+ #selectNow(id) {
465
+ const next = id ?? null;
466
+ if (this.#selectedGaugeId === next) {
467
+ return;
468
+ }
469
+ this.#selectedGaugeId = next;
470
+ this.#editor?.select(next);
471
+ const gauge = next ? this.#gauges.get(next) : null;
472
+ this.dispatchEvent(new CustomEvent('selectionchange', {
473
+ detail: {
474
+ id: next,
475
+ gauge: gauge ? serializeGauge(gauge, this.#layout.get(next)) : null
476
+ }
477
+ }));
478
+ }
479
+
480
+ #emitConfigurationChange(reason) {
481
+ this.dispatchEvent(new CustomEvent('configurationchange', {
482
+ detail: {
483
+ reason,
484
+ state: this.serialize()
485
+ }
486
+ }));
487
+ }
488
+
340
489
  #refreshEditor(rectangles) {
341
490
  if (!this.#editor) {
342
491
  return;
@@ -19,14 +19,18 @@ export class SharedGpuRuntime {
19
19
  #generation = 0;
20
20
  #recovering = false;
21
21
 
22
- static get() {
22
+ static get(options = {}) {
23
23
  if (!this.#promise) {
24
- const runtime = new SharedGpuRuntime();
24
+ const runtime = new SharedGpuRuntime(options);
25
25
  this.#promise = runtime.#initialize().then(() => runtime);
26
26
  }
27
27
  return this.#promise;
28
28
  }
29
29
 
30
+ constructor(options = {}) {
31
+ this.#fontFamily = options.fontFamily;
32
+ }
33
+
30
34
  get device() {
31
35
  return this.#device;
32
36
  }
@@ -82,7 +86,11 @@ export class SharedGpuRuntime {
82
86
  this.#device = await this.#adapter.requestDevice();
83
87
  this.#format = navigator.gpu.getPreferredCanvasFormat();
84
88
  this.#createSharedResources();
85
- this.#atlas = new TextAtlas(this.#device, globalThis.devicePixelRatio || 1);
89
+ this.#atlas = new TextAtlas(
90
+ this.#device,
91
+ globalThis.devicePixelRatio || 1,
92
+ this.#fontFamily
93
+ );
86
94
  await this.#atlas.initialize();
87
95
  this.#icons = new IconCache(this.#device);
88
96
  this.#generation += 1;
@@ -150,4 +158,6 @@ export class SharedGpuRuntime {
150
158
  this.#recovering = false;
151
159
  }
152
160
  }
161
+
162
+ #fontFamily;
153
163
  }
@@ -82,14 +82,15 @@ export class Renderer {
82
82
  return changed;
83
83
  }
84
84
 
85
- rebuildStatic(gauges, rectangles, header, theme, panelLayout) {
85
+ rebuildStatic(gauges, rectangles, header, theme, panelLayout, accents = {}) {
86
86
  const scene = buildStaticScene(
87
87
  this.#runtime.atlas,
88
88
  gauges,
89
89
  rectangles,
90
90
  header,
91
91
  theme,
92
- panelLayout
92
+ panelLayout,
93
+ accents
93
94
  );
94
95
  this.#shapeCount = scene.shapes.length / 20;
95
96
  this.#staticTextCount = scene.text.length / TEXT_INSTANCE_SIZE;
@@ -105,12 +106,12 @@ export class Renderer {
105
106
  this.#setIcon(header?.icon, theme, scene.iconRect);
106
107
  }
107
108
 
108
- updateDynamic(gauges, theme) {
109
+ updateDynamic(gauges, theme, accents = {}) {
109
110
  const valueLength = Math.max(8, gauges.length * 8);
110
111
  if (this.#dynamicValuesStaging.length !== valueLength) {
111
112
  this.#dynamicValuesStaging = new Float32Array(valueLength);
112
113
  }
113
- buildDynamicValues(gauges, theme, this.#dynamicValuesStaging);
114
+ buildDynamicValues(gauges, theme, this.#dynamicValuesStaging, accents);
114
115
  const dynamicChanged = this.#ensure('dynamic', valueLength * 4, STORAGE());
115
116
  this.#write('dynamic', this.#dynamicValuesStaging);
116
117
 
@@ -489,7 +489,7 @@ function addStatus(scene, rect, theme, index) {
489
489
  [5, radius, 0, 0], [0, index, 6, 0]);
490
490
  }
491
491
 
492
- export function buildStaticScene(atlas, gauges, rectangles, header, theme, panelLayout) {
492
+ export function buildStaticScene(atlas, gauges, rectangles, header, theme, panelLayout, accents = {}) {
493
493
  const scene = { shapes: [], text: [], atlas, cellLayouts: new Map(), iconRect: null };
494
494
  const { headerRect } = panelLayout;
495
495
  if (header && headerRect.height > 0) {
@@ -546,6 +546,7 @@ export function buildStaticScene(atlas, gauges, rectangles, header, theme, panel
546
546
  if (!rect) {
547
547
  return;
548
548
  }
549
+ const gaugeTheme = accents[gauge.id] ? { ...theme, accent: accents[gauge.id] } : theme;
549
550
  const layout = absoluteCellLayout(rect, gauge);
550
551
  scene.cellLayouts.set(gauge.id, layout);
551
552
  box(scene.shapes, rect.x, rect.y, rect.width, rect.height, color(theme.surface), 7);
@@ -573,13 +574,13 @@ export function buildStaticScene(atlas, gauges, rectangles, header, theme, panel
573
574
  }
574
575
 
575
576
  if (gauge.type === 'arc') {
576
- addArcGauge(scene, gauge, layout.gaugeRect, theme, index, layout);
577
+ addArcGauge(scene, gauge, layout.gaugeRect, gaugeTheme, index, layout);
577
578
  } else if (gauge.type === 'linear') {
578
- addLinearGauge(scene, gauge, layout.gaugeRect, theme, index, layout);
579
+ addLinearGauge(scene, gauge, layout.gaugeRect, gaugeTheme, index, layout);
579
580
  } else if (gauge.type === 'compass') {
580
- addCompass(scene, gauge, layout.gaugeRect, theme, index, layout);
581
+ addCompass(scene, gauge, layout.gaugeRect, gaugeTheme, index, layout);
581
582
  } else {
582
- addStatus(scene, layout.gaugeRect, theme, index);
583
+ addStatus(scene, layout.gaugeRect, gaugeTheme, index);
583
584
  }
584
585
  });
585
586
  return scene;
@@ -614,7 +615,7 @@ export function dynamicTextKey(gauges) {
614
615
  }
615
616
 
616
617
  /** Write the small per-gauge value/color records without allocating an intermediate array. */
617
- export function buildDynamicValues(gauges, theme, output = []) {
618
+ export function buildDynamicValues(gauges, theme, output = [], accents = {}) {
618
619
  const length = Math.max(8, gauges.length * 8);
619
620
  if (Array.isArray(output)) {
620
621
  output.length = length;
@@ -622,8 +623,8 @@ export function buildDynamicValues(gauges, theme, output = []) {
622
623
  throw new RangeError('Dynamic value output is smaller than required.');
623
624
  }
624
625
  let offset = 0;
625
- const accent = color(theme.accent);
626
626
  for (const gauge of gauges) {
627
+ const accent = color(accents[gauge.id] ?? theme.accent);
627
628
  const normalized = gauge.type === 'status' ? 0 : normalizeValue(gauge, gauge.value);
628
629
  const status = gauge.type === 'status' ? resolveStatus(gauge, gauge.value) : null;
629
630
  const rgba = status
@@ -1,5 +1,4 @@
1
- const FONT_FAMILY = 'MultiGauge Inter';
2
- const FONT_URL = new URL('../assets/fonts/InterVariable.woff2', import.meta.url);
1
+ const DEFAULT_FONT_FAMILY = 'sans-serif';
3
2
  const CHARACTERS = [...new Set(
4
3
  Array.from({ length: 95 }, (_, index) => String.fromCharCode(index + 32)).join('')
5
4
  + '°…µ·'
@@ -33,15 +32,23 @@ function makeCanvas(width, height) {
33
32
  : Object.assign(document.createElement('canvas'), { width, height });
34
33
  }
35
34
 
36
- function fontString(style, pixelRatio) {
37
- return `${style.weight} ${style.logicalSize * pixelRatio}px "${FONT_FAMILY}"`;
35
+ function normalizeFontFamily(value) {
36
+ const family = String(value ?? '').trim() || DEFAULT_FONT_FAMILY;
37
+ if (family.includes(',') || family.includes('"') || family.includes("'") || !family.includes(' ')) {
38
+ return family;
39
+ }
40
+ return `"${family}"`;
41
+ }
42
+
43
+ function fontString(style, pixelRatio, fontFamily) {
44
+ return `${style.weight} ${style.logicalSize * pixelRatio}px ${fontFamily}`;
38
45
  }
39
46
 
40
47
  function finiteMetric(value, fallback = 0) {
41
48
  return Number.isFinite(value) ? value : fallback;
42
49
  }
43
50
 
44
- /** A DPR-aware bitmap atlas with a small set of fixed Inter typography styles. */
51
+ /** A DPR-aware bitmap atlas using the font family supplied by the host application. */
45
52
  export class TextAtlas {
46
53
  static CHARACTERS = CHARACTERS;
47
54
  static STYLES = STYLE_DEFINITIONS;
@@ -56,11 +63,12 @@ export class TextAtlas {
56
63
  #styleCache = new Map();
57
64
  #metricsCache = new Map();
58
65
  #measureCache = new Map();
59
- #fontFace;
66
+ #fontFamily;
60
67
 
61
- constructor(device, pixelRatio = globalThis.devicePixelRatio || 1) {
68
+ constructor(device, pixelRatio = globalThis.devicePixelRatio || 1, fontFamily = DEFAULT_FONT_FAMILY) {
62
69
  this.#device = device;
63
70
  this.#pixelRatio = Math.max(1, Number(pixelRatio) || 1);
71
+ this.#fontFamily = normalizeFontFamily(fontFamily);
64
72
  }
65
73
 
66
74
  get view() {
@@ -138,7 +146,7 @@ export class TextAtlas {
138
146
  }
139
147
 
140
148
  async initialize() {
141
- await this.#loadFont();
149
+ await this.#waitForFont();
142
150
  const measurementCanvas = makeCanvas(1, 1);
143
151
  const measurement = measurementCanvas.getContext('2d', { alpha: true });
144
152
  measurement.textBaseline = 'alphabetic';
@@ -179,7 +187,7 @@ export class TextAtlas {
179
187
  context.textBaseline = 'alphabetic';
180
188
  context.fontKerning = 'none';
181
189
  for (const style of styles) {
182
- context.font = fontString(style, this.#pixelRatio);
190
+ context.font = fontString(style, this.#pixelRatio, this.#fontFamily);
183
191
  for (const [character, glyph] of style.glyphs) {
184
192
  if (character !== ' ') {
185
193
  context.fillText(
@@ -201,7 +209,7 @@ export class TextAtlas {
201
209
  }
202
210
 
203
211
  this.#texture = this.#device.createTexture({
204
- label: `MultiGauge Inter glyph atlas @${this.#pixelRatio}x`,
212
+ label: `MultiGauge glyph atlas (${this.#fontFamily}) @${this.#pixelRatio}x`,
205
213
  size: [atlasWidth, atlasHeight],
206
214
  format: 'rgba8unorm',
207
215
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
@@ -226,34 +234,23 @@ export class TextAtlas {
226
234
  this.#styleCache.clear();
227
235
  this.#metricsCache.clear();
228
236
  this.#measureCache.clear();
229
- if (this.#fontFace && globalThis.document?.fonts) {
230
- document.fonts.delete(this.#fontFace);
231
- }
232
237
  }
233
238
 
234
- async #loadFont() {
235
- if (typeof FontFace !== 'function' || !globalThis.document?.fonts) {
236
- throw new Error('MultiGauge requires the CSS Font Loading API to build its Inter atlas.');
239
+ async #waitForFont() {
240
+ if (!globalThis.document?.fonts) {
241
+ return;
237
242
  }
238
- const face = new FontFace(FONT_FAMILY, `url("${FONT_URL.href}") format("woff2")`, {
239
- style: 'normal',
240
- weight: '100 900'
241
- });
242
- this.#fontFace = await face.load();
243
- document.fonts.add(this.#fontFace);
244
243
  await Promise.all([...new Set(STYLE_DEFINITIONS.map(({ weight }) => weight))]
245
- .map((weight) => document.fonts.load(`${weight} 16px "${FONT_FAMILY}"`, 'Hgm0123°µ·')));
244
+ .map((weight) => document.fonts.load(
245
+ `${weight} 16px ${this.#fontFamily}`,
246
+ 'Hgm0123°µ·'
247
+ )));
246
248
  await document.fonts.ready;
247
- for (const weight of new Set(STYLE_DEFINITIONS.map((style) => style.weight))) {
248
- if (!document.fonts.check(`${weight} 16px "${FONT_FAMILY}"`, 'Hgm0123°µ·')) {
249
- throw new Error(`Inter Variable weight ${weight} did not load for the MultiGauge atlas.`);
250
- }
251
- }
252
249
  }
253
250
 
254
251
  #measureStyle(context, definition, padding) {
255
252
  const style = { ...definition, glyphs: new Map() };
256
- context.font = fontString(style, this.#pixelRatio);
253
+ context.font = fontString(style, this.#pixelRatio, this.#fontFamily);
257
254
  const records = [];
258
255
  for (const character of CHARACTERS) {
259
256
  const metrics = context.measureText(character);
package/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { MultiGauge } from './MultiGauge.js';
2
2
  export { MultiGaugeError } from './errors.js';
3
+ export { SEMANTIC_KINDS } from './theme.js';
@@ -23,6 +23,7 @@ export class GridEditor {
23
23
  #frame;
24
24
  #parentPosition;
25
25
  #panelLayout;
26
+ #selectedId = null;
26
27
 
27
28
  constructor(canvas, layout, actions) {
28
29
  this.#canvas = canvas;
@@ -62,6 +63,7 @@ export class GridEditor {
62
63
  this.#overlay.addEventListener('pointermove', this.#onPointerMove);
63
64
  this.#overlay.addEventListener('pointerup', this.#onPointerUp);
64
65
  this.#overlay.addEventListener('pointercancel', this.#onPointerCancel);
66
+ this.#overlay.addEventListener('click', this.#onClick);
65
67
  window.addEventListener('keydown', this.#onKeyDown);
66
68
  }
67
69
 
@@ -97,7 +99,17 @@ export class GridEditor {
97
99
  if (!this.#drag) {
98
100
  item.style.transform = 'translate3d(0, 0, 0)';
99
101
  }
100
- item.style.borderColor = this.#drag?.id === id ? '#00eaff' : 'transparent';
102
+ item.style.borderColor = this.#drag?.id === id || this.#selectedId === id
103
+ ? '#00eaff'
104
+ : 'transparent';
105
+ }
106
+ }
107
+
108
+ /** Highlight one gauge without changing the layout. */
109
+ select(id) {
110
+ this.#selectedId = id ?? null;
111
+ for (const [gaugeId, item] of this.#nodes) {
112
+ item.style.borderColor = gaugeId === this.#selectedId ? '#00eaff' : 'transparent';
101
113
  }
102
114
  }
103
115
 
@@ -108,6 +120,7 @@ export class GridEditor {
108
120
  this.#overlay.removeEventListener('pointermove', this.#onPointerMove);
109
121
  this.#overlay.removeEventListener('pointerup', this.#onPointerUp);
110
122
  this.#overlay.removeEventListener('pointercancel', this.#onPointerCancel);
123
+ this.#overlay.removeEventListener('click', this.#onClick);
111
124
  window.removeEventListener('keydown', this.#onKeyDown);
112
125
  this.#overlay.remove();
113
126
  if (parent) {
@@ -128,11 +141,38 @@ export class GridEditor {
128
141
  willChange: 'transform',
129
142
  contain: 'layout style paint'
130
143
  });
144
+ const remove = document.createElement('button');
145
+ remove.type = 'button';
146
+ remove.dataset.gaugeRemove = '';
147
+ remove.title = `Remove ${id}`;
148
+ remove.setAttribute('aria-label', remove.title);
149
+ remove.textContent = '×';
150
+ Object.assign(remove.style, {
151
+ position: 'absolute',
152
+ top: '4px',
153
+ right: '4px',
154
+ zIndex: '2',
155
+ width: '24px',
156
+ height: '24px',
157
+ padding: '0',
158
+ border: '0',
159
+ borderRadius: '0',
160
+ color: 'rgba(220, 239, 243, 0.72)',
161
+ background: 'transparent',
162
+ appearance: 'none',
163
+ font: 'inherit',
164
+ fontSize: '20px',
165
+ fontWeight: '400',
166
+ lineHeight: '1',
167
+ textShadow: '0 1px 2px rgba(0, 0, 0, 0.9)',
168
+ cursor: 'pointer'
169
+ });
170
+ item.append(remove);
131
171
  return item;
132
172
  }
133
173
 
134
174
  #onPointerDown = (event) => {
135
- if (this.#drag || event.button !== 0) {
175
+ if (this.#drag || event.button !== 0 || event.target.closest?.('[data-gauge-remove]')) {
136
176
  return;
137
177
  }
138
178
  const item = event.target.closest('[data-gauge-id]');
@@ -182,6 +222,15 @@ export class GridEditor {
182
222
  event.preventDefault();
183
223
  };
184
224
 
225
+ #onClick = (event) => {
226
+ const button = event.target.closest?.('[data-gauge-remove]');
227
+ const id = button?.closest?.('[data-gauge-id]')?.dataset.gaugeId;
228
+ if (!id) return;
229
+ event.preventDefault();
230
+ event.stopPropagation();
231
+ this.#actions.remove?.(id);
232
+ };
233
+
185
234
  #onPointerMove = (event) => {
186
235
  if (!this.#drag) {
187
236
  const item = event.target.closest?.('[data-gauge-id]');
@@ -328,8 +377,10 @@ export class GridEditor {
328
377
  return;
329
378
  }
330
379
  if (!this.#drag.previewed) {
380
+ const id = this.#drag.id;
331
381
  this.#drag.session.cancel();
332
382
  this.#finishDrag(null);
383
+ this.#actions.select?.(id);
333
384
  event.preventDefault();
334
385
  return;
335
386
  }
@@ -379,7 +430,7 @@ export class GridEditor {
379
430
  drag.item.style.transformOrigin = '';
380
431
  drag.item.style.cursor = 'grab';
381
432
  drag.item.style.zIndex = '';
382
- drag.item.style.borderColor = 'transparent';
433
+ drag.item.style.borderColor = drag.id === this.#selectedId ? '#00eaff' : 'transparent';
383
434
  drag.item.style.background = 'transparent';
384
435
  this.#placeholder.style.display = 'none';
385
436
  this.#drag = null;
package/src/theme.js CHANGED
@@ -1,3 +1,11 @@
1
+ export const SEMANTIC_KINDS = Object.freeze([
2
+ 'normal',
3
+ 'warning',
4
+ 'critical',
5
+ 'inactive',
6
+ 'target'
7
+ ]);
8
+
1
9
  export const DEFAULT_THEME = Object.freeze({
2
10
  background: '#071014',
3
11
  surface: '#0c171d',
@@ -9,8 +17,7 @@ export const DEFAULT_THEME = Object.freeze({
9
17
  warning: '#ffc857',
10
18
  critical: '#ff4d6d',
11
19
  inactive: '#40515a',
12
- target: '#f3f7a7',
13
- cruise: '#6b8cff'
20
+ target: '#f3f7a7'
14
21
  });
15
22
 
16
23
  /** Parse #rgb, #rrggbb, #rrggbbaa or a numeric RGBA array. */
@@ -0,0 +1,51 @@
1
+ export type MultiGaugeGrid = {
2
+ rows?: number;
3
+ columns?: number;
4
+ gap?: number;
5
+ };
6
+
7
+ export const SEMANTIC_KINDS: readonly [
8
+ 'normal',
9
+ 'warning',
10
+ 'critical',
11
+ 'inactive',
12
+ 'target'
13
+ ];
14
+
15
+ export type MultiGaugeState = {
16
+ version?: number;
17
+ grid?: MultiGaugeGrid;
18
+ accent?: string;
19
+ theme?: Record<string, unknown>;
20
+ header?: Record<string, unknown> | null;
21
+ gauges?: Array<Record<string, any>>;
22
+ };
23
+
24
+ export type MultiGaugeOptions = MultiGaugeState & {
25
+ fontFamily?: string;
26
+ };
27
+
28
+ export class MultiGaugeError extends Error {}
29
+
30
+ export class MultiGauge extends EventTarget {
31
+ static create(canvas: HTMLCanvasElement, options?: MultiGaugeOptions): Promise<MultiGauge>;
32
+ add(configuration: Record<string, any>): Record<string, any>;
33
+ remove(id: string): boolean;
34
+ set(id: string, value: unknown): this;
35
+ update(values: Record<string, unknown>): this;
36
+ setMarker(gaugeId: string, markerId: string, value: unknown, options?: Record<string, any>): this;
37
+ configure(id: string, patch?: Record<string, any>): Record<string, any>;
38
+ setAccents(accents?: Record<string, string>): this;
39
+ select(id?: string | null): this;
40
+ move(id: string, row: number, col: number): boolean;
41
+ resize(id: string, rowSpan: number, colSpan: number): boolean;
42
+ maximize(id: string): this;
43
+ minimize(id: string): boolean;
44
+ restoreGauge(id: string): boolean;
45
+ setGrid(configuration?: MultiGaugeGrid): this;
46
+ setEditing(enabled: boolean): this;
47
+ serialize(): Record<string, any>;
48
+ restore(state: MultiGaugeState): this;
49
+ getStats(): Record<string, number>;
50
+ destroy(): void;
51
+ }
@@ -1,92 +0,0 @@
1
- Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
-
3
- This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
- This license is copied below, and is also available with a FAQ at:
5
- http://scripts.sil.org/OFL
6
-
7
- -----------------------------------------------------------
8
- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
- -----------------------------------------------------------
10
-
11
- PREAMBLE
12
- The goals of the Open Font License (OFL) are to stimulate worldwide
13
- development of collaborative font projects, to support the font creation
14
- efforts of academic and linguistic communities, and to provide a free and
15
- open framework in which fonts may be shared and improved in partnership
16
- with others.
17
-
18
- The OFL allows the licensed fonts to be used, studied, modified and
19
- redistributed freely as long as they are not sold by themselves. The
20
- fonts, including any derivative works, can be bundled, embedded,
21
- redistributed and/or sold with any software provided that any reserved
22
- names are not used by derivative works. The fonts and derivatives,
23
- however, cannot be released under any other type of license. The
24
- requirement for fonts to remain under this license does not apply
25
- to any document created using the fonts or their derivatives.
26
-
27
- DEFINITIONS
28
- "Font Software" refers to the set of files released by the Copyright
29
- Holder(s) under this license and clearly marked as such. This may
30
- include source files, build scripts and documentation.
31
-
32
- "Reserved Font Name" refers to any names specified as such after the
33
- copyright statement(s).
34
-
35
- "Original Version" refers to the collection of Font Software components as
36
- distributed by the Copyright Holder(s).
37
-
38
- "Modified Version" refers to any derivative made by adding to, deleting,
39
- or substituting -- in part or in whole -- any of the components of the
40
- Original Version, by changing formats or by porting the Font Software to a
41
- new environment.
42
-
43
- "Author" refers to any designer, engineer, programmer, technical
44
- writer or other person who contributed to the Font Software.
45
-
46
- PERMISSION AND CONDITIONS
47
- Permission is hereby granted, free of charge, to any person obtaining
48
- a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
- redistribute, and sell modified and unmodified copies of the Font
50
- Software, subject to the following conditions:
51
-
52
- 1) Neither the Font Software nor any of its individual components,
53
- in Original or Modified Versions, may be sold by itself.
54
-
55
- 2) Original or Modified Versions of the Font Software may be bundled,
56
- redistributed and/or sold with any software, provided that each copy
57
- contains the above copyright notice and this license. These can be
58
- included either as stand-alone text files, human-readable headers or
59
- in the appropriate machine-readable metadata fields within text or
60
- binary files as long as those fields can be easily viewed by the user.
61
-
62
- 3) No Modified Version of the Font Software may use the Reserved Font
63
- Name(s) unless explicit written permission is granted by the corresponding
64
- Copyright Holder. This restriction only applies to the primary font name as
65
- presented to the users.
66
-
67
- 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
- Software shall not be used to promote, endorse or advertise any
69
- Modified Version, except to acknowledge the contribution(s) of the
70
- Copyright Holder(s) and the Author(s) or with their explicit written
71
- permission.
72
-
73
- 5) The Font Software, modified or unmodified, in part or in whole,
74
- must be distributed entirely under this license, and must not be
75
- distributed under any other license. The requirement for fonts to
76
- remain under this license does not apply to any document created
77
- using the Font Software.
78
-
79
- TERMINATION
80
- This license becomes null and void if any of the above conditions are
81
- not met.
82
-
83
- DISCLAIMER
84
- THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
- OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
- COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
- INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
- DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
- FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
- OTHER DEALINGS IN THE FONT SOFTWARE.