@fusion.dev/helix 0.58.1 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -193,9 +193,7 @@ export declare class HelixAura extends HelixElement {
193
193
  private release;
194
194
  /** What every context is asked for, here rather than at two call sites. */
195
195
  private static readonly CONTEXT;
196
- private frame;
197
196
  private elapsed;
198
- private last;
199
197
  private width;
200
198
  private height;
201
199
  /**
@@ -233,13 +231,16 @@ export declare class HelixAura extends HelixElement {
233
231
  */
234
232
  private readonly target;
235
233
  private readonly eased;
236
- private motion;
237
- private visible;
238
234
  /**
239
- * What the intersection observer last said, kept because it says so only on
240
- * a change: every other path to `play` has to ask it.
235
+ * Whether anybody is looking, and the loop that draws for them.
236
+ *
237
+ * **Four reasons to refuse a frame that the gate cannot know**: a context it
238
+ * was never given, one the browser has taken, a drawing buffer no measurement
239
+ * has sized, and an opening frame still owed. The first two draw nothing at
240
+ * all and the last two draw at the wrong scale, which `measure` and `opened`
241
+ * each have their own note about.
241
242
  */
242
- private onScreen;
243
+ private readonly gate;
243
244
  /**
244
245
  * The element's box, cached.
245
246
  *
@@ -251,8 +252,6 @@ export declare class HelixAura extends HelixElement {
251
252
  * from the handler that reads it.
252
253
  */
253
254
  private box;
254
- /** Watches the element's own size, so no frame has to ask for it. */
255
- private sized;
256
255
  /**
257
256
  * Whether the field has drawn its opening frame yet.
258
257
  *
@@ -397,9 +396,9 @@ export declare class HelixAura extends HelixElement {
397
396
  *
398
397
  * **Taken on a GPU reset, and past the cap** of about sixteen, where the
399
398
  * browser drops the oldest context for the newest while its aura is still on
400
- * screen. `tick` books its own next frame, so the loop went on at 30 frames a
401
- * second against a context that draws nothing, and nothing made the program
402
- * again once the context came back: the aura never recovered.
399
+ * screen. Unstopped, the loop runs at 30 frames a second against a context
400
+ * that draws nothing, and nothing makes the program again once the context
401
+ * comes back: the aura never recovers.
403
402
  *
404
403
  * **The canvas is hidden until then, because a lost one is not reliably
405
404
  * transparent.** Measured 15 Sep 2026: headless Chromium painted a plain
@@ -436,15 +435,27 @@ export declare class HelixAura extends HelixElement {
436
435
  * Re-reads the box and resizes the drawing buffer to match.
437
436
  *
438
437
  * **Called from the observer, never from the frame.** `clientWidth` is a
439
- * layout read, and doing it in `tick` meant a forced synchronous layout on
438
+ * layout read, and doing it per frame meant a forced synchronous layout on
440
439
  * every single frame to learn a number that changes when the box changes and
441
- * at no other time. A `ResizeObserver` is told when that is.
440
+ * at no other time. The gate's `ResizeObserver` is told when that is.
442
441
  *
443
442
  * Returns whether the drawing buffer was resized, which is the same question
444
443
  * as whether it was cleared, and so whether the caller owes it a frame.
445
444
  */
446
445
  private measure;
447
446
  private paint;
447
+ /**
448
+ * The box changed, which is the one moment the buffer is re-measured.
449
+ *
450
+ * **A resize repaints unconditionally rather than asking.** `measure` clears
451
+ * the drawing buffer whenever it resizes it, because assigning `canvas.width`
452
+ * is a reset and not a hint. `paintOnce` declines while the loop holds a
453
+ * frame, which is right for a property change the loop is about to draw
454
+ * anyway and wrong here: the canvas is already transparent by the time it
455
+ * declines, so the fallback gradient underneath showed through until the next
456
+ * frame landed. One frame, on every reflow that changed the box.
457
+ */
458
+ private resized;
448
459
  /**
449
460
  * The opening frame, one animation frame after the box last changed.
450
461
  *
@@ -456,12 +467,13 @@ export declare class HelixAura extends HelixElement {
456
467
  /** One frame, now, for a change that has to show while the loop is stopped. */
457
468
  private paintOnce;
458
469
  /**
459
- * The shortest gap between two painted frames, for a field nobody points at.
470
+ * The shortest gap between two painted frames, in seconds, for a field nobody
471
+ * points at.
460
472
  *
461
473
  * **A drifting field does not need the display's rate and a pointer does.**
462
- * `tick` advances on `(now - this.last) / 1000`, so motion is wall-clock: at
463
- * half the frames the field moves the same distance in the same time, sampled
464
- * more coarsely. `delta` is already clamped at 0.05, which is a floor of 20
474
+ * The field advances on the gate's own step, so motion is wall-clock: at half
475
+ * the frames it moves the same distance in the same time, sampled more
476
+ * coarsely. That step is clamped at `MAX_FRAME_STEP`, which is a floor of 20
465
477
  * frames a second, and 30 sits inside that with room. A cap at 15 would land
466
478
  * on the clamp and the field would genuinely drag, which is the failure that
467
479
  * would be misdiagnosed as choppiness.
@@ -480,23 +492,16 @@ export declare class HelixAura extends HelixElement {
480
492
  * callback. Capping the two that nobody points at is most of that back.
481
493
  */
482
494
  private static readonly CAPPED;
483
- private readonly tick;
484
495
  /**
485
- * Starts the loop, where it is wanted and can be ended.
486
- *
487
- * **Never while detached.** `tick` books its own next frame, so the only
488
- * thing that ends the loop is `stop`, and a disconnection is the last time
489
- * `stop` is called. Everything that plays is canceled or disconnected there,
490
- * and this holds for whatever callback was already on its way.
496
+ * What the skipped frames were worth, in seconds, spent on the next one drawn.
491
497
  *
492
- * **Nor off screen, whoever asks.** The observer is the one caller that
493
- * knows, and a tab coming back, a resize and a restored context all play
494
- * too. An aura 2600 pixels below the fold booked no frame at rest and one on
495
- * every frame after its tab came back, until it had been scrolled in and out
496
- * again. Nor on a lost context, which draws nothing.
498
+ * Dropping it instead would make the field move at a fraction of wall-clock
499
+ * speed, which is the thing the cap is written not to do: a capped frame is
500
+ * the same motion sampled more coarsely and not less motion. Held rather than
501
+ * measured from a stamp, because the gate owns the clock.
497
502
  */
498
- private play;
499
- private stop;
503
+ private held;
504
+ private advance;
500
505
  /**
501
506
  * The angle actually drawn, which is `terminus` resolved against the box.
502
507
  *
@@ -535,23 +540,6 @@ export declare class HelixAura extends HelixElement {
535
540
  */
536
541
  private advanceWake;
537
542
  private readonly handleLeave;
538
- /**
539
- * Answers the preference changing, rather than only its value at setup.
540
- *
541
- * **The query was created and never listened to**, so the preference was read
542
- * at `play` and at the pointer and nowhere else. `tick` books its own next
543
- * frame with no check of its own, which means a reader who turned reduce-motion
544
- * on while the page was open kept a full-page shader running for the rest of
545
- * it: the one reader the promise at the top of this file is about, and the one
546
- * it was broken for. The other direction failed the quieter way, a field frozen
547
- * on its opening frame until a scroll or a tab switch happened to ask again.
548
- *
549
- * The still frame is drawn rather than assumed, because the canvas holds
550
- * whatever was on it when the loop stopped and mid-drift is not the opening
551
- * shape. `Orb` does the same, on the same two arms.
552
- */
553
- private readonly handleMotion;
554
- private readonly handleVisibility;
555
543
  private readonly handleScroll;
556
544
  }
557
545
  declare global {
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixAura_1;
8
- import { customElement, HelixElement, TokenColorController, } from '@fusion.dev/helix-core';
8
+ import { customElement, HelixElement, MAX_FRAME_STEP, PaintGateController, TokenColorController, } from '@fusion.dev/helix-core';
9
9
  import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { tiltFor } from '../axis.js';
@@ -42,9 +42,7 @@ let HelixAura = class HelixAura extends HelixElement {
42
42
  this.program = null;
43
43
  this.uniforms = {};
44
44
  this.release = 0;
45
- this.frame = 0;
46
45
  this.elapsed = 0;
47
- this.last = 0;
48
46
  this.width = 0;
49
47
  this.height = 0;
50
48
  this.trail = new Float32Array(8 * 3).fill(1);
@@ -53,7 +51,20 @@ let HelixAura = class HelixAura extends HelixElement {
53
51
  this.head = { x: 1, y: 1 };
54
52
  this.target = { presence: 0, x: 0, y: 0 };
55
53
  this.eased = { presence: 0, x: 0, y: 0 };
56
- this.onScreen = false;
54
+ this.gate = new PaintGateController(this, {
55
+ canRun: () => !!this.gl && !this.gl.isContextLost() && this.width > 0 && this.opened,
56
+ onFrame: (delta) => {
57
+ this.advance(delta);
58
+ },
59
+ onMotion: (reduced) => {
60
+ if (reduced) {
61
+ this.paintOnce();
62
+ }
63
+ },
64
+ onResize: () => {
65
+ this.resized();
66
+ },
67
+ });
57
68
  this.box = { height: 1, left: 0, top: 0, width: 1 };
58
69
  this.opened = false;
59
70
  this.opening = 0;
@@ -69,7 +80,7 @@ let HelixAura = class HelixAura extends HelixElement {
69
80
  };
70
81
  this.handleLost = (event) => {
71
82
  event.preventDefault();
72
- this.stop();
83
+ this.gate.stop();
73
84
  if (this.opening) {
74
85
  cancelAnimationFrame(this.opening);
75
86
  this.opening = 0;
@@ -86,40 +97,15 @@ let HelixAura = class HelixAura extends HelixElement {
86
97
  this.measure();
87
98
  if (this.opened) {
88
99
  this.paint();
89
- this.play();
100
+ this.gate.play();
90
101
  }
91
102
  else if (this.width) {
92
103
  this.open();
93
104
  }
94
105
  };
95
- this.tick = (now) => {
96
- if (!this.last) {
97
- this.last = now;
98
- }
99
- if (!this.interactive && now - this.last < HelixAura_1.CAPPED) {
100
- this.frame = requestAnimationFrame(this.tick);
101
- return;
102
- }
103
- const delta = Math.min((now - this.last) / 1000, 0.05);
104
- this.last = now;
105
- this.elapsed += delta * Math.max(0, this.drifted(this.speed, 0.4, 0.043));
106
- const drift = Math.hypot(this.target.x - this.eased.x, this.target.y - this.eased.y);
107
- const follow = 1 - Math.exp(-delta / (0.028 / (1 + drift * 2.4)));
108
- if (!this.interactive) {
109
- this.target.presence = 0;
110
- }
111
- this.eased.x += (this.target.x - this.eased.x) * follow;
112
- this.eased.y += (this.target.y - this.eased.y) * follow;
113
- const rising = this.target.presence > this.eased.presence;
114
- const fade = 1 - Math.exp(-delta / (rising ? 0.05 : 0.22));
115
- this.eased.presence += (this.target.presence - this.eased.presence) * fade;
116
- this.age(delta);
117
- this.advanceWake();
118
- this.paint();
119
- this.frame = requestAnimationFrame(this.tick);
120
- };
106
+ this.held = 0;
121
107
  this.handlePointer = (event) => {
122
- if (this.motion?.matches) {
108
+ if (this.gate.reduced) {
123
109
  return;
124
110
  }
125
111
  const { height, left, top, width } = this.box;
@@ -131,23 +117,6 @@ let HelixAura = class HelixAura extends HelixElement {
131
117
  this.handleLeave = () => {
132
118
  this.target.presence = 0;
133
119
  };
134
- this.handleMotion = () => {
135
- if (this.motion?.matches) {
136
- this.stop();
137
- this.paintOnce();
138
- }
139
- else {
140
- this.play();
141
- }
142
- };
143
- this.handleVisibility = () => {
144
- if (document.hidden) {
145
- this.stop();
146
- }
147
- else {
148
- this.play();
149
- }
150
- };
151
120
  this.handleScroll = () => {
152
121
  const rect = this.getBoundingClientRect();
153
122
  this.box = { ...this.box, left: rect.left, top: rect.top };
@@ -185,35 +154,10 @@ let HelixAura = class HelixAura extends HelixElement {
185
154
  }
186
155
  this.canvas?.addEventListener('webglcontextlost', this.handleLost);
187
156
  this.canvas?.addEventListener('webglcontextrestored', this.handleRestored);
188
- this.motion ??= window.matchMedia('(prefers-reduced-motion: reduce)');
189
- this.motion.addEventListener('change', this.handleMotion);
190
157
  this.addEventListener('pointermove', this.handlePointer);
191
158
  this.addEventListener('pointerleave', this.handleLeave);
192
- this.visible = new IntersectionObserver(([entry]) => {
193
- this.onScreen = entry?.isIntersecting ?? false;
194
- if (this.onScreen) {
195
- this.play();
196
- }
197
- else {
198
- this.stop();
199
- }
200
- });
201
- this.visible.observe(this);
202
- document.addEventListener('visibilitychange', this.handleVisibility);
159
+ this.gate.watch();
203
160
  window.addEventListener('scroll', this.handleScroll, { capture: true, passive: true });
204
- this.sized = new ResizeObserver(() => {
205
- if (!this.measure()) {
206
- this.paintOnce();
207
- return;
208
- }
209
- if (this.opened) {
210
- this.paint();
211
- this.play();
212
- return;
213
- }
214
- this.open();
215
- });
216
- this.sized.observe(this);
217
161
  this.readPalette();
218
162
  if (this.measure() || (this.width && !this.opened)) {
219
163
  this.open();
@@ -307,25 +251,16 @@ let HelixAura = class HelixAura extends HelixElement {
307
251
  }
308
252
  disconnectedCallback() {
309
253
  super.disconnectedCallback();
310
- this.stop();
311
254
  if (this.opening) {
312
255
  cancelAnimationFrame(this.opening);
313
256
  this.opening = 0;
314
257
  this.openBy = 0;
315
258
  }
316
- this.visible?.disconnect();
317
- this.visible = undefined;
318
- this.onScreen = false;
319
259
  this.canvas?.removeEventListener('webglcontextlost', this.handleLost);
320
260
  this.canvas?.removeEventListener('webglcontextrestored', this.handleRestored);
321
261
  this.removeEventListener('pointermove', this.handlePointer);
322
262
  this.removeEventListener('pointerleave', this.handleLeave);
323
- this.motion?.removeEventListener('change', this.handleMotion);
324
- this.motion = undefined;
325
- document.removeEventListener('visibilitychange', this.handleVisibility);
326
263
  window.removeEventListener('scroll', this.handleScroll, { capture: true });
327
- this.sized?.disconnect();
328
- this.sized = undefined;
329
264
  this.release = window.setTimeout(this.drop, 0);
330
265
  }
331
266
  readPalette() {
@@ -443,6 +378,18 @@ let HelixAura = class HelixAura extends HelixElement {
443
378
  gl.uniform1i(uniforms.uInteraction ?? null, Math.max(0, INTERACTIONS.indexOf(this.interaction ?? 'native')));
444
379
  gl.drawArrays(gl.TRIANGLES, 0, 3);
445
380
  }
381
+ resized() {
382
+ if (!this.measure()) {
383
+ this.paintOnce();
384
+ return;
385
+ }
386
+ if (this.opened) {
387
+ this.paint();
388
+ this.gate.play();
389
+ return;
390
+ }
391
+ this.open();
392
+ }
446
393
  open() {
447
394
  if (this.opening) {
448
395
  cancelAnimationFrame(this.opening);
@@ -455,7 +402,7 @@ let HelixAura = class HelixAura extends HelixElement {
455
402
  this.opening = 0;
456
403
  this.opened = true;
457
404
  this.paint();
458
- this.play();
405
+ this.gate.play();
459
406
  };
460
407
  if (performance.now() >= this.openBy) {
461
408
  draw();
@@ -464,32 +411,33 @@ let HelixAura = class HelixAura extends HelixElement {
464
411
  this.opening = requestAnimationFrame(draw);
465
412
  }
466
413
  paintOnce() {
467
- if (this.frame || !this.opened || !this.isConnected) {
414
+ if (this.gate.running || !this.opened || !this.isConnected) {
468
415
  return;
469
416
  }
470
417
  this.paint();
471
418
  }
472
- static { this.CAPPED = 1000 / 30; }
473
- play() {
474
- if (this.frame ||
475
- !this.isConnected ||
476
- !this.onScreen ||
477
- this.motion?.matches ||
478
- document.hidden ||
479
- !this.gl ||
480
- this.gl.isContextLost() ||
481
- !this.width ||
482
- !this.opened) {
419
+ static { this.CAPPED = 1 / 30; }
420
+ advance(delta) {
421
+ this.held += delta;
422
+ if (!this.interactive && this.held < HelixAura_1.CAPPED) {
483
423
  return;
484
424
  }
485
- this.frame = requestAnimationFrame(this.tick);
486
- }
487
- stop() {
488
- if (this.frame) {
489
- cancelAnimationFrame(this.frame);
490
- this.frame = 0;
425
+ const step = Math.min(this.held, MAX_FRAME_STEP);
426
+ this.held = 0;
427
+ this.elapsed += step * Math.max(0, this.drifted(this.speed, 0.4, 0.043));
428
+ const drift = Math.hypot(this.target.x - this.eased.x, this.target.y - this.eased.y);
429
+ const follow = 1 - Math.exp(-step / (0.028 / (1 + drift * 2.4)));
430
+ if (!this.interactive) {
431
+ this.target.presence = 0;
491
432
  }
492
- this.last = 0;
433
+ this.eased.x += (this.target.x - this.eased.x) * follow;
434
+ this.eased.y += (this.target.y - this.eased.y) * follow;
435
+ const rising = this.target.presence > this.eased.presence;
436
+ const fade = 1 - Math.exp(-step / (rising ? 0.05 : 0.22));
437
+ this.eased.presence += (this.target.presence - this.eased.presence) * fade;
438
+ this.age(step);
439
+ this.advanceWake();
440
+ this.paint();
493
441
  }
494
442
  get bearing() {
495
443
  if (this.terminus === undefined) {
@@ -93,12 +93,16 @@ export declare class HelixOrb extends HelixElement {
93
93
  state: OrbState;
94
94
  private canvas;
95
95
  private context;
96
- private motion;
97
- private visible;
98
- private sized;
99
- private frame;
100
- private running;
101
- private onScreen;
96
+ /**
97
+ * Whether anybody is looking, and the loop that draws for them.
98
+ *
99
+ * A context it has not been given and one the browser has taken are both
100
+ * reasons to draw nothing that the gate cannot see, so they are said here.
101
+ *
102
+ * Every frame paints: a settled orb has movement of its own in every state,
103
+ * and only the gate's own three answers stop it.
104
+ */
105
+ private readonly gate;
102
106
  /** Whether the browser has taken the canvas, as its own two events say. */
103
107
  private lost;
104
108
  /** The box the canvas was last measured at, in CSS pixels. */
@@ -127,7 +131,6 @@ export declare class HelixOrb extends HelixElement {
127
131
  * rate has no such term.
128
132
  */
129
133
  private phase;
130
- private last;
131
134
  protected willUpdate(changed: PropertyValues): void;
132
135
  protected render(): TemplateResult;
133
136
  protected firstUpdated(changed: PropertyValues): void;
@@ -155,10 +158,6 @@ export declare class HelixOrb extends HelixElement {
155
158
  /** How far through a transition, `1` when there is not one. */
156
159
  private get progress();
157
160
  private paramsNow;
158
- private play;
159
- private stop;
160
- private readonly tick;
161
- private readonly handleVisibility;
162
161
  /**
163
162
  * Stops for a canvas the browser has taken, on a GPU reset, until it is
164
163
  * given back.
@@ -180,7 +179,6 @@ export declare class HelixOrb extends HelixElement {
180
179
  * the top left corner, for as long as the box holds still.
181
180
  */
182
181
  private readonly handleRestored;
183
- private readonly handleMotion;
184
182
  /**
185
183
  * One frame.
186
184
  *
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, hostAria, readColor, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, PaintGateController, readColor, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { blendStaged, evenSphere, fibonacci, meshFor, ORB_STATES, orbPoint, paramsFor, project, smoother, stateDistance, TRAIL, } from '../shapes.js';
@@ -29,37 +29,32 @@ let HelixOrb = class HelixOrb extends HelixElement {
29
29
  this.label = '';
30
30
  this.state = 'thinking';
31
31
  this.context = null;
32
- this.frame = 0;
33
- this.running = false;
34
- this.onScreen = false;
32
+ this.gate = new PaintGateController(this, {
33
+ canRun: () => !!this.context && !this.lost,
34
+ onFrame: (delta) => {
35
+ this.paint(delta);
36
+ },
37
+ onMotion: (reduced) => {
38
+ if (reduced) {
39
+ this.phase = STILL_PHASE;
40
+ this.morphAt = 0;
41
+ this.paint(0);
42
+ }
43
+ },
44
+ onResize: () => {
45
+ this.measure();
46
+ this.paint(0);
47
+ },
48
+ });
35
49
  this.lost = false;
36
50
  this.box = 0;
37
51
  this.from = paramsFor('thinking');
38
52
  this.morphAt = 0;
39
53
  this.reach = 0;
40
54
  this.phase = STILL_PHASE;
41
- this.last = 0;
42
- this.tick = () => {
43
- if (!this.running) {
44
- return;
45
- }
46
- const now = performance.now();
47
- const dt = Math.min(0.05, (now - this.last) / 1000 || 0.016);
48
- this.last = now;
49
- this.paint(dt);
50
- this.frame = requestAnimationFrame(this.tick);
51
- };
52
- this.handleVisibility = () => {
53
- if (document.visibilityState === 'hidden') {
54
- this.stop();
55
- }
56
- else {
57
- this.play();
58
- }
59
- };
60
55
  this.handleLost = () => {
61
56
  this.lost = true;
62
- this.stop();
57
+ this.gate.stop();
63
58
  this.canvas?.toggleAttribute('hidden', true);
64
59
  };
65
60
  this.handleRestored = () => {
@@ -68,18 +63,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
68
63
  this.box = 0;
69
64
  this.measure();
70
65
  this.paint(0);
71
- this.play();
72
- };
73
- this.handleMotion = () => {
74
- if (this.motion?.matches) {
75
- this.stop();
76
- this.phase = STILL_PHASE;
77
- this.morphAt = 0;
78
- this.paint(0);
79
- }
80
- else {
81
- this.play();
82
- }
66
+ this.gate.play();
83
67
  };
84
68
  }
85
69
  static { this.styles = styles; }
@@ -112,18 +96,9 @@ let HelixOrb = class HelixOrb extends HelixElement {
112
96
  }
113
97
  disconnectedCallback() {
114
98
  super.disconnectedCallback();
115
- this.stop();
116
- this.onScreen = false;
117
99
  this.box = 0;
118
100
  this.canvas?.removeEventListener('contextlost', this.handleLost);
119
101
  this.canvas?.removeEventListener('contextrestored', this.handleRestored);
120
- this.visible?.disconnect();
121
- this.visible = undefined;
122
- this.sized?.disconnect();
123
- this.sized = undefined;
124
- this.motion?.removeEventListener('change', this.handleMotion);
125
- this.motion = undefined;
126
- document.removeEventListener('visibilitychange', this.handleVisibility);
127
102
  }
128
103
  start() {
129
104
  if (!this.isConnected) {
@@ -144,24 +119,7 @@ let HelixOrb = class HelixOrb extends HelixElement {
144
119
  this.lost = false;
145
120
  canvas.toggleAttribute('hidden', false);
146
121
  }
147
- this.motion ??= window.matchMedia('(prefers-reduced-motion: reduce)');
148
- this.motion.addEventListener('change', this.handleMotion);
149
- this.visible ??= new IntersectionObserver(([entry]) => {
150
- this.onScreen = entry?.isIntersecting ?? false;
151
- if (this.onScreen && document.visibilityState !== 'hidden') {
152
- this.play();
153
- }
154
- else {
155
- this.stop();
156
- }
157
- });
158
- this.visible.observe(this);
159
- document.addEventListener('visibilitychange', this.handleVisibility);
160
- this.sized ??= new ResizeObserver(() => {
161
- this.measure();
162
- this.paint(0);
163
- });
164
- this.sized.observe(this);
122
+ this.gate.watch();
165
123
  this.measure();
166
124
  this.paint(0);
167
125
  }
@@ -185,9 +143,9 @@ let HelixOrb = class HelixOrb extends HelixElement {
185
143
  this.from = this.paramsNow();
186
144
  this.reach = stateDistance(this.from, paramsFor(this.state));
187
145
  this.morphAt = performance.now();
188
- if (!this.running) {
146
+ if (!this.gate.running) {
189
147
  this.paint(0);
190
- this.play();
148
+ this.gate.play();
191
149
  }
192
150
  }
193
151
  get span() {
@@ -204,25 +162,13 @@ let HelixOrb = class HelixOrb extends HelixElement {
204
162
  }
205
163
  return blendStaged(this.from, to, smoother(k), smoother(Math.max(0, k - TRAIL) / (1 - TRAIL)));
206
164
  }
207
- play() {
208
- if (this.running || this.motion?.matches || !this.onScreen || this.lost) {
209
- return;
210
- }
211
- this.running = true;
212
- this.last = performance.now();
213
- this.frame = requestAnimationFrame(this.tick);
214
- }
215
- stop() {
216
- this.running = false;
217
- cancelAnimationFrame(this.frame);
218
- }
219
165
  paint(dt) {
220
166
  const ctx = this.context;
221
167
  const size = this.box;
222
168
  if (!ctx || size <= 0) {
223
169
  return;
224
170
  }
225
- const still = this.motion?.matches ?? false;
171
+ const still = this.gate.reduced;
226
172
  const params = still ? paramsFor(this.state) : this.paramsNow();
227
173
  this.phase += dt * params.rate;
228
174
  const painted = getComputedStyle(ctx.canvas);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix",
3
3
  "description": "Styled component tier. Extends the headless tier with token-driven presentation.",
4
- "version": "0.58.1",
4
+ "version": "0.59.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -47,14 +47,14 @@
47
47
  },
48
48
  "bugs": "https://github.com/project-fusion/fusion/issues",
49
49
  "dependencies": {
50
- "@fusion.dev/helix-core": "0.58.1",
51
- "@fusion.dev/helix-headless": "0.58.1",
52
- "@fusion.dev/helix-styles": "0.58.1"
50
+ "@fusion.dev/helix-core": "0.59.0",
51
+ "@fusion.dev/helix-headless": "0.59.0",
52
+ "@fusion.dev/helix-styles": "0.59.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@custom-elements-manifest/analyzer": "^0.11.0",
56
- "@fusion.dev/helix-cem": "0.58.1",
57
- "@fusion.dev/helix-testing": "0.58.1",
56
+ "@fusion.dev/helix-cem": "0.59.0",
57
+ "@fusion.dev/helix-testing": "0.59.0",
58
58
  "@fusion.dev/scripts": "0.0.0-dev",
59
59
  "@happy-dom/global-registrator": "^20.0.0",
60
60
  "@lit-labs/ssr": "^4.1.0",