@woosh/meep-engine 3.60.0 → 3.60.1

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Advanced 3D game engine",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "3.60.0",
9
+ "version": "3.60.1",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -4,7 +4,12 @@
4
4
  * ray (`PROBE_RAY_NONE`) is past the frame's last. The radiance the ray brings back goes over its light,
5
5
  * three half floats:
6
6
  *
7
- * - a **miss** sees the environment along its direction, `sample_environment_color`;
7
+ * - a **miss** sees the prefiltered environment along its direction, `sample_environment_map`, at the
8
+ * roughness whose lobe covers the solid angle of a radiance texel, 4π/n_r². The prefilter's lobe is the
9
+ * spherical Gaussian exp(2(N·H − 1)/m²), m the roughness squared, whose integral over the sphere is
10
+ * 4πm²·(1 − (1 − e^(−k))/k), k = 2/m²: m = 1/n_r to 0.2% at n_r = 16, so the roughness is 1/√n_r. A sun
11
+ * a few texels wide at level 0 is then a lobe a texel wide, which a texel's jittered rays all see,
12
+ * rather than a point that only some probes' jitter ever lands on;
8
13
  * - a **front-face hit** is `emissive + ρ_d·E_dir/π` (D7), its surface from `probe_clipmap_hit_surface`
9
14
  * at the hit's position, the probe's origin plus t along the ray, with the hit's normal, at its
10
15
  * cascade's voxel; E_dir is what trace left in the light;
@@ -1 +1 @@
1
- {"version":3,"file":"shader_probe_clipmap_shade.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js"],"names":[],"mappings":"AAwDA;;;;;;;;;;;;;;;;;GAiBG;AACH,yCAFU,aAAa,CAyDpB;8BAhH2B,kCAAkC"}
1
+ {"version":3,"file":"shader_probe_clipmap_shade.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js"],"names":[],"mappings":"AA2DA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,yCAFU,aAAa,CAyDpB;8BAxH2B,kCAAkC"}
@@ -1,4 +1,3 @@
1
- import { chunk_sample_environment_color } from "../../../atmosphere/chunk_sample_environment_color.js";
2
1
  import {
3
2
  MESH_CARD_ALBEDO_RESOURCE,
4
3
  MESH_CARD_EMISSIVE_RESOURCE,
@@ -11,6 +10,7 @@ import { ENVIRONMENT_MAP_SAMPLER_RESOURCE } from "../../../light/environment/ENV
11
10
  import { SCENE_DATABASE_RESOURCE } from "../../../scene/SCENE_DATABASE_RESOURCE.js";
12
11
  import { chunk_rgbe9995_decode } from "../../../shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.js";
13
12
  import { chunk_rgbe9995_encode } from "../../../shader/chunk/color/rgbe9995/chunk_rgbe9995_encode.js";
13
+ import { chunk_sample_environment_map } from "../../../shader/chunk/texture/sample/chunk_sample_environment_map.js";
14
14
  import { chunk_uv_octahedral_unit_decode } from "../../../shader/chunk/geometry/octahedron/chunk_uv_octahedral_unit_decode.js";
15
15
  import { chunk_folded_workgroup_index } from "../../../shader/chunk/grid/chunk_folded_workgroup_index.js";
16
16
  import { RECIPROCAL_PI_DECLARATION } from "../../../shader/chunk/math/RECIPROCAL_PI_DECLARATION.js";
@@ -32,9 +32,12 @@ import {
32
32
  PROBE_TRACE_READ_RESOURCE
33
33
  } from "./chunk_probe_clipmap.js";
34
34
  import { PROBE_THREAD_WORKGROUP_SIZE } from "./PROBE_CLIPMAP.js";
35
+ import { PROBE_TRACE_SETTINGS } from "./shader_probe_clipmap_trace.js";
35
36
 
36
37
  const resources = new ShaderResourceSetDescriptor();
37
38
 
39
+ resources.setImmediate("settings", PROBE_TRACE_SETTINGS);
40
+
38
41
  resources.createGroup()
39
42
  .add(PROBE_TRACE_READ_RESOURCE)
40
43
  .add(PROBE_ORIGINS_READ_RESOURCE)
@@ -60,7 +63,12 @@ resources.createGroup()
60
63
  * ray (`PROBE_RAY_NONE`) is past the frame's last. The radiance the ray brings back goes over its light,
61
64
  * three half floats:
62
65
  *
63
- * - a **miss** sees the environment along its direction, `sample_environment_color`;
66
+ * - a **miss** sees the prefiltered environment along its direction, `sample_environment_map`, at the
67
+ * roughness whose lobe covers the solid angle of a radiance texel, 4π/n_r². The prefilter's lobe is the
68
+ * spherical Gaussian exp(2(N·H − 1)/m²), m the roughness squared, whose integral over the sphere is
69
+ * 4πm²·(1 − (1 − e^(−k))/k), k = 2/m²: m = 1/n_r to 0.2% at n_r = 16, so the roughness is 1/√n_r. A sun
70
+ * a few texels wide at level 0 is then a lobe a texel wide, which a texel's jittered rays all see,
71
+ * rather than a point that only some probes' jitter ever lands on;
64
72
  * - a **front-face hit** is `emissive + ρ_d·E_dir/π` (D7), its surface from `probe_clipmap_hit_surface`
65
73
  * at the hit's position, the probe's origin plus t along the ray, with the hit's normal, at its
66
74
  * cascade's voxel; E_dir is what trace left in the light;
@@ -96,7 +104,7 @@ fn main(
96
104
  var albedo = vec3<f32>(0.0);
97
105
 
98
106
  if (kind == ${BRICK_TRACE_MISS}u) {
99
- radiance = sample_environment_color(tEnvironment, sEnvironment, direction);
107
+ radiance = sample_environment_map(tEnvironment, sEnvironment, direction, inverseSqrt(f32(settings.radiance_resolution)));
100
108
  } else if (kind == ${BRICK_TRACE_HIT}u && (record.flags & PROBE_RAY_BACK_FACE) == 0u) {
101
109
  let k = record.flags >> PROBE_RAY_PROBE_SHIFT;
102
110
  let origin = probe_clipmap_ray_origin(probe_origins[k], probe_trace.elements[k], record.direction);
@@ -121,7 +129,7 @@ fn main(
121
129
  chunk_probe_clipmap_ray_origin,
122
130
  chunk_folded_workgroup_index,
123
131
  chunk_uv_octahedral_unit_decode,
124
- chunk_sample_environment_color,
132
+ chunk_sample_environment_map,
125
133
  chunk_probe_clipmap_hit_surface,
126
134
  chunk_rgbe9995_decode,
127
135
  chunk_rgbe9995_encode,
@@ -1 +1 @@
1
- {"version":3,"file":"WGSLLanguageCore.d.ts","sourceRoot":"","sources":["../../../../../src/shade/wgsl/emulator/WGSLLanguageCore.js"],"names":[],"mappings":";IAu1DA;;;;;;;;;;;;;OAaG;IACH,+DAyFC"}
1
+ {"version":3,"file":"WGSLLanguageCore.d.ts","sourceRoot":"","sources":["../../../../../src/shade/wgsl/emulator/WGSLLanguageCore.js"],"names":[],"mappings":";IAg3DA;;;;;;;;;;;;;OAaG;IACH,+DAyFC"}
@@ -1260,7 +1260,9 @@ export const WGSLLanguageCore = {
1260
1260
  *
1261
1261
  * Filtered sample at normalised UV coords. Honours the sampler's
1262
1262
  * address modes and `magFilter`. `coords` length matches the texture
1263
- * dimensionality. The `level` arg is ignored (no mipmaps).
1263
+ * dimensionality. The `level` reads that level of a bitmap carrying its levels, as `textureLoad`
1264
+ * does, and zero past them; between two levels, a `linear` `mipmapFilter` blends them and any other
1265
+ * takes the nearer. A bitmap carrying none serves every level from itself.
1264
1266
  *
1265
1267
  * @param {import("./CPUBitmapData.js").CPUBitmapData} t
1266
1268
  * @param {import("../../descriptor/texture/SamplerDescriptor.js").SamplerDescriptor | null} sampler
@@ -1275,21 +1277,44 @@ export const WGSLLanguageCore = {
1275
1277
 
1276
1278
  let array_index = undefined;
1277
1279
  let offset = undefined;
1280
+ let level = p3;
1278
1281
 
1279
1282
  if (p5 !== undefined) {
1280
1283
  // (t, s, coords, array_index, level, offset)
1281
1284
  array_index = p3;
1285
+ level = p4;
1282
1286
  offset = p5;
1283
1287
  } else if (p4 === undefined) {
1284
1288
  // (t, s, coords, level)
1285
1289
  } else if (typeof p4 === 'number') {
1286
1290
  // (t, s, coords, array_index, level)
1287
1291
  array_index = p3;
1292
+ level = p4;
1288
1293
  } else {
1289
1294
  // (t, s, coords, level, offset)
1290
1295
  offset = p4;
1291
1296
  }
1292
1297
 
1298
+ if (t.levels?.length > 0 && level !== 0) {
1299
+ const below = Math.max(Math.floor(level), 0);
1300
+ const fraction = level - below;
1301
+
1302
+ if (fraction > 0 && sampler?.mipmapFilter === 'linear') {
1303
+ const a = WGSLLanguageCore.textureSampleLevel(t, sampler, coords, ...(array_index === undefined ? [below] : [array_index, below]), ...(offset === undefined ? [] : [offset]));
1304
+ const b = WGSLLanguageCore.textureSampleLevel(t, sampler, coords, ...(array_index === undefined ? [below + 1] : [array_index, below + 1]), ...(offset === undefined ? [] : [offset]));
1305
+
1306
+ return a.map((v, i) => v + (b[i] - v) * fraction);
1307
+ }
1308
+
1309
+ const source = texture_level(t, fraction > 0.5 ? below + 1 : below);
1310
+
1311
+ if (source === undefined) {
1312
+ return new Float32Array(4);
1313
+ }
1314
+
1315
+ t = source;
1316
+ }
1317
+
1293
1318
  const dim = t.dimensionality;
1294
1319
  // Convert UV (normalised) to texel-space coords expected by
1295
1320
  // cpu_texture_sample. coords for a 1D texture is a scalar in WGSL.
@@ -1,39 +1,99 @@
1
1
  /**
2
2
  * A value that trails another one down: the "ghost" behind a bar's fill that shows how much was just lost.
3
3
  *
4
- * When the target drops, the trail holds where it was for {@link delay_ms}, then eases down to the target over
5
- * {@link duration_ms}. A further drop while it holds or eases restarts the hold from wherever the trail is. When
6
- * the target rises above the trail, the trail jumps to it at once.
4
+ * The knobs are the three Dota 2 exposes for its hero bars as console variables: a short hold after a drop
5
+ * (`dota_health_hurt_delay`) and a decay whose length sits between a floor and a ceiling
6
+ * (`dota_health_hurt_decay_time_min` / `_max`). The defaults, 0.1 s / 0.3 s / 0.8 s, are the values players'
7
+ * published configs list as Dota's own; Valve does not document them. Everything else here is this class's own
8
+ * choice, not known Dota behaviour: the decay's length growing linearly with the loss (the whole bar takes the
9
+ * ceiling), the ease-out curve, and how losses that overlap combine.
10
+ *
11
+ * Every drop is a loss with a trail of its own: it holds at the value from before the drop for {@link delay_ms},
12
+ * then eases down over a length sized by that loss alone. The trail shows the highest of the live losses' trails,
13
+ * and never less than the target. Because no loss looks at another, a drop is drawn the same whether the trail
14
+ * was at rest or still moving when it came: a damage tick that lands just before or just after the previous one
15
+ * has faded looks the same, where a single trail that re-plans has to pick one side of that line.
16
+ *
17
+ * Each loss eases toward the lowest the target has been since it happened, never toward the target itself, so a
18
+ * rise that stays under the trail (regeneration after a hit) cannot lift it. When a later drop lowers that floor,
19
+ * the rest of the loss's ease is rescaled toward the new floor from where it is, so it does not jump. The trail
20
+ * is continuous in time and never rises unless the target rises above it; when it does, the trail jumps to the
21
+ * target and every loss is forgotten.
22
+ *
23
+ * A target that is not a finite number is ignored: the trail carries on with what it had, so one bad value cannot
24
+ * leave it NaN, and its owner animating it forever, for every value after.
25
+ *
26
+ * Each loss's start and length are fixed when it happens, so changing the knobs never revives or reshapes a trail
27
+ * already under way.
7
28
  *
8
29
  * Time is passed in, in milliseconds on any monotonic clock, so the trail can be driven and checked without one.
9
30
  */
10
31
  export class GhostTrail {
11
32
  /**
12
- * How long the trail holds after a drop before it starts to move
33
+ * How long a loss's trail holds before it starts to ease down
13
34
  * @type {number}
14
35
  */
15
36
  delay_ms: number;
16
37
  /**
17
- * How long the ease from the held value down to the target takes; 0 snaps once the hold ends
38
+ * Length of the ease for the smallest loss; 0 with {@link duration_max_ms} 0 snaps once the hold ends
18
39
  * @type {number}
19
40
  */
20
- duration_ms: number;
41
+ duration_min_ms: number;
21
42
  /**
43
+ * Length of the ease for a loss of the whole bar (1); smaller losses take proportionally less, down to
44
+ * {@link duration_min_ms}
22
45
  * @type {number}
23
- * @private
24
46
  */
25
- private __from;
47
+ duration_max_ms: number;
26
48
  /**
49
+ * The target, as last set
27
50
  * @type {number}
28
51
  * @private
29
52
  */
30
53
  private __to;
31
54
  /**
32
- * When the current hold began
55
+ * Live losses, {@link LOSS_STRIDE} floats each: when the ease starts (after the hold), the ease's length, the
56
+ * lowest the target has been since the loss, and how far above that the trail is while u is 1
57
+ * @type {Float64Array}
58
+ * @private
59
+ */
60
+ private __losses;
61
+ /**
33
62
  * @type {number}
34
63
  * @private
35
64
  */
36
- private __drop_time;
65
+ private __loss_count;
66
+ /**
67
+ * Length of the ease for a loss of `gap`, a fraction of the bar
68
+ * @param {number} gap
69
+ * @returns {number}
70
+ * @private
71
+ */
72
+ private __duration_for;
73
+ /**
74
+ * How much of one loss's ease is left at `time_ms`: 1 while it holds, falling to 0 when the ease is over
75
+ * @param {number} index
76
+ * @param {number} time_ms
77
+ * @returns {number}
78
+ * @private
79
+ */
80
+ private __loss_u;
81
+ /**
82
+ * Where one loss's trail is at `time_ms`; an ease-out, fastest at the start of the ease
83
+ * @param {number} index
84
+ * @param {number} time_ms
85
+ * @returns {number}
86
+ * @private
87
+ */
88
+ private __loss_at;
89
+ /**
90
+ * @param {number} start
91
+ * @param {number} duration
92
+ * @param {number} low
93
+ * @param {number} amplitude
94
+ * @private
95
+ */
96
+ private __add_loss;
37
97
  /**
38
98
  * Tell the trail its target's new value.
39
99
  * @param {number} target
@@ -41,13 +101,13 @@ export class GhostTrail {
41
101
  */
42
102
  set(target: number, time_ms: number): void;
43
103
  /**
44
- * Where the trail is at a given time; never below its target.
104
+ * Where the trail is at a given time: the highest of the live losses' trails, and never below the target.
45
105
  * @param {number} time_ms
46
106
  * @returns {number}
47
107
  */
48
108
  sample(time_ms: number): number;
49
109
  /**
50
- * Whether the trail has reached its target and will not move until {@link set} is called again
110
+ * Whether the trail shows the target and will not move until {@link set} is called again
51
111
  * @param {number} time_ms
52
112
  * @returns {boolean}
53
113
  */
@@ -1 +1 @@
1
- {"version":3,"file":"GhostTrail.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/GhostTrail.js"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH;IACI;;;OAGG;IACH,UAFU,MAAM,CAED;IAEf;;;OAGG;IACH,aAFU,MAAM,CAEE;IAElB;;;OAGG;IACH,eAAW;IAEX;;;OAGG;IACH,aAAS;IAET;;;;OAIG;IACH,oBAAgB;IAEhB;;;;OAIG;IACH,YAHW,MAAM,WACN,MAAM,QAahB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GACJ,MAAM,CAuBlB;IAED;;;;OAIG;IACH,oBAHW,MAAM,GACJ,OAAO,CAInB;CACJ"}
1
+ {"version":3,"file":"GhostTrail.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/GhostTrail.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IACI;;;OAGG;IACH,UAFU,MAAM,CAED;IAEf;;;OAGG;IACH,iBAFU,MAAM,CAEM;IAEtB;;;;OAIG;IACH,iBAFU,MAAM,CAEM;IAEtB;;;;OAIG;IACH,aAAS;IAET;;;;;OAKG;IACH,iBAA6C;IAE7C;;;OAGG;IACH,qBAAiB;IAEjB;;;;;OAKG;IACH,uBAIC;IAED;;;;;;OAMG;IACH,iBAgBC;IAED;;;;;;OAMG;IACH,kBAKC;IAED;;;;;;OAMG;IACH,mBAoBC;IAED;;;;OAIG;IACH,YAHW,MAAM,WACN,MAAM,QAsEhB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GACJ,MAAM,CAgBlB;IAED;;;;OAIG;IACH,oBAHW,MAAM,GACJ,OAAO,CAkBnB;CACJ"}
@@ -1,98 +1,280 @@
1
- import { smoothStep } from "../../../../core/math/smoothStep.js";
2
-
3
- /**
4
- * A value that trails another one down: the "ghost" behind a bar's fill that shows how much was just lost.
5
- *
6
- * When the target drops, the trail holds where it was for {@link delay_ms}, then eases down to the target over
7
- * {@link duration_ms}. A further drop while it holds or eases restarts the hold from wherever the trail is. When
8
- * the target rises above the trail, the trail jumps to it at once.
9
- *
10
- * Time is passed in, in milliseconds on any monotonic clock, so the trail can be driven and checked without one.
11
- */
12
- export class GhostTrail {
13
- /**
14
- * How long the trail holds after a drop before it starts to move
15
- * @type {number}
16
- */
17
- delay_ms = 450;
18
-
19
- /**
20
- * How long the ease from the held value down to the target takes; 0 snaps once the hold ends
21
- * @type {number}
22
- */
23
- duration_ms = 350;
24
-
25
- /**
26
- * @type {number}
27
- * @private
28
- */
29
- __from = 0;
30
-
31
- /**
32
- * @type {number}
33
- * @private
34
- */
35
- __to = 0;
36
-
37
- /**
38
- * When the current hold began
39
- * @type {number}
40
- * @private
41
- */
42
- __drop_time = 0;
43
-
44
- /**
45
- * Tell the trail its target's new value.
46
- * @param {number} target
47
- * @param {number} time_ms
48
- */
49
- set(target, time_ms) {
50
- const current = this.sample(time_ms);
51
-
52
- if (target >= current) {
53
- this.__from = target;
54
- this.__to = target;
55
- } else {
56
- this.__from = current;
57
- this.__to = target;
58
- this.__drop_time = time_ms;
59
- }
60
- }
61
-
62
- /**
63
- * Where the trail is at a given time; never below its target.
64
- * @param {number} time_ms
65
- * @returns {number}
66
- */
67
- sample(time_ms) {
68
- const from = this.__from;
69
- const to = this.__to;
70
-
71
- if (from === to) {
72
- return to;
73
- }
74
-
75
- const start = this.__drop_time + this.delay_ms;
76
-
77
- if (time_ms < start) {
78
- return from;
79
- }
80
-
81
- const end = start + this.duration_ms;
82
-
83
- if (time_ms >= end) {
84
- return to;
85
- }
86
-
87
- return from + (to - from) * smoothStep(start, end, time_ms);
88
- }
89
-
90
- /**
91
- * Whether the trail has reached its target and will not move until {@link set} is called again
92
- * @param {number} time_ms
93
- * @returns {boolean}
94
- */
95
- is_settled(time_ms) {
96
- return this.__from === this.__to || time_ms >= this.__drop_time + this.delay_ms + this.duration_ms;
97
- }
98
- }
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { clamp01 } from "../../../../core/math/clamp01.js";
3
+
4
+ /**
5
+ * Floats per loss in {@link GhostTrail}'s store: [start, duration, low, amplitude]. A loss's trail is
6
+ * `low + amplitude * u²`, where u runs from 1 (held, and at the start of the ease) to 0 (the ease is over).
7
+ * @type {number}
8
+ */
9
+ const LOSS_STRIDE = 4;
10
+
11
+ /**
12
+ * A value that trails another one down: the "ghost" behind a bar's fill that shows how much was just lost.
13
+ *
14
+ * The knobs are the three Dota 2 exposes for its hero bars as console variables: a short hold after a drop
15
+ * (`dota_health_hurt_delay`) and a decay whose length sits between a floor and a ceiling
16
+ * (`dota_health_hurt_decay_time_min` / `_max`). The defaults, 0.1 s / 0.3 s / 0.8 s, are the values players'
17
+ * published configs list as Dota's own; Valve does not document them. Everything else here is this class's own
18
+ * choice, not known Dota behaviour: the decay's length growing linearly with the loss (the whole bar takes the
19
+ * ceiling), the ease-out curve, and how losses that overlap combine.
20
+ *
21
+ * Every drop is a loss with a trail of its own: it holds at the value from before the drop for {@link delay_ms},
22
+ * then eases down over a length sized by that loss alone. The trail shows the highest of the live losses' trails,
23
+ * and never less than the target. Because no loss looks at another, a drop is drawn the same whether the trail
24
+ * was at rest or still moving when it came: a damage tick that lands just before or just after the previous one
25
+ * has faded looks the same, where a single trail that re-plans has to pick one side of that line.
26
+ *
27
+ * Each loss eases toward the lowest the target has been since it happened, never toward the target itself, so a
28
+ * rise that stays under the trail (regeneration after a hit) cannot lift it. When a later drop lowers that floor,
29
+ * the rest of the loss's ease is rescaled toward the new floor from where it is, so it does not jump. The trail
30
+ * is continuous in time and never rises unless the target rises above it; when it does, the trail jumps to the
31
+ * target and every loss is forgotten.
32
+ *
33
+ * A target that is not a finite number is ignored: the trail carries on with what it had, so one bad value cannot
34
+ * leave it NaN, and its owner animating it forever, for every value after.
35
+ *
36
+ * Each loss's start and length are fixed when it happens, so changing the knobs never revives or reshapes a trail
37
+ * already under way.
38
+ *
39
+ * Time is passed in, in milliseconds on any monotonic clock, so the trail can be driven and checked without one.
40
+ */
41
+ export class GhostTrail {
42
+ /**
43
+ * How long a loss's trail holds before it starts to ease down
44
+ * @type {number}
45
+ */
46
+ delay_ms = 100;
47
+
48
+ /**
49
+ * Length of the ease for the smallest loss; 0 with {@link duration_max_ms} 0 snaps once the hold ends
50
+ * @type {number}
51
+ */
52
+ duration_min_ms = 300;
53
+
54
+ /**
55
+ * Length of the ease for a loss of the whole bar (1); smaller losses take proportionally less, down to
56
+ * {@link duration_min_ms}
57
+ * @type {number}
58
+ */
59
+ duration_max_ms = 800;
60
+
61
+ /**
62
+ * The target, as last set
63
+ * @type {number}
64
+ * @private
65
+ */
66
+ __to = 0;
67
+
68
+ /**
69
+ * Live losses, {@link LOSS_STRIDE} floats each: when the ease starts (after the hold), the ease's length, the
70
+ * lowest the target has been since the loss, and how far above that the trail is while u is 1
71
+ * @type {Float64Array}
72
+ * @private
73
+ */
74
+ __losses = new Float64Array(LOSS_STRIDE * 8);
75
+
76
+ /**
77
+ * @type {number}
78
+ * @private
79
+ */
80
+ __loss_count = 0;
81
+
82
+ /**
83
+ * Length of the ease for a loss of `gap`, a fraction of the bar
84
+ * @param {number} gap
85
+ * @returns {number}
86
+ * @private
87
+ */
88
+ __duration_for(gap) {
89
+ const min = this.duration_min_ms;
90
+
91
+ return min + (this.duration_max_ms - min) * clamp01(gap);
92
+ }
93
+
94
+ /**
95
+ * How much of one loss's ease is left at `time_ms`: 1 while it holds, falling to 0 when the ease is over
96
+ * @param {number} index
97
+ * @param {number} time_ms
98
+ * @returns {number}
99
+ * @private
100
+ */
101
+ __loss_u(index, time_ms) {
102
+ const losses = this.__losses;
103
+ const offset = index * LOSS_STRIDE;
104
+
105
+ const elapsed = time_ms - losses[offset];
106
+ const duration = losses[offset + 1];
107
+
108
+ if (elapsed < 0) {
109
+ return 1;
110
+ }
111
+
112
+ if (elapsed >= duration) {
113
+ return 0;
114
+ }
115
+
116
+ return 1 - elapsed / duration;
117
+ }
118
+
119
+ /**
120
+ * Where one loss's trail is at `time_ms`; an ease-out, fastest at the start of the ease
121
+ * @param {number} index
122
+ * @param {number} time_ms
123
+ * @returns {number}
124
+ * @private
125
+ */
126
+ __loss_at(index, time_ms) {
127
+ const offset = index * LOSS_STRIDE;
128
+ const u = this.__loss_u(index, time_ms);
129
+
130
+ return this.__losses[offset + 2] + this.__losses[offset + 3] * u * u;
131
+ }
132
+
133
+ /**
134
+ * @param {number} start
135
+ * @param {number} duration
136
+ * @param {number} low
137
+ * @param {number} amplitude
138
+ * @private
139
+ */
140
+ __add_loss(start, duration, low, amplitude) {
141
+ const count = this.__loss_count;
142
+
143
+ if ((count + 1) * LOSS_STRIDE > this.__losses.length) {
144
+ const grown = new Float64Array(this.__losses.length * 2);
145
+
146
+ grown.set(this.__losses);
147
+
148
+ this.__losses = grown;
149
+ }
150
+
151
+ const losses = this.__losses;
152
+ const offset = count * LOSS_STRIDE;
153
+
154
+ losses[offset] = start;
155
+ losses[offset + 1] = duration;
156
+ losses[offset + 2] = low;
157
+ losses[offset + 3] = amplitude;
158
+
159
+ this.__loss_count = count + 1;
160
+ }
161
+
162
+ /**
163
+ * Tell the trail its target's new value.
164
+ * @param {number} target
165
+ * @param {number} time_ms
166
+ */
167
+ set(target, time_ms) {
168
+ // the knobs are public and may change between calls, so they are checked where they are read
169
+ assert.isFinite(this.delay_ms, 'delay_ms');
170
+ assert.greaterThanOrEqual(this.delay_ms, 0, 'delay_ms must not be negative');
171
+ assert.isFinite(this.duration_max_ms, 'duration_max_ms');
172
+ assert.greaterThanOrEqual(this.duration_min_ms, 0, 'duration_min_ms must not be negative');
173
+ assert.greaterThanOrEqual(this.duration_max_ms, this.duration_min_ms, 'duration_max_ms must not be under duration_min_ms');
174
+ assert.isFinite(time_ms, 'time_ms');
175
+
176
+ if (!Number.isFinite(target)) {
177
+ return;
178
+ }
179
+
180
+ if (target >= this.sample(time_ms)) {
181
+ // a rise to or above the trail: jump to it, and forget every loss
182
+ this.__to = target;
183
+ this.__loss_count = 0;
184
+
185
+ return;
186
+ }
187
+
188
+ // drop the losses whose ease is over (each rests at its low, which is never above the target); for the rest,
189
+ // if the target went under the low, lower it and rescale what is left of the ease so the trail is where it was
190
+ const losses = this.__losses;
191
+ const count = this.__loss_count;
192
+
193
+ let kept = 0;
194
+
195
+ for (let i = 0; i < count; i++) {
196
+ const offset = i * LOSS_STRIDE;
197
+
198
+ const u = this.__loss_u(i, time_ms);
199
+
200
+ if (u <= 0) {
201
+ continue;
202
+ }
203
+
204
+ let low = losses[offset + 2];
205
+ let amplitude = losses[offset + 3];
206
+
207
+ if (target < low) {
208
+ // where the trail is now, kept: low + amplitude * u² before and after. u > 0 here, and the
209
+ // product amplitude * u² stays at most the trail's height over the new low however small u is
210
+ const value = low + amplitude * u * u;
211
+
212
+ low = target;
213
+ amplitude = (value - low) / (u * u);
214
+ }
215
+
216
+ const kept_offset = kept * LOSS_STRIDE;
217
+
218
+ losses[kept_offset] = losses[offset];
219
+ losses[kept_offset + 1] = losses[offset + 1];
220
+ losses[kept_offset + 2] = low;
221
+ losses[kept_offset + 3] = amplitude;
222
+
223
+ kept++;
224
+ }
225
+
226
+ this.__loss_count = kept;
227
+
228
+ const before = this.__to;
229
+
230
+ if (target < before) {
231
+ this.__add_loss(time_ms + this.delay_ms, this.__duration_for(before - target), target, before - target);
232
+ }
233
+
234
+ this.__to = target;
235
+ }
236
+
237
+ /**
238
+ * Where the trail is at a given time: the highest of the live losses' trails, and never below the target.
239
+ * @param {number} time_ms
240
+ * @returns {number}
241
+ */
242
+ sample(time_ms) {
243
+ let value = this.__to;
244
+
245
+ const count = this.__loss_count;
246
+
247
+ for (let i = 0; i < count; i++) {
248
+ const v = this.__loss_at(i, time_ms);
249
+
250
+ if (v > value) {
251
+ value = v;
252
+ }
253
+ }
254
+
255
+ return value;
256
+ }
257
+
258
+ /**
259
+ * Whether the trail shows the target and will not move until {@link set} is called again
260
+ * @param {number} time_ms
261
+ * @returns {boolean}
262
+ */
263
+ is_settled(time_ms) {
264
+ const to = this.__to;
265
+ const losses = this.__losses;
266
+ const count = this.__loss_count;
267
+
268
+ for (let i = 0; i < count; i++) {
269
+ const offset = i * LOSS_STRIDE;
270
+
271
+ // a loss still easing and still above the target; one at or under it stays there, since its trail
272
+ // only falls and the target holds until the next set
273
+ if (time_ms < losses[offset] + losses[offset + 1] && this.__loss_at(i, time_ms) > to) {
274
+ return false;
275
+ }
276
+ }
277
+
278
+ return true;
279
+ }
280
+ }
@@ -18,10 +18,12 @@ export class SegmentedResourceBarView extends View {
18
18
  * @param {string[]} [options.classList]
19
19
  * @param {{unit:number, class_name:string, width_px:number, min_spacing_px:number}[]} [options.tiers] notch tiers, finest first; see {@link SegmentTier}
20
20
  * @param {{count:number, group:number}|null} [options.cells] draw `count` cells instead of notches, in groups of `group` (0 for none)
21
- * @param {number} [options.ghost_delay_ms=450] how long the ghost holds after a drop
22
- * @param {number} [options.ghost_duration_ms=350] how long the ghost then takes to catch up; 0 under prefers-reduced-motion
21
+ * @param {number} [options.ghost_delay_ms=100] how long the ghost holds after a drop; see {@link GhostTrail}
22
+ * @param {number} [options.ghost_duration_min_ms=300] how long the ghost takes to close the smallest loss
23
+ * @param {number} [options.ghost_duration_max_ms=800] how long it takes to close a loss of the whole bar; both
24
+ * durations are 0 under prefers-reduced-motion, so the ghost holds, then snaps
23
25
  */
24
- constructor({ values, max, classList, tiers, cells, ghost_delay_ms, ghost_duration_ms }: {
26
+ constructor({ values, max, classList, tiers, cells, ghost_delay_ms, ghost_duration_min_ms, ghost_duration_max_ms }: {
25
27
  values: (Vector1 | ObservedInteger)[];
26
28
  max?: Vector1 | ObservedInteger | ReactiveExpression;
27
29
  classList?: string[];
@@ -36,7 +38,8 @@ export class SegmentedResourceBarView extends View {
36
38
  group: number;
37
39
  } | null;
38
40
  ghost_delay_ms?: number;
39
- ghost_duration_ms?: number;
41
+ ghost_duration_min_ms?: number;
42
+ ghost_duration_max_ms?: number;
40
43
  });
41
44
  /**
42
45
  *
@@ -69,7 +72,12 @@ export class SegmentedResourceBarView extends View {
69
72
  * @type {number}
70
73
  * @private
71
74
  */
72
- private __ghost_duration_ms;
75
+ private __ghost_duration_min_ms;
76
+ /**
77
+ * @type {number}
78
+ * @private
79
+ */
80
+ private __ghost_duration_max_ms;
73
81
  /**
74
82
  * Total fill as a fraction of max, as last drawn
75
83
  * @type {number}
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedResourceBarView.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/SegmentedResourceBarView.js"],"names":[],"mappings":"AA0CA;;;;;;;;;;GAUG;AACH;IACI;;;;;;;;;;OAUG;IACH;QARgD,MAAM,EAA3C,CAAC,OAAO,kBAAgB,CAAC,EAAE;QAC0B,GAAG,GAAxD,OAAO,uCAAmC;QACvB,SAAS,GAA5B,MAAM,EAAE;QAC6E,KAAK,GAA1F;YAAC,IAAI,EAAC,MAAM,CAAC;YAAC,UAAU,EAAC,MAAM,CAAC;YAAC,QAAQ,EAAC,MAAM,CAAC;YAAC,cAAc,EAAC,MAAM,CAAA;SAAC,EAAE;QAC9B,KAAK,GAAjD;YAAC,KAAK,EAAC,MAAM,CAAC;YAAC,KAAK,EAAC,MAAM,CAAA;SAAC,GAAC,IAAI;QAChB,cAAc,GAA/B,MAAM;QACW,iBAAiB,GAAlC,MAAM;OAsQhB;IA9OG;;;;OAIG;IACH,yBAA8B;IAE9B;;;;OAIG;IACH,oBAAsB;IAEtB;;;OAGG;IACH,gBAAoB;IAEpB;;;OAGG;IACH,gBAAoB;IAEpB;;;OAGG;IACH,gBAA+B;IAG/B;;;OAGG;IACH,4BAA4C;IAE5C;;;;OAIG;IACH,yBAAyB;IAEzB;;;OAGG;IACH,yBAAgH;IAEhH;;;;OAIG;IACH,oBAAuB;IAie3B;;OAEG;IACH,4BASC;IAzeG,yBAA4B;IAC5B,mBAAmB;IA8bvB;;OAEG;IACH,wBAQC;IArcG,oBAAsB;IACtB,sBAAwB;IACxB,sBAAwB;IACxB,6BAA+B;IAE/B;;;;OAIG;IACH,gBAAkC;IAElC;;;OAGG;IACH,YAFU,qBAAqB,CAEH;IAG5B,mBAAuC;IASvC;;;OAGG;IACH,mBAA+C;IAG/C;;;OAGG;IACH,sBAAuB;IAEvB;;;OAGG;IACH,yBAA0B;IAE1B;;;OAGG;IACH,wBAAyB;IACzB,uCAA8B;IAC9B,wCAA+B;IAC/B,0BAAqE;IAGrE,gCAA2E;IAC3E,iCAA4E;IAkBxE;;;;OAIG;IACH,oBAAgD;IA8BpD;;;OAGG;IACH,gBAFU,QAAQ,CAsChB;IAWF;;;;OAIG;IACH,uBAA2C;IAuC/C,eAGC;IAED,wBAGC;IAED;;;OAGG;IACH,uBAmDC;IAED;;;;;;OAMG;IACH,wBAoCC;IAED;;;;;OAKG;IACH,sBAkBC;IAED,mBAwCC;IAED;;;;OAIG;IACH,eAaC;IAED;;;;OAIG;IACH,qBA6BC;IAeD;;;;OAIG;IACH,mCAUC;CAeJ;iBAxnBgB,kBAAkB;iBALlB,0CAA0C;qBAGtC,6BAA6B;oBAF9B,kCAAkC"}
1
+ {"version":3,"file":"SegmentedResourceBarView.d.ts","sourceRoot":"","sources":["../../../../../../src/view/elements/progress/segmented/SegmentedResourceBarView.js"],"names":[],"mappings":"AA0CA;;;;;;;;;;GAUG;AACH;IACI;;;;;;;;;;;;OAYG;IACH;QAVgD,MAAM,EAA3C,CAAC,OAAO,kBAAgB,CAAC,EAAE;QAC0B,GAAG,GAAxD,OAAO,uCAAmC;QACvB,SAAS,GAA5B,MAAM,EAAE;QAC6E,KAAK,GAA1F;YAAC,IAAI,EAAC,MAAM,CAAC;YAAC,UAAU,EAAC,MAAM,CAAC;YAAC,QAAQ,EAAC,MAAM,CAAC;YAAC,cAAc,EAAC,MAAM,CAAA;SAAC,EAAE;QAC9B,KAAK,GAAjD;YAAC,KAAK,EAAC,MAAM,CAAC;YAAC,KAAK,EAAC,MAAM,CAAA;SAAC,GAAC,IAAI;QAChB,cAAc,GAA/B,MAAM;QACW,qBAAqB,GAAtC,MAAM;QACW,qBAAqB,GAAtC,MAAM;OAiRhB;IApPG;;;;OAIG;IACH,yBAA8B;IAE9B;;;;OAIG;IACH,oBAAsB;IAEtB;;;OAGG;IACH,gBAAoB;IAEpB;;;OAGG;IACH,gBAAoB;IAEpB;;;OAGG;IACH,gBAA+B;IAG/B;;;OAGG;IACH,gCAAoD;IAEpD;;;OAGG;IACH,gCAAoD;IAEpD;;;;OAIG;IACH,yBAAyB;IAEzB;;;OAGG;IACH,yBAAgH;IAEhH;;;;OAIG;IACH,oBAAuB;IAoe3B;;OAEG;IACH,4BASC;IA5eG,yBAA4B;IAC5B,mBAAmB;IAicvB;;OAEG;IACH,wBAQC;IAxcG,oBAAsB;IACtB,sBAAwB;IACxB,sBAAwB;IACxB,6BAA+B;IAE/B;;;;OAIG;IACH,gBAAkC;IAElC;;;OAGG;IACH,YAFU,qBAAqB,CAEH;IAG5B,mBAAuC;IASvC;;;OAGG;IACH,mBAA+C;IAG/C;;;OAGG;IACH,sBAAuB;IAEvB;;;OAGG;IACH,yBAA0B;IAE1B;;;OAGG;IACH,wBAAyB;IACzB,uCAA8B;IAC9B,wCAA+B;IAC/B,0BAAqE;IAGrE,gCAA2E;IAC3E,iCAA4E;IAkBxE;;;;OAIG;IACH,oBAAgD;IA8BpD;;;OAGG;IACH,gBAFU,QAAQ,CAsChB;IAWF;;;;OAIG;IACH,uBAA2C;IAuC/C,eAGC;IAED,wBAGC;IAED;;;OAGG;IACH,uBAmDC;IAED;;;;;;OAMG;IACH,wBAoCC;IAED;;;;;OAKG;IACH,sBAkBC;IAED,mBA2CC;IAED;;;;OAIG;IACH,eAaC;IAED;;;;OAIG;IACH,qBA6BC;IAeD;;;;OAIG;IACH,mCAUC;CAeJ;iBAvoBgB,kBAAkB;iBALlB,0CAA0C;qBAGtC,6BAA6B;oBAF9B,kCAAkC"}
@@ -60,8 +60,10 @@ export class SegmentedResourceBarView extends View {
60
60
  * @param {string[]} [options.classList]
61
61
  * @param {{unit:number, class_name:string, width_px:number, min_spacing_px:number}[]} [options.tiers] notch tiers, finest first; see {@link SegmentTier}
62
62
  * @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=450] how long the ghost holds after a drop
64
- * @param {number} [options.ghost_duration_ms=350] how long the ghost then takes to catch up; 0 under prefers-reduced-motion
63
+ * @param {number} [options.ghost_delay_ms=100] how long the ghost holds after a drop; see {@link GhostTrail}
64
+ * @param {number} [options.ghost_duration_min_ms=300] how long the ghost takes to close the smallest loss
65
+ * @param {number} [options.ghost_duration_max_ms=800] how long it takes to close a loss of the whole bar; both
66
+ * durations are 0 under prefers-reduced-motion, so the ghost holds, then snaps
65
67
  */
66
68
  constructor({
67
69
  values,
@@ -69,16 +71,20 @@ export class SegmentedResourceBarView extends View {
69
71
  classList = [],
70
72
  tiers = SEGMENT_TIERS_DEFAULT,
71
73
  cells = null,
72
- ghost_delay_ms = 450,
73
- ghost_duration_ms = 350
74
+ ghost_delay_ms = 100,
75
+ ghost_duration_min_ms = 300,
76
+ ghost_duration_max_ms = 800
74
77
  }) {
75
78
  super();
76
79
 
77
80
  assert.isArray(values, 'values');
78
81
  assert.defined(max, 'max');
79
82
  assert.isArray(tiers, 'tiers');
80
- assert.isNumber(ghost_delay_ms, 'ghost_delay_ms');
81
- assert.isNumber(ghost_duration_ms, 'ghost_duration_ms');
83
+ assert.isFinite(ghost_delay_ms, 'ghost_delay_ms');
84
+ assert.greaterThanOrEqual(ghost_delay_ms, 0, 'ghost_delay_ms must not be negative');
85
+ assert.isFinite(ghost_duration_max_ms, 'ghost_duration_max_ms');
86
+ assert.greaterThanOrEqual(ghost_duration_min_ms, 0, 'ghost_duration_min_ms must not be negative');
87
+ assert.greaterThanOrEqual(ghost_duration_max_ms, ghost_duration_min_ms, 'ghost_duration_max_ms must not be under ghost_duration_min_ms');
82
88
 
83
89
  if (cells !== null) {
84
90
  assert.isNonNegativeInteger(cells.count, 'cells.count');
@@ -122,7 +128,13 @@ export class SegmentedResourceBarView extends View {
122
128
  * @type {number}
123
129
  * @private
124
130
  */
125
- this.__ghost_duration_ms = ghost_duration_ms;
131
+ this.__ghost_duration_min_ms = ghost_duration_min_ms;
132
+
133
+ /**
134
+ * @type {number}
135
+ * @private
136
+ */
137
+ this.__ghost_duration_max_ms = ghost_duration_max_ms;
126
138
 
127
139
  /**
128
140
  * Total fill as a fraction of max, as last drawn
@@ -531,7 +543,10 @@ export class SegmentedResourceBarView extends View {
531
543
 
532
544
  const ghost = this.__ghost;
533
545
 
534
- ghost.duration_ms = this.__reduced_motion !== null && this.__reduced_motion.matches ? 0 : this.__ghost_duration_ms;
546
+ const reduced_motion = this.__reduced_motion !== null && this.__reduced_motion.matches;
547
+
548
+ ghost.duration_min_ms = reduced_motion ? 0 : this.__ghost_duration_min_ms;
549
+ ghost.duration_max_ms = reduced_motion ? 0 : this.__ghost_duration_max_ms;
535
550
  ghost.set(total_fraction, now);
536
551
 
537
552
  this.__draw(now);