@woosh/meep-engine 3.60.1 → 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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/shade/playground/sh_exponential/main.js +2 -2
  3. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +4 -0
  4. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  5. package/src/shade/renderer/light/GPULightCollection.js +3 -5
  6. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts +13 -5
  7. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
  8. package/src/shade/renderer/light/environment/execute_filter_environment_map.js +133 -7
  9. package/src/shade/renderer/light/environment/graph_filter_environment_map.d.ts +9 -0
  10. package/src/shade/renderer/light/environment/graph_filter_environment_map.d.ts.map +1 -1
  11. package/src/shade/renderer/light/environment/graph_filter_environment_map.js +33 -4
  12. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts +13 -0
  13. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.d.ts.map +1 -0
  14. package/src/shade/renderer/light/environment/shader_convolve_reflection_map_from_averages.js +169 -0
  15. package/src/shade/renderer/light/environment/shader_downsample_environment_map.d.ts +3 -0
  16. package/src/shade/renderer/light/environment/shader_downsample_environment_map.d.ts.map +1 -0
  17. package/src/shade/renderer/light/environment/shader_downsample_environment_map.js +105 -0
  18. package/src/shade/renderer/light/environment/sphere_probe_lod_to_roughness.d.ts +10 -0
  19. package/src/shade/renderer/light/environment/sphere_probe_lod_to_roughness.d.ts.map +1 -0
  20. package/src/shade/renderer/light/environment/sphere_probe_lod_to_roughness.js +23 -0
  21. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts +8 -4
  22. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts.map +1 -1
  23. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.js +9 -5
  24. package/src/view/elements/progress/segmented/GhostTrail.d.ts +6 -0
  25. package/src/view/elements/progress/segmented/GhostTrail.d.ts.map +1 -1
  26. package/src/view/elements/progress/segmented/GhostTrail.js +14 -0
  27. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts +55 -19
  28. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
  29. package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +173 -120
  30. package/src/view/elements/progress/segmented/playground/main.js +18 -11
  31. package/src/view/elements/progress/segmented/segment_cell_extent.d.ts +14 -0
  32. package/src/view/elements/progress/segmented/segment_cell_extent.d.ts.map +1 -0
  33. package/src/view/elements/progress/segmented/segment_cell_extent.js +44 -0
  34. package/src/view/elements/progress/segmented/segment_mask_url.d.ts +24 -0
  35. package/src/view/elements/progress/segmented/segment_mask_url.d.ts.map +1 -0
  36. package/src/view/elements/progress/segmented/segment_mask_url.js +74 -0
  37. package/src/view/elements/progress/segmented/segment_mask_width.d.ts +18 -0
  38. package/src/view/elements/progress/segmented/segment_mask_width.d.ts.map +1 -0
  39. package/src/view/elements/progress/segmented/segment_mask_width.js +46 -0
  40. 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 { segment_notch_place } from "./segment_notch_place.js";
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
- * Cell fills within this of a whole cell count as whole, so 7 of 30 rounds is 7 full cells and not 7 and a sliver
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 CELL_FILL_TOLERANCE = 1e-9;
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
- * - cells (`cells` given): `count` separate cells with exact, identical gaps, for counting rounds and the like.
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
- * Notches and cells sit on whole device pixels at any pixel ratio and under a CSS scale. Colours, notch heights
52
- * and the ghost are styled by `segmented-resource-bar.css` through `--segmented-bar-*` custom properties.
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
  /**
@@ -162,6 +155,13 @@ export class SegmentedResourceBarView extends View {
162
155
  this.__frame_id = 0;
163
156
  this.__animate_ghost = this.__animate_ghost.bind(this);
164
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
+
165
165
  // what the layout was last built for, so it is rebuilt only when one of them moves
166
166
  this.__built_max = NaN;
167
167
  this.__built_width = NaN;
@@ -169,11 +169,25 @@ export class SegmentedResourceBarView extends View {
169
169
  this.__built_origin_phase = NaN;
170
170
 
171
171
  /**
172
- * scratch for {@link segment_notch_place}
173
- * @type {Float64Array}
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}
174
181
  * @private
175
182
  */
176
- this.__place = new Float64Array(2);
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 = [];
177
191
 
178
192
  /**
179
193
  *
@@ -205,23 +219,20 @@ export class SegmentedResourceBarView extends View {
205
219
  this.__value_fills = [];
206
220
 
207
221
  /**
222
+ * Notch mode: one layer per tier, the whole bar wide, styled by the tier's class and masked to its notches
208
223
  * @type {HTMLElement[]}
209
224
  * @private
210
225
  */
211
- this.__notch_elements = [];
226
+ this.__tier_layers = [];
212
227
 
213
228
  /**
214
- * @type {HTMLElement[]}
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}
215
231
  * @private
216
232
  */
217
- this.__cell_elements = [];
218
- this.__cell_fill_elements = [];
219
- this.__cell_ghost_elements = [];
220
- this.__cell_states = new Uint8Array(cells === null ? 0 : cells.count);
221
-
222
- // what each cell's fill and ghost were last drawn at, so an animation frame writes only the cells that moved
223
- this.__cell_fill_drawn = new Float64Array(cells === null ? 0 : cells.count);
224
- 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;
225
236
 
226
237
  if (cells === null) {
227
238
  this.el.appendChild(this.__el_ghost);
@@ -246,29 +257,35 @@ export class SegmentedResourceBarView extends View {
246
257
  */
247
258
  this.__el_layout = document.createElement('div');
248
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
+ }
249
269
  } else {
250
270
  this.__el_layout = document.createElement('div');
251
271
  this.__el_layout.className = 'cells';
252
272
 
253
- for (let i = 0; i < cells.count; i++) {
254
- const el_cell = document.createElement('div');
255
- el_cell.className = CELL_STATE_CLASS[CELL_STATE_EMPTY];
256
-
257
- const el_cell_ghost = document.createElement('div');
258
- el_cell_ghost.className = 'cell-ghost';
273
+ const el_strip = document.createElement('div');
274
+ el_strip.className = 'cell-strip';
259
275
 
260
- const el_cell_fill = document.createElement('div');
261
- el_cell_fill.className = 'cell-fill';
276
+ const el_cell_ghost = document.createElement('div');
277
+ el_cell_ghost.className = 'cell-ghost';
262
278
 
263
- el_cell.appendChild(el_cell_ghost);
264
- el_cell.appendChild(el_cell_fill);
279
+ const el_cell_fill = document.createElement('div');
280
+ el_cell_fill.className = 'cell-fill';
265
281
 
266
- this.__el_layout.appendChild(el_cell);
282
+ el_strip.appendChild(el_cell_ghost);
283
+ el_strip.appendChild(el_cell_fill);
284
+ this.__el_layout.appendChild(el_strip);
267
285
 
268
- this.__cell_elements[i] = el_cell;
269
- this.__cell_ghost_elements[i] = el_cell_ghost;
270
- this.__cell_fill_elements[i] = el_cell_fill;
271
- }
286
+ this.__el_strip = el_strip;
287
+ this.__el_cell_ghost = el_cell_ghost;
288
+ this.__el_cell_fill = el_cell_fill;
272
289
  }
273
290
 
274
291
  this.el.appendChild(this.__el_layout);
@@ -375,6 +392,9 @@ export class SegmentedResourceBarView extends View {
375
392
  }
376
393
 
377
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
+
378
398
  this.updateSegments();
379
399
  this.updateFill();
380
400
  }
@@ -382,15 +402,24 @@ export class SegmentedResourceBarView extends View {
382
402
  /**
383
403
  * Place the notches or cells again, if the max, the bar's width (by {@link REBUILD_WIDTH_THRESHOLD_PX} or more),
384
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%).
385
409
  */
386
410
  updateSegments() {
387
411
  const el = this.__el_layout;
388
412
 
389
413
  /**
390
- * 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)
391
416
  * @type {number}
392
417
  */
393
- const layout_width = el.offsetWidth;
418
+ let layout_width = typeof getComputedStyle === 'function' ? parseFloat(getComputedStyle(el).width) : NaN;
419
+
420
+ if (!(layout_width > 0)) {
421
+ layout_width = el.offsetWidth;
422
+ }
394
423
 
395
424
  if (!(layout_width > 0)) {
396
425
  // not laid out (detached, display:none); nothing to place against
@@ -401,8 +430,11 @@ export class SegmentedResourceBarView extends View {
401
430
 
402
431
  const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
403
432
 
404
- // a CSS scale on an ancestor shows each CSS pixel of the bar as `scale` page pixels
405
- const scale = rect.width / layout_width;
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;
406
438
 
407
439
  // device pixels per CSS pixel of the bar
408
440
  const ratio = dpr * scale;
@@ -426,85 +458,109 @@ export class SegmentedResourceBarView extends View {
426
458
  this.__built_ratio = ratio;
427
459
  this.__built_origin_phase = origin_phase;
428
460
 
429
- // the bar's left edge in its own CSS pixels, such that left * ratio lands where the page puts it
430
- const local_left = origin_device / ratio;
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);
431
464
 
432
465
  if (this.__cells === null) {
433
- this.__place_notches(max, layout_width, local_left, ratio);
466
+ this.__place_notches(max, width_device, shift_px, ratio);
434
467
  } else {
435
- this.__place_cells(layout_width, local_left, ratio);
468
+ this.__place_cells(width_device, shift_px, ratio);
436
469
  }
437
470
  }
438
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
+
439
486
  /**
440
487
  * @param {number} max
441
- * @param {number} width
442
- * @param {number} left
488
+ * @param {number} width_device
489
+ * @param {number} shift_px
443
490
  * @param {number} ratio
444
491
  * @private
445
492
  */
446
- __place_notches(max, width, left, ratio) {
493
+ __place_notches(max, width_device, shift_px, ratio) {
447
494
  const tiers = this.__tiers;
448
- const ticks = segment_tick_positions(max, width, tiers);
495
+ const ticks = segment_tick_positions(max, width_device / ratio, tiers);
449
496
 
450
497
  const tick_count = ticks.length;
451
- const elements = this.__notch_elements;
452
- const container = this.__el_layout;
498
+ const columns = this.__columns;
499
+ const layers = this.__tier_layers;
453
500
 
454
- while (elements.length < tick_count) {
455
- const el = document.createElement('div');
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));
456
503
 
457
- container.appendChild(el);
458
- elements.push(el);
459
- }
504
+ for (let t = 0; t < tiers.length; t++) {
505
+ const notch_width = tiers[t].width_px * notch_scale;
460
506
 
461
- while (elements.length > tick_count) {
462
- container.removeChild(elements.pop());
463
- }
507
+ let count = 0;
464
508
 
465
- const place = this.__place;
509
+ for (let i = 0; i < tick_count; i++) {
510
+ const tick = ticks[i];
511
+
512
+ if (tick.tier !== t) {
513
+ continue;
514
+ }
466
515
 
467
- for (let i = 0; i < tick_count; i++) {
468
- const tick = ticks[i];
469
- 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);
470
518
 
471
- segment_notch_place(place, 0, tick.value / max, tier.width_px, left, width, ratio);
519
+ columns[count * 2] = x;
520
+ columns[count * 2 + 1] = notch_width;
521
+ count++;
522
+ }
472
523
 
473
- const el = elements[i];
524
+ const el = layers[t];
474
525
 
475
- el.className = `notch ${tier.class_name}`;
526
+ if (count === 0) {
527
+ el.style.display = 'none';
528
+ continue;
529
+ }
476
530
 
477
- const style = el.style;
531
+ el.style.display = '';
478
532
 
479
- style.left = `${place[0]}px`;
480
- style.width = `${place[1]}px`;
533
+ this.__place_layer(el, width_device, shift_px, ratio);
534
+ segment_mask_apply(el, segment_mask_url(width_device, columns, count));
481
535
  }
482
536
  }
483
537
 
484
538
  /**
485
- * @param {number} width
486
- * @param {number} left
539
+ * @param {number} width_device
540
+ * @param {number} shift_px
487
541
  * @param {number} ratio
488
542
  * @private
489
543
  */
490
- __place_cells(width, left, ratio) {
544
+ __place_cells(width_device, shift_px, ratio) {
491
545
  const cells = this.__cells;
492
546
 
493
- const layout = segment_cell_layout(width, cells.count, cells.group, ratio);
547
+ const layout = segment_cell_layout(width_device / ratio, cells.count, cells.group, ratio);
494
548
 
495
- const origin_device = left * ratio;
496
- const origin_shift_device = Math.round(origin_device) - origin_device;
497
-
498
- const cell_x = layout.cell_x_device;
499
- const cell_width = layout.cell_width_device;
500
- const elements = this.__cell_elements;
549
+ const columns = this.__columns;
501
550
 
502
551
  for (let i = 0; i < cells.count; i++) {
503
- const style = elements[i].style;
504
-
505
- style.left = `${(cell_x[i] + origin_shift_device) / ratio}px`;
506
- 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];
507
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());
508
564
  }
509
565
 
510
566
  updateFill() {
@@ -547,7 +603,13 @@ export class SegmentedResourceBarView extends View {
547
603
 
548
604
  ghost.duration_min_ms = reduced_motion ? 0 : this.__ghost_duration_min_ms;
549
605
  ghost.duration_max_ms = reduced_motion ? 0 : this.__ghost_duration_max_ms;
550
- ghost.set(total_fraction, now);
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
+ }
551
613
 
552
614
  this.__draw(now);
553
615
  }
@@ -579,33 +641,24 @@ export class SegmentedResourceBarView extends View {
579
641
  */
580
642
  __draw_cells(total_fraction, ghost_fraction) {
581
643
  const count = this.__cells.count;
644
+ const layout = this.__cell_layout;
582
645
 
583
- const fill_cells = snap_whole(total_fraction * count);
584
- const ghost_cells = snap_whole(ghost_fraction * count);
585
-
586
- const states = this.__cell_states;
646
+ const fill = this.__el_cell_fill.style;
647
+ const ghost = this.__el_cell_ghost.style;
587
648
 
588
- for (let i = 0; i < count; i++) {
589
- const fill = clamp01(fill_cells - i);
590
- const ghost = clamp01(ghost_cells - i);
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}%`;
591
653
 
592
- const state = fill >= 1 ? CELL_STATE_FILLED : (fill > 0 ? CELL_STATE_PARTIAL : CELL_STATE_EMPTY);
654
+ return;
655
+ }
593
656
 
594
- if (states[i] !== state) {
595
- states[i] = state;
596
- this.__cell_elements[i].className = CELL_STATE_CLASS[state];
597
- }
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;
598
659
 
599
- if (this.__cell_fill_drawn[i] !== fill) {
600
- this.__cell_fill_drawn[i] = fill;
601
- this.__cell_fill_elements[i].style.width = `${fill * 100}%`;
602
- }
603
-
604
- if (this.__cell_ghost_drawn[i] !== ghost) {
605
- this.__cell_ghost_drawn[i] = ghost;
606
- this.__cell_ghost_elements[i].style.width = `${ghost * 100}%`;
607
- }
608
- }
660
+ fill.width = `${segment_cell_extent(layout, total_fraction * count) / ratio}px`;
661
+ ghost.width = `${segment_cell_extent(layout, ghost_fraction * count) / ratio}px`;
609
662
  }
610
663
 
611
664
  /**
@@ -138,9 +138,12 @@ document.getElementById('reload').addEventListener('click', () => {
138
138
  });
139
139
 
140
140
  /**
141
- * For a driver: every notch and cell edge in device pixels, and whether it is whole. Chrome keeps layout in
142
- * 1/64 CSS pixel steps, so an edge placed at 0.8 CSS px reads back as 0.796875; anything within one such step of
143
- * a whole device pixel paints on it.
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
- for (const el of document.querySelectorAll('.notch, .cell')) {
156
- const r = el.getBoundingClientRect();
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
- for (const [name, v] of [['left', r.left], ['right', r.right]]) {
159
- const device = v * dpr;
165
+ const r = el.getBoundingClientRect();
160
166
 
161
- const miss = Math.abs(device - Math.round(device));
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, miss);
171
+ worst = Math.max(worst, m);
165
172
 
166
- if (miss > tolerance) {
167
- off_grid.push(`${el.className} ${name} ${device.toFixed(4)}`);
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"}