@woosh/meep-engine 3.60.0 → 3.60.2
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/package.json +1 -1
- package/src/shade/playground/sh_exponential/main.js +2 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +4 -0
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts +6 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +12 -4
- package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
- package/src/shade/renderer/light/GPULightCollection.js +3 -5
- package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts +13 -5
- package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
- package/src/shade/renderer/light/environment/execute_filter_environment_map.js +133 -7
- package/src/shade/renderer/light/environment/graph_filter_environment_map.d.ts +9 -0
- package/src/shade/renderer/light/environment/graph_filter_environment_map.d.ts.map +1 -1
- package/src/shade/renderer/light/environment/graph_filter_environment_map.js +33 -4
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts +13 -0
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.js +169 -0
- package/src/shade/renderer/light/environment/shader_downsample_environment_map.d.ts +3 -0
- package/src/shade/renderer/light/environment/shader_downsample_environment_map.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/shader_downsample_environment_map.js +105 -0
- package/src/shade/renderer/light/environment/sphere_probe_lod_to_roughness.d.ts +10 -0
- package/src/shade/renderer/light/environment/sphere_probe_lod_to_roughness.d.ts.map +1 -0
- package/src/shade/renderer/light/environment/sphere_probe_lod_to_roughness.js +23 -0
- package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts +8 -4
- package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.js +9 -5
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +26 -1
- package/src/view/elements/progress/segmented/GhostTrail.d.ts +78 -12
- package/src/view/elements/progress/segmented/GhostTrail.d.ts.map +1 -1
- package/src/view/elements/progress/segmented/GhostTrail.js +294 -98
- package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts +68 -24
- package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
- package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +196 -128
- package/src/view/elements/progress/segmented/playground/main.js +18 -11
- package/src/view/elements/progress/segmented/segment_cell_extent.d.ts +14 -0
- package/src/view/elements/progress/segmented/segment_cell_extent.d.ts.map +1 -0
- package/src/view/elements/progress/segmented/segment_cell_extent.js +44 -0
- package/src/view/elements/progress/segmented/segment_mask_url.d.ts +24 -0
- package/src/view/elements/progress/segmented/segment_mask_url.d.ts.map +1 -0
- package/src/view/elements/progress/segmented/segment_mask_url.js +74 -0
- package/src/view/elements/progress/segmented/segment_mask_width.d.ts +18 -0
- package/src/view/elements/progress/segmented/segment_mask_width.d.ts.map +1 -0
- package/src/view/elements/progress/segmented/segment_mask_width.js +46 -0
- package/src/view/elements/progress/segmented/segmented-resource-bar.css +6 -10
|
@@ -8,8 +8,10 @@ import View from "../../../View.js";
|
|
|
8
8
|
import EmptyView from "../../EmptyView.js";
|
|
9
9
|
import { GhostTrail } from "./GhostTrail.js";
|
|
10
10
|
import { SEGMENT_TIERS_DEFAULT } from "./SEGMENT_TIERS_DEFAULT.js";
|
|
11
|
+
import { segment_cell_extent } from "./segment_cell_extent.js";
|
|
11
12
|
import { segment_cell_layout } from "./segment_cell_layout.js";
|
|
12
|
-
import {
|
|
13
|
+
import { segment_mask_apply, segment_mask_url } from "./segment_mask_url.js";
|
|
14
|
+
import { segment_mask_width } from "./segment_mask_width.js";
|
|
13
15
|
import { segment_tick_positions } from "./segment_tick_positions.js";
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -19,37 +21,28 @@ import { segment_tick_positions } from "./segment_tick_positions.js";
|
|
|
19
21
|
const REBUILD_WIDTH_THRESHOLD_PX = 0.5;
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
24
|
+
* A measured CSS scale this close to 1 is no scale at all, only the rounding in how the browser reports widths
|
|
23
25
|
* @type {number}
|
|
24
26
|
*/
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const CELL_STATE_EMPTY = 0;
|
|
28
|
-
const CELL_STATE_PARTIAL = 1;
|
|
29
|
-
const CELL_STATE_FILLED = 2;
|
|
30
|
-
|
|
31
|
-
const CELL_STATE_CLASS = ['cell empty', 'cell partial', 'cell filled'];
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {number} x
|
|
35
|
-
* @returns {number}
|
|
36
|
-
*/
|
|
37
|
-
function snap_whole(x) {
|
|
38
|
-
const r = Math.round(x);
|
|
39
|
-
|
|
40
|
-
return Math.abs(x - r) <= CELL_FILL_TOLERANCE ? r : x;
|
|
41
|
-
}
|
|
27
|
+
const UNSCALED_TOLERANCE = 2e-3;
|
|
42
28
|
|
|
43
29
|
/**
|
|
44
30
|
* Resource bar in the style of a MOBA health bar: the values fill it end to end, notches mark fixed amounts, and
|
|
45
31
|
* a ghost behind the fill trails a drop so the loss stays readable for a moment.
|
|
46
32
|
*
|
|
47
33
|
* Two modes:
|
|
48
|
-
* - notches (default): a continuous fill, with a notch at every multiple of each {@link SegmentTier}'s unit
|
|
49
|
-
*
|
|
34
|
+
* - notches (default): a continuous fill, with a notch at every multiple of each {@link SegmentTier}'s unit. Each
|
|
35
|
+
* tier is one layer the width of the bar (`.notch.<class_name>`), which CSS paints — colour, height — and a mask
|
|
36
|
+
* cuts to that tier's notches;
|
|
37
|
+
* - cells (`cells` given): `count` separate cells with exact, identical gaps, for counting rounds and the like. One
|
|
38
|
+
* strip (`.cell-strip`, the empty cells' colour) holding a `.cell-ghost` and a `.cell-fill`, cut into cells by a
|
|
39
|
+
* mask; the fill and ghost end on a cell's edge, or part way across one that is filling.
|
|
50
40
|
*
|
|
51
|
-
*
|
|
52
|
-
* and the
|
|
41
|
+
* The masks are drawn in whole device pixels and the layers they cut placed so those pixels land on the screen's,
|
|
42
|
+
* at any pixel ratio and under a CSS scale: every notch of a tier is the same width and every gap the same, at
|
|
43
|
+
* 125% as at 100%. Colours, notch heights and the ghost are styled by `segmented-resource-bar.css` through
|
|
44
|
+
* `--segmented-bar-*` custom properties. Where masks cannot be drawn (no canvas, as under jsdom) the layers are
|
|
45
|
+
* placed but uncut.
|
|
53
46
|
*/
|
|
54
47
|
export class SegmentedResourceBarView extends View {
|
|
55
48
|
/**
|
|
@@ -60,8 +53,10 @@ export class SegmentedResourceBarView extends View {
|
|
|
60
53
|
* @param {string[]} [options.classList]
|
|
61
54
|
* @param {{unit:number, class_name:string, width_px:number, min_spacing_px:number}[]} [options.tiers] notch tiers, finest first; see {@link SegmentTier}
|
|
62
55
|
* @param {{count:number, group:number}|null} [options.cells] draw `count` cells instead of notches, in groups of `group` (0 for none)
|
|
63
|
-
* @param {number} [options.ghost_delay_ms=
|
|
64
|
-
* @param {number} [options.
|
|
56
|
+
* @param {number} [options.ghost_delay_ms=100] how long the ghost holds after a drop; see {@link GhostTrail}
|
|
57
|
+
* @param {number} [options.ghost_duration_min_ms=300] how long the ghost takes to close the smallest loss
|
|
58
|
+
* @param {number} [options.ghost_duration_max_ms=800] how long it takes to close a loss of the whole bar; both
|
|
59
|
+
* durations are 0 under prefers-reduced-motion, so the ghost holds, then snaps
|
|
65
60
|
*/
|
|
66
61
|
constructor({
|
|
67
62
|
values,
|
|
@@ -69,16 +64,20 @@ export class SegmentedResourceBarView extends View {
|
|
|
69
64
|
classList = [],
|
|
70
65
|
tiers = SEGMENT_TIERS_DEFAULT,
|
|
71
66
|
cells = null,
|
|
72
|
-
ghost_delay_ms =
|
|
73
|
-
|
|
67
|
+
ghost_delay_ms = 100,
|
|
68
|
+
ghost_duration_min_ms = 300,
|
|
69
|
+
ghost_duration_max_ms = 800
|
|
74
70
|
}) {
|
|
75
71
|
super();
|
|
76
72
|
|
|
77
73
|
assert.isArray(values, 'values');
|
|
78
74
|
assert.defined(max, 'max');
|
|
79
75
|
assert.isArray(tiers, 'tiers');
|
|
80
|
-
assert.
|
|
81
|
-
assert.
|
|
76
|
+
assert.isFinite(ghost_delay_ms, 'ghost_delay_ms');
|
|
77
|
+
assert.greaterThanOrEqual(ghost_delay_ms, 0, 'ghost_delay_ms must not be negative');
|
|
78
|
+
assert.isFinite(ghost_duration_max_ms, 'ghost_duration_max_ms');
|
|
79
|
+
assert.greaterThanOrEqual(ghost_duration_min_ms, 0, 'ghost_duration_min_ms must not be negative');
|
|
80
|
+
assert.greaterThanOrEqual(ghost_duration_max_ms, ghost_duration_min_ms, 'ghost_duration_max_ms must not be under ghost_duration_min_ms');
|
|
82
81
|
|
|
83
82
|
if (cells !== null) {
|
|
84
83
|
assert.isNonNegativeInteger(cells.count, 'cells.count');
|
|
@@ -122,7 +121,13 @@ export class SegmentedResourceBarView extends View {
|
|
|
122
121
|
* @type {number}
|
|
123
122
|
* @private
|
|
124
123
|
*/
|
|
125
|
-
this.
|
|
124
|
+
this.__ghost_duration_min_ms = ghost_duration_min_ms;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @type {number}
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
this.__ghost_duration_max_ms = ghost_duration_max_ms;
|
|
126
131
|
|
|
127
132
|
/**
|
|
128
133
|
* Total fill as a fraction of max, as last drawn
|
|
@@ -150,6 +155,13 @@ export class SegmentedResourceBarView extends View {
|
|
|
150
155
|
this.__frame_id = 0;
|
|
151
156
|
this.__animate_ghost = this.__animate_ghost.bind(this);
|
|
152
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The max moved since the fill was last drawn: the next fill resets the ghost rather than trailing a loss
|
|
160
|
+
* @type {boolean}
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
this.__max_moved = false;
|
|
164
|
+
|
|
153
165
|
// what the layout was last built for, so it is rebuilt only when one of them moves
|
|
154
166
|
this.__built_max = NaN;
|
|
155
167
|
this.__built_width = NaN;
|
|
@@ -157,11 +169,25 @@ export class SegmentedResourceBarView extends View {
|
|
|
157
169
|
this.__built_origin_phase = NaN;
|
|
158
170
|
|
|
159
171
|
/**
|
|
160
|
-
*
|
|
161
|
-
* @type {
|
|
172
|
+
* Cell mode: the cells as last laid out across {@link __el_strip}, or null before the bar has been measured
|
|
173
|
+
* @type {SegmentCellLayout|null}
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
this.__cell_layout = null;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Device pixels per CSS pixel the cells were laid out at
|
|
180
|
+
* @type {number}
|
|
162
181
|
* @private
|
|
163
182
|
*/
|
|
164
|
-
this.
|
|
183
|
+
this.__cell_ratio = 1;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* scratch: [x, width, ...] device-pixel columns for a mask
|
|
187
|
+
* @type {number[]}
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
this.__columns = [];
|
|
165
191
|
|
|
166
192
|
/**
|
|
167
193
|
*
|
|
@@ -193,23 +219,20 @@ export class SegmentedResourceBarView extends View {
|
|
|
193
219
|
this.__value_fills = [];
|
|
194
220
|
|
|
195
221
|
/**
|
|
222
|
+
* Notch mode: one layer per tier, the whole bar wide, styled by the tier's class and masked to its notches
|
|
196
223
|
* @type {HTMLElement[]}
|
|
197
224
|
* @private
|
|
198
225
|
*/
|
|
199
|
-
this.
|
|
226
|
+
this.__tier_layers = [];
|
|
200
227
|
|
|
201
228
|
/**
|
|
202
|
-
*
|
|
229
|
+
* Cell mode: the strip the cells are cut from by a mask, and the ghost and fill drawn along it
|
|
230
|
+
* @type {HTMLElement|null}
|
|
203
231
|
* @private
|
|
204
232
|
*/
|
|
205
|
-
this.
|
|
206
|
-
this.
|
|
207
|
-
this.
|
|
208
|
-
this.__cell_states = new Uint8Array(cells === null ? 0 : cells.count);
|
|
209
|
-
|
|
210
|
-
// what each cell's fill and ghost were last drawn at, so an animation frame writes only the cells that moved
|
|
211
|
-
this.__cell_fill_drawn = new Float64Array(cells === null ? 0 : cells.count);
|
|
212
|
-
this.__cell_ghost_drawn = new Float64Array(cells === null ? 0 : cells.count);
|
|
233
|
+
this.__el_strip = null;
|
|
234
|
+
this.__el_cell_ghost = null;
|
|
235
|
+
this.__el_cell_fill = null;
|
|
213
236
|
|
|
214
237
|
if (cells === null) {
|
|
215
238
|
this.el.appendChild(this.__el_ghost);
|
|
@@ -234,29 +257,35 @@ export class SegmentedResourceBarView extends View {
|
|
|
234
257
|
*/
|
|
235
258
|
this.__el_layout = document.createElement('div');
|
|
236
259
|
this.__el_layout.className = 'notches';
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
262
|
+
const el_tier = document.createElement('div');
|
|
263
|
+
el_tier.className = `notch ${tiers[i].class_name}`;
|
|
264
|
+
el_tier.style.display = 'none';
|
|
265
|
+
|
|
266
|
+
this.__el_layout.appendChild(el_tier);
|
|
267
|
+
this.__tier_layers[i] = el_tier;
|
|
268
|
+
}
|
|
237
269
|
} else {
|
|
238
270
|
this.__el_layout = document.createElement('div');
|
|
239
271
|
this.__el_layout.className = 'cells';
|
|
240
272
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
el_cell.className = CELL_STATE_CLASS[CELL_STATE_EMPTY];
|
|
273
|
+
const el_strip = document.createElement('div');
|
|
274
|
+
el_strip.className = 'cell-strip';
|
|
244
275
|
|
|
245
|
-
|
|
246
|
-
|
|
276
|
+
const el_cell_ghost = document.createElement('div');
|
|
277
|
+
el_cell_ghost.className = 'cell-ghost';
|
|
247
278
|
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
const el_cell_fill = document.createElement('div');
|
|
280
|
+
el_cell_fill.className = 'cell-fill';
|
|
250
281
|
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
el_strip.appendChild(el_cell_ghost);
|
|
283
|
+
el_strip.appendChild(el_cell_fill);
|
|
284
|
+
this.__el_layout.appendChild(el_strip);
|
|
253
285
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.__cell_ghost_elements[i] = el_cell_ghost;
|
|
258
|
-
this.__cell_fill_elements[i] = el_cell_fill;
|
|
259
|
-
}
|
|
286
|
+
this.__el_strip = el_strip;
|
|
287
|
+
this.__el_cell_ghost = el_cell_ghost;
|
|
288
|
+
this.__el_cell_fill = el_cell_fill;
|
|
260
289
|
}
|
|
261
290
|
|
|
262
291
|
this.el.appendChild(this.__el_layout);
|
|
@@ -363,6 +392,9 @@ export class SegmentedResourceBarView extends View {
|
|
|
363
392
|
}
|
|
364
393
|
|
|
365
394
|
handleMaxChange() {
|
|
395
|
+
// the same amount out of a different max is a different share of the bar, and nothing was lost
|
|
396
|
+
this.__max_moved = true;
|
|
397
|
+
|
|
366
398
|
this.updateSegments();
|
|
367
399
|
this.updateFill();
|
|
368
400
|
}
|
|
@@ -370,15 +402,24 @@ export class SegmentedResourceBarView extends View {
|
|
|
370
402
|
/**
|
|
371
403
|
* Place the notches or cells again, if the max, the bar's width (by {@link REBUILD_WIDTH_THRESHOLD_PX} or more),
|
|
372
404
|
* its pixel ratio or the device-pixel phase of its left edge has moved since they were last placed.
|
|
405
|
+
*
|
|
406
|
+
* Both are drawn as a mask in whole device pixels over a layer CSS paints (see {@link segment_mask_url}): a DOM
|
|
407
|
+
* box per notch or cell cannot be put on the device-pixel grid at a fractional pixel ratio, where Chrome paints
|
|
408
|
+
* a row of equal 1 px gaps as 1, 2, 1, 2... whatever positions it is given (measured at 125% and 150%).
|
|
373
409
|
*/
|
|
374
410
|
updateSegments() {
|
|
375
411
|
const el = this.__el_layout;
|
|
376
412
|
|
|
377
413
|
/**
|
|
378
|
-
* width as laid out, before any transform on an ancestor
|
|
414
|
+
* width as laid out, before any transform on an ancestor. Not `offsetWidth`, which is rounded to a whole
|
|
415
|
+
* pixel and would read as a scale of its own (176 laid out against 175.84 drawn: 0.9991)
|
|
379
416
|
* @type {number}
|
|
380
417
|
*/
|
|
381
|
-
|
|
418
|
+
let layout_width = typeof getComputedStyle === 'function' ? parseFloat(getComputedStyle(el).width) : NaN;
|
|
419
|
+
|
|
420
|
+
if (!(layout_width > 0)) {
|
|
421
|
+
layout_width = el.offsetWidth;
|
|
422
|
+
}
|
|
382
423
|
|
|
383
424
|
if (!(layout_width > 0)) {
|
|
384
425
|
// not laid out (detached, display:none); nothing to place against
|
|
@@ -389,8 +430,11 @@ export class SegmentedResourceBarView extends View {
|
|
|
389
430
|
|
|
390
431
|
const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
391
432
|
|
|
392
|
-
// a CSS scale on an ancestor shows each CSS pixel of the bar as `scale` page pixels
|
|
393
|
-
|
|
433
|
+
// a CSS scale on an ancestor shows each CSS pixel of the bar as `scale` page pixels. The computed width is
|
|
434
|
+
// serialised to a few decimals (175.844 for 175.84375), so an unscaled bar measures a hair off 1 — enough to
|
|
435
|
+
// put a width that should be whole in device pixels just past it — and is read as unscaled
|
|
436
|
+
const measured_scale = rect.width > 0 ? rect.width / layout_width : 1;
|
|
437
|
+
const scale = Math.abs(measured_scale - 1) < UNSCALED_TOLERANCE ? 1 : measured_scale;
|
|
394
438
|
|
|
395
439
|
// device pixels per CSS pixel of the bar
|
|
396
440
|
const ratio = dpr * scale;
|
|
@@ -414,85 +458,109 @@ export class SegmentedResourceBarView extends View {
|
|
|
414
458
|
this.__built_ratio = ratio;
|
|
415
459
|
this.__built_origin_phase = origin_phase;
|
|
416
460
|
|
|
417
|
-
// the
|
|
418
|
-
const
|
|
461
|
+
// the masked layer starts on a whole device pixel, and is a width whole in CSS and device pixels both
|
|
462
|
+
const shift_px = -origin_phase / ratio;
|
|
463
|
+
const width_device = segment_mask_width(layout_width - Math.max(0, shift_px), ratio);
|
|
419
464
|
|
|
420
465
|
if (this.__cells === null) {
|
|
421
|
-
this.__place_notches(max,
|
|
466
|
+
this.__place_notches(max, width_device, shift_px, ratio);
|
|
422
467
|
} else {
|
|
423
|
-
this.__place_cells(
|
|
468
|
+
this.__place_cells(width_device, shift_px, ratio);
|
|
424
469
|
}
|
|
425
470
|
}
|
|
426
471
|
|
|
472
|
+
/**
|
|
473
|
+
* @param {HTMLElement} el
|
|
474
|
+
* @param {number} width_device
|
|
475
|
+
* @param {number} shift_px
|
|
476
|
+
* @param {number} ratio
|
|
477
|
+
* @private
|
|
478
|
+
*/
|
|
479
|
+
__place_layer(el, width_device, shift_px, ratio) {
|
|
480
|
+
const style = el.style;
|
|
481
|
+
|
|
482
|
+
style.left = `${shift_px}px`;
|
|
483
|
+
style.width = `${width_device / ratio}px`;
|
|
484
|
+
}
|
|
485
|
+
|
|
427
486
|
/**
|
|
428
487
|
* @param {number} max
|
|
429
|
-
* @param {number}
|
|
430
|
-
* @param {number}
|
|
488
|
+
* @param {number} width_device
|
|
489
|
+
* @param {number} shift_px
|
|
431
490
|
* @param {number} ratio
|
|
432
491
|
* @private
|
|
433
492
|
*/
|
|
434
|
-
__place_notches(max,
|
|
493
|
+
__place_notches(max, width_device, shift_px, ratio) {
|
|
435
494
|
const tiers = this.__tiers;
|
|
436
|
-
const ticks = segment_tick_positions(max,
|
|
495
|
+
const ticks = segment_tick_positions(max, width_device / ratio, tiers);
|
|
437
496
|
|
|
438
497
|
const tick_count = ticks.length;
|
|
439
|
-
const
|
|
440
|
-
const
|
|
498
|
+
const columns = this.__columns;
|
|
499
|
+
const layers = this.__tier_layers;
|
|
441
500
|
|
|
442
|
-
|
|
443
|
-
|
|
501
|
+
// a notch is `width_px` CSS pixels wide, scaled by the whole part of the ratio, so it stays whole
|
|
502
|
+
const notch_scale = Math.max(1, Math.round(ratio));
|
|
444
503
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
504
|
+
for (let t = 0; t < tiers.length; t++) {
|
|
505
|
+
const notch_width = tiers[t].width_px * notch_scale;
|
|
448
506
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
507
|
+
let count = 0;
|
|
508
|
+
|
|
509
|
+
for (let i = 0; i < tick_count; i++) {
|
|
510
|
+
const tick = ticks[i];
|
|
452
511
|
|
|
453
|
-
|
|
512
|
+
if (tick.tier !== t) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
454
515
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const tier = tiers[tick.tier];
|
|
516
|
+
const centre = Math.round(tick.value / max * width_device);
|
|
517
|
+
const x = Math.min(Math.max(0, centre - Math.floor(notch_width / 2)), width_device - notch_width);
|
|
458
518
|
|
|
459
|
-
|
|
519
|
+
columns[count * 2] = x;
|
|
520
|
+
columns[count * 2 + 1] = notch_width;
|
|
521
|
+
count++;
|
|
522
|
+
}
|
|
460
523
|
|
|
461
|
-
const el =
|
|
524
|
+
const el = layers[t];
|
|
462
525
|
|
|
463
|
-
|
|
526
|
+
if (count === 0) {
|
|
527
|
+
el.style.display = 'none';
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
464
530
|
|
|
465
|
-
|
|
531
|
+
el.style.display = '';
|
|
466
532
|
|
|
467
|
-
|
|
468
|
-
|
|
533
|
+
this.__place_layer(el, width_device, shift_px, ratio);
|
|
534
|
+
segment_mask_apply(el, segment_mask_url(width_device, columns, count));
|
|
469
535
|
}
|
|
470
536
|
}
|
|
471
537
|
|
|
472
538
|
/**
|
|
473
|
-
* @param {number}
|
|
474
|
-
* @param {number}
|
|
539
|
+
* @param {number} width_device
|
|
540
|
+
* @param {number} shift_px
|
|
475
541
|
* @param {number} ratio
|
|
476
542
|
* @private
|
|
477
543
|
*/
|
|
478
|
-
__place_cells(
|
|
544
|
+
__place_cells(width_device, shift_px, ratio) {
|
|
479
545
|
const cells = this.__cells;
|
|
480
546
|
|
|
481
|
-
const layout = segment_cell_layout(
|
|
482
|
-
|
|
483
|
-
const origin_device = left * ratio;
|
|
484
|
-
const origin_shift_device = Math.round(origin_device) - origin_device;
|
|
547
|
+
const layout = segment_cell_layout(width_device / ratio, cells.count, cells.group, ratio);
|
|
485
548
|
|
|
486
|
-
const
|
|
487
|
-
const cell_width = layout.cell_width_device;
|
|
488
|
-
const elements = this.__cell_elements;
|
|
549
|
+
const columns = this.__columns;
|
|
489
550
|
|
|
490
551
|
for (let i = 0; i < cells.count; i++) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
style.left = `${(cell_x[i] + origin_shift_device) / ratio}px`;
|
|
494
|
-
style.width = `${cell_width[i] / ratio}px`;
|
|
552
|
+
columns[i * 2] = layout.cell_x_device[i];
|
|
553
|
+
columns[i * 2 + 1] = layout.cell_width_device[i];
|
|
495
554
|
}
|
|
555
|
+
|
|
556
|
+
this.__cell_layout = layout;
|
|
557
|
+
this.__cell_ratio = ratio;
|
|
558
|
+
|
|
559
|
+
this.__place_layer(this.__el_strip, width_device, shift_px, ratio);
|
|
560
|
+
segment_mask_apply(this.__el_strip, segment_mask_url(width_device, columns, cells.count));
|
|
561
|
+
|
|
562
|
+
// the fill is drawn against the layout, which just moved
|
|
563
|
+
this.__draw(performance.now());
|
|
496
564
|
}
|
|
497
565
|
|
|
498
566
|
updateFill() {
|
|
@@ -531,8 +599,17 @@ export class SegmentedResourceBarView extends View {
|
|
|
531
599
|
|
|
532
600
|
const ghost = this.__ghost;
|
|
533
601
|
|
|
534
|
-
|
|
535
|
-
|
|
602
|
+
const reduced_motion = this.__reduced_motion !== null && this.__reduced_motion.matches;
|
|
603
|
+
|
|
604
|
+
ghost.duration_min_ms = reduced_motion ? 0 : this.__ghost_duration_min_ms;
|
|
605
|
+
ghost.duration_max_ms = reduced_motion ? 0 : this.__ghost_duration_max_ms;
|
|
606
|
+
|
|
607
|
+
if (this.__max_moved) {
|
|
608
|
+
this.__max_moved = false;
|
|
609
|
+
ghost.reset(total_fraction);
|
|
610
|
+
} else {
|
|
611
|
+
ghost.set(total_fraction, now);
|
|
612
|
+
}
|
|
536
613
|
|
|
537
614
|
this.__draw(now);
|
|
538
615
|
}
|
|
@@ -564,33 +641,24 @@ export class SegmentedResourceBarView extends View {
|
|
|
564
641
|
*/
|
|
565
642
|
__draw_cells(total_fraction, ghost_fraction) {
|
|
566
643
|
const count = this.__cells.count;
|
|
644
|
+
const layout = this.__cell_layout;
|
|
567
645
|
|
|
568
|
-
const
|
|
569
|
-
const
|
|
646
|
+
const fill = this.__el_cell_fill.style;
|
|
647
|
+
const ghost = this.__el_cell_ghost.style;
|
|
570
648
|
|
|
571
|
-
|
|
649
|
+
if (layout === null) {
|
|
650
|
+
// not measured yet: a share of the strip, until the cells are laid out
|
|
651
|
+
fill.width = `${total_fraction * 100}%`;
|
|
652
|
+
ghost.width = `${ghost_fraction * 100}%`;
|
|
572
653
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const ghost = clamp01(ghost_cells - i);
|
|
576
|
-
|
|
577
|
-
const state = fill >= 1 ? CELL_STATE_FILLED : (fill > 0 ? CELL_STATE_PARTIAL : CELL_STATE_EMPTY);
|
|
578
|
-
|
|
579
|
-
if (states[i] !== state) {
|
|
580
|
-
states[i] = state;
|
|
581
|
-
this.__cell_elements[i].className = CELL_STATE_CLASS[state];
|
|
582
|
-
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
583
656
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
this.__cell_fill_elements[i].style.width = `${fill * 100}%`;
|
|
587
|
-
}
|
|
657
|
+
// to the edge of the last whole cell (or part way across the next), never into a gap
|
|
658
|
+
const ratio = this.__cell_ratio;
|
|
588
659
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
this.__cell_ghost_elements[i].style.width = `${ghost * 100}%`;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
660
|
+
fill.width = `${segment_cell_extent(layout, total_fraction * count) / ratio}px`;
|
|
661
|
+
ghost.width = `${segment_cell_extent(layout, ghost_fraction * count) / ratio}px`;
|
|
594
662
|
}
|
|
595
663
|
|
|
596
664
|
/**
|
|
@@ -138,9 +138,12 @@ document.getElementById('reload').addEventListener('click', () => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* For a driver: every
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* For a driver: every masked layer (a notch tier, a cell strip) and whether it starts on a whole device pixel and is
|
|
142
|
+
* a width whole in CSS and device pixels both, which is what lets its mask's columns paint where they were drawn.
|
|
143
|
+
* Chrome keeps layout in 1/64 CSS pixel steps, so anything within one such step of whole counts as whole.
|
|
144
|
+
*
|
|
145
|
+
* Necessary, not sufficient: boxes whose edges all read as whole still painted 1, 2, 1, 2 px gaps at 125% when each
|
|
146
|
+
* cell was its own box. Judge the look from the painted pixels of a screenshot.
|
|
144
147
|
* @returns {{dpr:number, checked:number, worst:number, off_grid:string[]}}
|
|
145
148
|
*/
|
|
146
149
|
globalThis.segmented_bar_pixel_report = function () {
|
|
@@ -152,19 +155,23 @@ globalThis.segmented_bar_pixel_report = function () {
|
|
|
152
155
|
let checked = 0;
|
|
153
156
|
let worst = 0;
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
const miss = v => Math.abs(v - Math.round(v));
|
|
159
|
+
|
|
160
|
+
for (const el of document.querySelectorAll('.notch, .cell-strip')) {
|
|
161
|
+
if (el.style.display === 'none') {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
157
164
|
|
|
158
|
-
|
|
159
|
-
const device = v * dpr;
|
|
165
|
+
const r = el.getBoundingClientRect();
|
|
160
166
|
|
|
161
|
-
|
|
167
|
+
for (const [name, v] of [['left (device)', r.left * dpr], ['width (device)', r.width * dpr], ['width (css)', r.width]]) {
|
|
168
|
+
const m = miss(v);
|
|
162
169
|
|
|
163
170
|
checked++;
|
|
164
|
-
worst = Math.max(worst,
|
|
171
|
+
worst = Math.max(worst, m);
|
|
165
172
|
|
|
166
|
-
if (
|
|
167
|
-
off_grid.push(`${el.className} ${name} ${
|
|
173
|
+
if (m > tolerance) {
|
|
174
|
+
off_grid.push(`${el.className} ${name} ${v.toFixed(4)}`);
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a fill of `cells` cells ends on a cell layout, in device pixels from its left edge: at the right edge of the
|
|
3
|
+
* last whole cell, or part way across the next one for a fraction of a cell. Never in a gap, so a fill drawn to it
|
|
4
|
+
* paints whole cells.
|
|
5
|
+
*
|
|
6
|
+
* @param {{cell_x_device: number[], cell_width_device: number[]}} layout from {@link segment_cell_layout}
|
|
7
|
+
* @param {number} cells how many cells are filled; fractional for one filling up, clamped to the layout
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function segment_cell_extent(layout: {
|
|
11
|
+
cell_x_device: number[];
|
|
12
|
+
cell_width_device: number[];
|
|
13
|
+
}, cells: number): number;
|
|
14
|
+
//# sourceMappingURL=segment_cell_extent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment_cell_extent.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/segment_cell_extent.js"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,4CAJW;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAAC,SACtD,MAAM,GACJ,MAAM,CA8BlB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell counts within this of a whole one count as whole, so 7 of 30 rounds is 7 full cells and not 7 and a sliver
|
|
3
|
+
* @type {number}
|
|
4
|
+
*/
|
|
5
|
+
const WHOLE_TOLERANCE = 1e-9;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where a fill of `cells` cells ends on a cell layout, in device pixels from its left edge: at the right edge of the
|
|
9
|
+
* last whole cell, or part way across the next one for a fraction of a cell. Never in a gap, so a fill drawn to it
|
|
10
|
+
* paints whole cells.
|
|
11
|
+
*
|
|
12
|
+
* @param {{cell_x_device: number[], cell_width_device: number[]}} layout from {@link segment_cell_layout}
|
|
13
|
+
* @param {number} cells how many cells are filled; fractional for one filling up, clamped to the layout
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
export function segment_cell_extent(layout, cells) {
|
|
17
|
+
const xs = layout.cell_x_device;
|
|
18
|
+
const widths = layout.cell_width_device;
|
|
19
|
+
const count = xs.length;
|
|
20
|
+
|
|
21
|
+
if (count === 0 || !(cells > 0)) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let c = cells;
|
|
26
|
+
const whole = Math.round(c);
|
|
27
|
+
|
|
28
|
+
if (Math.abs(c - whole) <= WHOLE_TOLERANCE) {
|
|
29
|
+
c = whole;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (c >= count) {
|
|
33
|
+
return xs[count - 1] + widths[count - 1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const k = Math.floor(c);
|
|
37
|
+
const part = c - k;
|
|
38
|
+
|
|
39
|
+
if (part > 0) {
|
|
40
|
+
return xs[k] + part * widths[k];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return xs[k - 1] + widths[k - 1];
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether masks can be drawn here. Keyed on `OffscreenCanvas`, which every browser that draws a canvas has and
|
|
3
|
+
* jsdom does not: jsdom's own canvas reports "not implemented" on the console for every call, so it is never asked.
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function segment_mask_supported(): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* A mask image of opaque columns, one device pixel tall, for a layer `width_device` device pixels wide: stretched
|
|
9
|
+
* over the layer with `mask-size: 100% 100%`, each column covers exactly the device pixels it was drawn on. CSS keeps
|
|
10
|
+
* painting the layer (its colour, gradient, height); the mask only decides which columns of it show.
|
|
11
|
+
*
|
|
12
|
+
* @param {number} width_device
|
|
13
|
+
* @param {number[]} columns [x, width, x, width, ...] in whole device pixels from the layer's left edge
|
|
14
|
+
* @param {number} column_count how many pairs of `columns` to draw
|
|
15
|
+
* @returns {string|null} a `url(...)` for `mask-image`, or null where masks cannot be drawn
|
|
16
|
+
*/
|
|
17
|
+
export function segment_mask_url(width_device: number, columns: number[], column_count: number): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Put a mask from {@link segment_mask_url} on an element, sampled nearest so its columns stay whole, or take it off
|
|
20
|
+
* @param {HTMLElement} el
|
|
21
|
+
* @param {string|null} url
|
|
22
|
+
*/
|
|
23
|
+
export function segment_mask_apply(el: HTMLElement, url: string | null): void;
|
|
24
|
+
//# sourceMappingURL=segment_mask_url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment_mask_url.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/segment_mask_url.js"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,0CAFa,OAAO,CAInB;AAED;;;;;;;;;GASG;AACH,+CALW,MAAM,WACN,MAAM,EAAE,gBACR,MAAM,GACJ,MAAM,GAAC,IAAI,CA4BvB;AAED;;;;GAIG;AACH,uCAHW,WAAW,OACX,MAAM,GAAC,IAAI,QAkBrB"}
|