@lovo/matter 0.5.0 → 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.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,24 @@ 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 filmGrain(intensity: TSLScalar, timeOffset?: TSLScalar): ShaderNodeObject<Node>;
319
+
320
+ /**
321
+ * Add sub-LSB dither to break up 8-bit quantization banding (most visible on
322
+ * wide-gamut/P3 output, where the same 256 levels span a wider gamut).
323
+ *
324
+ * `coord` is a per-pixel coordinate (pass `uv()`); `amount` is the noise
325
+ * magnitude in the color's units (default ~1/255, roughly one 8-bit step). Uses a
326
+ * triangular PDF (difference of two hashes) for flatter, less "gritty" noise than
327
+ * uniform white noise.
328
+ *
329
+ * SPIKE NOTE: applied here in linear-sRGB working space (before the renderer's
330
+ * output transfer), so the effective dither is uneven across tones (over-dithers
331
+ * shadows, under-dithers highlights). The correct home is a final output pass
332
+ * after color-space conversion; this is good enough to evaluate the banding fix.
333
+ */
334
+ declare function dither(color: TSLNode, coord: TSLNode, amount?: number): ShaderNodeObject<Node>;
262
335
 
263
336
  type ReducedMotionPolicy = 'auto' | 'off' | 'slow' | 'paused';
264
337
  /**
@@ -294,7 +367,7 @@ declare function createReducedMotionWatcher(): ReducedMotionWatcher;
294
367
  * imperatively when policy changes is safe — TSL re-reads the uniform every
295
368
  * frame.
296
369
  */
297
- declare function getReducedMotionTimeScale(): ShaderNodeObject<Node>;
370
+ declare function getReducedMotionTimeScale(): ReturnType<typeof uniform<number>>;
298
371
 
299
372
  interface VisibilityWatcher {
300
373
  isVisible(): boolean;
@@ -380,4 +453,4 @@ declare class FrameScheduler {
380
453
  private readonly frame;
381
454
  }
382
455
 
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 };
456
+ 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, filmGrain, fractalNoise, getReducedMotionPolicy, getReducedMotionTimeScale, 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);
132
436
  }
133
- return result;
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;
473
+ }
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,8 +668,8 @@ 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
  }
@@ -303,6 +682,19 @@ function filmGrain(intensity, timeOffset = 0) {
303
682
  return hash(seed).sub(0.5).mul(intensity);
304
683
  }
305
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));
696
+ }
697
+
306
698
  // src/runtime/visibility/visibility.ts
307
699
  function createVisibilityWatcher() {
308
700
  if (typeof document === "undefined") {
@@ -513,15 +905,21 @@ export {
513
905
  createVisibilityWatcher,
514
906
  cursorRipple,
515
907
  displace,
908
+ dither,
516
909
  elapsedTime,
517
910
  filmGrain,
518
911
  fractalNoise,
519
912
  getReducedMotionPolicy,
520
913
  getReducedMotionTimeScale,
914
+ mixColor,
915
+ oklabToLinearSrgb,
916
+ oklchToLinearSrgb,
917
+ parseColorString,
521
918
  quantize,
522
919
  setReducedMotionPolicy,
523
920
  signedDistanceFieldCircle,
524
921
  simplexNoise,
922
+ srgbChannelToLinear,
525
923
  voronoi
526
924
  };
527
925
  //# sourceMappingURL=index.js.map