@lovo/matter 0.3.0 → 0.4.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.
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { WebGPURenderer, Node } from 'three/webgpu';
2
1
  import { Color } from 'three';
2
+ import { WebGPURenderer, Node } from 'three/webgpu';
3
3
  import { ShaderNodeObject } from 'three/tsl';
4
4
 
5
- type MatterBackend = 'webgpu' | 'webgl2';
5
+ type GpuBackend = 'webgpu' | 'webgl2';
6
6
  interface CreateRendererOptions {
7
7
  /** Anti-alias the framebuffer. Default: true. */
8
8
  antialias?: boolean;
@@ -15,11 +15,11 @@ interface CreateRendererOptions {
15
15
  /** Cap on devicePixelRatio. Default: 2. Pass Infinity to disable. */
16
16
  maxDPR?: number;
17
17
  }
18
- interface MatterRenderer {
18
+ interface GpuRenderer {
19
19
  /** The underlying Three.js WebGPURenderer (which may be running on a WebGL2 backend). */
20
20
  three: WebGPURenderer;
21
21
  /** Which backend the renderer initialized with. */
22
- backend: MatterBackend;
22
+ backend: GpuBackend;
23
23
  /** Tear down the renderer and release GPU resources. */
24
24
  dispose: () => void;
25
25
  /** Resize the renderer to the canvas's current client dimensions. */
@@ -32,57 +32,7 @@ interface MatterRenderer {
32
32
  * unavailable on the host. The returned object exposes the underlying
33
33
  * three renderer plus a small wrapper for resize and disposal.
34
34
  */
35
- declare function createRenderer(canvas: HTMLCanvasElement, opts?: CreateRendererOptions): Promise<MatterRenderer>;
36
-
37
- interface SchedulerTick {
38
- /** Seconds since the previous tick. 0 on the first call. */
39
- delta: number;
40
- /** Total seconds since the scheduler started its current run. */
41
- elapsed: number;
42
- /** The raw `performance.now()` timestamp the rAF callback received. */
43
- now: number;
44
- }
45
- type SchedulerClient = (tick: SchedulerTick) => void;
46
- /**
47
- * Batches `requestAnimationFrame` calls across all clients registered with
48
- * a single scheduler. One scheduler is created per <MatterScene>; clients
49
- * are typically a Three.js renderer's render call.
50
- */
51
- declare class MatterScheduler {
52
- private readonly clients;
53
- private rafId;
54
- private running;
55
- private paused;
56
- private idle;
57
- private flushPending;
58
- private startedAt;
59
- private lastTickAt;
60
- /** Activate the scheduler. The rAF loop starts on the first client added. */
61
- start(): void;
62
- /** Halt the rAF loop entirely. Use dispose() for permanent teardown. */
63
- stop(): void;
64
- /** Temporarily skip ticks without losing client registrations. */
65
- pause(): void;
66
- /** Resume after pause(). */
67
- resume(): void;
68
- /** Register a client to be called every frame. */
69
- add(client: SchedulerClient): void;
70
- /** Unregister a client. */
71
- remove(client: SchedulerClient): void;
72
- /** Permanent teardown: stop the loop and drop all clients. */
73
- dispose(): void;
74
- /**
75
- * Mark the scheduler idle. The next tick still fires (a final flush so
76
- * uniform changes that triggered the idle state are rendered), then the
77
- * rAF loop halts. Use `requestRender()` or `setIdle(false)` to wake.
78
- */
79
- setIdle(idle: boolean): void;
80
- /** Force a single tick while idle. Useful for prop-change invalidation. */
81
- requestRender(): void;
82
- private maybeQueue;
83
- private cancel;
84
- private readonly frame;
85
- }
35
+ declare function createRenderer(canvas: HTMLCanvasElement, opts?: CreateRendererOptions): Promise<GpuRenderer>;
86
36
 
87
37
  type Vec2 = readonly [number, number];
88
38
  interface CursorInputOptions {
@@ -122,7 +72,7 @@ interface CursorInputOptions {
122
72
  type ChangeListener = (value: Vec2) => void;
123
73
  /**
124
74
  * Smoothed pointer tracker emitting a normalized (0..1) Vec2 position.
125
- * Implements the MatterSignal protocol (`get()` + `on('change', cb)`)
75
+ * Implements the AnimatableSignal protocol (`get()` + `on('change', cb)`)
126
76
  * so it composes with Motion's `useTransform` and similar tools.
127
77
  */
128
78
  declare class CursorInput {
@@ -136,7 +86,7 @@ declare class CursorInput {
136
86
  private readonly handleMouseMove;
137
87
  private disposed;
138
88
  constructor(opts?: CursorInputOptions);
139
- /** Current smoothed position. Implements MatterSignal protocol. */
89
+ /** Current smoothed position. Implements AnimatableSignal protocol. */
140
90
  get(): Vec2;
141
91
  /** Subscribe to change events. Returns an unsubscribe function. */
142
92
  on(_event: 'change', cb: ChangeListener): () => void;
@@ -407,4 +357,54 @@ interface IntersectionWatcher {
407
357
  */
408
358
  declare function createIntersectionWatcher(canvas: HTMLCanvasElement): IntersectionWatcher;
409
359
 
410
- export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, type IntersectionWatcher, type MatterBackend, type MatterRenderer, MatterScheduler, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, filmGrain, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
360
+ interface SchedulerTick {
361
+ /** Seconds since the previous tick. 0 on the first call. */
362
+ delta: number;
363
+ /** Total seconds since the scheduler started its current run. */
364
+ elapsed: number;
365
+ /** The raw `performance.now()` timestamp the rAF callback received. */
366
+ now: number;
367
+ }
368
+ type SchedulerClient = (tick: SchedulerTick) => void;
369
+ /**
370
+ * Batches `requestAnimationFrame` calls across all clients registered with
371
+ * a single scheduler. One scheduler is created per <ShaderScene>; clients
372
+ * are typically a Three.js renderer's render call.
373
+ */
374
+ declare class FrameScheduler {
375
+ private readonly clients;
376
+ private rafId;
377
+ private running;
378
+ private paused;
379
+ private idle;
380
+ private flushPending;
381
+ private startedAt;
382
+ private lastTickAt;
383
+ /** Activate the scheduler. The rAF loop starts on the first client added. */
384
+ start(): void;
385
+ /** Halt the rAF loop entirely. Use dispose() for permanent teardown. */
386
+ stop(): void;
387
+ /** Temporarily skip ticks without losing client registrations. */
388
+ pause(): void;
389
+ /** Resume after pause(). */
390
+ resume(): void;
391
+ /** Register a client to be called every frame. */
392
+ add(client: SchedulerClient): void;
393
+ /** Unregister a client. */
394
+ remove(client: SchedulerClient): void;
395
+ /** Permanent teardown: stop the loop and drop all clients. */
396
+ dispose(): void;
397
+ /**
398
+ * Mark the scheduler idle. The next tick still fires (a final flush so
399
+ * uniform changes that triggered the idle state are rendered), then the
400
+ * rAF loop halts. Use `requestRender()` or `setIdle(false)` to wake.
401
+ */
402
+ setIdle(idle: boolean): void;
403
+ /** Force a single tick while idle. Useful for prop-change invalidation. */
404
+ requestRender(): void;
405
+ private maybeQueue;
406
+ private cancel;
407
+ private readonly frame;
408
+ }
409
+
410
+ export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, FrameScheduler, type GpuBackend, type GpuRenderer, type IntersectionWatcher, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, filmGrain, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { WebGPURenderer, Node } from 'three/webgpu';
2
1
  import { Color } from 'three';
2
+ import { WebGPURenderer, Node } from 'three/webgpu';
3
3
  import { ShaderNodeObject } from 'three/tsl';
4
4
 
5
- type MatterBackend = 'webgpu' | 'webgl2';
5
+ type GpuBackend = 'webgpu' | 'webgl2';
6
6
  interface CreateRendererOptions {
7
7
  /** Anti-alias the framebuffer. Default: true. */
8
8
  antialias?: boolean;
@@ -15,11 +15,11 @@ interface CreateRendererOptions {
15
15
  /** Cap on devicePixelRatio. Default: 2. Pass Infinity to disable. */
16
16
  maxDPR?: number;
17
17
  }
18
- interface MatterRenderer {
18
+ interface GpuRenderer {
19
19
  /** The underlying Three.js WebGPURenderer (which may be running on a WebGL2 backend). */
20
20
  three: WebGPURenderer;
21
21
  /** Which backend the renderer initialized with. */
22
- backend: MatterBackend;
22
+ backend: GpuBackend;
23
23
  /** Tear down the renderer and release GPU resources. */
24
24
  dispose: () => void;
25
25
  /** Resize the renderer to the canvas's current client dimensions. */
@@ -32,57 +32,7 @@ interface MatterRenderer {
32
32
  * unavailable on the host. The returned object exposes the underlying
33
33
  * three renderer plus a small wrapper for resize and disposal.
34
34
  */
35
- declare function createRenderer(canvas: HTMLCanvasElement, opts?: CreateRendererOptions): Promise<MatterRenderer>;
36
-
37
- interface SchedulerTick {
38
- /** Seconds since the previous tick. 0 on the first call. */
39
- delta: number;
40
- /** Total seconds since the scheduler started its current run. */
41
- elapsed: number;
42
- /** The raw `performance.now()` timestamp the rAF callback received. */
43
- now: number;
44
- }
45
- type SchedulerClient = (tick: SchedulerTick) => void;
46
- /**
47
- * Batches `requestAnimationFrame` calls across all clients registered with
48
- * a single scheduler. One scheduler is created per <MatterScene>; clients
49
- * are typically a Three.js renderer's render call.
50
- */
51
- declare class MatterScheduler {
52
- private readonly clients;
53
- private rafId;
54
- private running;
55
- private paused;
56
- private idle;
57
- private flushPending;
58
- private startedAt;
59
- private lastTickAt;
60
- /** Activate the scheduler. The rAF loop starts on the first client added. */
61
- start(): void;
62
- /** Halt the rAF loop entirely. Use dispose() for permanent teardown. */
63
- stop(): void;
64
- /** Temporarily skip ticks without losing client registrations. */
65
- pause(): void;
66
- /** Resume after pause(). */
67
- resume(): void;
68
- /** Register a client to be called every frame. */
69
- add(client: SchedulerClient): void;
70
- /** Unregister a client. */
71
- remove(client: SchedulerClient): void;
72
- /** Permanent teardown: stop the loop and drop all clients. */
73
- dispose(): void;
74
- /**
75
- * Mark the scheduler idle. The next tick still fires (a final flush so
76
- * uniform changes that triggered the idle state are rendered), then the
77
- * rAF loop halts. Use `requestRender()` or `setIdle(false)` to wake.
78
- */
79
- setIdle(idle: boolean): void;
80
- /** Force a single tick while idle. Useful for prop-change invalidation. */
81
- requestRender(): void;
82
- private maybeQueue;
83
- private cancel;
84
- private readonly frame;
85
- }
35
+ declare function createRenderer(canvas: HTMLCanvasElement, opts?: CreateRendererOptions): Promise<GpuRenderer>;
86
36
 
87
37
  type Vec2 = readonly [number, number];
88
38
  interface CursorInputOptions {
@@ -122,7 +72,7 @@ interface CursorInputOptions {
122
72
  type ChangeListener = (value: Vec2) => void;
123
73
  /**
124
74
  * Smoothed pointer tracker emitting a normalized (0..1) Vec2 position.
125
- * Implements the MatterSignal protocol (`get()` + `on('change', cb)`)
75
+ * Implements the AnimatableSignal protocol (`get()` + `on('change', cb)`)
126
76
  * so it composes with Motion's `useTransform` and similar tools.
127
77
  */
128
78
  declare class CursorInput {
@@ -136,7 +86,7 @@ declare class CursorInput {
136
86
  private readonly handleMouseMove;
137
87
  private disposed;
138
88
  constructor(opts?: CursorInputOptions);
139
- /** Current smoothed position. Implements MatterSignal protocol. */
89
+ /** Current smoothed position. Implements AnimatableSignal protocol. */
140
90
  get(): Vec2;
141
91
  /** Subscribe to change events. Returns an unsubscribe function. */
142
92
  on(_event: 'change', cb: ChangeListener): () => void;
@@ -407,4 +357,54 @@ interface IntersectionWatcher {
407
357
  */
408
358
  declare function createIntersectionWatcher(canvas: HTMLCanvasElement): IntersectionWatcher;
409
359
 
410
- export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, type IntersectionWatcher, type MatterBackend, type MatterRenderer, MatterScheduler, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, filmGrain, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
360
+ interface SchedulerTick {
361
+ /** Seconds since the previous tick. 0 on the first call. */
362
+ delta: number;
363
+ /** Total seconds since the scheduler started its current run. */
364
+ elapsed: number;
365
+ /** The raw `performance.now()` timestamp the rAF callback received. */
366
+ now: number;
367
+ }
368
+ type SchedulerClient = (tick: SchedulerTick) => void;
369
+ /**
370
+ * Batches `requestAnimationFrame` calls across all clients registered with
371
+ * a single scheduler. One scheduler is created per <ShaderScene>; clients
372
+ * are typically a Three.js renderer's render call.
373
+ */
374
+ declare class FrameScheduler {
375
+ private readonly clients;
376
+ private rafId;
377
+ private running;
378
+ private paused;
379
+ private idle;
380
+ private flushPending;
381
+ private startedAt;
382
+ private lastTickAt;
383
+ /** Activate the scheduler. The rAF loop starts on the first client added. */
384
+ start(): void;
385
+ /** Halt the rAF loop entirely. Use dispose() for permanent teardown. */
386
+ stop(): void;
387
+ /** Temporarily skip ticks without losing client registrations. */
388
+ pause(): void;
389
+ /** Resume after pause(). */
390
+ resume(): void;
391
+ /** Register a client to be called every frame. */
392
+ add(client: SchedulerClient): void;
393
+ /** Unregister a client. */
394
+ remove(client: SchedulerClient): void;
395
+ /** Permanent teardown: stop the loop and drop all clients. */
396
+ dispose(): void;
397
+ /**
398
+ * Mark the scheduler idle. The next tick still fires (a final flush so
399
+ * uniform changes that triggered the idle state are rendered), then the
400
+ * rAF loop halts. Use `requestRender()` or `setIdle(false)` to wake.
401
+ */
402
+ setIdle(idle: boolean): void;
403
+ /** Force a single tick while idle. Useful for prop-change invalidation. */
404
+ requestRender(): void;
405
+ private maybeQueue;
406
+ private cancel;
407
+ private readonly frame;
408
+ }
409
+
410
+ export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, FrameScheduler, type GpuBackend, type GpuRenderer, type IntersectionWatcher, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, filmGrain, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/runtime/createRenderer.ts
2
- import { WebGPURenderer } from "three/webgpu";
3
2
  import { Color } from "three";
3
+ import { WebGPURenderer } from "three/webgpu";
4
4
  async function createRenderer(canvas, opts = {}) {
5
5
  const {
6
6
  antialias = true,
@@ -26,7 +26,8 @@ async function createRenderer(canvas, opts = {}) {
26
26
  }
27
27
  };
28
28
  resize();
29
- const backend = forceWebGL || three.backend?.isWebGLBackend ? "webgl2" : "webgpu";
29
+ const isWebGL = "isWebGLBackend" in three.backend && three.backend.isWebGLBackend === true;
30
+ const backend = forceWebGL || isWebGL ? "webgl2" : "webgpu";
30
31
  return {
31
32
  three,
32
33
  backend,
@@ -35,104 +36,6 @@ async function createRenderer(canvas, opts = {}) {
35
36
  };
36
37
  }
37
38
 
38
- // src/runtime/MatterScheduler.ts
39
- var MatterScheduler = class {
40
- clients = /* @__PURE__ */ new Set();
41
- rafId = null;
42
- running = false;
43
- paused = false;
44
- idle = false;
45
- flushPending = false;
46
- startedAt = 0;
47
- lastTickAt = 0;
48
- /** Activate the scheduler. The rAF loop starts on the first client added. */
49
- start() {
50
- this.running = true;
51
- this.paused = false;
52
- this.maybeQueue();
53
- }
54
- /** Halt the rAF loop entirely. Use dispose() for permanent teardown. */
55
- stop() {
56
- this.running = false;
57
- this.cancel();
58
- }
59
- /** Temporarily skip ticks without losing client registrations. */
60
- pause() {
61
- this.paused = true;
62
- }
63
- /** Resume after pause(). */
64
- resume() {
65
- this.paused = false;
66
- if (this.running) this.maybeQueue();
67
- }
68
- /** Register a client to be called every frame. */
69
- add(client) {
70
- this.clients.add(client);
71
- if (this.running) this.maybeQueue();
72
- }
73
- /** Unregister a client. */
74
- remove(client) {
75
- this.clients.delete(client);
76
- }
77
- /** Permanent teardown: stop the loop and drop all clients. */
78
- dispose() {
79
- this.stop();
80
- this.clients.clear();
81
- }
82
- /**
83
- * Mark the scheduler idle. The next tick still fires (a final flush so
84
- * uniform changes that triggered the idle state are rendered), then the
85
- * rAF loop halts. Use `requestRender()` or `setIdle(false)` to wake.
86
- */
87
- setIdle(idle) {
88
- if (this.idle === idle) return;
89
- this.idle = idle;
90
- if (idle) {
91
- this.flushPending = true;
92
- this.maybeQueue();
93
- } else {
94
- this.flushPending = false;
95
- this.maybeQueue();
96
- }
97
- }
98
- /** Force a single tick while idle. Useful for prop-change invalidation. */
99
- requestRender() {
100
- if (!this.idle) return;
101
- this.flushPending = true;
102
- this.maybeQueue();
103
- }
104
- maybeQueue() {
105
- if (this.rafId !== null) return;
106
- if (!this.running) return;
107
- if (this.clients.size === 0) return;
108
- if (this.idle && !this.flushPending) return;
109
- this.rafId = requestAnimationFrame(this.frame);
110
- }
111
- cancel() {
112
- if (this.rafId !== null) {
113
- cancelAnimationFrame(this.rafId);
114
- this.rafId = null;
115
- }
116
- }
117
- frame = (now) => {
118
- this.rafId = null;
119
- if (!this.running || this.paused) return;
120
- if (this.startedAt === 0) {
121
- this.startedAt = now;
122
- this.lastTickAt = now;
123
- }
124
- const delta = (now - this.lastTickAt) / 1e3;
125
- const elapsed = (now - this.startedAt) / 1e3;
126
- this.lastTickAt = now;
127
- const tick = { delta, elapsed, now };
128
- for (const client of this.clients) {
129
- client(tick);
130
- }
131
- this.flushPending = false;
132
- this.maybeQueue();
133
- };
134
- };
135
-
136
39
  // src/inputs/CursorInput.ts
137
40
  var CursorInput = class {
138
41
  value;
@@ -152,6 +55,7 @@ var CursorInput = class {
152
55
  this.eventTarget = target ?? (typeof window !== "undefined" ? window : new EventTarget());
153
56
  this.element = element;
154
57
  this.handleMouseMove = (e) => {
58
+ if (!(e instanceof MouseEvent)) return;
155
59
  const me = e;
156
60
  if (this.element) {
157
61
  const r = this.element.getBoundingClientRect();
@@ -167,7 +71,7 @@ var CursorInput = class {
167
71
  };
168
72
  this.eventTarget.addEventListener("mousemove", this.handleMouseMove);
169
73
  }
170
- /** Current smoothed position. Implements MatterSignal protocol. */
74
+ /** Current smoothed position. Implements AnimatableSignal protocol. */
171
75
  get() {
172
76
  return this.value;
173
77
  }
@@ -210,12 +114,14 @@ var lerp = (a, b, t) => a + (b - a) * t;
210
114
  import { mix, vec3 } from "three/tsl";
211
115
  import { clamp, div, sub } from "three/tsl";
212
116
  function colorRamp(t, stops) {
213
- if (stops.length === 0) return vec3(0, 0, 0);
214
- if (stops.length === 1) return stops[0].color;
215
- let result = stops[0].color;
216
- for (let i = 1; i < stops.length; i++) {
117
+ const first = stops[0];
118
+ if (first === void 0) return vec3(0, 0, 0);
119
+ if (stops.length === 1) return mix(first.color, first.color, 0);
120
+ let result = mix(first.color, first.color, 0);
121
+ for (let i = 1; i < stops.length; i += 1) {
217
122
  const prev = stops[i - 1];
218
123
  const next = stops[i];
124
+ if (prev === void 0 || next === void 0) continue;
219
125
  const span = next.position - prev.position;
220
126
  if (span <= 0) continue;
221
127
  const localT = clamp(div(sub(t, prev.position), span), 0, 1);
@@ -240,7 +146,7 @@ function fbm(p, opts = {}) {
240
146
  let amp = 1;
241
147
  let freq = 1;
242
148
  let total = amp;
243
- for (let i = 1; i < octaves; i++) {
149
+ for (let i = 1; i < octaves; i += 1) {
244
150
  freq *= lacunarity;
245
151
  amp *= gain;
246
152
  total += amp;
@@ -279,7 +185,7 @@ function displace(p, by) {
279
185
  }
280
186
 
281
187
  // src/primitives/cursorRipple.ts
282
- import { sin, length as length2, smoothstep, sub as sub2 } from "three/tsl";
188
+ import { length as length2, sin, smoothstep, sub as sub2 } from "three/tsl";
283
189
 
284
190
  // src/primitives/time.ts
285
191
  import { time as _builtinTime } from "three/tsl";
@@ -387,7 +293,7 @@ function cursorRipple(p, center, opts = {}) {
387
293
  }
388
294
 
389
295
  // src/primitives/filmGrain.ts
390
- import { vec2, sin as sin2, fract, length as length3 } from "three/tsl";
296
+ import { fract, length as length3, sin as sin2, vec2 } from "three/tsl";
391
297
  function filmGrain(uvNode, intensity, timeOffset = 0) {
392
298
  const HASH_C1 = vec2(2127.1, 81.17);
393
299
  const HASH_C2 = vec2(1269.5, 283.37);
@@ -461,9 +367,107 @@ function createIntersectionWatcher(canvas) {
461
367
  }
462
368
  };
463
369
  }
370
+
371
+ // src/runtime/frame-scheduler.ts
372
+ var FrameScheduler = class {
373
+ clients = /* @__PURE__ */ new Set();
374
+ rafId = null;
375
+ running = false;
376
+ paused = false;
377
+ idle = false;
378
+ flushPending = false;
379
+ startedAt = 0;
380
+ lastTickAt = 0;
381
+ /** Activate the scheduler. The rAF loop starts on the first client added. */
382
+ start() {
383
+ this.running = true;
384
+ this.paused = false;
385
+ this.maybeQueue();
386
+ }
387
+ /** Halt the rAF loop entirely. Use dispose() for permanent teardown. */
388
+ stop() {
389
+ this.running = false;
390
+ this.cancel();
391
+ }
392
+ /** Temporarily skip ticks without losing client registrations. */
393
+ pause() {
394
+ this.paused = true;
395
+ }
396
+ /** Resume after pause(). */
397
+ resume() {
398
+ this.paused = false;
399
+ if (this.running) this.maybeQueue();
400
+ }
401
+ /** Register a client to be called every frame. */
402
+ add(client) {
403
+ this.clients.add(client);
404
+ if (this.running) this.maybeQueue();
405
+ }
406
+ /** Unregister a client. */
407
+ remove(client) {
408
+ this.clients.delete(client);
409
+ }
410
+ /** Permanent teardown: stop the loop and drop all clients. */
411
+ dispose() {
412
+ this.stop();
413
+ this.clients.clear();
414
+ }
415
+ /**
416
+ * Mark the scheduler idle. The next tick still fires (a final flush so
417
+ * uniform changes that triggered the idle state are rendered), then the
418
+ * rAF loop halts. Use `requestRender()` or `setIdle(false)` to wake.
419
+ */
420
+ setIdle(idle) {
421
+ if (this.idle === idle) return;
422
+ this.idle = idle;
423
+ if (idle) {
424
+ this.flushPending = true;
425
+ this.maybeQueue();
426
+ } else {
427
+ this.flushPending = false;
428
+ this.maybeQueue();
429
+ }
430
+ }
431
+ /** Force a single tick while idle. Useful for prop-change invalidation. */
432
+ requestRender() {
433
+ if (!this.idle) return;
434
+ this.flushPending = true;
435
+ this.maybeQueue();
436
+ }
437
+ maybeQueue() {
438
+ if (this.rafId !== null) return;
439
+ if (!this.running) return;
440
+ if (this.clients.size === 0) return;
441
+ if (this.idle && !this.flushPending) return;
442
+ this.rafId = requestAnimationFrame(this.frame);
443
+ }
444
+ cancel() {
445
+ if (this.rafId !== null) {
446
+ cancelAnimationFrame(this.rafId);
447
+ this.rafId = null;
448
+ }
449
+ }
450
+ frame = (now) => {
451
+ this.rafId = null;
452
+ if (!this.running || this.paused) return;
453
+ if (this.startedAt === 0) {
454
+ this.startedAt = now;
455
+ this.lastTickAt = now;
456
+ }
457
+ const delta = (now - this.lastTickAt) / 1e3;
458
+ const elapsed = (now - this.startedAt) / 1e3;
459
+ this.lastTickAt = now;
460
+ const tick = { delta, elapsed, now };
461
+ for (const client of this.clients) {
462
+ client(tick);
463
+ }
464
+ this.flushPending = false;
465
+ this.maybeQueue();
466
+ };
467
+ };
464
468
  export {
465
469
  CursorInput,
466
- MatterScheduler,
470
+ FrameScheduler,
467
471
  colorRamp,
468
472
  createIntersectionWatcher,
469
473
  createReducedMotionWatcher,