@lovo/matter 0.5.0 → 1.0.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.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Color } from 'three';
2
2
  import { WebGPURenderer, Node } from 'three/webgpu';
3
- import { ShaderNodeObject } from 'three/tsl';
3
+ import { ShaderNodeObject, uniform } from 'three/tsl';
4
+
5
+ /** The output color gamut the renderer encodes its framebuffer for. */
6
+ type OutputGamut = 'srgb' | 'p3';
4
7
 
5
8
  type GpuBackend = 'webgpu' | 'webgl2';
6
9
  interface CreateRendererOptions {
@@ -14,6 +17,8 @@ interface CreateRendererOptions {
14
17
  clearAlpha?: number;
15
18
  /** Cap on devicePixelRatio. Default: 2. Pass Infinity to disable. */
16
19
  maxDPR?: number;
20
+ /** Output color gamut the framebuffer is encoded for. Default: 'srgb'. */
21
+ gamut?: OutputGamut;
17
22
  }
18
23
  interface GpuRenderer {
19
24
  /** The underlying Three.js WebGPURenderer (which may be running on a WebGL2 backend). */
@@ -99,6 +104,16 @@ declare class CursorInput {
99
104
  dispose(): void;
100
105
  }
101
106
 
107
+ /** Interpolation space for blending colors. Always converts via linear-sRGB. */
108
+ type ColorSpace = 'linear' | 'oklab' | 'oklch' | 'lch' | 'hsl' | 'hsv';
109
+ /**
110
+ * Which way around the hue wheel cylindrical spaces (oklch/lch/hsl/hsv)
111
+ * interpolate — the CSS Color 4 `hue-interpolation-method` keywords.
112
+ * `shorter`/`longer` pick the arc by length; `increasing`/`decreasing` pick it
113
+ * by direction (and keep multi-stop ramps marching one way around the wheel).
114
+ */
115
+ type HueInterpolation = 'shorter' | 'longer' | 'increasing' | 'decreasing';
116
+
102
117
  /**
103
118
  * Canonical TSL-node *input* shape used throughout `@lovo/matter`.
104
119
  *
@@ -112,7 +127,7 @@ declare class CursorInput {
112
127
  */
113
128
  type TSLNode = Node | ShaderNodeObject<Node>;
114
129
  interface ColorRampStop {
115
- /** Color expressed as a TSL node (typically `vec3(r,g,b)`). */
130
+ /** Color expressed as a TSL node (typically `vec3(r,g,b)`), in linear-sRGB. */
116
131
  color: TSLNode;
117
132
  /** Position 0..1 along the ramp. */
118
133
  position: number;
@@ -121,9 +136,50 @@ interface ColorRampStop {
121
136
  * Multi-stop color interpolation. Given a t in [0..1] and N color stops at
122
137
  * fixed positions, returns the smoothly-interpolated color.
123
138
  *
139
+ * `colorSpace` controls the interpolation space (default `'linear'` — a plain
140
+ * per-channel mix that preserves the input values). Stops are converted into
141
+ * the space up front, the nested-mix chain runs IN that space, and the result
142
+ * is converted back to linear-sRGB once at the end.
143
+ *
144
+ * `hueInterpolation` chooses which way around the wheel cylindrical spaces
145
+ * travel (default `'shorter'`); it's inert for rectangular spaces (linear/oklab).
146
+ *
124
147
  * Falls back to the first/last stop's color outside the bracketing positions.
125
148
  */
126
- declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): ShaderNodeObject<Node>;
149
+ declare function colorRamp(t: TSLNode, stops: ColorRampStop[], colorSpace?: ColorSpace, hueInterpolation?: HueInterpolation): ShaderNodeObject<Node>;
150
+
151
+ /**
152
+ * Blend two linear-sRGB colors in `colorSpace`: convert both endpoints into the
153
+ * space, interpolate, convert back to linear-sRGB. `hueInterpolation` chooses
154
+ * the hue-wheel direction for cylindrical spaces (default `'shorter'`; inert for
155
+ * rectangular spaces). The result is NOT clamped — extended (out-of-sRGB) values
156
+ * are preserved so a wide-gamut (P3) output can display them; an sRGB output
157
+ * clamps per-channel at the framebuffer, identical to the prior behavior.
158
+ */
159
+ declare function mixColor(colorA: TSLNode, colorB: TSLNode, t: TSLNode, colorSpace?: ColorSpace, hueInterpolation?: HueInterpolation): ShaderNodeObject<Node>;
160
+
161
+ /**
162
+ * OKLab (L, a, b) -> extended linear-sRGB. CPU mirror of the TSL `oklabToLinear`
163
+ * in `oklab.ts` (same M2^-1 / cube / M1^-1 matrices). The result is NOT clamped:
164
+ * colors outside sRGB return channels below 0 or above 1, which a wide-gamut
165
+ * output can render and an sRGB output clamps at the framebuffer.
166
+ */
167
+ declare function oklabToLinearSrgb(lightness: number, greenRed: number, blueYellow: number): [number, number, number];
168
+ /** OKLch (L, C, h-in-degrees) -> extended linear-sRGB. */
169
+ declare function oklchToLinearSrgb(lightness: number, chroma: number, hueDegrees: number): [number, number, number];
170
+ /**
171
+ * Parse a color string to **extended** linear-sRGB. Accepts `#rrggbb`,
172
+ * `oklab(L a b)`, and `oklch(L C H)` (CSS Color 4 syntax: L/C may be percentages,
173
+ * H may carry a `deg` suffix, an optional `/ alpha` is parsed and dropped).
174
+ * Throws on any other syntax.
175
+ */
176
+ declare function parseColorString(input: string): [number, number, number];
177
+
178
+ /**
179
+ * sRGB-encoded channel in [0,1] -> linear-sRGB. Standard sRGB EOTF.
180
+ * Mirrors three's `convertSRGBToLinear` (e.g. 0.5 -> 0.21404114).
181
+ */
182
+ declare function srgbChannelToLinear(channel: number): number;
127
183
 
128
184
  /**
129
185
  * 2D simplex noise sampled at a point. Returns a scalar TSL node in
@@ -258,7 +314,27 @@ declare function cursorRipple(p: TSLNode, center: TSLNode, opts?: CursorRippleOp
258
314
 
259
315
  declare const elapsedTime: ShaderNodeObject<Node>;
260
316
 
261
- declare function filmGrain(intensity: ShaderNodeObject<Node> | number, timeOffset?: ShaderNodeObject<Node> | number): ShaderNodeObject<Node>;
317
+ type TSLScalar = TSLNode | number;
318
+ declare function grain(intensity: TSLScalar, timeOffset?: TSLScalar): ShaderNodeObject<Node>;
319
+
320
+ /**
321
+ * Add a sub-LSB ordered dither to break up 8-bit quantization banding (most
322
+ * visible on smooth gradients and on wide-gamut/P3 output, where the same 256
323
+ * levels span a wider gamut so each step is a coarser perceptual jump).
324
+ *
325
+ * `amount` is the noise magnitude in the color's units (default ~1/255, roughly
326
+ * one 8-bit step). The dither is an ordered Bayer 8x8 pattern keyed on the
327
+ * per-pixel `screenCoordinate`, so the grain is a crisp one device-pixel cell
328
+ * regardless of geometry or zoom, and is deterministic (no temporal shimmer —
329
+ * important for static scenes).
330
+ *
331
+ * For correctness the dither belongs in display-encoded space, immediately
332
+ * before 8-bit quantization. In a Matter-managed scene that placement is handled
333
+ * for you (`ShaderScene` applies it as a final output stage after the color
334
+ * transfer). This primitive is the entry point for Mode 2 (your own r3f canvas),
335
+ * where you apply it to your shader's output yourself.
336
+ */
337
+ declare function dither(color: TSLNode, amount?: number): ShaderNodeObject<Node>;
262
338
 
263
339
  type ReducedMotionPolicy = 'auto' | 'off' | 'slow' | 'paused';
264
340
  /**
@@ -294,7 +370,7 @@ declare function createReducedMotionWatcher(): ReducedMotionWatcher;
294
370
  * imperatively when policy changes is safe — TSL re-reads the uniform every
295
371
  * frame.
296
372
  */
297
- declare function getReducedMotionTimeScale(): ShaderNodeObject<Node>;
373
+ declare function getReducedMotionTimeScale(): ReturnType<typeof uniform<number>>;
298
374
 
299
375
  interface VisibilityWatcher {
300
376
  isVisible(): boolean;
@@ -380,4 +456,4 @@ declare class FrameScheduler {
380
456
  private readonly frame;
381
457
  }
382
458
 
383
- export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FractalNoiseOptions, FrameScheduler, type GpuBackend, type GpuRenderer, type IntersectionWatcher, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vector2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, elapsedTime, filmGrain, fractalNoise, getReducedMotionPolicy, getReducedMotionTimeScale, quantize, setReducedMotionPolicy, signedDistanceFieldCircle, simplexNoise, voronoi };
459
+ export { type ColorRampStop, type ColorSpace, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FractalNoiseOptions, FrameScheduler, type GpuBackend, type GpuRenderer, type HueInterpolation, type IntersectionWatcher, type OutputGamut, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vector2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, dither, elapsedTime, fractalNoise, getReducedMotionPolicy, getReducedMotionTimeScale, grain, mixColor, oklabToLinearSrgb, oklchToLinearSrgb, parseColorString, quantize, setReducedMotionPolicy, signedDistanceFieldCircle, simplexNoise, srgbChannelToLinear, voronoi };
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
62
  const canvasWidth = canvas.clientWidth;
23
63
  const canvasHeight = canvas.clientHeight;
24
- if (canvas.width !== canvasWidth * three.getPixelRatio() || canvas.height !== canvasHeight * three.getPixelRatio()) {
64
+ if (canvasWidth === 0 || canvasHeight === 0) return;
65
+ three.getSize(rendererSize);
66
+ if (rendererSize.width !== canvasWidth || rendererSize.height !== canvasHeight) {
25
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,
@@ -114,23 +157,359 @@ var clamp01 = (value) => Math.max(0, Math.min(1, value));
114
157
  var lerp = (startValue, endValue, blendFactor) => startValue + (endValue - startValue) * blendFactor;
115
158
 
116
159
  // src/primitives/color-ramp/color-ramp.ts
117
- import { mix, vec3 } from "three/tsl";
118
- import { clamp, div, sub } from "three/tsl";
119
- 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];
120
422
  const first = stops[0];
121
- if (first === void 0) return vec3(0, 0, 0);
122
- if (stops.length === 1) return mix(first.color, first.color, 0);
123
- 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;
124
427
  for (let i = 1; i < stops.length; i += 1) {
125
428
  const previousStop = stops[i - 1];
126
429
  const next = stops[i];
127
430
  if (previousStop === void 0 || next === void 0) continue;
128
431
  const positionSpan = next.position - previousStop.position;
129
432
  if (positionSpan <= 0) continue;
130
- const localT = clamp(div(sub(t, previousStop.position), positionSpan), 0, 1);
131
- result = mix(result, next.color, localT);
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;
132
473
  }
133
- 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(...).`);
134
513
  }
135
514
 
136
515
  // src/primitives/noise/noise.ts
@@ -176,9 +555,9 @@ function quantize(t, steps) {
176
555
  }
177
556
 
178
557
  // src/primitives/sdf-circle/sdf-circle.ts
179
- import { length } from "three/tsl";
558
+ import { length as length3 } from "three/tsl";
180
559
  function signedDistanceFieldCircle(p, radius) {
181
- return length(p).sub(radius);
560
+ return length3(p).sub(radius);
182
561
  }
183
562
 
184
563
  // src/primitives/displace/displace.ts
@@ -188,7 +567,7 @@ function displace(p, by) {
188
567
  }
189
568
 
190
569
  // src/primitives/cursor-ripple/cursor-ripple.ts
191
- 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";
192
571
 
193
572
  // src/primitives/time/time.ts
194
573
  import { time as _builtinTime } from "three/tsl";
@@ -289,20 +668,41 @@ function cursorRipple(p, center, opts = {}) {
289
668
  const frequency = opts.frequency ?? 30;
290
669
  const speed = opts.speed ?? 6;
291
670
  const amplitude = opts.amplitude ?? 0.5;
292
- const d = length2(sub2(p, center));
293
- const wave = sin(d.mul(frequency).sub(elapsedTime.mul(speed)));
671
+ const d = length4(sub2(p, center));
672
+ const wave = sin3(d.mul(frequency).sub(elapsedTime.mul(speed)));
294
673
  const decay = smoothstep(reach, 0, d);
295
674
  return wave.mul(amplitude).mul(decay);
296
675
  }
297
676
 
298
- // src/primitives/film-grain/film-grain.ts
299
- import { hash, mul as mul2, screenCoordinate } from "three/tsl";
300
- function filmGrain(intensity, timeOffset = 0) {
677
+ // src/primitives/grain/grain.ts
678
+ import { float, hash, screenCoordinate } from "three/tsl";
679
+ function grain(intensity, timeOffset = 0) {
301
680
  const pixel = screenCoordinate.xy.floor();
302
- const seed = pixel.x.toUint().mul(1973).add(pixel.y.toUint().mul(9277)).add(mul2(timeOffset, 26699).toUint());
681
+ const column = pixel.x.toUint();
682
+ const row = pixel.y.toUint();
683
+ const frameHash = hash(float(timeOffset)).mul(16777215).toUint();
684
+ const rowHash = hash(row.add(frameHash)).mul(16777215).toUint();
685
+ const seed = column.add(rowHash);
303
686
  return hash(seed).sub(0.5).mul(intensity);
304
687
  }
305
688
 
689
+ // src/primitives/dither/dither.ts
690
+ import { floor, fract as fract3, screenCoordinate as screenCoordinate2, vec2 as vec23, vec3 as vec39, vec4 as vec43 } from "three/tsl";
691
+ function bayer2(coord) {
692
+ const cell = floor(coord);
693
+ return fract3(cell.x.mul(0.5).add(cell.y.mul(cell.y).mul(0.75)));
694
+ }
695
+ function bayer4(coord) {
696
+ return bayer2(coord.mul(0.5)).mul(0.25).add(bayer2(coord));
697
+ }
698
+ function bayer8(coord) {
699
+ return bayer4(coord.mul(0.5)).mul(0.25).add(bayer2(coord));
700
+ }
701
+ function dither(color, amount = 1 / 255) {
702
+ const threshold = bayer8(vec23(screenCoordinate2.xy)).sub(0.5);
703
+ return vec43(vec39(color).add(threshold.mul(amount)), vec43(color).a);
704
+ }
705
+
306
706
  // src/runtime/visibility/visibility.ts
307
707
  function createVisibilityWatcher() {
308
708
  if (typeof document === "undefined") {
@@ -381,7 +781,7 @@ var FrameScheduler = class {
381
781
  // Reference-counted idle voting. The scheduler is idle only when at least
382
782
  // one component has voted idle AND no component has voted animated. This
383
783
  // prevents a static component (e.g. LinearGradient speed=0) from halting
384
- // the loop while an animated overlay (e.g. FilmGrain) is still running.
784
+ // the loop while an animated overlay (e.g. Grain) is still running.
385
785
  idleVotes = 0;
386
786
  animatedVotes = 0;
387
787
  /** True when all participating components prefer idle and none need animation. */
@@ -513,15 +913,21 @@ export {
513
913
  createVisibilityWatcher,
514
914
  cursorRipple,
515
915
  displace,
916
+ dither,
516
917
  elapsedTime,
517
- filmGrain,
518
918
  fractalNoise,
519
919
  getReducedMotionPolicy,
520
920
  getReducedMotionTimeScale,
921
+ grain,
922
+ mixColor,
923
+ oklabToLinearSrgb,
924
+ oklchToLinearSrgb,
925
+ parseColorString,
521
926
  quantize,
522
927
  setReducedMotionPolicy,
523
928
  signedDistanceFieldCircle,
524
929
  simplexNoise,
930
+ srgbChannelToLinear,
525
931
  voronoi
526
932
  };
527
933
  //# sourceMappingURL=index.js.map