@lovo/matter 0.4.1 → 0.6.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.js CHANGED
@@ -1,13 +1,51 @@
1
1
  // src/runtime/create-renderer/create-renderer.ts
2
- import { Color } from "three";
2
+ import { Color, Vector2 } from "three";
3
3
  import { WebGPURenderer } from "three/webgpu";
4
+
5
+ // src/runtime/create-renderer/gamut.ts
6
+ import { ColorManagement, SRGBColorSpace } from "three";
7
+ import {
8
+ DisplayP3ColorSpace,
9
+ DisplayP3ColorSpaceImpl,
10
+ LinearDisplayP3ColorSpace,
11
+ LinearDisplayP3ColorSpaceImpl
12
+ } from "three/examples/jsm/math/ColorSpaces.js";
13
+ ColorManagement.define({
14
+ [DisplayP3ColorSpace]: DisplayP3ColorSpaceImpl,
15
+ [LinearDisplayP3ColorSpace]: LinearDisplayP3ColorSpaceImpl
16
+ });
17
+ function gamutToColorSpace(gamut) {
18
+ return gamut === "p3" ? DisplayP3ColorSpace : SRGBColorSpace;
19
+ }
20
+ function hasWebGpuBackendInternals(backend) {
21
+ if (typeof backend !== "object" || backend === null) return false;
22
+ if (!("device" in backend) || !("context" in backend)) return false;
23
+ const { device, context } = backend;
24
+ return typeof device === "object" && device !== null && typeof context === "object" && context !== null && "configure" in context && typeof context.configure === "function";
25
+ }
26
+ function applyCanvasGamut(renderer, backend, gamut) {
27
+ if (gamut !== "p3" || backend !== "webgpu") return;
28
+ if (typeof navigator === "undefined" || !("gpu" in navigator)) return;
29
+ const webGpuBackend = renderer.backend;
30
+ if (!hasWebGpuBackendInternals(webGpuBackend)) return;
31
+ webGpuBackend.context.configure({
32
+ device: webGpuBackend.device,
33
+ format: navigator.gpu.getPreferredCanvasFormat(),
34
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
35
+ alphaMode: "premultiplied",
36
+ colorSpace: "display-p3"
37
+ });
38
+ }
39
+
40
+ // src/runtime/create-renderer/create-renderer.ts
4
41
  async function createRenderer(canvas, opts = {}) {
5
42
  const {
6
43
  antialias = true,
7
44
  forceWebGL = false,
8
45
  clearColor = 0,
9
46
  clearAlpha = 0,
10
- maxDPR = 2
47
+ maxDPR = 2,
48
+ gamut = "srgb"
11
49
  } = opts;
12
50
  const three = new WebGPURenderer({
13
51
  canvas,
@@ -15,19 +53,24 @@ async function createRenderer(canvas, opts = {}) {
15
53
  forceWebGL
16
54
  });
17
55
  await three.init();
56
+ three.outputColorSpace = gamutToColorSpace(gamut);
18
57
  three.setPixelRatio(Math.min(window.devicePixelRatio, maxDPR));
19
58
  const resolvedClearColor = clearColor instanceof Color ? clearColor : new Color(clearColor);
20
59
  three.setClearColor(resolvedClearColor, clearAlpha);
60
+ const rendererSize = new Vector2();
21
61
  const resize = () => {
22
- const w = canvas.clientWidth;
23
- const h = canvas.clientHeight;
24
- if (canvas.width !== w * three.getPixelRatio() || canvas.height !== h * three.getPixelRatio()) {
25
- three.setSize(w, h, false);
62
+ const canvasWidth = canvas.clientWidth;
63
+ const canvasHeight = canvas.clientHeight;
64
+ if (canvasWidth === 0 || canvasHeight === 0) return;
65
+ three.getSize(rendererSize);
66
+ if (rendererSize.width !== canvasWidth || rendererSize.height !== canvasHeight) {
67
+ three.setSize(canvasWidth, canvasHeight, false);
26
68
  }
27
69
  };
28
70
  resize();
29
71
  const isWebGL = "isWebGLBackend" in three.backend && three.backend.isWebGLBackend === true;
30
72
  const backend = forceWebGL || isWebGL ? "webgl2" : "webgpu";
73
+ applyCanvasGamut(three, backend, gamut);
31
74
  return {
32
75
  three,
33
76
  backend,
@@ -56,16 +99,19 @@ var CursorInput = class {
56
99
  this.element = element;
57
100
  this.handleMouseMove = (e) => {
58
101
  if (!(e instanceof MouseEvent)) return;
59
- const me = e;
102
+ const mouseEvent = e;
60
103
  if (this.element) {
61
- const r = this.element.getBoundingClientRect();
62
- const w = r.width || 1;
63
- const h = r.height || 1;
64
- this.target = [(me.clientX - r.left) / w, (me.clientY - r.top) / h];
104
+ const elementRect = this.element.getBoundingClientRect();
105
+ const elementWidth = elementRect.width || 1;
106
+ const elementHeight = elementRect.height || 1;
107
+ this.target = [
108
+ (mouseEvent.clientX - elementRect.left) / elementWidth,
109
+ (mouseEvent.clientY - elementRect.top) / elementHeight
110
+ ];
65
111
  } else {
66
- const w = typeof window !== "undefined" && window.innerWidth || 1;
67
- const h = typeof window !== "undefined" && window.innerHeight || 1;
68
- this.target = [me.clientX / w, me.clientY / h];
112
+ const viewportWidth = typeof window !== "undefined" && window.innerWidth || 1;
113
+ const viewportHeight = typeof window !== "undefined" && window.innerHeight || 1;
114
+ this.target = [mouseEvent.clientX / viewportWidth, mouseEvent.clientY / viewportHeight];
69
115
  }
70
116
  this.targetDirty = true;
71
117
  };
@@ -76,9 +122,9 @@ var CursorInput = class {
76
122
  return this.value;
77
123
  }
78
124
  /** Subscribe to change events. Returns an unsubscribe function. */
79
- on(_event, cb) {
80
- this.listeners.add(cb);
81
- return () => this.listeners.delete(cb);
125
+ on(_eventType, changeListener) {
126
+ this.listeners.add(changeListener);
127
+ return () => this.listeners.delete(changeListener);
82
128
  }
83
129
  /**
84
130
  * Advance the smoothing one tick. Called by the host scheduler; not
@@ -107,51 +153,387 @@ var CursorInput = class {
107
153
  this.listeners.clear();
108
154
  }
109
155
  };
110
- var clamp01 = (n) => Math.max(0, Math.min(1, n));
111
- var lerp = (a, b, t) => a + (b - a) * t;
156
+ var clamp01 = (value) => Math.max(0, Math.min(1, value));
157
+ var lerp = (startValue, endValue, blendFactor) => startValue + (endValue - startValue) * blendFactor;
112
158
 
113
159
  // src/primitives/color-ramp/color-ramp.ts
114
- import { mix, vec3 } from "three/tsl";
115
- import { clamp, div, sub } from "three/tsl";
116
- function colorRamp(t, stops) {
160
+ import { clamp as clamp3, div, sub, vec3 as vec37 } from "three/tsl";
161
+
162
+ // src/primitives/color-space/hue.ts
163
+ import { mod, sign, step } from "three/tsl";
164
+ var EQUAL_HUE_EPSILON = 1e-6;
165
+ var shortestArcHue = (h1, h2, t, period) => {
166
+ const half = period / 2;
167
+ const delta = mod(h2.sub(h1).add(half), period).sub(half);
168
+ return h1.add(delta.mul(t));
169
+ };
170
+ var longestArcHue = (h1, h2, t, period) => {
171
+ const half = period / 2;
172
+ const short = mod(h2.sub(h1).add(half), period).sub(half);
173
+ const delta = short.sub(sign(short).mul(period));
174
+ return h1.add(delta.mul(t));
175
+ };
176
+ var increasingArcHue = (h1, h2, t, period) => {
177
+ const delta = mod(h2.sub(h1), period);
178
+ return h1.add(delta.mul(t));
179
+ };
180
+ var decreasingArcHue = (h1, h2, t, period) => {
181
+ const up = mod(h2.sub(h1), period);
182
+ const delta = up.sub(step(EQUAL_HUE_EPSILON, up).mul(period));
183
+ return h1.add(delta.mul(t));
184
+ };
185
+ var hueArcInterpolators = {
186
+ shorter: shortestArcHue,
187
+ longer: longestArcHue,
188
+ increasing: increasingArcHue,
189
+ decreasing: decreasingArcHue
190
+ };
191
+
192
+ // src/primitives/color-space/hsl.ts
193
+ import { abs, clamp, fract, max, min, mix as mix2, step as step3, vec3, vec4 } from "three/tsl";
194
+
195
+ // src/primitives/color-space/transfer.ts
196
+ import { mix, pow, step as step2 } from "three/tsl";
197
+ function srgbChannelToLinear(channel) {
198
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
199
+ }
200
+ function srgbToLinear(srgb) {
201
+ const value = pow(srgb, 1);
202
+ const lowSegment = value.div(12.92);
203
+ const highSegment = pow(value.add(0.055).div(1.055), 2.4);
204
+ return mix(lowSegment, highSegment, step2(0.04045, value));
205
+ }
206
+ function linearToSrgb(linear) {
207
+ const value = pow(linear, 1);
208
+ const lowSegment = value.mul(12.92);
209
+ const highSegment = pow(value, 1 / 2.4).mul(1.055).sub(0.055);
210
+ return mix(lowSegment, highSegment, step2(31308e-7, value));
211
+ }
212
+
213
+ // src/primitives/color-space/hsl.ts
214
+ var EPSILON = 1e-10;
215
+ function gammaRgbHue(c) {
216
+ const p = mix2(vec4(c.b, c.g, -1 / 3, 2 / 3), vec4(c.g, c.b, 0, -1 / 3), step3(c.b, c.g));
217
+ const q = mix2(vec4(p.x, p.y, p.w, c.r), vec4(c.r, p.y, p.z, p.x), step3(p.x, c.r));
218
+ const chroma = q.x.sub(min(q.w, q.y));
219
+ return abs(q.z.add(q.w.sub(q.y).div(chroma.mul(6).add(EPSILON))));
220
+ }
221
+ function gammaRgbToHsl(c) {
222
+ const maxChannel = max(c.r, max(c.g, c.b));
223
+ const minChannel = min(c.r, min(c.g, c.b));
224
+ const lightness = maxChannel.add(minChannel).mul(0.5);
225
+ const chroma = maxChannel.sub(minChannel);
226
+ const saturation = chroma.div(abs(lightness.mul(2).sub(1)).oneMinus().add(EPSILON));
227
+ return vec3(gammaRgbHue(c), saturation, lightness);
228
+ }
229
+ function hslToGammaRgb(hsl) {
230
+ const hue = hsl.x;
231
+ const saturation = hsl.y;
232
+ const lightness = hsl.z;
233
+ const chroma = abs(lightness.mul(2).sub(1)).oneMinus().mul(saturation);
234
+ const ramp = abs(
235
+ fract(vec3(hue).add(vec3(1, 2 / 3, 1 / 3))).mul(6).sub(vec3(3))
236
+ );
237
+ const hueRgb = clamp(ramp.sub(vec3(1)), 0, 1);
238
+ return hueRgb.sub(0.5).mul(chroma).add(lightness);
239
+ }
240
+ var hslSpace = {
241
+ // Clamp into sRGB before the gamma transfer: HSL is an sRGB-gamut concept, and
242
+ // the sRGB OETF's pow() can't be WGSL const-evaluated on the negative channels
243
+ // of an out-of-sRGB (wide-gamut) stop color — that crashed the shader compile.
244
+ fromLinear: (rgb) => gammaRgbToHsl(linearToSrgb(clamp(rgb, 0, 1))),
245
+ toLinear: (hsl) => srgbToLinear(hslToGammaRgb(hsl)),
246
+ lerp: (a, b, t, hue) => vec3(hue(a.x, b.x, t, 1), mix2(a.y, b.y, t), mix2(a.z, b.z, t))
247
+ };
248
+
249
+ // src/primitives/color-space/hsv.ts
250
+ import { abs as abs2, clamp as clamp2, fract as fract2, min as min2, mix as mix3, step as step4, vec3 as vec32, vec4 as vec42 } from "three/tsl";
251
+ var EPSILON2 = 1e-10;
252
+ function gammaRgbToHsv(c) {
253
+ const p = mix3(vec42(c.b, c.g, -1 / 3, 2 / 3), vec42(c.g, c.b, 0, -1 / 3), step4(c.b, c.g));
254
+ const q = mix3(vec42(p.x, p.y, p.w, c.r), vec42(c.r, p.y, p.z, p.x), step4(p.x, c.r));
255
+ const chroma = q.x.sub(min2(q.w, q.y));
256
+ const hue = abs2(q.z.add(q.w.sub(q.y).div(chroma.mul(6).add(EPSILON2))));
257
+ const saturation = chroma.div(q.x.add(EPSILON2));
258
+ return vec32(hue, saturation, q.x);
259
+ }
260
+ function hsvToGammaRgb(hsv) {
261
+ const hue = hsv.x;
262
+ const saturation = hsv.y;
263
+ const value = hsv.z;
264
+ const ramp = abs2(
265
+ fract2(vec32(hue).add(vec32(1, 2 / 3, 1 / 3))).mul(6).sub(vec32(3))
266
+ );
267
+ return mix3(vec32(1), clamp2(ramp.sub(vec32(1)), 0, 1), saturation).mul(value);
268
+ }
269
+ var hsvSpace = {
270
+ // Clamp into sRGB before the gamma transfer: HSV is an sRGB-gamut concept, and
271
+ // the sRGB OETF's pow() can't be WGSL const-evaluated on the negative channels
272
+ // of an out-of-sRGB (wide-gamut) stop color — that crashed the shader compile.
273
+ fromLinear: (rgb) => gammaRgbToHsv(linearToSrgb(clamp2(rgb, 0, 1))),
274
+ toLinear: (hsv) => srgbToLinear(hsvToGammaRgb(hsv)),
275
+ lerp: (a, b, t, hue) => vec32(hue(a.x, b.x, t, 1), mix3(a.y, b.y, t), mix3(a.z, b.z, t))
276
+ };
277
+
278
+ // src/primitives/color-space/lch.ts
279
+ import { atan2, cbrt, cos, length, mix as mix4, sin, step as step5, vec2, vec3 as vec33 } from "three/tsl";
280
+ var TWO_PI = Math.PI * 2;
281
+ var WHITE_X = 0.95047;
282
+ var WHITE_Y = 1;
283
+ var WHITE_Z = 1.08883;
284
+ var EPSILON3 = 216 / 24389;
285
+ var KAPPA = 24389 / 27;
286
+ function labForward(ratio) {
287
+ const linearPart = ratio.mul(KAPPA).add(16).div(116);
288
+ const cubeRootPart = cbrt(ratio);
289
+ return mix4(linearPart, cubeRootPart, step5(EPSILON3, ratio));
290
+ }
291
+ function labInverse(f) {
292
+ const cubed = f.mul(f).mul(f);
293
+ const linearPart = f.mul(116).sub(16).div(KAPPA);
294
+ return mix4(linearPart, cubed, step5(EPSILON3, cubed));
295
+ }
296
+ function linearToLch(rgb) {
297
+ const r = rgb.r;
298
+ const g = rgb.g;
299
+ const b = rgb.b;
300
+ const x = r.mul(0.4123907993).add(g.mul(0.3575843394)).add(b.mul(0.1804807884));
301
+ const y = r.mul(0.2126390059).add(g.mul(0.7151686788)).add(b.mul(0.0721923154));
302
+ const z = r.mul(0.0193308187).add(g.mul(0.1191947798)).add(b.mul(0.9505321522));
303
+ const fx = labForward(x.div(WHITE_X));
304
+ const fy = labForward(y.div(WHITE_Y));
305
+ const fz = labForward(z.div(WHITE_Z));
306
+ const lightness = fy.mul(116).sub(16);
307
+ const greenRed = fx.sub(fy).mul(500);
308
+ const blueYellow = fy.sub(fz).mul(200);
309
+ const chroma = length(vec2(greenRed, blueYellow));
310
+ const hue = atan2(blueYellow, greenRed);
311
+ return vec33(lightness, chroma, hue);
312
+ }
313
+ function lchToLinear(lch) {
314
+ const lightness = lch.x;
315
+ const chroma = lch.y;
316
+ const hue = lch.z;
317
+ const greenRed = chroma.mul(cos(hue));
318
+ const blueYellow = chroma.mul(sin(hue));
319
+ const fy = lightness.add(16).div(116);
320
+ const fx = fy.add(greenRed.div(500));
321
+ const fz = fy.sub(blueYellow.div(200));
322
+ const x = labInverse(fx).mul(WHITE_X);
323
+ const y = labInverse(fy).mul(WHITE_Y);
324
+ const z = labInverse(fz).mul(WHITE_Z);
325
+ const r = x.mul(3.2409699419).sub(y.mul(1.5373831776)).sub(z.mul(0.4986107603));
326
+ const g = x.mul(-0.9692436363).add(y.mul(1.8759675015)).add(z.mul(0.0415550574));
327
+ const b = x.mul(0.0556300797).sub(y.mul(0.2039769589)).add(z.mul(1.0569715142));
328
+ return vec33(r, g, b);
329
+ }
330
+ var lchSpace = {
331
+ fromLinear: linearToLch,
332
+ toLinear: lchToLinear,
333
+ lerp: (a, b, t, hue) => vec33(mix4(a.x, b.x, t), mix4(a.y, b.y, t), hue(a.z, b.z, t, TWO_PI))
334
+ };
335
+
336
+ // src/primitives/color-space/linear.ts
337
+ import { mix as mix5, vec3 as vec34 } from "three/tsl";
338
+ var linearSpace = {
339
+ fromLinear: (rgb) => vec34(rgb),
340
+ toLinear: (coords) => vec34(coords),
341
+ lerp: (a, b, t) => mix5(a, b, t)
342
+ };
343
+
344
+ // src/primitives/color-space/oklab.ts
345
+ import { cbrt as cbrt2, mix as mix6, vec3 as vec35 } from "three/tsl";
346
+ function linearToOklab(rgb) {
347
+ const r = rgb.r;
348
+ const g = rgb.g;
349
+ const b = rgb.b;
350
+ const longCone = r.mul(0.4122214708).add(g.mul(0.5363325363)).add(b.mul(0.0514459929));
351
+ const mediumCone = r.mul(0.2119034982).add(g.mul(0.6806995451)).add(b.mul(0.1073969566));
352
+ const shortCone = r.mul(0.0883024619).add(g.mul(0.2817188376)).add(b.mul(0.6299787005));
353
+ const longRoot = cbrt2(longCone);
354
+ const mediumRoot = cbrt2(mediumCone);
355
+ const shortRoot = cbrt2(shortCone);
356
+ const lightness = longRoot.mul(0.2104542553).add(mediumRoot.mul(0.793617785)).sub(shortRoot.mul(0.0040720468));
357
+ const greenRed = longRoot.mul(1.9779984951).sub(mediumRoot.mul(2.428592205)).add(shortRoot.mul(0.4505937099));
358
+ const blueYellow = longRoot.mul(0.0259040371).add(mediumRoot.mul(0.7827717662)).sub(shortRoot.mul(0.808675766));
359
+ return vec35(lightness, greenRed, blueYellow);
360
+ }
361
+ function oklabToLinear(lab) {
362
+ const lightness = lab.x;
363
+ const greenRed = lab.y;
364
+ const blueYellow = lab.z;
365
+ const longRoot = lightness.add(greenRed.mul(0.3963377774)).add(blueYellow.mul(0.2158037573));
366
+ const mediumRoot = lightness.sub(greenRed.mul(0.1055613458)).sub(blueYellow.mul(0.0638541728));
367
+ const shortRoot = lightness.sub(greenRed.mul(0.0894841775)).sub(blueYellow.mul(1.291485548));
368
+ const longCone = longRoot.mul(longRoot).mul(longRoot);
369
+ const mediumCone = mediumRoot.mul(mediumRoot).mul(mediumRoot);
370
+ const shortCone = shortRoot.mul(shortRoot).mul(shortRoot);
371
+ const r = longCone.mul(4.0767416621).sub(mediumCone.mul(3.3077115913)).add(shortCone.mul(0.2309699292));
372
+ const g = longCone.mul(-1.2684380046).add(mediumCone.mul(2.6097574011)).sub(shortCone.mul(0.3413193965));
373
+ const b = longCone.mul(-0.0041960863).sub(mediumCone.mul(0.7034186147)).add(shortCone.mul(1.707614701));
374
+ return vec35(r, g, b);
375
+ }
376
+ var oklabSpace = {
377
+ fromLinear: linearToOklab,
378
+ toLinear: oklabToLinear,
379
+ lerp: (a, b, t) => mix6(a, b, t)
380
+ };
381
+
382
+ // src/primitives/color-space/oklch.ts
383
+ import { atan2 as atan22, cos as cos2, length as length2, mix as mix7, sin as sin2, vec2 as vec22, vec3 as vec36 } from "three/tsl";
384
+ var TWO_PI2 = Math.PI * 2;
385
+ function linearToOklch(rgb) {
386
+ const lab = linearToOklab(rgb);
387
+ const lightness = lab.x;
388
+ const greenRed = lab.y;
389
+ const blueYellow = lab.z;
390
+ const chroma = length2(vec22(greenRed, blueYellow));
391
+ const hue = atan22(blueYellow, greenRed);
392
+ return vec36(lightness, chroma, hue);
393
+ }
394
+ function oklchToLinear(lch) {
395
+ const lightness = lch.x;
396
+ const chroma = lch.y;
397
+ const hue = lch.z;
398
+ const greenRed = chroma.mul(cos2(hue));
399
+ const blueYellow = chroma.mul(sin2(hue));
400
+ return oklabToLinear(vec36(lightness, greenRed, blueYellow));
401
+ }
402
+ var oklchSpace = {
403
+ fromLinear: linearToOklch,
404
+ toLinear: oklchToLinear,
405
+ lerp: (a, b, t, hue) => vec36(mix7(a.x, b.x, t), mix7(a.y, b.y, t), hue(a.z, b.z, t, TWO_PI2))
406
+ };
407
+
408
+ // src/primitives/color-space/registry.ts
409
+ var colorSpaces = {
410
+ linear: linearSpace,
411
+ oklab: oklabSpace,
412
+ oklch: oklchSpace,
413
+ lch: lchSpace,
414
+ hsl: hslSpace,
415
+ hsv: hsvSpace
416
+ };
417
+
418
+ // src/primitives/color-ramp/color-ramp.ts
419
+ function colorRamp(t, stops, colorSpace = "linear", hueInterpolation = "shorter") {
420
+ const space = colorSpaces[colorSpace];
421
+ const hue = hueArcInterpolators[hueInterpolation];
117
422
  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);
423
+ if (first === void 0) return vec37(0, 0, 0);
424
+ const firstCoords = space.fromLinear(vec37(first.color));
425
+ if (stops.length === 1) return space.toLinear(firstCoords);
426
+ let resultCoords = firstCoords;
121
427
  for (let i = 1; i < stops.length; i += 1) {
122
- const prev = stops[i - 1];
428
+ const previousStop = stops[i - 1];
123
429
  const next = stops[i];
124
- if (prev === void 0 || next === void 0) continue;
125
- const span = next.position - prev.position;
126
- if (span <= 0) continue;
127
- const localT = clamp(div(sub(t, prev.position), span), 0, 1);
128
- result = mix(result, next.color, localT);
430
+ if (previousStop === void 0 || next === void 0) continue;
431
+ const positionSpan = next.position - previousStop.position;
432
+ if (positionSpan <= 0) continue;
433
+ const localT = clamp3(div(sub(t, previousStop.position), positionSpan), 0, 1);
434
+ const nextCoords = space.fromLinear(vec37(next.color));
435
+ resultCoords = space.lerp(resultCoords, nextCoords, localT, hue);
436
+ }
437
+ return space.toLinear(resultCoords);
438
+ }
439
+
440
+ // src/primitives/color-space/mix-color.ts
441
+ import { vec3 as vec38 } from "three/tsl";
442
+ function mixColor(colorA, colorB, t, colorSpace = "oklab", hueInterpolation = "shorter") {
443
+ const space = colorSpaces[colorSpace];
444
+ const hue = hueArcInterpolators[hueInterpolation];
445
+ const a = space.fromLinear(vec38(colorA));
446
+ const b = space.fromLinear(vec38(colorB));
447
+ return space.toLinear(space.lerp(a, b, t, hue));
448
+ }
449
+
450
+ // src/primitives/color-space/cpu-convert.ts
451
+ function oklabToLinearSrgb(lightness, greenRed, blueYellow) {
452
+ const longRoot = lightness + 0.3963377774 * greenRed + 0.2158037573 * blueYellow;
453
+ const mediumRoot = lightness - 0.1055613458 * greenRed - 0.0638541728 * blueYellow;
454
+ const shortRoot = lightness - 0.0894841775 * greenRed - 1.291485548 * blueYellow;
455
+ const longCone = longRoot * longRoot * longRoot;
456
+ const mediumCone = mediumRoot * mediumRoot * mediumRoot;
457
+ const shortCone = shortRoot * shortRoot * shortRoot;
458
+ const red = 4.0767416621 * longCone - 3.3077115913 * mediumCone + 0.2309699292 * shortCone;
459
+ const green = -1.2684380046 * longCone + 2.6097574011 * mediumCone - 0.3413193965 * shortCone;
460
+ const blue = -0.0041960863 * longCone - 0.7034186147 * mediumCone + 1.707614701 * shortCone;
461
+ return [red, green, blue];
462
+ }
463
+ function oklchToLinearSrgb(lightness, chroma, hueDegrees) {
464
+ const hueRadians = hueDegrees * Math.PI / 180;
465
+ const greenRed = chroma * Math.cos(hueRadians);
466
+ const blueYellow = chroma * Math.sin(hueRadians);
467
+ return oklabToLinearSrgb(lightness, greenRed, blueYellow);
468
+ }
469
+ function parseComponent(token, scale) {
470
+ const trimmed = token.trim();
471
+ if (trimmed.endsWith("%")) {
472
+ return parseFloat(trimmed.slice(0, -1)) / 100 * scale;
129
473
  }
130
- return result;
474
+ return parseFloat(trimmed);
475
+ }
476
+ function functionArgs(input, prefix) {
477
+ const inner = input.slice(prefix.length, input.lastIndexOf(")"));
478
+ const beforeAlpha = inner.split("/")[0] ?? "";
479
+ return beforeAlpha.trim().split(/[\s,]+/).filter((token) => token.length > 0);
480
+ }
481
+ function parseColorString(input) {
482
+ const value = input.trim();
483
+ if (value.startsWith("#")) {
484
+ const hex = value.slice(1);
485
+ return [
486
+ srgbChannelToLinear(parseInt(hex.slice(0, 2), 16) / 255),
487
+ srgbChannelToLinear(parseInt(hex.slice(2, 4), 16) / 255),
488
+ srgbChannelToLinear(parseInt(hex.slice(4, 6), 16) / 255)
489
+ ];
490
+ }
491
+ if (value.startsWith("oklch(")) {
492
+ const [lightnessToken, chromaToken, hueToken] = functionArgs(value, "oklch(");
493
+ if (lightnessToken === void 0 || chromaToken === void 0 || hueToken === void 0) {
494
+ throw new Error(`Invalid oklch() color: "${input}"`);
495
+ }
496
+ const lightness = parseComponent(lightnessToken, 1);
497
+ const chroma = parseComponent(chromaToken, 0.4);
498
+ const hueDegrees = parseFloat(hueToken.replace(/deg$/, ""));
499
+ return oklchToLinearSrgb(lightness, chroma, hueDegrees);
500
+ }
501
+ if (value.startsWith("oklab(")) {
502
+ const [lightnessToken, aToken, bToken] = functionArgs(value, "oklab(");
503
+ if (lightnessToken === void 0 || aToken === void 0 || bToken === void 0) {
504
+ throw new Error(`Invalid oklab() color: "${input}"`);
505
+ }
506
+ return oklabToLinearSrgb(
507
+ parseComponent(lightnessToken, 1),
508
+ parseComponent(aToken, 0.4),
509
+ parseComponent(bToken, 0.4)
510
+ );
511
+ }
512
+ throw new Error(`Unsupported color syntax: "${input}". Use #rrggbb, oklch(...), or oklab(...).`);
131
513
  }
132
514
 
133
515
  // src/primitives/noise/noise.ts
134
516
  import { mx_noise_float } from "three/tsl";
135
- function noise(p) {
517
+ function simplexNoise(p) {
136
518
  return mx_noise_float(p);
137
519
  }
138
520
 
139
521
  // src/primitives/fbm/fbm.ts
140
522
  import { add, mul } from "three/tsl";
141
- function fbm(p, opts = {}) {
523
+ function fractalNoise(p, opts = {}) {
142
524
  const octaves = opts.octaves ?? 4;
143
525
  const lacunarity = opts.lacunarity ?? 2;
144
526
  const gain = opts.gain ?? 0.5;
145
- let sum = noise(p);
146
- let amp = 1;
147
- let freq = 1;
148
- let total = amp;
527
+ let sum = simplexNoise(p);
528
+ let amplitude = 1;
529
+ let frequency = 1;
530
+ let total = amplitude;
149
531
  for (let i = 1; i < octaves; i += 1) {
150
- freq *= lacunarity;
151
- amp *= gain;
152
- total += amp;
153
- const pAtFreq = add(mul(p, freq), i * 100);
154
- const layer = noise(pAtFreq).mul(amp);
532
+ frequency *= lacunarity;
533
+ amplitude *= gain;
534
+ total += amplitude;
535
+ const pAtFreq = add(mul(p, frequency), i * 100);
536
+ const layer = simplexNoise(pAtFreq).mul(amplitude);
155
537
  sum = sum.add(layer);
156
538
  }
157
539
  return sum.div(total);
@@ -168,14 +550,14 @@ function quantize(t, steps) {
168
550
  if (steps <= 1) {
169
551
  return t.mul(0);
170
552
  }
171
- const denom = steps - 1;
172
- return t.mul(denom).add(0.5).floor().div(denom);
553
+ const denominator = steps - 1;
554
+ return t.mul(denominator).add(0.5).floor().div(denominator);
173
555
  }
174
556
 
175
557
  // src/primitives/sdf-circle/sdf-circle.ts
176
- import { length } from "three/tsl";
177
- function sdfCircle(p, radius) {
178
- return length(p).sub(radius);
558
+ import { length as length3 } from "three/tsl";
559
+ function signedDistanceFieldCircle(p, radius) {
560
+ return length3(p).sub(radius);
179
561
  }
180
562
 
181
563
  // src/primitives/displace/displace.ts
@@ -185,7 +567,7 @@ function displace(p, by) {
185
567
  }
186
568
 
187
569
  // src/primitives/cursor-ripple/cursor-ripple.ts
188
- import { length as length2, sin, smoothstep, sub as sub2 } from "three/tsl";
570
+ import { length as length4, sin as sin3, smoothstep, sub as sub2 } from "three/tsl";
189
571
 
190
572
  // src/primitives/time/time.ts
191
573
  import { time as _builtinTime } from "three/tsl";
@@ -199,7 +581,7 @@ var state = {
199
581
  function setReducedMotionPolicy(policy) {
200
582
  if (state.policy === policy) return;
201
583
  state.policy = policy;
202
- for (const w of state.watchers) w.recompute();
584
+ for (const watcher of state.watchers) watcher.recompute();
203
585
  }
204
586
  function getReducedMotionPolicy() {
205
587
  return state.policy;
@@ -220,8 +602,8 @@ function createReducedMotionWatcher() {
220
602
  if (typeof matchMedia !== "function") {
221
603
  return {
222
604
  scale: () => computeScale(false),
223
- subscribe: (cb) => {
224
- void cb;
605
+ subscribe: (listener) => {
606
+ void listener;
225
607
  return () => {
226
608
  };
227
609
  },
@@ -230,33 +612,33 @@ function createReducedMotionWatcher() {
230
612
  }
231
613
  };
232
614
  }
233
- const mql = matchMedia("(prefers-reduced-motion: reduce)");
234
- const subs = /* @__PURE__ */ new Set();
235
- let last = computeScale(mql.matches);
615
+ const mediaQueryList = matchMedia("(prefers-reduced-motion: reduce)");
616
+ const subscriptions = /* @__PURE__ */ new Set();
617
+ let lastComputedScale = computeScale(mediaQueryList.matches);
236
618
  const onChange = () => {
237
- const next = computeScale(mql.matches);
238
- if (next !== last) {
239
- last = next;
240
- for (const cb of subs) cb(next);
619
+ const next = computeScale(mediaQueryList.matches);
620
+ if (next !== lastComputedScale) {
621
+ lastComputedScale = next;
622
+ for (const listener of subscriptions) listener(next);
241
623
  }
242
624
  };
243
- mql.addEventListener("change", onChange);
625
+ mediaQueryList.addEventListener("change", onChange);
244
626
  const watcher = {
245
- scale: () => last,
246
- subscribe(cb) {
247
- subs.add(cb);
248
- return () => subs.delete(cb);
627
+ scale: () => lastComputedScale,
628
+ subscribe(listener) {
629
+ subscriptions.add(listener);
630
+ return () => subscriptions.delete(listener);
249
631
  },
250
632
  recompute() {
251
- const next = computeScale(mql.matches);
252
- if (next !== last) {
253
- last = next;
254
- for (const cb of subs) cb(next);
633
+ const next = computeScale(mediaQueryList.matches);
634
+ if (next !== lastComputedScale) {
635
+ lastComputedScale = next;
636
+ for (const listener of subscriptions) listener(next);
255
637
  }
256
638
  },
257
639
  dispose() {
258
- mql.removeEventListener("change", onChange);
259
- subs.clear();
640
+ mediaQueryList.removeEventListener("change", onChange);
641
+ subscriptions.clear();
260
642
  state.watchers.delete(watcher);
261
643
  }
262
644
  };
@@ -278,7 +660,7 @@ function getReducedMotionTimeScale() {
278
660
  }
279
661
 
280
662
  // src/primitives/time/time.ts
281
- var time = _builtinTime.mul(getReducedMotionTimeScale());
663
+ var elapsedTime = _builtinTime.mul(getReducedMotionTimeScale());
282
664
 
283
665
  // src/primitives/cursor-ripple/cursor-ripple.ts
284
666
  function cursorRipple(p, center, opts = {}) {
@@ -286,20 +668,31 @@ function cursorRipple(p, center, opts = {}) {
286
668
  const frequency = opts.frequency ?? 30;
287
669
  const speed = opts.speed ?? 6;
288
670
  const amplitude = opts.amplitude ?? 0.5;
289
- const d = length2(sub2(p, center));
290
- const wave = sin(d.mul(frequency).sub(time.mul(speed)));
671
+ const d = length4(sub2(p, center));
672
+ const wave = sin3(d.mul(frequency).sub(elapsedTime.mul(speed)));
291
673
  const decay = smoothstep(reach, 0, d);
292
674
  return wave.mul(amplitude).mul(decay);
293
675
  }
294
676
 
295
677
  // src/primitives/film-grain/film-grain.ts
296
- import { fract, length as length3, sin as sin2, vec2 } from "three/tsl";
297
- function filmGrain(uvNode, intensity, timeOffset = 0) {
298
- const HASH_C1 = vec2(2127.1, 81.17);
299
- const HASH_C2 = vec2(1269.5, 283.37);
300
- const base = vec2(uvNode.dot(HASH_C1).add(timeOffset), uvNode.dot(HASH_C2).add(timeOffset));
301
- const hash = fract(sin2(base).mul(43758.5453));
302
- return length3(hash).sub(0.765).mul(intensity);
678
+ import { hash, mul as mul2, screenCoordinate } from "three/tsl";
679
+ function filmGrain(intensity, timeOffset = 0) {
680
+ const pixel = screenCoordinate.xy.floor();
681
+ const seed = pixel.x.toUint().mul(1973).add(pixel.y.toUint().mul(9277)).add(mul2(timeOffset, 26699).toUint());
682
+ return hash(seed).sub(0.5).mul(intensity);
683
+ }
684
+
685
+ // src/primitives/dither/dither.ts
686
+ import { dot, fract as fract3, sin as sin4, vec2 as vec23, vec3 as vec39 } from "three/tsl";
687
+ function hash21(coord) {
688
+ return fract3(sin4(dot(coord, vec23(12.9898, 78.233))).mul(43758.5453));
689
+ }
690
+ function dither(color, coord, amount = 1 / 255) {
691
+ const pixelCoord = vec23(coord);
692
+ const firstHash = hash21(pixelCoord);
693
+ const secondHash = hash21(pixelCoord.add(vec23(0.5, 0.5)));
694
+ const triangularNoise = firstHash.sub(secondHash).mul(0.5);
695
+ return vec39(color).add(triangularNoise.mul(amount));
303
696
  }
304
697
 
305
698
  // src/runtime/visibility/visibility.ts
@@ -313,21 +706,21 @@ function createVisibilityWatcher() {
313
706
  }
314
707
  };
315
708
  }
316
- const subs = /* @__PURE__ */ new Set();
709
+ const subscriptions = /* @__PURE__ */ new Set();
317
710
  const onChange = () => {
318
- const v = document.visibilityState === "visible";
319
- for (const cb of subs) cb(v);
711
+ const isVisible = document.visibilityState === "visible";
712
+ for (const listener of subscriptions) listener(isVisible);
320
713
  };
321
714
  document.addEventListener("visibilitychange", onChange);
322
715
  return {
323
716
  isVisible: () => document.visibilityState === "visible",
324
- subscribe(cb) {
325
- subs.add(cb);
326
- return () => subs.delete(cb);
717
+ subscribe(listener) {
718
+ subscriptions.add(listener);
719
+ return () => subscriptions.delete(listener);
327
720
  },
328
721
  dispose() {
329
722
  document.removeEventListener("visibilitychange", onChange);
330
- subs.clear();
723
+ subscriptions.clear();
331
724
  }
332
725
  };
333
726
  }
@@ -343,27 +736,27 @@ function createIntersectionWatcher(canvas) {
343
736
  }
344
737
  };
345
738
  }
346
- const subs = /* @__PURE__ */ new Set();
739
+ const subscriptions = /* @__PURE__ */ new Set();
347
740
  let inView = true;
348
- const obs = new IntersectionObserver(
741
+ const observer = new IntersectionObserver(
349
742
  (entries) => {
350
- const next = entries.some((e) => e.isIntersecting);
743
+ const next = entries.some((entry) => entry.isIntersecting);
351
744
  if (next === inView) return;
352
745
  inView = next;
353
- for (const cb of subs) cb(inView);
746
+ for (const listener of subscriptions) listener(inView);
354
747
  },
355
748
  { threshold: 0 }
356
749
  );
357
- obs.observe(canvas);
750
+ observer.observe(canvas);
358
751
  return {
359
752
  isInView: () => inView,
360
- subscribe(cb) {
361
- subs.add(cb);
362
- return () => subs.delete(cb);
753
+ subscribe(listener) {
754
+ subscriptions.add(listener);
755
+ return () => subscriptions.delete(listener);
363
756
  },
364
757
  dispose() {
365
- obs.disconnect();
366
- subs.clear();
758
+ observer.disconnect();
759
+ subscriptions.clear();
367
760
  }
368
761
  };
369
762
  }
@@ -374,10 +767,19 @@ var FrameScheduler = class {
374
767
  rafId = null;
375
768
  running = false;
376
769
  paused = false;
377
- idle = false;
378
770
  flushPending = false;
379
771
  startedAt = 0;
380
772
  lastTickAt = 0;
773
+ // Reference-counted idle voting. The scheduler is idle only when at least
774
+ // one component has voted idle AND no component has voted animated. This
775
+ // prevents a static component (e.g. LinearGradient speed=0) from halting
776
+ // the loop while an animated overlay (e.g. FilmGrain) is still running.
777
+ idleVotes = 0;
778
+ animatedVotes = 0;
779
+ /** True when all participating components prefer idle and none need animation. */
780
+ get idle() {
781
+ return this.idleVotes > 0 && this.animatedVotes === 0;
782
+ }
381
783
  /** Activate the scheduler. The rAF loop starts on the first client added. */
382
784
  start() {
383
785
  this.running = true;
@@ -413,19 +815,39 @@ var FrameScheduler = class {
413
815
  this.clients.clear();
414
816
  }
415
817
  /**
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.
818
+ * Cast a vote on whether the scheduler should be idle.
819
+ *
820
+ * `setIdle(true)` increments the idle-vote count; the returned cleanup
821
+ * decrements it. `setIdle(false)` increments the animated-vote count;
822
+ * its cleanup decrements that. The scheduler halts (after one flush tick)
823
+ * only when `idleVotes > 0 && animatedVotes === 0`.
824
+ *
825
+ * Callers are responsible for calling the returned cleanup on unmount.
826
+ * Use `requestRender()` or cast a `setIdle(false)` vote to wake the loop
827
+ * without permanently registering an animated preference.
419
828
  */
420
829
  setIdle(idle) {
421
- if (this.idle === idle) return;
422
- this.idle = idle;
423
830
  if (idle) {
424
- this.flushPending = true;
425
- this.maybeQueue();
831
+ const wasIdle = this.idle;
832
+ this.idleVotes += 1;
833
+ const nowIdle = this.idle;
834
+ if (!wasIdle && nowIdle) this.onBecameIdle();
835
+ return () => {
836
+ const prevIdle = this.idle;
837
+ this.idleVotes = Math.max(0, this.idleVotes - 1);
838
+ const afterIdle = this.idle;
839
+ if (prevIdle && !afterIdle) this.onBecameAnimated();
840
+ };
426
841
  } else {
427
- this.flushPending = false;
428
- this.maybeQueue();
842
+ const wasIdle = this.idle;
843
+ this.animatedVotes += 1;
844
+ if (wasIdle) this.onBecameAnimated();
845
+ return () => {
846
+ const prevIdle = this.idle;
847
+ this.animatedVotes = Math.max(0, this.animatedVotes - 1);
848
+ const nowIdle = this.idle;
849
+ if (!prevIdle && nowIdle) this.onBecameIdle();
850
+ };
429
851
  }
430
852
  }
431
853
  /** Force a single tick while idle. Useful for prop-change invalidation. */
@@ -434,6 +856,14 @@ var FrameScheduler = class {
434
856
  this.flushPending = true;
435
857
  this.maybeQueue();
436
858
  }
859
+ onBecameIdle() {
860
+ this.flushPending = true;
861
+ this.maybeQueue();
862
+ }
863
+ onBecameAnimated() {
864
+ this.flushPending = false;
865
+ this.maybeQueue();
866
+ }
437
867
  maybeQueue() {
438
868
  if (this.rafId !== null) return;
439
869
  if (!this.running) return;
@@ -475,15 +905,21 @@ export {
475
905
  createVisibilityWatcher,
476
906
  cursorRipple,
477
907
  displace,
478
- fbm,
908
+ dither,
909
+ elapsedTime,
479
910
  filmGrain,
911
+ fractalNoise,
480
912
  getReducedMotionPolicy,
481
913
  getReducedMotionTimeScale,
482
- noise,
914
+ mixColor,
915
+ oklabToLinearSrgb,
916
+ oklchToLinearSrgb,
917
+ parseColorString,
483
918
  quantize,
484
- sdfCircle,
485
919
  setReducedMotionPolicy,
486
- time,
920
+ signedDistanceFieldCircle,
921
+ simplexNoise,
922
+ srgbChannelToLinear,
487
923
  voronoi
488
924
  };
489
925
  //# sourceMappingURL=index.js.map