@woosh/meep-engine 3.60.1 → 3.60.3

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 (43) 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 +198 -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 +74 -19
  28. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
  29. package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +219 -116
  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_frame_transform.d.ts +15 -0
  35. package/src/view/elements/progress/segmented/segment_frame_transform.d.ts.map +1 -0
  36. package/src/view/elements/progress/segmented/segment_frame_transform.js +69 -0
  37. package/src/view/elements/progress/segmented/segment_mask_url.d.ts +24 -0
  38. package/src/view/elements/progress/segmented/segment_mask_url.d.ts.map +1 -0
  39. package/src/view/elements/progress/segmented/segment_mask_url.js +74 -0
  40. package/src/view/elements/progress/segmented/segment_mask_width.d.ts +18 -0
  41. package/src/view/elements/progress/segmented/segment_mask_width.d.ts.map +1 -0
  42. package/src/view/elements/progress/segmented/segment_mask_width.js +46 -0
  43. package/src/view/elements/progress/segmented/segmented-resource-bar.css +6 -10
@@ -8,8 +8,11 @@ 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_frame_transform } from "./segment_frame_transform.js";
14
+ import { segment_mask_apply, segment_mask_url } from "./segment_mask_url.js";
15
+ import { segment_mask_width } from "./segment_mask_width.js";
13
16
  import { segment_tick_positions } from "./segment_tick_positions.js";
14
17
 
15
18
  /**
@@ -19,37 +22,35 @@ import { segment_tick_positions } from "./segment_tick_positions.js";
19
22
  const REBUILD_WIDTH_THRESHOLD_PX = 0.5;
20
23
 
21
24
  /**
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
25
+ * A measured CSS scale this close to 1 is no scale at all, only the rounding in how the browser reports widths
23
26
  * @type {number}
24
27
  */
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'];
28
+ const UNSCALED_TOLERANCE = 2e-3;
32
29
 
33
30
  /**
34
- * @param {number} x
35
- * @returns {number}
31
+ * How often a linked bar checks that its notches or cells still sit on the screen's pixels, in milliseconds. A
32
+ * transform on an ancestor or on a containing iframe moves them without any event the bar hears.
33
+ * @type {number}
36
34
  */
37
- function snap_whole(x) {
38
- const r = Math.round(x);
39
-
40
- return Math.abs(x - r) <= CELL_FILL_TOLERANCE ? r : x;
41
- }
35
+ const RECHECK_INTERVAL_MS = 250;
42
36
 
43
37
  /**
44
38
  * Resource bar in the style of a MOBA health bar: the values fill it end to end, notches mark fixed amounts, and
45
39
  * a ghost behind the fill trails a drop so the loss stays readable for a moment.
46
40
  *
47
41
  * 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.
42
+ * - notches (default): a continuous fill, with a notch at every multiple of each {@link SegmentTier}'s unit. Each
43
+ * tier is one layer the width of the bar (`.notch.<class_name>`), which CSS paints — colour, height — and a mask
44
+ * cuts to that tier's notches;
45
+ * - cells (`cells` given): `count` separate cells with exact, identical gaps, for counting rounds and the like. One
46
+ * strip (`.cell-strip`, the empty cells' colour) holding a `.cell-ghost` and a `.cell-fill`, cut into cells by a
47
+ * mask; the fill and ghost end on a cell's edge, or part way across one that is filling.
50
48
  *
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.
49
+ * The masks are drawn in whole device pixels and the layers they cut placed so those pixels land on the screen's,
50
+ * at any pixel ratio and under a CSS scale: every notch of a tier is the same width and every gap the same, at
51
+ * 125% as at 100%. Colours, notch heights and the ghost are styled by `segmented-resource-bar.css` through
52
+ * `--segmented-bar-*` custom properties. Where masks cannot be drawn (no canvas, as under jsdom) the layers are
53
+ * placed but uncut.
53
54
  */
54
55
  export class SegmentedResourceBarView extends View {
55
56
  /**
@@ -162,6 +163,13 @@ export class SegmentedResourceBarView extends View {
162
163
  this.__frame_id = 0;
163
164
  this.__animate_ghost = this.__animate_ghost.bind(this);
164
165
 
166
+ /**
167
+ * The max moved since the fill was last drawn: the next fill resets the ghost rather than trailing a loss
168
+ * @type {boolean}
169
+ * @private
170
+ */
171
+ this.__max_moved = false;
172
+
165
173
  // what the layout was last built for, so it is rebuilt only when one of them moves
166
174
  this.__built_max = NaN;
167
175
  this.__built_width = NaN;
@@ -169,11 +177,41 @@ export class SegmentedResourceBarView extends View {
169
177
  this.__built_origin_phase = NaN;
170
178
 
171
179
  /**
172
- * scratch for {@link segment_notch_place}
173
- * @type {Float64Array}
180
+ * Cell mode: the cells as last laid out across {@link __el_strip}, or null before the bar has been measured
181
+ * @type {SegmentCellLayout|null}
182
+ * @private
183
+ */
184
+ this.__cell_layout = null;
185
+
186
+ /**
187
+ * Device pixels per CSS pixel the cells were laid out at
188
+ * @type {number}
189
+ * @private
190
+ */
191
+ this.__cell_ratio = 1;
192
+
193
+ /**
194
+ * scratch: [x, width, ...] device-pixel columns for a mask
195
+ * @type {number[]}
174
196
  * @private
175
197
  */
176
- this.__place = new Float64Array(2);
198
+ this.__columns = [];
199
+
200
+ /**
201
+ * scratch for {@link segment_frame_transform}: [scale, offset]
202
+ * @type {number[]}
203
+ * @private
204
+ */
205
+ this.__frame = [1, 0];
206
+
207
+ /**
208
+ * The timer that re-checks the placement while linked; 0 when none
209
+ * @type {number}
210
+ * @private
211
+ */
212
+ this.__recheck_timer = 0;
213
+
214
+ this.__recheck = this.__recheck.bind(this);
177
215
 
178
216
  /**
179
217
  *
@@ -205,23 +243,20 @@ export class SegmentedResourceBarView extends View {
205
243
  this.__value_fills = [];
206
244
 
207
245
  /**
246
+ * Notch mode: one layer per tier, the whole bar wide, styled by the tier's class and masked to its notches
208
247
  * @type {HTMLElement[]}
209
248
  * @private
210
249
  */
211
- this.__notch_elements = [];
250
+ this.__tier_layers = [];
212
251
 
213
252
  /**
214
- * @type {HTMLElement[]}
253
+ * Cell mode: the strip the cells are cut from by a mask, and the ghost and fill drawn along it
254
+ * @type {HTMLElement|null}
215
255
  * @private
216
256
  */
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);
257
+ this.__el_strip = null;
258
+ this.__el_cell_ghost = null;
259
+ this.__el_cell_fill = null;
225
260
 
226
261
  if (cells === null) {
227
262
  this.el.appendChild(this.__el_ghost);
@@ -246,29 +281,35 @@ export class SegmentedResourceBarView extends View {
246
281
  */
247
282
  this.__el_layout = document.createElement('div');
248
283
  this.__el_layout.className = 'notches';
284
+
285
+ for (let i = 0; i < tiers.length; i++) {
286
+ const el_tier = document.createElement('div');
287
+ el_tier.className = `notch ${tiers[i].class_name}`;
288
+ el_tier.style.display = 'none';
289
+
290
+ this.__el_layout.appendChild(el_tier);
291
+ this.__tier_layers[i] = el_tier;
292
+ }
249
293
  } else {
250
294
  this.__el_layout = document.createElement('div');
251
295
  this.__el_layout.className = 'cells';
252
296
 
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];
297
+ const el_strip = document.createElement('div');
298
+ el_strip.className = 'cell-strip';
256
299
 
257
- const el_cell_ghost = document.createElement('div');
258
- el_cell_ghost.className = 'cell-ghost';
300
+ const el_cell_ghost = document.createElement('div');
301
+ el_cell_ghost.className = 'cell-ghost';
259
302
 
260
- const el_cell_fill = document.createElement('div');
261
- el_cell_fill.className = 'cell-fill';
303
+ const el_cell_fill = document.createElement('div');
304
+ el_cell_fill.className = 'cell-fill';
262
305
 
263
- el_cell.appendChild(el_cell_ghost);
264
- el_cell.appendChild(el_cell_fill);
306
+ el_strip.appendChild(el_cell_ghost);
307
+ el_strip.appendChild(el_cell_fill);
308
+ this.__el_layout.appendChild(el_strip);
265
309
 
266
- this.__el_layout.appendChild(el_cell);
267
-
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
- }
310
+ this.__el_strip = el_strip;
311
+ this.__el_cell_ghost = el_cell_ghost;
312
+ this.__el_cell_fill = el_cell_fill;
272
313
  }
273
314
 
274
315
  this.el.appendChild(this.__el_layout);
@@ -347,6 +388,10 @@ export class SegmentedResourceBarView extends View {
347
388
  this.__sizeObserver.start();
348
389
  this.__watch_device_pixel_ratio();
349
390
  this.update();
391
+
392
+ if (typeof setInterval === 'function') {
393
+ this.__recheck_timer = setInterval(this.__recheck, RECHECK_INTERVAL_MS);
394
+ }
350
395
  }
351
396
 
352
397
  unlink() {
@@ -356,6 +401,11 @@ export class SegmentedResourceBarView extends View {
356
401
 
357
402
  this.__sizeObserver.stop();
358
403
 
404
+ if (this.__recheck_timer !== 0) {
405
+ clearInterval(this.__recheck_timer);
406
+ this.__recheck_timer = 0;
407
+ }
408
+
359
409
  if (this.__dpr_query !== null) {
360
410
  this.__dpr_query.removeEventListener('change', this.__handle_dpr_change);
361
411
  this.__dpr_query = null;
@@ -375,6 +425,9 @@ export class SegmentedResourceBarView extends View {
375
425
  }
376
426
 
377
427
  handleMaxChange() {
428
+ // the same amount out of a different max is a different share of the bar, and nothing was lost
429
+ this.__max_moved = true;
430
+
378
431
  this.updateSegments();
379
432
  this.updateFill();
380
433
  }
@@ -382,15 +435,24 @@ export class SegmentedResourceBarView extends View {
382
435
  /**
383
436
  * Place the notches or cells again, if the max, the bar's width (by {@link REBUILD_WIDTH_THRESHOLD_PX} or more),
384
437
  * its pixel ratio or the device-pixel phase of its left edge has moved since they were last placed.
438
+ *
439
+ * Both are drawn as a mask in whole device pixels over a layer CSS paints (see {@link segment_mask_url}): a DOM
440
+ * box per notch or cell cannot be put on the device-pixel grid at a fractional pixel ratio, where Chrome paints
441
+ * a row of equal 1 px gaps as 1, 2, 1, 2... whatever positions it is given (measured at 125% and 150%).
385
442
  */
386
443
  updateSegments() {
387
444
  const el = this.__el_layout;
388
445
 
389
446
  /**
390
- * width as laid out, before any transform on an ancestor
447
+ * width as laid out, before any transform on an ancestor. Not `offsetWidth`, which is rounded to a whole
448
+ * pixel and would read as a scale of its own (176 laid out against 175.84 drawn: 0.9991)
391
449
  * @type {number}
392
450
  */
393
- const layout_width = el.offsetWidth;
451
+ let layout_width = typeof getComputedStyle === 'function' ? parseFloat(getComputedStyle(el).width) : NaN;
452
+
453
+ if (!(layout_width > 0)) {
454
+ layout_width = el.offsetWidth;
455
+ }
394
456
 
395
457
  if (!(layout_width > 0)) {
396
458
  // not laid out (detached, display:none); nothing to place against
@@ -401,13 +463,21 @@ export class SegmentedResourceBarView extends View {
401
463
 
402
464
  const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
403
465
 
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;
466
+ // how the iframes this bar sits in are drawn by the pages around them — invisible from in here
467
+ const frame = segment_frame_transform(el.ownerDocument.defaultView, this.__frame);
468
+ const frame_scale = frame[0];
469
+ const frame_offset = frame[1];
470
+
471
+ // a CSS scale on an ancestor shows each CSS pixel of the bar as `scale` page pixels. The computed width is
472
+ // serialised to a few decimals (175.844 for 175.84375), so an unscaled bar measures a hair off 1 — enough to
473
+ // put a width that should be whole in device pixels just past it — and is read as unscaled
474
+ const measured_scale = (rect.width > 0 ? rect.width / layout_width : 1) * frame_scale;
475
+ const scale = Math.abs(measured_scale - 1) < UNSCALED_TOLERANCE ? 1 : measured_scale;
406
476
 
407
477
  // device pixels per CSS pixel of the bar
408
478
  const ratio = dpr * scale;
409
479
 
410
- const origin_device = rect.left * dpr;
480
+ const origin_device = (frame_offset + rect.left * frame_scale) * dpr;
411
481
  const origin_phase = origin_device - Math.round(origin_device);
412
482
 
413
483
  const max = this.__value_max.getValue();
@@ -426,85 +496,109 @@ export class SegmentedResourceBarView extends View {
426
496
  this.__built_ratio = ratio;
427
497
  this.__built_origin_phase = origin_phase;
428
498
 
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;
499
+ // the masked layer starts on a whole device pixel, and is a width whole in CSS and device pixels both
500
+ const shift_px = -origin_phase / ratio;
501
+ const width_device = segment_mask_width(layout_width - Math.max(0, shift_px), ratio);
431
502
 
432
503
  if (this.__cells === null) {
433
- this.__place_notches(max, layout_width, local_left, ratio);
504
+ this.__place_notches(max, width_device, shift_px, ratio);
434
505
  } else {
435
- this.__place_cells(layout_width, local_left, ratio);
506
+ this.__place_cells(width_device, shift_px, ratio);
436
507
  }
437
508
  }
438
509
 
510
+ /**
511
+ * @param {HTMLElement} el
512
+ * @param {number} width_device
513
+ * @param {number} shift_px
514
+ * @param {number} ratio
515
+ * @private
516
+ */
517
+ __place_layer(el, width_device, shift_px, ratio) {
518
+ const style = el.style;
519
+
520
+ style.left = `${shift_px}px`;
521
+ style.width = `${width_device / ratio}px`;
522
+ }
523
+
439
524
  /**
440
525
  * @param {number} max
441
- * @param {number} width
442
- * @param {number} left
526
+ * @param {number} width_device
527
+ * @param {number} shift_px
443
528
  * @param {number} ratio
444
529
  * @private
445
530
  */
446
- __place_notches(max, width, left, ratio) {
531
+ __place_notches(max, width_device, shift_px, ratio) {
447
532
  const tiers = this.__tiers;
448
- const ticks = segment_tick_positions(max, width, tiers);
533
+ const ticks = segment_tick_positions(max, width_device / ratio, tiers);
449
534
 
450
535
  const tick_count = ticks.length;
451
- const elements = this.__notch_elements;
452
- const container = this.__el_layout;
536
+ const columns = this.__columns;
537
+ const layers = this.__tier_layers;
453
538
 
454
- while (elements.length < tick_count) {
455
- const el = document.createElement('div');
539
+ // a notch is `width_px` CSS pixels wide, scaled by the whole part of the ratio, so it stays whole
540
+ const notch_scale = Math.max(1, Math.round(ratio));
456
541
 
457
- container.appendChild(el);
458
- elements.push(el);
459
- }
542
+ for (let t = 0; t < tiers.length; t++) {
543
+ const notch_width = tiers[t].width_px * notch_scale;
460
544
 
461
- while (elements.length > tick_count) {
462
- container.removeChild(elements.pop());
463
- }
545
+ let count = 0;
464
546
 
465
- const place = this.__place;
547
+ for (let i = 0; i < tick_count; i++) {
548
+ const tick = ticks[i];
466
549
 
467
- for (let i = 0; i < tick_count; i++) {
468
- const tick = ticks[i];
469
- const tier = tiers[tick.tier];
550
+ if (tick.tier !== t) {
551
+ continue;
552
+ }
470
553
 
471
- segment_notch_place(place, 0, tick.value / max, tier.width_px, left, width, ratio);
554
+ const centre = Math.round(tick.value / max * width_device);
555
+ const x = Math.min(Math.max(0, centre - Math.floor(notch_width / 2)), width_device - notch_width);
472
556
 
473
- const el = elements[i];
557
+ columns[count * 2] = x;
558
+ columns[count * 2 + 1] = notch_width;
559
+ count++;
560
+ }
474
561
 
475
- el.className = `notch ${tier.class_name}`;
562
+ const el = layers[t];
476
563
 
477
- const style = el.style;
564
+ if (count === 0) {
565
+ el.style.display = 'none';
566
+ continue;
567
+ }
478
568
 
479
- style.left = `${place[0]}px`;
480
- style.width = `${place[1]}px`;
569
+ el.style.display = '';
570
+
571
+ this.__place_layer(el, width_device, shift_px, ratio);
572
+ segment_mask_apply(el, segment_mask_url(width_device, columns, count));
481
573
  }
482
574
  }
483
575
 
484
576
  /**
485
- * @param {number} width
486
- * @param {number} left
577
+ * @param {number} width_device
578
+ * @param {number} shift_px
487
579
  * @param {number} ratio
488
580
  * @private
489
581
  */
490
- __place_cells(width, left, ratio) {
582
+ __place_cells(width_device, shift_px, ratio) {
491
583
  const cells = this.__cells;
492
584
 
493
- const layout = segment_cell_layout(width, cells.count, cells.group, ratio);
494
-
495
- const origin_device = left * ratio;
496
- const origin_shift_device = Math.round(origin_device) - origin_device;
585
+ const layout = segment_cell_layout(width_device / ratio, cells.count, cells.group, ratio);
497
586
 
498
- const cell_x = layout.cell_x_device;
499
- const cell_width = layout.cell_width_device;
500
- const elements = this.__cell_elements;
587
+ const columns = this.__columns;
501
588
 
502
589
  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`;
590
+ columns[i * 2] = layout.cell_x_device[i];
591
+ columns[i * 2 + 1] = layout.cell_width_device[i];
507
592
  }
593
+
594
+ this.__cell_layout = layout;
595
+ this.__cell_ratio = ratio;
596
+
597
+ this.__place_layer(this.__el_strip, width_device, shift_px, ratio);
598
+ segment_mask_apply(this.__el_strip, segment_mask_url(width_device, columns, cells.count));
599
+
600
+ // the fill is drawn against the layout, which just moved
601
+ this.__draw(performance.now());
508
602
  }
509
603
 
510
604
  updateFill() {
@@ -547,7 +641,13 @@ export class SegmentedResourceBarView extends View {
547
641
 
548
642
  ghost.duration_min_ms = reduced_motion ? 0 : this.__ghost_duration_min_ms;
549
643
  ghost.duration_max_ms = reduced_motion ? 0 : this.__ghost_duration_max_ms;
550
- ghost.set(total_fraction, now);
644
+
645
+ if (this.__max_moved) {
646
+ this.__max_moved = false;
647
+ ghost.reset(total_fraction);
648
+ } else {
649
+ ghost.set(total_fraction, now);
650
+ }
551
651
 
552
652
  this.__draw(now);
553
653
  }
@@ -579,32 +679,35 @@ export class SegmentedResourceBarView extends View {
579
679
  */
580
680
  __draw_cells(total_fraction, ghost_fraction) {
581
681
  const count = this.__cells.count;
682
+ const layout = this.__cell_layout;
582
683
 
583
- const fill_cells = snap_whole(total_fraction * count);
584
- const ghost_cells = snap_whole(ghost_fraction * count);
684
+ const fill = this.__el_cell_fill.style;
685
+ const ghost = this.__el_cell_ghost.style;
585
686
 
586
- const states = this.__cell_states;
687
+ if (layout === null) {
688
+ // not measured yet: a share of the strip, until the cells are laid out
689
+ fill.width = `${total_fraction * 100}%`;
690
+ ghost.width = `${ghost_fraction * 100}%`;
587
691
 
588
- for (let i = 0; i < count; i++) {
589
- const fill = clamp01(fill_cells - i);
590
- const ghost = clamp01(ghost_cells - i);
591
-
592
- const state = fill >= 1 ? CELL_STATE_FILLED : (fill > 0 ? CELL_STATE_PARTIAL : CELL_STATE_EMPTY);
692
+ return;
693
+ }
593
694
 
594
- if (states[i] !== state) {
595
- states[i] = state;
596
- this.__cell_elements[i].className = CELL_STATE_CLASS[state];
597
- }
695
+ // to the edge of the last whole cell (or part way across the next), never into a gap
696
+ const ratio = this.__cell_ratio;
598
697
 
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
- }
698
+ fill.width = `${segment_cell_extent(layout, total_fraction * count) / ratio}px`;
699
+ ghost.width = `${segment_cell_extent(layout, ghost_fraction * count) / ratio}px`;
700
+ }
603
701
 
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
- }
702
+ /**
703
+ * Place again if anything moved that no event reports: an ancestor's transform, the transform of an iframe the
704
+ * bar sits in, a move by a fraction of a pixel. Cheap when nothing did — a computed width and a few rects —
705
+ * and {@link updateSegments} rebuilds only on a change.
706
+ * @private
707
+ */
708
+ __recheck() {
709
+ if (this.isLinked) {
710
+ this.updateSegments();
608
711
  }
609
712
  }
610
713
 
@@ -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,15 @@
1
+ /**
2
+ * How a point in a window's CSS pixels lands in the top-most window this one can see: `top = offset + local * scale`,
3
+ * through every same-origin iframe it sits in, each frame's own transform included.
4
+ *
5
+ * Nothing inside an iframe can see how the page around it draws it: an iframe the host scales to fit (a preview, a
6
+ * playground) reports `devicePixelRatio` and every rect as though unscaled, so something drawn for that pixel grid
7
+ * is painted on another. Walking out through `frameElement` is the one way to find out. A cross-origin frame stops
8
+ * the walk (its `frameElement` reads null, or throws), and is taken as unscaled from there out.
9
+ *
10
+ * @param {Window|null|undefined} win the window the element is in
11
+ * @param {number[]} result receives [scale, offset]
12
+ * @returns {number[]} result
13
+ */
14
+ export function segment_frame_transform(win: Window | null | undefined, result: number[]): number[];
15
+ //# sourceMappingURL=segment_frame_transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment_frame_transform.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/segment_frame_transform.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,6CAJW,MAAM,GAAC,IAAI,GAAC,SAAS,UACrB,MAAM,EAAE,GACN,MAAM,EAAE,CAyDpB"}