@lovo/matter 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { WebGPURenderer, Node } from 'three/webgpu';
2
2
  import { Color } from 'three';
3
3
  import { ShaderNodeObject } from 'three/tsl';
4
- export { cos, dot, length, max, min, mix, mod, normalize, sin, smoothstep, uniform, uv, vec2, vec3, vec4 } from 'three/tsl';
5
4
 
6
5
  type MatterBackend = 'webgpu' | 'webgl2';
7
6
  interface CreateRendererOptions {
@@ -150,6 +149,17 @@ declare class CursorInput {
150
149
  dispose(): void;
151
150
  }
152
151
 
152
+ /**
153
+ * Canonical TSL-node *input* shape used throughout `@lovo/matter`.
154
+ *
155
+ * Stays as the broad `Node | ShaderNodeObject<Node>` union so callers can
156
+ * pass uniform-typed nodes (e.g. `ShaderNodeObject<UniformNode<Vector2>>`)
157
+ * without casting at the call site — those are subtypes of `Node` but NOT
158
+ * subtypes of `ShaderNodeObject<Node>` due to invariant generic parameters.
159
+ *
160
+ * Wrappers should return the narrower `ShaderNodeObject<Node>` so the
161
+ * **output** is always chainable without casts.
162
+ */
153
163
  type TSLNode = Node | ShaderNodeObject<Node>;
154
164
  interface ColorRampStop {
155
165
  /** Color expressed as a TSL node (typically `vec3(r,g,b)`). */
@@ -163,7 +173,7 @@ interface ColorRampStop {
163
173
  *
164
174
  * Falls back to the first/last stop's color outside the bracketing positions.
165
175
  */
166
- declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): TSLNode;
176
+ declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): ShaderNodeObject<Node>;
167
177
 
168
178
  /**
169
179
  * 2D simplex noise sampled at a point. Returns a scalar TSL node in
@@ -174,8 +184,11 @@ declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): TSLNode;
174
184
  * Built on top of three's `mx_noise_float`; we wrap it so consumers have a
175
185
  * stable import path through `@lovo/matter` and we can swap the
176
186
  * implementation if a different noise primitive proves better in practice.
187
+ *
188
+ * Returns `ShaderNodeObject<Node>` (chainable) rather than the broader
189
+ * `TSLNode` union, so callers can `.add(...)`/`.mul(...)` without casting.
177
190
  */
178
- declare function noise(p: TSLNode): TSLNode;
191
+ declare function noise(p: TSLNode): ShaderNodeObject<Node>;
179
192
 
180
193
  interface FBMOptions {
181
194
  /** Number of octaves to sum. JS-side number — fixed at TSL build time, not a uniform. Default: 4. */
@@ -188,17 +201,27 @@ interface FBMOptions {
188
201
  /**
189
202
  * Fractal Brownian Motion — sum of N octaves of 2D simplex noise.
190
203
  *
204
+ * Each octave samples noise at a higher frequency (× `lacunarity`) and lower
205
+ * amplitude (× `gain`) than the previous one, AND at a translated coordinate
206
+ * so the octaves sample uncorrelated regions of noise space. Without the
207
+ * per-octave translation, octaves at related frequencies tend to pile up
208
+ * peaks and troughs at the same input coordinates, producing visibly muddy
209
+ * "spotty" output. With it, the octaves look like independent noise patterns
210
+ * layered together — Inigo Quilez's classic FBM technique.
211
+ *
191
212
  * `octaves`, `lacunarity`, and `gain` are JavaScript numbers (NOT TSL
192
213
  * uniforms) because the loop must be unrolled at TSL-build time — TSL has
193
214
  * no dynamic-length loop primitive that maps cleanly to all backends.
194
215
  * Animatable parameters that *do* survive on the GPU are the input UV
195
216
  * (which the caller can scale/translate per frame) and `time`.
196
217
  *
197
- * @param p Vec2 TSL node (UV-space position).
198
- * @returns scalar TSL node, roughly [-1..1] but normalized closer to
199
- * [-0.5..0.5] when amplitude sums approach 1 with the default gain.
218
+ * Returns `ShaderNodeObject<Node>` (chainable) for cast-free call sites.
219
+ *
220
+ * @param p Vec2 or Vec3 TSL node (UV-space position).
221
+ * @returns scalar TSL node, normalized to roughly [-1..1] regardless of
222
+ * octave count thanks to the amplitude-sum division at the end.
200
223
  */
201
- declare function fbm(p: TSLNode, opts?: FBMOptions): TSLNode;
224
+ declare function fbm(p: TSLNode, opts?: FBMOptions): ShaderNodeObject<Node>;
202
225
 
203
226
  /**
204
227
  * 2D voronoi (Worley) noise — distance to the nearest jittered cell point,
@@ -209,9 +232,11 @@ declare function fbm(p: TSLNode, opts?: FBMOptions): TSLNode;
209
232
  * a multi-color cellular pattern; threshold via `step`/`smoothstep` for
210
233
  * hard cell shapes.
211
234
  *
235
+ * Returns `ShaderNodeObject<Node>` (chainable) for cast-free call sites.
236
+ *
212
237
  * @param p — Vec2 TSL node, typically `uv() * scale`.
213
238
  */
214
- declare function voronoi(p: TSLNode): TSLNode;
239
+ declare function voronoi(p: TSLNode): ShaderNodeObject<Node>;
215
240
 
216
241
  /**
217
242
  * Quantize a scalar TSL node to `steps` discrete levels.
@@ -221,7 +246,7 @@ declare function voronoi(p: TSLNode): TSLNode;
221
246
  * `steps` is a JS-side number (loop-equivalent at TSL build time, baked in).
222
247
  * If you need an animatable step count, rebuild the TSL fragment.
223
248
  */
224
- declare function quantize(t: TSLNode, steps: number): TSLNode;
249
+ declare function quantize(t: ShaderNodeObject<Node>, steps: number): ShaderNodeObject<Node>;
225
250
 
226
251
  /**
227
252
  * Signed distance field for a circle centered at the origin.
@@ -234,7 +259,7 @@ declare function quantize(t: TSLNode, steps: number): TSLNode;
234
259
  * @param p — Vec2 TSL node (typically a UV-space offset from the center).
235
260
  * @param radius — JS-side scalar OR a scalar TSL node.
236
261
  */
237
- declare function sdfCircle(p: TSLNode, radius: TSLNode | number): TSLNode;
262
+ declare function sdfCircle(p: TSLNode, radius: TSLNode | number): ShaderNodeObject<Node>;
238
263
 
239
264
  /**
240
265
  * Naive vector addition: returns `p + by`.
@@ -252,7 +277,7 @@ declare function sdfCircle(p: TSLNode, radius: TSLNode | number): TSLNode;
252
277
  * @param p — Vec2 TSL node (the position being displaced).
253
278
  * @param by — Vec2 TSL node (the displacement vector).
254
279
  */
255
- declare function displace(p: TSLNode, by: TSLNode): TSLNode;
280
+ declare function displace(p: TSLNode, by: TSLNode): ShaderNodeObject<Node>;
256
281
 
257
282
  interface CursorRippleOptions {
258
283
  /** Decay radius (UV space). Beyond this, the ripple is ~0. Default: 0.4. */
@@ -279,18 +304,42 @@ interface CursorRippleOptions {
279
304
  * @param p — Vec2 TSL node (typically `uv()`).
280
305
  * @param center — Vec2 TSL node (cursor uniform, in UV space).
281
306
  */
282
- declare function cursorRipple(p: TSLNode, center: TSLNode, opts?: CursorRippleOptions): TSLNode;
307
+ declare function cursorRipple(p: TSLNode, center: TSLNode, opts?: CursorRippleOptions): ShaderNodeObject<Node>;
308
+
309
+ declare const time: ShaderNodeObject<Node>;
283
310
 
284
311
  /**
285
- * Engine-gated `time`: equals the TSL built-in `time` multiplied by the
286
- * reduced-motion scale uniform. Components consuming `time` from `@lovo/matter`
287
- * automatically respect `prefers-reduced-motion` and the policy override set
288
- * via `setReducedMotionPolicy`.
312
+ * Hash-based film grain chaotic, uncorrelated per-pixel noise sampled
313
+ * from `uvNode`. The output is *centered* around zero so it acts as a
314
+ * brightness-preserving texture overlay (half the pixels brighten by up
315
+ * to `intensity`, half darken, mean unchanged). ADD the result to a color.
316
+ *
317
+ * filmGrain(uv, k) → static grain
318
+ * filmGrain(uv, k, time) → twinkling grain. Pass a quantized time node
319
+ * (e.g. `time.mul(speed).mul(60).floor()`) so
320
+ * the grain re-randomizes at a controllable
321
+ * "shutter rate" instead of every frame.
322
+ *
323
+ * Recipe:
289
324
  *
290
- * If you want raw uncapped time (e.g. for a debug overlay), import
291
- * `time` from `three/tsl` directly.
325
+ * base = vec2(uv·c1, uv·c2) + timeOffset
326
+ * hash = fract(sin(base) * 43758.5453)
327
+ * out = (length(hash) - 0.765) * intensity
328
+ *
329
+ * `c1 = (2127.1, 81.17)` and `c2 = (1269.5, 283.37)` are arbitrary
330
+ * near-prime constants that produce visually-uncorrelated noise. `0.765`
331
+ * is the empirical mean of `length(vec2(u, v))` for uniform u, v ∈ [0, 1),
332
+ * computed once so we don't have to subtract it at runtime per pixel.
333
+ *
334
+ * For a film-stock look (darkens as grain rises — silver-emulsion
335
+ * physics) subtract the result from the color instead of adding.
336
+ *
337
+ * @param uvNode vec2 TSL node, typically `uv()`.
338
+ * @param intensity number or TSL node in [0, 1]; scales the grain.
339
+ * @param timeOffset optional number or TSL node added to each sample
340
+ * before hashing. `0` (default) → static grain.
292
341
  */
293
- declare const time: ShaderNodeObject<Node>;
342
+ declare function filmGrain(uvNode: ShaderNodeObject<Node>, intensity: ShaderNodeObject<Node> | number, timeOffset?: ShaderNodeObject<Node> | number): ShaderNodeObject<Node>;
294
343
 
295
344
  type ReducedMotionPolicy = 'auto' | 'off' | 'slow' | 'paused';
296
345
  /**
@@ -358,4 +407,4 @@ interface IntersectionWatcher {
358
407
  */
359
408
  declare function createIntersectionWatcher(canvas: HTMLCanvasElement): IntersectionWatcher;
360
409
 
361
- export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, type IntersectionWatcher, type MatterBackend, type MatterRenderer, MatterScheduler, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
410
+ export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, type IntersectionWatcher, type MatterBackend, type MatterRenderer, MatterScheduler, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, filmGrain, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { WebGPURenderer, Node } from 'three/webgpu';
2
2
  import { Color } from 'three';
3
3
  import { ShaderNodeObject } from 'three/tsl';
4
- export { cos, dot, length, max, min, mix, mod, normalize, sin, smoothstep, uniform, uv, vec2, vec3, vec4 } from 'three/tsl';
5
4
 
6
5
  type MatterBackend = 'webgpu' | 'webgl2';
7
6
  interface CreateRendererOptions {
@@ -150,6 +149,17 @@ declare class CursorInput {
150
149
  dispose(): void;
151
150
  }
152
151
 
152
+ /**
153
+ * Canonical TSL-node *input* shape used throughout `@lovo/matter`.
154
+ *
155
+ * Stays as the broad `Node | ShaderNodeObject<Node>` union so callers can
156
+ * pass uniform-typed nodes (e.g. `ShaderNodeObject<UniformNode<Vector2>>`)
157
+ * without casting at the call site — those are subtypes of `Node` but NOT
158
+ * subtypes of `ShaderNodeObject<Node>` due to invariant generic parameters.
159
+ *
160
+ * Wrappers should return the narrower `ShaderNodeObject<Node>` so the
161
+ * **output** is always chainable without casts.
162
+ */
153
163
  type TSLNode = Node | ShaderNodeObject<Node>;
154
164
  interface ColorRampStop {
155
165
  /** Color expressed as a TSL node (typically `vec3(r,g,b)`). */
@@ -163,7 +173,7 @@ interface ColorRampStop {
163
173
  *
164
174
  * Falls back to the first/last stop's color outside the bracketing positions.
165
175
  */
166
- declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): TSLNode;
176
+ declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): ShaderNodeObject<Node>;
167
177
 
168
178
  /**
169
179
  * 2D simplex noise sampled at a point. Returns a scalar TSL node in
@@ -174,8 +184,11 @@ declare function colorRamp(t: TSLNode, stops: ColorRampStop[]): TSLNode;
174
184
  * Built on top of three's `mx_noise_float`; we wrap it so consumers have a
175
185
  * stable import path through `@lovo/matter` and we can swap the
176
186
  * implementation if a different noise primitive proves better in practice.
187
+ *
188
+ * Returns `ShaderNodeObject<Node>` (chainable) rather than the broader
189
+ * `TSLNode` union, so callers can `.add(...)`/`.mul(...)` without casting.
177
190
  */
178
- declare function noise(p: TSLNode): TSLNode;
191
+ declare function noise(p: TSLNode): ShaderNodeObject<Node>;
179
192
 
180
193
  interface FBMOptions {
181
194
  /** Number of octaves to sum. JS-side number — fixed at TSL build time, not a uniform. Default: 4. */
@@ -188,17 +201,27 @@ interface FBMOptions {
188
201
  /**
189
202
  * Fractal Brownian Motion — sum of N octaves of 2D simplex noise.
190
203
  *
204
+ * Each octave samples noise at a higher frequency (× `lacunarity`) and lower
205
+ * amplitude (× `gain`) than the previous one, AND at a translated coordinate
206
+ * so the octaves sample uncorrelated regions of noise space. Without the
207
+ * per-octave translation, octaves at related frequencies tend to pile up
208
+ * peaks and troughs at the same input coordinates, producing visibly muddy
209
+ * "spotty" output. With it, the octaves look like independent noise patterns
210
+ * layered together — Inigo Quilez's classic FBM technique.
211
+ *
191
212
  * `octaves`, `lacunarity`, and `gain` are JavaScript numbers (NOT TSL
192
213
  * uniforms) because the loop must be unrolled at TSL-build time — TSL has
193
214
  * no dynamic-length loop primitive that maps cleanly to all backends.
194
215
  * Animatable parameters that *do* survive on the GPU are the input UV
195
216
  * (which the caller can scale/translate per frame) and `time`.
196
217
  *
197
- * @param p Vec2 TSL node (UV-space position).
198
- * @returns scalar TSL node, roughly [-1..1] but normalized closer to
199
- * [-0.5..0.5] when amplitude sums approach 1 with the default gain.
218
+ * Returns `ShaderNodeObject<Node>` (chainable) for cast-free call sites.
219
+ *
220
+ * @param p Vec2 or Vec3 TSL node (UV-space position).
221
+ * @returns scalar TSL node, normalized to roughly [-1..1] regardless of
222
+ * octave count thanks to the amplitude-sum division at the end.
200
223
  */
201
- declare function fbm(p: TSLNode, opts?: FBMOptions): TSLNode;
224
+ declare function fbm(p: TSLNode, opts?: FBMOptions): ShaderNodeObject<Node>;
202
225
 
203
226
  /**
204
227
  * 2D voronoi (Worley) noise — distance to the nearest jittered cell point,
@@ -209,9 +232,11 @@ declare function fbm(p: TSLNode, opts?: FBMOptions): TSLNode;
209
232
  * a multi-color cellular pattern; threshold via `step`/`smoothstep` for
210
233
  * hard cell shapes.
211
234
  *
235
+ * Returns `ShaderNodeObject<Node>` (chainable) for cast-free call sites.
236
+ *
212
237
  * @param p — Vec2 TSL node, typically `uv() * scale`.
213
238
  */
214
- declare function voronoi(p: TSLNode): TSLNode;
239
+ declare function voronoi(p: TSLNode): ShaderNodeObject<Node>;
215
240
 
216
241
  /**
217
242
  * Quantize a scalar TSL node to `steps` discrete levels.
@@ -221,7 +246,7 @@ declare function voronoi(p: TSLNode): TSLNode;
221
246
  * `steps` is a JS-side number (loop-equivalent at TSL build time, baked in).
222
247
  * If you need an animatable step count, rebuild the TSL fragment.
223
248
  */
224
- declare function quantize(t: TSLNode, steps: number): TSLNode;
249
+ declare function quantize(t: ShaderNodeObject<Node>, steps: number): ShaderNodeObject<Node>;
225
250
 
226
251
  /**
227
252
  * Signed distance field for a circle centered at the origin.
@@ -234,7 +259,7 @@ declare function quantize(t: TSLNode, steps: number): TSLNode;
234
259
  * @param p — Vec2 TSL node (typically a UV-space offset from the center).
235
260
  * @param radius — JS-side scalar OR a scalar TSL node.
236
261
  */
237
- declare function sdfCircle(p: TSLNode, radius: TSLNode | number): TSLNode;
262
+ declare function sdfCircle(p: TSLNode, radius: TSLNode | number): ShaderNodeObject<Node>;
238
263
 
239
264
  /**
240
265
  * Naive vector addition: returns `p + by`.
@@ -252,7 +277,7 @@ declare function sdfCircle(p: TSLNode, radius: TSLNode | number): TSLNode;
252
277
  * @param p — Vec2 TSL node (the position being displaced).
253
278
  * @param by — Vec2 TSL node (the displacement vector).
254
279
  */
255
- declare function displace(p: TSLNode, by: TSLNode): TSLNode;
280
+ declare function displace(p: TSLNode, by: TSLNode): ShaderNodeObject<Node>;
256
281
 
257
282
  interface CursorRippleOptions {
258
283
  /** Decay radius (UV space). Beyond this, the ripple is ~0. Default: 0.4. */
@@ -279,18 +304,42 @@ interface CursorRippleOptions {
279
304
  * @param p — Vec2 TSL node (typically `uv()`).
280
305
  * @param center — Vec2 TSL node (cursor uniform, in UV space).
281
306
  */
282
- declare function cursorRipple(p: TSLNode, center: TSLNode, opts?: CursorRippleOptions): TSLNode;
307
+ declare function cursorRipple(p: TSLNode, center: TSLNode, opts?: CursorRippleOptions): ShaderNodeObject<Node>;
308
+
309
+ declare const time: ShaderNodeObject<Node>;
283
310
 
284
311
  /**
285
- * Engine-gated `time`: equals the TSL built-in `time` multiplied by the
286
- * reduced-motion scale uniform. Components consuming `time` from `@lovo/matter`
287
- * automatically respect `prefers-reduced-motion` and the policy override set
288
- * via `setReducedMotionPolicy`.
312
+ * Hash-based film grain chaotic, uncorrelated per-pixel noise sampled
313
+ * from `uvNode`. The output is *centered* around zero so it acts as a
314
+ * brightness-preserving texture overlay (half the pixels brighten by up
315
+ * to `intensity`, half darken, mean unchanged). ADD the result to a color.
316
+ *
317
+ * filmGrain(uv, k) → static grain
318
+ * filmGrain(uv, k, time) → twinkling grain. Pass a quantized time node
319
+ * (e.g. `time.mul(speed).mul(60).floor()`) so
320
+ * the grain re-randomizes at a controllable
321
+ * "shutter rate" instead of every frame.
322
+ *
323
+ * Recipe:
289
324
  *
290
- * If you want raw uncapped time (e.g. for a debug overlay), import
291
- * `time` from `three/tsl` directly.
325
+ * base = vec2(uv·c1, uv·c2) + timeOffset
326
+ * hash = fract(sin(base) * 43758.5453)
327
+ * out = (length(hash) - 0.765) * intensity
328
+ *
329
+ * `c1 = (2127.1, 81.17)` and `c2 = (1269.5, 283.37)` are arbitrary
330
+ * near-prime constants that produce visually-uncorrelated noise. `0.765`
331
+ * is the empirical mean of `length(vec2(u, v))` for uniform u, v ∈ [0, 1),
332
+ * computed once so we don't have to subtract it at runtime per pixel.
333
+ *
334
+ * For a film-stock look (darkens as grain rises — silver-emulsion
335
+ * physics) subtract the result from the color instead of adding.
336
+ *
337
+ * @param uvNode vec2 TSL node, typically `uv()`.
338
+ * @param intensity number or TSL node in [0, 1]; scales the grain.
339
+ * @param timeOffset optional number or TSL node added to each sample
340
+ * before hashing. `0` (default) → static grain.
292
341
  */
293
- declare const time: ShaderNodeObject<Node>;
342
+ declare function filmGrain(uvNode: ShaderNodeObject<Node>, intensity: ShaderNodeObject<Node> | number, timeOffset?: ShaderNodeObject<Node> | number): ShaderNodeObject<Node>;
294
343
 
295
344
  type ReducedMotionPolicy = 'auto' | 'off' | 'slow' | 'paused';
296
345
  /**
@@ -358,4 +407,4 @@ interface IntersectionWatcher {
358
407
  */
359
408
  declare function createIntersectionWatcher(canvas: HTMLCanvasElement): IntersectionWatcher;
360
409
 
361
- export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, type IntersectionWatcher, type MatterBackend, type MatterRenderer, MatterScheduler, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
410
+ export { type ColorRampStop, type CreateRendererOptions, CursorInput, type CursorInputOptions, type CursorRippleOptions, type FBMOptions, type IntersectionWatcher, type MatterBackend, type MatterRenderer, MatterScheduler, type ReducedMotionPolicy, type ReducedMotionWatcher, type SchedulerClient, type SchedulerTick, type TSLNode, type Vec2, type VisibilityWatcher, colorRamp, createIntersectionWatcher, createReducedMotionWatcher, createRenderer, createVisibilityWatcher, cursorRipple, displace, fbm, filmGrain, getReducedMotionPolicy, getReducedMotionTimeScale, noise, quantize, sdfCircle, setReducedMotionPolicy, time, voronoi };
package/dist/index.js CHANGED
@@ -208,6 +208,7 @@ var lerp = (a, b, t) => a + (b - a) * t;
208
208
 
209
209
  // src/primitives/colorRamp.ts
210
210
  import { mix, vec3 } from "three/tsl";
211
+ import { clamp, div, sub } from "three/tsl";
211
212
  function colorRamp(t, stops) {
212
213
  if (stops.length === 0) return vec3(0, 0, 0);
213
214
  if (stops.length === 1) return stops[0].color;
@@ -217,8 +218,7 @@ function colorRamp(t, stops) {
217
218
  const next = stops[i];
218
219
  const span = next.position - prev.position;
219
220
  if (span <= 0) continue;
220
- const tNode = t;
221
- const localT = tNode.sub(prev.position).div(span).clamp(0, 1);
221
+ const localT = clamp(div(sub(t, prev.position), span), 0, 1);
222
222
  result = mix(result, next.color, localT);
223
223
  }
224
224
  return result;
@@ -231,6 +231,7 @@ function noise(p) {
231
231
  }
232
232
 
233
233
  // src/primitives/fbm.ts
234
+ import { add, mul } from "three/tsl";
234
235
  function fbm(p, opts = {}) {
235
236
  const octaves = opts.octaves ?? 4;
236
237
  const lacunarity = opts.lacunarity ?? 2;
@@ -243,7 +244,7 @@ function fbm(p, opts = {}) {
243
244
  freq *= lacunarity;
244
245
  amp *= gain;
245
246
  total += amp;
246
- const pAtFreq = p.mul(freq);
247
+ const pAtFreq = add(mul(p, freq), i * 100);
247
248
  const layer = noise(pAtFreq).mul(amp);
248
249
  sum = sum.add(layer);
249
250
  }
@@ -268,39 +269,19 @@ function quantize(t, steps) {
268
269
  // src/primitives/sdfCircle.ts
269
270
  import { length } from "three/tsl";
270
271
  function sdfCircle(p, radius) {
271
- const lp = length(p);
272
- if (typeof radius === "number") {
273
- return lp.sub(radius);
274
- }
275
- return lp.sub(radius);
272
+ return length(p).sub(radius);
276
273
  }
277
274
 
278
275
  // src/primitives/displace.ts
276
+ import { add as add2 } from "three/tsl";
279
277
  function displace(p, by) {
280
- return p.add(by);
278
+ return add2(p, by);
281
279
  }
282
280
 
283
281
  // src/primitives/cursorRipple.ts
284
- import { sin as sin2, length as length3, smoothstep as smoothstep2 } from "three/tsl";
282
+ import { sin, length as length2, smoothstep, sub as sub2 } from "three/tsl";
285
283
 
286
- // src/primitives/tsl-reexports.ts
287
- import {
288
- uniform as uniform2,
289
- vec2,
290
- vec3 as vec32,
291
- vec4,
292
- mix as mix2,
293
- smoothstep,
294
- mod,
295
- sin,
296
- cos,
297
- length as length2,
298
- dot,
299
- normalize,
300
- uv,
301
- max,
302
- min
303
- } from "three/tsl";
284
+ // src/primitives/time.ts
304
285
  import { time as _builtinTime } from "three/tsl";
305
286
 
306
287
  // src/runtime/reducedMotion.ts
@@ -390,10 +371,8 @@ function getReducedMotionTimeScale() {
390
371
  return globalScaleUniform;
391
372
  }
392
373
 
393
- // src/primitives/tsl-reexports.ts
394
- var time = _builtinTime.mul(
395
- getReducedMotionTimeScale()
396
- );
374
+ // src/primitives/time.ts
375
+ var time = _builtinTime.mul(getReducedMotionTimeScale());
397
376
 
398
377
  // src/primitives/cursorRipple.ts
399
378
  function cursorRipple(p, center, opts = {}) {
@@ -401,14 +380,22 @@ function cursorRipple(p, center, opts = {}) {
401
380
  const frequency = opts.frequency ?? 30;
402
381
  const speed = opts.speed ?? 6;
403
382
  const amplitude = opts.amplitude ?? 0.5;
404
- const d = length3(
405
- p.sub(center)
406
- );
407
- const wave = sin2(d.mul(frequency).sub(time.mul(speed)));
408
- const decay = smoothstep2(reach, 0, d);
383
+ const d = length2(sub2(p, center));
384
+ const wave = sin(d.mul(frequency).sub(time.mul(speed)));
385
+ const decay = smoothstep(reach, 0, d);
409
386
  return wave.mul(amplitude).mul(decay);
410
387
  }
411
388
 
389
+ // src/primitives/filmGrain.ts
390
+ import { vec2, sin as sin2, fract, length as length3 } from "three/tsl";
391
+ function filmGrain(uvNode, intensity, timeOffset = 0) {
392
+ const HASH_C1 = vec2(2127.1, 81.17);
393
+ const HASH_C2 = vec2(1269.5, 283.37);
394
+ const base = vec2(uvNode.dot(HASH_C1).add(timeOffset), uvNode.dot(HASH_C2).add(timeOffset));
395
+ const hash = fract(sin2(base).mul(43758.5453));
396
+ return length3(hash).sub(0.765).mul(intensity);
397
+ }
398
+
412
399
  // src/runtime/visibility.ts
413
400
  function createVisibilityWatcher() {
414
401
  if (typeof document === "undefined") {
@@ -478,35 +465,21 @@ export {
478
465
  CursorInput,
479
466
  MatterScheduler,
480
467
  colorRamp,
481
- cos,
482
468
  createIntersectionWatcher,
483
469
  createReducedMotionWatcher,
484
470
  createRenderer,
485
471
  createVisibilityWatcher,
486
472
  cursorRipple,
487
473
  displace,
488
- dot,
489
474
  fbm,
475
+ filmGrain,
490
476
  getReducedMotionPolicy,
491
477
  getReducedMotionTimeScale,
492
- length2 as length,
493
- max,
494
- min,
495
- mix2 as mix,
496
- mod,
497
478
  noise,
498
- normalize,
499
479
  quantize,
500
480
  sdfCircle,
501
481
  setReducedMotionPolicy,
502
- sin,
503
- smoothstep,
504
482
  time,
505
- uniform2 as uniform,
506
- uv,
507
- vec2,
508
- vec32 as vec3,
509
- vec4,
510
483
  voronoi
511
484
  };
512
485
  //# sourceMappingURL=index.js.map