@lovo/matter-react 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @lovo/matter-react
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c4cbb52: Add the overlay-component category. `MatterScene` now drives its render via `three/webgpu`'s `PostProcessing` pipeline so child components can register chained TSL transforms instead of each owning their own material draw.
8
+
9
+ **New: `useOverlayPass(transform, deps)` hook**
10
+
11
+ ```ts
12
+ import { useOverlayPass, useAnimatableUniform } from "@lovo/matter-react";
13
+
14
+ export function MyOverlay({ intensity }) {
15
+ const intensityU = useAnimatableUniform(intensity);
16
+ useOverlayPass(
17
+ (input) => input.mul(intensityU), // takes upstream pixel, returns modified pixel
18
+ [intensityU]
19
+ );
20
+ return null;
21
+ }
22
+ ```
23
+
24
+ Mount the component inside any `<MatterScene>` and it composes onto the pipeline; multiple overlays chain in mount order. Uniforms captured inside `transform` update in place and don't need to be in `deps` — only put structural changes (mode toggles, etc.) in `deps` so the transform gets re-registered.
25
+
26
+ **Registry-side ships (delivered via `@lovo/matter-cli` copy-paste):**
27
+
28
+ - `<FilmGrain>` — additive or subtractive grain overlay.
29
+ - `<Vignette>` — radial edge darkening, aspect-corrected so the mask is a circle on widescreen.
30
+ - **Breaking:** `<MeshGradient>` no longer accepts `grain` / `grainSpeed` props. Stack `<FilmGrain />` as a sibling inside `<MatterScene>` instead. Existing copies pulled before this release keep working; new pulls / CLI refreshes pick up the new shape. The MeshGradient docs page has the new pattern.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [3856367]
35
+ - @lovo/matter@0.3.0
36
+
3
37
  ## 0.2.0
4
38
 
5
39
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  useAnimatableUniform: () => useAnimatableUniform,
27
27
  useCursor: () => useCursor,
28
28
  useMatterContext: () => useMatterContext,
29
+ useOverlayPass: () => useOverlayPass,
29
30
  useResize: () => useResize,
30
31
  useScroll: () => useScroll,
31
32
  useShaderMaterial: () => useShaderMaterial,
@@ -36,6 +37,8 @@ module.exports = __toCommonJS(index_exports);
36
37
  // src/MatterScene.tsx
37
38
  var import_react2 = require("react");
38
39
  var import_three = require("three");
40
+ var import_webgpu = require("three/webgpu");
41
+ var import_tsl = require("three/tsl");
39
42
  var import_matter = require("@lovo/matter");
40
43
 
41
44
  // src/matter-context.ts
@@ -71,8 +74,29 @@ function MatterScene(props) {
71
74
  const scene = new import_three.Scene();
72
75
  const camera = new import_three.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
73
76
  camera.position.z = 1;
77
+ const postProcessing = new import_webgpu.PostProcessing(renderer.three);
74
78
  const scheduler = new import_matter.MatterScheduler();
75
- scheduler.add(() => renderer.three.render(scene, camera));
79
+ const overlays = /* @__PURE__ */ new Map();
80
+ const basePass = (0, import_tsl.pass)(scene, camera);
81
+ const rebuildOutputNode = () => {
82
+ const transforms = Array.from(overlays.values());
83
+ postProcessing.outputNode = transforms.reduce(
84
+ (node, transform) => transform(node),
85
+ basePass
86
+ );
87
+ postProcessing.needsUpdate = true;
88
+ };
89
+ rebuildOutputNode();
90
+ const registerOverlay = (transform) => {
91
+ const key = /* @__PURE__ */ Symbol("overlay");
92
+ overlays.set(key, transform);
93
+ rebuildOutputNode();
94
+ return () => {
95
+ overlays.delete(key);
96
+ rebuildOutputNode();
97
+ };
98
+ };
99
+ scheduler.add(() => postProcessing.render());
76
100
  scheduler.start();
77
101
  const visibility = (0, import_matter.createVisibilityWatcher)();
78
102
  const intersection = (0, import_matter.createIntersectionWatcher)(canvas);
@@ -95,7 +119,7 @@ function MatterScene(props) {
95
119
  scheduler.dispose();
96
120
  renderer.dispose();
97
121
  };
98
- setCtx({ renderer, scene, camera, scheduler });
122
+ setCtx({ renderer, scene, camera, scheduler, registerOverlay });
99
123
  } catch (err) {
100
124
  if (cancelled) return;
101
125
  const e = err instanceof Error ? err : new Error(String(err));
@@ -147,10 +171,10 @@ function useMatterContext() {
147
171
 
148
172
  // src/useShaderMaterial.ts
149
173
  var import_react4 = require("react");
150
- var import_webgpu = require("three/webgpu");
174
+ var import_webgpu2 = require("three/webgpu");
151
175
  function useShaderMaterial(build) {
152
176
  const material = (0, import_react4.useMemo)(() => {
153
- const m = new import_webgpu.MeshBasicNodeMaterial();
177
+ const m = new import_webgpu2.MeshBasicNodeMaterial();
154
178
  m.colorNode = build();
155
179
  return m;
156
180
  }, [build]);
@@ -324,14 +348,14 @@ function useScroll() {
324
348
 
325
349
  // src/useAnimatableUniform.ts
326
350
  var import_react8 = require("react");
327
- var import_tsl = require("three/tsl");
351
+ var import_tsl2 = require("three/tsl");
328
352
  var isSignal = (value) => {
329
353
  return typeof value === "object" && value !== null && typeof value.get === "function" && typeof value.on === "function";
330
354
  };
331
355
  function useAnimatableUniform(value) {
332
356
  const uniformNode = (0, import_react8.useMemo)(() => {
333
357
  const initial = isSignal(value) ? value.get() : value;
334
- return (0, import_tsl.uniform)(initial);
358
+ return (0, import_tsl2.uniform)(initial);
335
359
  }, []);
336
360
  (0, import_react8.useEffect)(() => {
337
361
  if (isSignal(value)) {
@@ -348,22 +372,33 @@ function useAnimatableUniform(value) {
348
372
  return uniformNode;
349
373
  }
350
374
 
351
- // src/FallbackBoundary.tsx
375
+ // src/useOverlayPass.ts
352
376
  var import_react9 = require("react");
377
+ function useOverlayPass(transform, deps) {
378
+ const ctx = useMatterContext();
379
+ (0, import_react9.useEffect)(() => {
380
+ if (!ctx) return;
381
+ const unregister = ctx.registerOverlay(transform);
382
+ return unregister;
383
+ }, [ctx, ...deps]);
384
+ }
385
+
386
+ // src/FallbackBoundary.tsx
387
+ var import_react10 = require("react");
353
388
  var import_jsx_runtime2 = require("react/jsx-runtime");
354
389
  function FallbackBoundary({ fallback, children }) {
355
- const [mounted, setMounted] = (0, import_react9.useState)(false);
356
- (0, import_react9.useEffect)(() => {
390
+ const [mounted, setMounted] = (0, import_react10.useState)(false);
391
+ (0, import_react10.useEffect)(() => {
357
392
  setMounted(true);
358
393
  }, []);
359
394
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: mounted ? children : fallback ?? null });
360
395
  }
361
396
 
362
397
  // src/useStaticHint.ts
363
- var import_react10 = require("react");
398
+ var import_react11 = require("react");
364
399
  function useStaticHint(hint) {
365
400
  const ctx = useMatterContext();
366
- (0, import_react10.useEffect)(() => {
401
+ (0, import_react11.useEffect)(() => {
367
402
  if (!ctx) return;
368
403
  ctx.scheduler.setIdle(hint);
369
404
  return () => ctx.scheduler.setIdle(false);
@@ -371,7 +406,7 @@ function useStaticHint(hint) {
371
406
  }
372
407
 
373
408
  // src/MatterMonitor.tsx
374
- var import_react11 = require("react");
409
+ var import_react12 = require("react");
375
410
  var import_jsx_runtime3 = require("react/jsx-runtime");
376
411
  var anchorStyle = {
377
412
  "top-left": { top: 8, left: 8 },
@@ -392,11 +427,11 @@ var baseStyle = {
392
427
  whiteSpace: "pre"
393
428
  };
394
429
  function MatterMonitor({ anchor = "top-right" }) {
395
- const ctx = (0, import_react11.useContext)(MatterContext);
396
- const [stats, setStats] = (0, import_react11.useState)({ fps: 0, ticks: 0, frames: 0 });
397
- const ticksRef = (0, import_react11.useRef)(0);
398
- const fpsAccumRef = (0, import_react11.useRef)({ frames: 0, lastSampleAt: 0, fps: 0 });
399
- (0, import_react11.useEffect)(() => {
430
+ const ctx = (0, import_react12.useContext)(MatterContext);
431
+ const [stats, setStats] = (0, import_react12.useState)({ fps: 0, ticks: 0, frames: 0 });
432
+ const ticksRef = (0, import_react12.useRef)(0);
433
+ const fpsAccumRef = (0, import_react12.useRef)({ frames: 0, lastSampleAt: 0, fps: 0 });
434
+ (0, import_react12.useEffect)(() => {
400
435
  if (!ctx) return;
401
436
  const client = (tick) => {
402
437
  ticksRef.current += 1;
@@ -437,6 +472,7 @@ function MatterMonitor({ anchor = "top-right" }) {
437
472
  useAnimatableUniform,
438
473
  useCursor,
439
474
  useMatterContext,
475
+ useOverlayPass,
440
476
  useResize,
441
477
  useScroll,
442
478
  useShaderMaterial,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/MatterScene.tsx","../src/matter-context.ts","../src/useMatterContext.ts","../src/useShaderMaterial.ts","../src/useCursor.ts","../src/useResize.ts","../src/useScroll.ts","../src/useAnimatableUniform.ts","../src/FallbackBoundary.tsx","../src/useStaticHint.ts","../src/MatterMonitor.tsx"],"sourcesContent":["// @lovo/matter-react — React binding for Matter.\n\nexport { MatterScene } from './MatterScene.js'\nexport type { MatterSceneProps } from './MatterScene.js'\n\nexport { useMatterContext } from './useMatterContext.js'\nexport type { MatterContextValue } from './matter-context.js'\n\nexport { useShaderMaterial } from './useShaderMaterial.js'\n\nexport { useCursor } from './useCursor.js'\nexport type { CursorSignal } from './useCursor.js'\n\nexport { useResize } from './useResize.js'\nexport type { ResizeSignal, ResizeValue } from './useResize.js'\n\nexport { useScroll } from './useScroll.js'\nexport type { ScrollSignal, ScrollValue } from './useScroll.js'\n\nexport { useAnimatableUniform } from './useAnimatableUniform.js'\nexport type { AnimatableProp, MatterSignal } from './useAnimatableUniform.js'\n\nexport { FallbackBoundary } from './FallbackBoundary.js'\nexport type { FallbackBoundaryProps } from './FallbackBoundary.js'\n\nexport { useStaticHint } from './useStaticHint.js'\n\nexport { MatterMonitor } from './MatterMonitor.js'\nexport type { MatterMonitorProps, MonitorAnchor } from './MatterMonitor.js'\n","'use client'\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'\nimport { Scene, OrthographicCamera } from 'three'\nimport {\n createRenderer,\n MatterScheduler,\n createVisibilityWatcher,\n createIntersectionWatcher,\n} from '@lovo/matter'\nimport { MatterContext, type MatterContextValue } from './matter-context.js'\n\nexport interface MatterSceneProps {\n children?: ReactNode\n /** Rendered server-side and during WebGPU init. Default: empty. */\n fallback?: ReactNode\n className?: string\n style?: CSSProperties\n /** Cap on devicePixelRatio. Default: 2. */\n maxDPR?: number\n}\n\nconst defaultStyle: CSSProperties = {\n position: 'absolute',\n inset: 0,\n display: 'block',\n width: '100%',\n height: '100%',\n}\n\n/**\n * Owns a canvas, a Three.js renderer (WebGPU + WebGL2 fallback), an\n * orthographic camera covering the canvas, an empty Scene, and a\n * MatterScheduler. Children consume these via useMatterContext().\n */\nexport function MatterScene(props: MatterSceneProps) {\n const { children, fallback, className, style, maxDPR } = props\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const [ctx, setCtx] = useState<MatterContextValue | null>(null)\n const [error, setError] = useState<Error | null>(null)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n let cancelled = false\n let cleanup: (() => void) | null = null\n\n const setup = async () => {\n try {\n const renderer = await createRenderer(canvas, { maxDPR })\n if (cancelled) {\n renderer.dispose()\n return\n }\n const scene = new Scene()\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10)\n camera.position.z = 1\n const scheduler = new MatterScheduler()\n\n scheduler.add(() => renderer.three.render(scene, camera))\n scheduler.start()\n\n const visibility = createVisibilityWatcher()\n const intersection = createIntersectionWatcher(canvas)\n\n const updatePauseState = () => {\n const shouldRun = visibility.isVisible() && intersection.isInView()\n if (shouldRun) scheduler.resume()\n else scheduler.pause()\n }\n updatePauseState()\n\n const unsubVisibility = visibility.subscribe(updatePauseState)\n const unsubIntersection = intersection.subscribe(updatePauseState)\n\n const onResize = () => renderer.resize()\n window.addEventListener('resize', onResize)\n\n cleanup = () => {\n unsubVisibility()\n unsubIntersection()\n visibility.dispose()\n intersection.dispose()\n window.removeEventListener('resize', onResize)\n scheduler.dispose()\n renderer.dispose()\n }\n\n setCtx({ renderer, scene, camera, scheduler })\n } catch (err) {\n if (cancelled) return\n const e = err instanceof Error ? err : new Error(String(err))\n console.error('[MatterScene] renderer init failed:', e)\n setError(e)\n }\n }\n\n void setup()\n return () => {\n cancelled = true\n cleanup?.()\n cleanup = null\n setCtx(null)\n }\n }, [maxDPR])\n\n return (\n <div className={className} style={{ ...defaultStyle, ...style }}>\n <canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />\n {error ? (\n <div\n style={{\n position: 'absolute',\n inset: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '1rem',\n color: '#fff',\n background: 'rgba(120, 30, 30, 0.85)',\n font: '0.85rem ui-monospace, monospace',\n whiteSpace: 'pre-wrap',\n textAlign: 'center',\n }}\n >\n MatterScene init failed:\n {'\\n'}\n {error.message}\n </div>\n ) : ctx ? (\n <MatterContext.Provider value={ctx}>{children}</MatterContext.Provider>\n ) : (\n (fallback ?? null)\n )}\n </div>\n )\n}\n","import { createContext } from 'react'\nimport type { Scene, Camera } from 'three'\nimport type { MatterRenderer, MatterScheduler } from '@lovo/matter'\n\nexport interface MatterContextValue {\n renderer: MatterRenderer\n scene: Scene\n camera: Camera\n scheduler: MatterScheduler\n}\n\nexport const MatterContext = createContext<MatterContextValue | null>(null)\n","import { useContext } from 'react'\nimport { MatterContext, type MatterContextValue } from './matter-context.js'\n\n/**\n * Read the matter scene context. Returns null when called outside a\n * <MatterScene>; useShaderMaterial and similar hooks check this and\n * auto-provision a scene if missing (auto-wrap behavior).\n */\nexport function useMatterContext(): MatterContextValue | null {\n return useContext(MatterContext)\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { MeshBasicNodeMaterial } from 'three/webgpu'\nimport type { Node } from 'three/webgpu'\nimport type { ShaderNodeObject } from 'three/tsl'\n\n/** A TSL fragment that produces a color. Accept any Node or TSL-wrapped node. */\nexport type ColorTSL = Node | ShaderNodeObject<Node>\n\n/**\n * Bind a TSL color expression to a NodeMaterial. Returns the material;\n * caller is responsible for adding it to a mesh and disposing when done.\n *\n * The TSL fragment is computed once via `useMemo` and re-applied if the\n * factory function changes. For dynamic uniforms, mutate `.value` on the\n * uniform nodes — don't recreate the TSL fragment per render.\n */\nexport function useShaderMaterial(build: () => ColorTSL): MeshBasicNodeMaterial {\n const material = useMemo(() => {\n const m = new MeshBasicNodeMaterial()\n m.colorNode = build() as Node\n return m\n }, [build])\n\n useEffect(() => {\n return () => material.dispose()\n }, [material])\n\n return material\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { CursorInput, type CursorInputOptions, type Vec2 } from '@lovo/matter'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport interface CursorSignal {\n /** Current smoothed cursor position (Vec2 in 0..1 viewport space). */\n get(): Vec2\n /** Subscribe to change events. Returns unsubscribe. */\n on(event: 'change', cb: (value: Vec2) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect\n// has created the real CursorInput. Calling .on returns an unsub no-op.\nconst STUB_SIGNAL: CursorSignal = {\n get: () => [0.5, 0.5] as const,\n on: () => () => undefined,\n}\n\n/**\n * React wrapper for CursorInput. Auto-attaches to the parent <MatterScene>'s\n * scheduler if available; otherwise creates a free-running rAF tick.\n *\n * Lifecycle is in a single effect so React 19 Strict Mode's intentional\n * mount→unmount→mount cycle creates a *fresh* CursorInput per real mount\n * instead of disposing a long-lived one (which would silently break the\n * window mousemove listener and the smoothing tick).\n */\nexport function useCursor(opts: CursorInputOptions = {}): CursorSignal {\n const ctx = useMatterContext()\n const [input, setInput] = useState<CursorInput | null>(null)\n\n useEffect(() => {\n // Plumb the parent <MatterScene>'s canvas as the cursor's normalization\n // element. Without this, cursor coords are viewport-normalized — fine for\n // a full-page scene but visibly offset when the canvas sits inside a\n // smaller wrapper (e.g., 70vh hero). DotField's cell tiling makes the\n // mismatch obvious; LinearGradient mostly gets away with it. Caller can\n // override by passing `opts.element` explicitly.\n const canvas = ctx?.renderer.three.domElement\n const elementOpt = opts.element ?? (canvas instanceof HTMLElement ? canvas : undefined)\n const fresh = new CursorInput({ ...opts, element: elementOpt })\n setInput(fresh)\n\n let detach: (() => void) | null = null\n if (ctx?.scheduler) {\n const client = ({ delta }: { delta: number }) => fresh.tick(delta)\n ctx.scheduler.add(client)\n detach = () => ctx.scheduler.remove(client)\n } else {\n let raf: number | null = null\n let lastNow = performance.now()\n const loop = (now: number) => {\n const delta = (now - lastNow) / 1000\n lastNow = now\n fresh.tick(delta)\n raf = requestAnimationFrame(loop)\n }\n raf = requestAnimationFrame(loop)\n detach = () => {\n if (raf !== null) cancelAnimationFrame(raf)\n }\n }\n\n return () => {\n detach?.()\n fresh.dispose()\n setInput(null)\n }\n // We intentionally only re-create on ctx change, not opts (which is a\n // fresh object literal each render). Smoothing tweaks during dev are\n // applied by remounting the parent component.\n // oxlint-disable-next-line react/exhaustive-deps\n }, [ctx])\n\n return input ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport type ResizeValue = readonly [width: number, height: number, dpr: number]\n\nexport interface ResizeSignal {\n /** Current size in CSS pixels + devicePixelRatio. */\n get(): ResizeValue\n on(event: 'change', cb: (value: ResizeValue) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect has\n// observed the canvas. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ResizeSignal = {\n get: () => [0, 0, 1] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track the parent <MatterScene>'s canvas size + DPR. Exposes a MatterSignal\n * that components can pass into a TSL uniform to make pixel-aware effects\n * (e.g., DotField's pixel-spacing math).\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle creates a fresh ResizeObserver per real mount\n * (CLAUDE.md gotcha #14).\n *\n * Falls back to the stub signal until the parent context is ready.\n */\nexport function useResize(): ResizeSignal {\n const ctx = useMatterContext()\n const [signal, setSignal] = useState<ResizeSignal | null>(null)\n\n useEffect(() => {\n if (!ctx) return undefined\n\n const canvas = ctx.renderer.three.domElement\n if (!(canvas instanceof HTMLCanvasElement)) return undefined\n\n let value: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n const listeners = new Set<(v: ResizeValue) => void>()\n const fresh: ResizeSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n const emit = () => {\n const next: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n if (next[0] === value[0] && next[1] === value[1] && next[2] === value[2]) return\n value = next\n for (const cb of listeners) cb(next)\n }\n\n const observer = new ResizeObserver(emit)\n observer.observe(canvas)\n\n // Cross-browser DPR-change watch. matchMedia(`(resolution: <dpr>dppx)`)\n // matches at the *current* DPR; when the user zooms the page the query\n // stops matching, fires `change`, and we re-arm the watch at the new DPR.\n // We track the current MQL + handler so we can fully detach in cleanup\n // (the handler is captured by the listener — passing a fresh closure to\n // removeEventListener wouldn't actually unregister it).\n let mql: MediaQueryList | null = null\n let mqlHandler: (() => void) | null = null\n const setupDprWatch = () => {\n if (typeof window === 'undefined') return\n const dpr = window.devicePixelRatio\n const next = window.matchMedia(`(resolution: ${dpr}dppx)`)\n const handler = () => {\n emit()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n setupDprWatch()\n }\n next.addEventListener('change', handler)\n mql = next\n mqlHandler = handler\n }\n setupDprWatch()\n\n return () => {\n observer.disconnect()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n mql = null\n mqlHandler = null\n listeners.clear()\n setSignal(null)\n }\n }, [ctx])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\nexport type ScrollValue = readonly [scrollY: number, progress: number]\n\nexport interface ScrollSignal {\n /** Current scroll Y (px) and normalized progress in [0,1]. */\n get(): ScrollValue\n on(event: 'change', cb: (value: ScrollValue) => void): () => void\n}\n\n// Inert stub returned during SSR + on the first client render before the\n// lifecycle effect attaches. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ScrollSignal = {\n get: () => [0, 0] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track window scroll position. Exposes a MatterSignal of `[scrollY, progress]`\n * where `progress` is `scrollY / max(documentHeight - innerHeight, 1)` clamped\n * to [0, 1]. Listener is rAF-throttled and `passive: true` so it never blocks\n * scrolling.\n *\n * No v1 Tier 1 component consumes this hook; it ships so users can pass\n * `inputs={{ scroll: useScroll() }}` to any Matter component.\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle in dev creates a fresh listener pair per real\n * mount and tears down cleanly on each pseudo-unmount (CLAUDE.md gotcha #14).\n *\n * **Known limitation (v1):** `progress` is computed against whichever\n * `documentHeight` was current when the last scroll fired. If the page grows\n * after mount (async content, font load reflow, expanding panels) without\n * the user scrolling, the denominator goes stale. A future ResizeObserver/\n * MutationObserver pass would close the gap; deferred until a v1 component\n * consumes scroll input.\n */\nexport function useScroll(): ScrollSignal {\n const [signal, setSignal] = useState<ScrollSignal | null>(null)\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined\n\n const compute = (): ScrollValue => {\n const y = window.scrollY\n // For pages shorter than the viewport, `documentHeight - innerHeight` is\n // <= 0; clamp to 1 to avoid div-by-zero. Progress stays at 0 in that\n // case because scrollY is also 0.\n const max = Math.max(document.documentElement.scrollHeight - window.innerHeight, 1)\n const progress = Math.max(0, Math.min(1, y / max))\n return [y, progress]\n }\n\n let value: ScrollValue = compute()\n const listeners = new Set<(v: ScrollValue) => void>()\n const fresh: ScrollSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n let rafPending = false\n const onScroll = () => {\n if (rafPending) return\n rafPending = true\n requestAnimationFrame(() => {\n rafPending = false\n const next = compute()\n if (next[0] === value[0] && next[1] === value[1]) return\n value = next\n for (const cb of listeners) cb(next)\n })\n }\n window.addEventListener('scroll', onScroll, { passive: true })\n\n return () => {\n window.removeEventListener('scroll', onScroll)\n listeners.clear()\n setSignal(null)\n }\n }, [])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { uniform } from 'three/tsl'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport type { Node } from 'three/webgpu'\n\nexport interface MatterSignal<T> {\n get(): T\n on(event: 'change', cb: (value: T) => void): () => void\n}\n\nexport type AnimatableProp<T> = T | MatterSignal<T>\n\nconst isSignal = <T>(value: AnimatableProp<T>): value is MatterSignal<T> => {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as MatterSignal<T>).get === 'function' &&\n typeof (value as MatterSignal<T>).on === 'function'\n )\n}\n\n/**\n * Bind an AnimatableProp<T> to a TSL uniform. Plain values create a\n * static uniform that updates only when the prop changes (React render\n * path). Signals subscribe via .on('change') and write into the uniform\n * imperatively without re-rendering.\n */\nexport function useAnimatableUniform<T>(value: AnimatableProp<T>): ShaderNodeObject<Node> {\n // Create the uniform once with the initial value; subsequent updates flow\n // through the effect below (either via signal subscription or direct write).\n const uniformNode = useMemo(() => {\n const initial = isSignal(value) ? value.get() : value\n return uniform(initial) as unknown as ShaderNodeObject<Node>\n // oxlint-disable-next-line react/exhaustive-deps\n }, [])\n\n useEffect(() => {\n if (isSignal(value)) {\n const unsub = value.on('change', (next) => {\n ;(uniformNode as unknown as { value: T }).value = next\n })\n return unsub\n }\n ;(uniformNode as unknown as { value: T }).value = value\n return undefined\n }, [value, uniformNode])\n\n return uniformNode\n}\n","'use client'\n\nimport { useEffect, useState, type ReactNode } from 'react'\n\nexport interface FallbackBoundaryProps {\n /** Rendered until WebGPU/WebGL is available on the client. */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Render `fallback` until the component mounts on the client. Gates the\n * children behind client-only mounting so SSR/no-WebGPU users see a\n * sensible static placeholder rather than a flash of nothing.\n */\nexport function FallbackBoundary({ fallback, children }: FallbackBoundaryProps) {\n const [mounted, setMounted] = useState(false)\n useEffect(() => {\n setMounted(true)\n }, [])\n return <>{mounted ? children : (fallback ?? null)}</>\n}\n","'use client'\n\nimport { useEffect } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\n/**\n * Opt a component out of the rAF loop while it has no dynamic uniforms.\n *\n * When `hint` is true, the scheduler runs one final flush tick (so any\n * uniform changes since the last frame are rendered) and then halts the\n * rAF loop until either `hint` becomes false or another component in the\n * same scene calls `scheduler.requestRender()`.\n *\n * Use for components whose animation is fully derived from props that don't\n * include `time`, e.g. `<LinearGradient speed={0}>` with no `interactive`.\n */\nexport function useStaticHint(hint: boolean): void {\n const ctx = useMatterContext()\n useEffect(() => {\n if (!ctx) return\n ctx.scheduler.setIdle(hint)\n return () => ctx.scheduler.setIdle(false)\n }, [ctx, hint])\n}\n","'use client'\n\nimport { useEffect, useRef, useState, useContext, type CSSProperties } from 'react'\nimport { MatterContext } from './matter-context.js'\n\nexport type MonitorAnchor = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\nconst anchorStyle: Record<MonitorAnchor, CSSProperties> = {\n 'top-left': { top: 8, left: 8 },\n 'top-right': { top: 8, right: 8 },\n 'bottom-left': { bottom: 8, left: 8 },\n 'bottom-right': { bottom: 8, right: 8 },\n}\n\nconst baseStyle: CSSProperties = {\n position: 'absolute',\n zIndex: 10,\n padding: '6px 8px',\n borderRadius: 6,\n background: 'rgba(0, 0, 0, 0.6)',\n color: '#fff',\n font: '11px ui-monospace, monospace',\n lineHeight: 1.4,\n pointerEvents: 'none',\n whiteSpace: 'pre',\n}\n\nexport interface MatterMonitorProps {\n anchor?: MonitorAnchor\n}\n\n/**\n * Dev-only overlay that displays the current scene's FPS, tick count, and\n * paused/idle state. Reads from the surrounding `<MatterScene>` via context\n * and subscribes to its scheduler. Renders nothing useful if mounted outside\n * a scene.\n */\nexport function MatterMonitor({ anchor = 'top-right' }: MatterMonitorProps) {\n const ctx = useContext(MatterContext)\n const [stats, setStats] = useState({ fps: 0, ticks: 0, frames: 0 })\n const ticksRef = useRef(0)\n const fpsAccumRef = useRef({ frames: 0, lastSampleAt: 0, fps: 0 })\n\n useEffect(() => {\n if (!ctx) return\n const client = (tick: { now: number }) => {\n ticksRef.current += 1\n const acc = fpsAccumRef.current\n acc.frames += 1\n if (acc.lastSampleAt === 0) acc.lastSampleAt = tick.now\n const dt = tick.now - acc.lastSampleAt\n if (dt >= 500) {\n acc.fps = Math.round((acc.frames * 1000) / dt)\n acc.frames = 0\n acc.lastSampleAt = tick.now\n }\n setStats({ fps: acc.fps, ticks: ticksRef.current, frames: acc.frames })\n }\n ctx.scheduler.add(client)\n return () => ctx.scheduler.remove(client)\n }, [ctx])\n\n if (!ctx) {\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n no scene\n </div>\n )\n }\n\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n <span data-testid=\"matter-monitor-fps\">fps: {stats.fps || '—'}</span>\n {'\\n'}\n <span data-testid=\"matter-monitor-ticks\">ticks: {stats.ticks}</span>\n </div>\n )\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_matter","import_react","STUB_SIGNAL","import_react","STUB_SIGNAL","import_react","import_react","import_jsx_runtime","import_react","import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/MatterScene.tsx","../src/matter-context.ts","../src/useMatterContext.ts","../src/useShaderMaterial.ts","../src/useCursor.ts","../src/useResize.ts","../src/useScroll.ts","../src/useAnimatableUniform.ts","../src/useOverlayPass.ts","../src/FallbackBoundary.tsx","../src/useStaticHint.ts","../src/MatterMonitor.tsx"],"sourcesContent":["// @lovo/matter-react — React binding for Matter.\n\nexport { MatterScene } from './MatterScene.js'\nexport type { MatterSceneProps } from './MatterScene.js'\n\nexport { useMatterContext } from './useMatterContext.js'\nexport type { MatterContextValue } from './matter-context.js'\n\nexport { useShaderMaterial } from './useShaderMaterial.js'\n\nexport { useCursor } from './useCursor.js'\nexport type { CursorSignal } from './useCursor.js'\n\nexport { useResize } from './useResize.js'\nexport type { ResizeSignal, ResizeValue } from './useResize.js'\n\nexport { useScroll } from './useScroll.js'\nexport type { ScrollSignal, ScrollValue } from './useScroll.js'\n\nexport { useAnimatableUniform } from './useAnimatableUniform.js'\nexport type { AnimatableProp, MatterSignal } from './useAnimatableUniform.js'\n\nexport { useOverlayPass } from './useOverlayPass.js'\nexport type { OverlayTransform } from './matter-context.js'\n\nexport { FallbackBoundary } from './FallbackBoundary.js'\nexport type { FallbackBoundaryProps } from './FallbackBoundary.js'\n\nexport { useStaticHint } from './useStaticHint.js'\n\nexport { MatterMonitor } from './MatterMonitor.js'\nexport type { MatterMonitorProps, MonitorAnchor } from './MatterMonitor.js'\n","'use client'\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'\nimport { Scene, OrthographicCamera } from 'three'\nimport { PostProcessing } from 'three/webgpu'\nimport type { Node } from 'three/webgpu'\nimport { pass } from 'three/tsl'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport {\n createRenderer,\n MatterScheduler,\n createVisibilityWatcher,\n createIntersectionWatcher,\n} from '@lovo/matter'\nimport { MatterContext, type MatterContextValue, type OverlayTransform } from './matter-context.js'\n\nexport interface MatterSceneProps {\n children?: ReactNode\n /** Rendered server-side and during WebGPU init. Default: empty. */\n fallback?: ReactNode\n className?: string\n style?: CSSProperties\n /** Cap on devicePixelRatio. Default: 2. */\n maxDPR?: number\n}\n\nconst defaultStyle: CSSProperties = {\n position: 'absolute',\n inset: 0,\n display: 'block',\n width: '100%',\n height: '100%',\n}\n\n/**\n * Owns a canvas, a Three.js renderer (WebGPU + WebGL2 fallback), an\n * orthographic camera covering the canvas, an empty Scene, and a\n * MatterScheduler. Children consume these via useMatterContext().\n */\nexport function MatterScene(props: MatterSceneProps) {\n const { children, fallback, className, style, maxDPR } = props\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const [ctx, setCtx] = useState<MatterContextValue | null>(null)\n const [error, setError] = useState<Error | null>(null)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n let cancelled = false\n let cleanup: (() => void) | null = null\n\n const setup = async () => {\n try {\n const renderer = await createRenderer(canvas, { maxDPR })\n if (cancelled) {\n renderer.dispose()\n return\n }\n const scene = new Scene()\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10)\n camera.position.z = 1\n const postProcessing = new PostProcessing(renderer.three)\n const scheduler = new MatterScheduler()\n\n const overlays = new Map<symbol, OverlayTransform>()\n\n // Allocate the base PassNode once per setup so rebuilds reuse the same\n // node identity instead of churning a fresh one (and a fresh render\n // target binding) on every register/unregister.\n const basePass = pass(scene, camera) as unknown as ShaderNodeObject<Node>\n\n const rebuildOutputNode = () => {\n const transforms = Array.from(overlays.values())\n postProcessing.outputNode = transforms.reduce(\n (node, transform) => transform(node),\n basePass,\n )\n postProcessing.needsUpdate = true\n }\n\n rebuildOutputNode() // initial: just basePass, no overlays\n\n const registerOverlay = (transform: OverlayTransform): (() => void) => {\n const key = Symbol('overlay')\n overlays.set(key, transform)\n rebuildOutputNode()\n return () => {\n overlays.delete(key)\n rebuildOutputNode()\n }\n }\n\n scheduler.add(() => postProcessing.render())\n scheduler.start()\n\n const visibility = createVisibilityWatcher()\n const intersection = createIntersectionWatcher(canvas)\n\n const updatePauseState = () => {\n const shouldRun = visibility.isVisible() && intersection.isInView()\n if (shouldRun) scheduler.resume()\n else scheduler.pause()\n }\n updatePauseState()\n\n const unsubVisibility = visibility.subscribe(updatePauseState)\n const unsubIntersection = intersection.subscribe(updatePauseState)\n\n const onResize = () => renderer.resize()\n window.addEventListener('resize', onResize)\n\n cleanup = () => {\n unsubVisibility()\n unsubIntersection()\n visibility.dispose()\n intersection.dispose()\n window.removeEventListener('resize', onResize)\n scheduler.dispose()\n renderer.dispose()\n }\n\n setCtx({ renderer, scene, camera, scheduler, registerOverlay })\n } catch (err) {\n if (cancelled) return\n const e = err instanceof Error ? err : new Error(String(err))\n console.error('[MatterScene] renderer init failed:', e)\n setError(e)\n }\n }\n\n void setup()\n return () => {\n cancelled = true\n cleanup?.()\n cleanup = null\n setCtx(null)\n }\n }, [maxDPR])\n\n return (\n <div className={className} style={{ ...defaultStyle, ...style }}>\n <canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />\n {error ? (\n <div\n style={{\n position: 'absolute',\n inset: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '1rem',\n color: '#fff',\n background: 'rgba(120, 30, 30, 0.85)',\n font: '0.85rem ui-monospace, monospace',\n whiteSpace: 'pre-wrap',\n textAlign: 'center',\n }}\n >\n MatterScene init failed:\n {'\\n'}\n {error.message}\n </div>\n ) : ctx ? (\n <MatterContext.Provider value={ctx}>{children}</MatterContext.Provider>\n ) : (\n (fallback ?? null)\n )}\n </div>\n )\n}\n","import { createContext } from 'react'\nimport type { Scene, Camera } from 'three'\nimport type { Node } from 'three/webgpu'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport type { MatterRenderer, MatterScheduler } from '@lovo/matter'\n\nexport type OverlayTransform = (input: ShaderNodeObject<Node>) => ShaderNodeObject<Node>\n\nexport interface MatterContextValue {\n renderer: MatterRenderer\n scene: Scene\n camera: Camera\n scheduler: MatterScheduler\n registerOverlay: (transform: OverlayTransform) => () => void\n}\n\nexport const MatterContext = createContext<MatterContextValue | null>(null)\n","import { useContext } from 'react'\nimport { MatterContext, type MatterContextValue } from './matter-context.js'\n\n/**\n * Read the matter scene context. Returns null when called outside a\n * <MatterScene>; useShaderMaterial and similar hooks check this and\n * auto-provision a scene if missing (auto-wrap behavior).\n */\nexport function useMatterContext(): MatterContextValue | null {\n return useContext(MatterContext)\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { MeshBasicNodeMaterial } from 'three/webgpu'\nimport type { Node } from 'three/webgpu'\nimport type { ShaderNodeObject } from 'three/tsl'\n\n/** A TSL fragment that produces a color. Accept any Node or TSL-wrapped node. */\nexport type ColorTSL = Node | ShaderNodeObject<Node>\n\n/**\n * Bind a TSL color expression to a NodeMaterial. Returns the material;\n * caller is responsible for adding it to a mesh and disposing when done.\n *\n * The TSL fragment is computed once via `useMemo` and re-applied if the\n * factory function changes. For dynamic uniforms, mutate `.value` on the\n * uniform nodes — don't recreate the TSL fragment per render.\n */\nexport function useShaderMaterial(build: () => ColorTSL): MeshBasicNodeMaterial {\n const material = useMemo(() => {\n const m = new MeshBasicNodeMaterial()\n m.colorNode = build() as Node\n return m\n }, [build])\n\n useEffect(() => {\n return () => material.dispose()\n }, [material])\n\n return material\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { CursorInput, type CursorInputOptions, type Vec2 } from '@lovo/matter'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport interface CursorSignal {\n /** Current smoothed cursor position (Vec2 in 0..1 viewport space). */\n get(): Vec2\n /** Subscribe to change events. Returns unsubscribe. */\n on(event: 'change', cb: (value: Vec2) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect\n// has created the real CursorInput. Calling .on returns an unsub no-op.\nconst STUB_SIGNAL: CursorSignal = {\n get: () => [0.5, 0.5] as const,\n on: () => () => undefined,\n}\n\n/**\n * React wrapper for CursorInput. Auto-attaches to the parent <MatterScene>'s\n * scheduler if available; otherwise creates a free-running rAF tick.\n *\n * Lifecycle is in a single effect so React 19 Strict Mode's intentional\n * mount→unmount→mount cycle creates a *fresh* CursorInput per real mount\n * instead of disposing a long-lived one (which would silently break the\n * window mousemove listener and the smoothing tick).\n */\nexport function useCursor(opts: CursorInputOptions = {}): CursorSignal {\n const ctx = useMatterContext()\n const [input, setInput] = useState<CursorInput | null>(null)\n\n useEffect(() => {\n // Plumb the parent <MatterScene>'s canvas as the cursor's normalization\n // element. Without this, cursor coords are viewport-normalized — fine for\n // a full-page scene but visibly offset when the canvas sits inside a\n // smaller wrapper (e.g., 70vh hero). DotField's cell tiling makes the\n // mismatch obvious; LinearGradient mostly gets away with it. Caller can\n // override by passing `opts.element` explicitly.\n const canvas = ctx?.renderer.three.domElement\n const elementOpt = opts.element ?? (canvas instanceof HTMLElement ? canvas : undefined)\n const fresh = new CursorInput({ ...opts, element: elementOpt })\n setInput(fresh)\n\n let detach: (() => void) | null = null\n if (ctx?.scheduler) {\n const client = ({ delta }: { delta: number }) => fresh.tick(delta)\n ctx.scheduler.add(client)\n detach = () => ctx.scheduler.remove(client)\n } else {\n let raf: number | null = null\n let lastNow = performance.now()\n const loop = (now: number) => {\n const delta = (now - lastNow) / 1000\n lastNow = now\n fresh.tick(delta)\n raf = requestAnimationFrame(loop)\n }\n raf = requestAnimationFrame(loop)\n detach = () => {\n if (raf !== null) cancelAnimationFrame(raf)\n }\n }\n\n return () => {\n detach?.()\n fresh.dispose()\n setInput(null)\n }\n // We intentionally only re-create on ctx change, not opts (which is a\n // fresh object literal each render). Smoothing tweaks during dev are\n // applied by remounting the parent component.\n // oxlint-disable-next-line react/exhaustive-deps\n }, [ctx])\n\n return input ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport type ResizeValue = readonly [width: number, height: number, dpr: number]\n\nexport interface ResizeSignal {\n /** Current size in CSS pixels + devicePixelRatio. */\n get(): ResizeValue\n on(event: 'change', cb: (value: ResizeValue) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect has\n// observed the canvas. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ResizeSignal = {\n get: () => [0, 0, 1] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track the parent <MatterScene>'s canvas size + DPR. Exposes a MatterSignal\n * that components can pass into a TSL uniform to make pixel-aware effects\n * (e.g., DotField's pixel-spacing math).\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle creates a fresh ResizeObserver per real mount\n * (CLAUDE.md gotcha #14).\n *\n * Falls back to the stub signal until the parent context is ready.\n */\nexport function useResize(): ResizeSignal {\n const ctx = useMatterContext()\n const [signal, setSignal] = useState<ResizeSignal | null>(null)\n\n useEffect(() => {\n if (!ctx) return undefined\n\n const canvas = ctx.renderer.three.domElement\n if (!(canvas instanceof HTMLCanvasElement)) return undefined\n\n let value: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n const listeners = new Set<(v: ResizeValue) => void>()\n const fresh: ResizeSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n const emit = () => {\n const next: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n if (next[0] === value[0] && next[1] === value[1] && next[2] === value[2]) return\n value = next\n for (const cb of listeners) cb(next)\n }\n\n const observer = new ResizeObserver(emit)\n observer.observe(canvas)\n\n // Cross-browser DPR-change watch. matchMedia(`(resolution: <dpr>dppx)`)\n // matches at the *current* DPR; when the user zooms the page the query\n // stops matching, fires `change`, and we re-arm the watch at the new DPR.\n // We track the current MQL + handler so we can fully detach in cleanup\n // (the handler is captured by the listener — passing a fresh closure to\n // removeEventListener wouldn't actually unregister it).\n let mql: MediaQueryList | null = null\n let mqlHandler: (() => void) | null = null\n const setupDprWatch = () => {\n if (typeof window === 'undefined') return\n const dpr = window.devicePixelRatio\n const next = window.matchMedia(`(resolution: ${dpr}dppx)`)\n const handler = () => {\n emit()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n setupDprWatch()\n }\n next.addEventListener('change', handler)\n mql = next\n mqlHandler = handler\n }\n setupDprWatch()\n\n return () => {\n observer.disconnect()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n mql = null\n mqlHandler = null\n listeners.clear()\n setSignal(null)\n }\n }, [ctx])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\nexport type ScrollValue = readonly [scrollY: number, progress: number]\n\nexport interface ScrollSignal {\n /** Current scroll Y (px) and normalized progress in [0,1]. */\n get(): ScrollValue\n on(event: 'change', cb: (value: ScrollValue) => void): () => void\n}\n\n// Inert stub returned during SSR + on the first client render before the\n// lifecycle effect attaches. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ScrollSignal = {\n get: () => [0, 0] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track window scroll position. Exposes a MatterSignal of `[scrollY, progress]`\n * where `progress` is `scrollY / max(documentHeight - innerHeight, 1)` clamped\n * to [0, 1]. Listener is rAF-throttled and `passive: true` so it never blocks\n * scrolling.\n *\n * No v1 Tier 1 component consumes this hook; it ships so users can pass\n * `inputs={{ scroll: useScroll() }}` to any Matter component.\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle in dev creates a fresh listener pair per real\n * mount and tears down cleanly on each pseudo-unmount (CLAUDE.md gotcha #14).\n *\n * **Known limitation (v1):** `progress` is computed against whichever\n * `documentHeight` was current when the last scroll fired. If the page grows\n * after mount (async content, font load reflow, expanding panels) without\n * the user scrolling, the denominator goes stale. A future ResizeObserver/\n * MutationObserver pass would close the gap; deferred until a v1 component\n * consumes scroll input.\n */\nexport function useScroll(): ScrollSignal {\n const [signal, setSignal] = useState<ScrollSignal | null>(null)\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined\n\n const compute = (): ScrollValue => {\n const y = window.scrollY\n // For pages shorter than the viewport, `documentHeight - innerHeight` is\n // <= 0; clamp to 1 to avoid div-by-zero. Progress stays at 0 in that\n // case because scrollY is also 0.\n const max = Math.max(document.documentElement.scrollHeight - window.innerHeight, 1)\n const progress = Math.max(0, Math.min(1, y / max))\n return [y, progress]\n }\n\n let value: ScrollValue = compute()\n const listeners = new Set<(v: ScrollValue) => void>()\n const fresh: ScrollSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n let rafPending = false\n const onScroll = () => {\n if (rafPending) return\n rafPending = true\n requestAnimationFrame(() => {\n rafPending = false\n const next = compute()\n if (next[0] === value[0] && next[1] === value[1]) return\n value = next\n for (const cb of listeners) cb(next)\n })\n }\n window.addEventListener('scroll', onScroll, { passive: true })\n\n return () => {\n window.removeEventListener('scroll', onScroll)\n listeners.clear()\n setSignal(null)\n }\n }, [])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { uniform } from 'three/tsl'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport type { Node } from 'three/webgpu'\n\nexport interface MatterSignal<T> {\n get(): T\n on(event: 'change', cb: (value: T) => void): () => void\n}\n\nexport type AnimatableProp<T> = T | MatterSignal<T>\n\nconst isSignal = <T>(value: AnimatableProp<T>): value is MatterSignal<T> => {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as MatterSignal<T>).get === 'function' &&\n typeof (value as MatterSignal<T>).on === 'function'\n )\n}\n\n/**\n * Bind an AnimatableProp<T> to a TSL uniform. Plain values create a\n * static uniform that updates only when the prop changes (React render\n * path). Signals subscribe via .on('change') and write into the uniform\n * imperatively without re-rendering.\n */\nexport function useAnimatableUniform<T>(value: AnimatableProp<T>): ShaderNodeObject<Node> {\n // Create the uniform once with the initial value; subsequent updates flow\n // through the effect below (either via signal subscription or direct write).\n const uniformNode = useMemo(() => {\n const initial = isSignal(value) ? value.get() : value\n return uniform(initial) as unknown as ShaderNodeObject<Node>\n // oxlint-disable-next-line react/exhaustive-deps\n }, [])\n\n useEffect(() => {\n if (isSignal(value)) {\n const unsub = value.on('change', (next) => {\n ;(uniformNode as unknown as { value: T }).value = next\n })\n return unsub\n }\n ;(uniformNode as unknown as { value: T }).value = value\n return undefined\n }, [value, uniformNode])\n\n return uniformNode\n}\n","'use client'\n\nimport { useEffect, type DependencyList } from 'react'\nimport type { OverlayTransform } from './matter-context.js'\nimport { useMatterContext } from './useMatterContext.js'\n\n/**\n * Register a TSL transform as an overlay pass on the parent <MatterScene>.\n *\n * The transform takes the \"color so far\" — base scene + any earlier\n * overlays as a TSL vec4 node — and returns a modified vec4. Registration\n * happens on mount; unregistration on unmount. The hook re-registers\n * whenever any value in `deps` changes (useEffect semantics): use this\n * for structural changes (e.g., a `mode: 'additive' | 'subtractive'`\n * toggle) that swap the transform function itself. Uniforms captured\n * inside the transform mutate in place, so uniform value changes do\n * NOT need to be in deps.\n *\n * When called outside a <MatterScene> provider, this hook is a no-op.\n * Matches the existing useMatterContext convention.\n */\nexport function useOverlayPass(transform: OverlayTransform, deps: DependencyList): void {\n const ctx = useMatterContext()\n\n useEffect(() => {\n if (!ctx) return\n const unregister = ctx.registerOverlay(transform)\n return unregister\n // The transform captures the latest values via the deps array; we re-register\n // when deps change. ctx is included so a remounted MatterScene re-attaches.\n // oxlint-disable-next-line react/exhaustive-deps\n }, [ctx, ...deps])\n}\n","'use client'\n\nimport { useEffect, useState, type ReactNode } from 'react'\n\nexport interface FallbackBoundaryProps {\n /** Rendered until WebGPU/WebGL is available on the client. */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Render `fallback` until the component mounts on the client. Gates the\n * children behind client-only mounting so SSR/no-WebGPU users see a\n * sensible static placeholder rather than a flash of nothing.\n */\nexport function FallbackBoundary({ fallback, children }: FallbackBoundaryProps) {\n const [mounted, setMounted] = useState(false)\n useEffect(() => {\n setMounted(true)\n }, [])\n return <>{mounted ? children : (fallback ?? null)}</>\n}\n","'use client'\n\nimport { useEffect } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\n/**\n * Opt a component out of the rAF loop while it has no dynamic uniforms.\n *\n * When `hint` is true, the scheduler runs one final flush tick (so any\n * uniform changes since the last frame are rendered) and then halts the\n * rAF loop until either `hint` becomes false or another component in the\n * same scene calls `scheduler.requestRender()`.\n *\n * Use for components whose animation is fully derived from props that don't\n * include `time`, e.g. `<LinearGradient speed={0}>` with no `interactive`.\n */\nexport function useStaticHint(hint: boolean): void {\n const ctx = useMatterContext()\n useEffect(() => {\n if (!ctx) return\n ctx.scheduler.setIdle(hint)\n return () => ctx.scheduler.setIdle(false)\n }, [ctx, hint])\n}\n","'use client'\n\nimport { useEffect, useRef, useState, useContext, type CSSProperties } from 'react'\nimport { MatterContext } from './matter-context.js'\n\nexport type MonitorAnchor = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\nconst anchorStyle: Record<MonitorAnchor, CSSProperties> = {\n 'top-left': { top: 8, left: 8 },\n 'top-right': { top: 8, right: 8 },\n 'bottom-left': { bottom: 8, left: 8 },\n 'bottom-right': { bottom: 8, right: 8 },\n}\n\nconst baseStyle: CSSProperties = {\n position: 'absolute',\n zIndex: 10,\n padding: '6px 8px',\n borderRadius: 6,\n background: 'rgba(0, 0, 0, 0.6)',\n color: '#fff',\n font: '11px ui-monospace, monospace',\n lineHeight: 1.4,\n pointerEvents: 'none',\n whiteSpace: 'pre',\n}\n\nexport interface MatterMonitorProps {\n anchor?: MonitorAnchor\n}\n\n/**\n * Dev-only overlay that displays the current scene's FPS, tick count, and\n * paused/idle state. Reads from the surrounding `<MatterScene>` via context\n * and subscribes to its scheduler. Renders nothing useful if mounted outside\n * a scene.\n */\nexport function MatterMonitor({ anchor = 'top-right' }: MatterMonitorProps) {\n const ctx = useContext(MatterContext)\n const [stats, setStats] = useState({ fps: 0, ticks: 0, frames: 0 })\n const ticksRef = useRef(0)\n const fpsAccumRef = useRef({ frames: 0, lastSampleAt: 0, fps: 0 })\n\n useEffect(() => {\n if (!ctx) return\n const client = (tick: { now: number }) => {\n ticksRef.current += 1\n const acc = fpsAccumRef.current\n acc.frames += 1\n if (acc.lastSampleAt === 0) acc.lastSampleAt = tick.now\n const dt = tick.now - acc.lastSampleAt\n if (dt >= 500) {\n acc.fps = Math.round((acc.frames * 1000) / dt)\n acc.frames = 0\n acc.lastSampleAt = tick.now\n }\n setStats({ fps: acc.fps, ticks: ticksRef.current, frames: acc.frames })\n }\n ctx.scheduler.add(client)\n return () => ctx.scheduler.remove(client)\n }, [ctx])\n\n if (!ctx) {\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n no scene\n </div>\n )\n }\n\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n <span data-testid=\"matter-monitor-fps\">fps: {stats.fps || '—'}</span>\n {'\\n'}\n <span data-testid=\"matter-monitor-ticks\">ticks: {stats.ticks}</span>\n </div>\n )\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_webgpu","import_react","import_matter","import_react","STUB_SIGNAL","import_react","STUB_SIGNAL","import_react","import_tsl","import_react","import_react","import_jsx_runtime","import_react","import_react","import_jsx_runtime"]}
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode, CSSProperties } from 'react';
2
+ import { ReactNode, CSSProperties, DependencyList } from 'react';
3
3
  import { Scene, Camera } from 'three';
4
- import { MatterRenderer, MatterScheduler, Vec2, CursorInputOptions } from '@lovo/matter';
5
4
  import { Node, MeshBasicNodeMaterial } from 'three/webgpu';
6
5
  import { ShaderNodeObject } from 'three/tsl';
6
+ import { MatterRenderer, MatterScheduler, Vec2, CursorInputOptions } from '@lovo/matter';
7
7
 
8
8
  interface MatterSceneProps {
9
9
  children?: ReactNode;
@@ -21,11 +21,13 @@ interface MatterSceneProps {
21
21
  */
22
22
  declare function MatterScene(props: MatterSceneProps): react_jsx_runtime.JSX.Element;
23
23
 
24
+ type OverlayTransform = (input: ShaderNodeObject<Node>) => ShaderNodeObject<Node>;
24
25
  interface MatterContextValue {
25
26
  renderer: MatterRenderer;
26
27
  scene: Scene;
27
28
  camera: Camera;
28
29
  scheduler: MatterScheduler;
30
+ registerOverlay: (transform: OverlayTransform) => () => void;
29
31
  }
30
32
 
31
33
  /**
@@ -124,6 +126,23 @@ type AnimatableProp<T> = T | MatterSignal<T>;
124
126
  */
125
127
  declare function useAnimatableUniform<T>(value: AnimatableProp<T>): ShaderNodeObject<Node>;
126
128
 
129
+ /**
130
+ * Register a TSL transform as an overlay pass on the parent <MatterScene>.
131
+ *
132
+ * The transform takes the "color so far" — base scene + any earlier
133
+ * overlays as a TSL vec4 node — and returns a modified vec4. Registration
134
+ * happens on mount; unregistration on unmount. The hook re-registers
135
+ * whenever any value in `deps` changes (useEffect semantics): use this
136
+ * for structural changes (e.g., a `mode: 'additive' | 'subtractive'`
137
+ * toggle) that swap the transform function itself. Uniforms captured
138
+ * inside the transform mutate in place, so uniform value changes do
139
+ * NOT need to be in deps.
140
+ *
141
+ * When called outside a <MatterScene> provider, this hook is a no-op.
142
+ * Matches the existing useMatterContext convention.
143
+ */
144
+ declare function useOverlayPass(transform: OverlayTransform, deps: DependencyList): void;
145
+
127
146
  interface FallbackBoundaryProps {
128
147
  /** Rendered until WebGPU/WebGL is available on the client. */
129
148
  fallback?: ReactNode;
@@ -161,4 +180,4 @@ interface MatterMonitorProps {
161
180
  */
162
181
  declare function MatterMonitor({ anchor }: MatterMonitorProps): react_jsx_runtime.JSX.Element;
163
182
 
164
- export { type AnimatableProp, type CursorSignal, FallbackBoundary, type FallbackBoundaryProps, type MatterContextValue, MatterMonitor, type MatterMonitorProps, MatterScene, type MatterSceneProps, type MatterSignal, type MonitorAnchor, type ResizeSignal, type ResizeValue, type ScrollSignal, type ScrollValue, useAnimatableUniform, useCursor, useMatterContext, useResize, useScroll, useShaderMaterial, useStaticHint };
183
+ export { type AnimatableProp, type CursorSignal, FallbackBoundary, type FallbackBoundaryProps, type MatterContextValue, MatterMonitor, type MatterMonitorProps, MatterScene, type MatterSceneProps, type MatterSignal, type MonitorAnchor, type OverlayTransform, type ResizeSignal, type ResizeValue, type ScrollSignal, type ScrollValue, useAnimatableUniform, useCursor, useMatterContext, useOverlayPass, useResize, useScroll, useShaderMaterial, useStaticHint };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode, CSSProperties } from 'react';
2
+ import { ReactNode, CSSProperties, DependencyList } from 'react';
3
3
  import { Scene, Camera } from 'three';
4
- import { MatterRenderer, MatterScheduler, Vec2, CursorInputOptions } from '@lovo/matter';
5
4
  import { Node, MeshBasicNodeMaterial } from 'three/webgpu';
6
5
  import { ShaderNodeObject } from 'three/tsl';
6
+ import { MatterRenderer, MatterScheduler, Vec2, CursorInputOptions } from '@lovo/matter';
7
7
 
8
8
  interface MatterSceneProps {
9
9
  children?: ReactNode;
@@ -21,11 +21,13 @@ interface MatterSceneProps {
21
21
  */
22
22
  declare function MatterScene(props: MatterSceneProps): react_jsx_runtime.JSX.Element;
23
23
 
24
+ type OverlayTransform = (input: ShaderNodeObject<Node>) => ShaderNodeObject<Node>;
24
25
  interface MatterContextValue {
25
26
  renderer: MatterRenderer;
26
27
  scene: Scene;
27
28
  camera: Camera;
28
29
  scheduler: MatterScheduler;
30
+ registerOverlay: (transform: OverlayTransform) => () => void;
29
31
  }
30
32
 
31
33
  /**
@@ -124,6 +126,23 @@ type AnimatableProp<T> = T | MatterSignal<T>;
124
126
  */
125
127
  declare function useAnimatableUniform<T>(value: AnimatableProp<T>): ShaderNodeObject<Node>;
126
128
 
129
+ /**
130
+ * Register a TSL transform as an overlay pass on the parent <MatterScene>.
131
+ *
132
+ * The transform takes the "color so far" — base scene + any earlier
133
+ * overlays as a TSL vec4 node — and returns a modified vec4. Registration
134
+ * happens on mount; unregistration on unmount. The hook re-registers
135
+ * whenever any value in `deps` changes (useEffect semantics): use this
136
+ * for structural changes (e.g., a `mode: 'additive' | 'subtractive'`
137
+ * toggle) that swap the transform function itself. Uniforms captured
138
+ * inside the transform mutate in place, so uniform value changes do
139
+ * NOT need to be in deps.
140
+ *
141
+ * When called outside a <MatterScene> provider, this hook is a no-op.
142
+ * Matches the existing useMatterContext convention.
143
+ */
144
+ declare function useOverlayPass(transform: OverlayTransform, deps: DependencyList): void;
145
+
127
146
  interface FallbackBoundaryProps {
128
147
  /** Rendered until WebGPU/WebGL is available on the client. */
129
148
  fallback?: ReactNode;
@@ -161,4 +180,4 @@ interface MatterMonitorProps {
161
180
  */
162
181
  declare function MatterMonitor({ anchor }: MatterMonitorProps): react_jsx_runtime.JSX.Element;
163
182
 
164
- export { type AnimatableProp, type CursorSignal, FallbackBoundary, type FallbackBoundaryProps, type MatterContextValue, MatterMonitor, type MatterMonitorProps, MatterScene, type MatterSceneProps, type MatterSignal, type MonitorAnchor, type ResizeSignal, type ResizeValue, type ScrollSignal, type ScrollValue, useAnimatableUniform, useCursor, useMatterContext, useResize, useScroll, useShaderMaterial, useStaticHint };
183
+ export { type AnimatableProp, type CursorSignal, FallbackBoundary, type FallbackBoundaryProps, type MatterContextValue, MatterMonitor, type MatterMonitorProps, MatterScene, type MatterSceneProps, type MatterSignal, type MonitorAnchor, type OverlayTransform, type ResizeSignal, type ResizeValue, type ScrollSignal, type ScrollValue, useAnimatableUniform, useCursor, useMatterContext, useOverlayPass, useResize, useScroll, useShaderMaterial, useStaticHint };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // src/MatterScene.tsx
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { Scene, OrthographicCamera } from "three";
4
+ import { PostProcessing } from "three/webgpu";
5
+ import { pass } from "three/tsl";
4
6
  import {
5
7
  createRenderer,
6
8
  MatterScheduler,
@@ -41,8 +43,29 @@ function MatterScene(props) {
41
43
  const scene = new Scene();
42
44
  const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
43
45
  camera.position.z = 1;
46
+ const postProcessing = new PostProcessing(renderer.three);
44
47
  const scheduler = new MatterScheduler();
45
- scheduler.add(() => renderer.three.render(scene, camera));
48
+ const overlays = /* @__PURE__ */ new Map();
49
+ const basePass = pass(scene, camera);
50
+ const rebuildOutputNode = () => {
51
+ const transforms = Array.from(overlays.values());
52
+ postProcessing.outputNode = transforms.reduce(
53
+ (node, transform) => transform(node),
54
+ basePass
55
+ );
56
+ postProcessing.needsUpdate = true;
57
+ };
58
+ rebuildOutputNode();
59
+ const registerOverlay = (transform) => {
60
+ const key = /* @__PURE__ */ Symbol("overlay");
61
+ overlays.set(key, transform);
62
+ rebuildOutputNode();
63
+ return () => {
64
+ overlays.delete(key);
65
+ rebuildOutputNode();
66
+ };
67
+ };
68
+ scheduler.add(() => postProcessing.render());
46
69
  scheduler.start();
47
70
  const visibility = createVisibilityWatcher();
48
71
  const intersection = createIntersectionWatcher(canvas);
@@ -65,7 +88,7 @@ function MatterScene(props) {
65
88
  scheduler.dispose();
66
89
  renderer.dispose();
67
90
  };
68
- setCtx({ renderer, scene, camera, scheduler });
91
+ setCtx({ renderer, scene, camera, scheduler, registerOverlay });
69
92
  } catch (err) {
70
93
  if (cancelled) return;
71
94
  const e = err instanceof Error ? err : new Error(String(err));
@@ -318,22 +341,33 @@ function useAnimatableUniform(value) {
318
341
  return uniformNode;
319
342
  }
320
343
 
344
+ // src/useOverlayPass.ts
345
+ import { useEffect as useEffect7 } from "react";
346
+ function useOverlayPass(transform, deps) {
347
+ const ctx = useMatterContext();
348
+ useEffect7(() => {
349
+ if (!ctx) return;
350
+ const unregister = ctx.registerOverlay(transform);
351
+ return unregister;
352
+ }, [ctx, ...deps]);
353
+ }
354
+
321
355
  // src/FallbackBoundary.tsx
322
- import { useEffect as useEffect7, useState as useState5 } from "react";
356
+ import { useEffect as useEffect8, useState as useState5 } from "react";
323
357
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
324
358
  function FallbackBoundary({ fallback, children }) {
325
359
  const [mounted, setMounted] = useState5(false);
326
- useEffect7(() => {
360
+ useEffect8(() => {
327
361
  setMounted(true);
328
362
  }, []);
329
363
  return /* @__PURE__ */ jsx2(Fragment, { children: mounted ? children : fallback ?? null });
330
364
  }
331
365
 
332
366
  // src/useStaticHint.ts
333
- import { useEffect as useEffect8 } from "react";
367
+ import { useEffect as useEffect9 } from "react";
334
368
  function useStaticHint(hint) {
335
369
  const ctx = useMatterContext();
336
- useEffect8(() => {
370
+ useEffect9(() => {
337
371
  if (!ctx) return;
338
372
  ctx.scheduler.setIdle(hint);
339
373
  return () => ctx.scheduler.setIdle(false);
@@ -341,7 +375,7 @@ function useStaticHint(hint) {
341
375
  }
342
376
 
343
377
  // src/MatterMonitor.tsx
344
- import { useEffect as useEffect9, useRef as useRef2, useState as useState6, useContext as useContext2 } from "react";
378
+ import { useEffect as useEffect10, useRef as useRef2, useState as useState6, useContext as useContext2 } from "react";
345
379
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
346
380
  var anchorStyle = {
347
381
  "top-left": { top: 8, left: 8 },
@@ -366,7 +400,7 @@ function MatterMonitor({ anchor = "top-right" }) {
366
400
  const [stats, setStats] = useState6({ fps: 0, ticks: 0, frames: 0 });
367
401
  const ticksRef = useRef2(0);
368
402
  const fpsAccumRef = useRef2({ frames: 0, lastSampleAt: 0, fps: 0 });
369
- useEffect9(() => {
403
+ useEffect10(() => {
370
404
  if (!ctx) return;
371
405
  const client = (tick) => {
372
406
  ticksRef.current += 1;
@@ -406,6 +440,7 @@ export {
406
440
  useAnimatableUniform,
407
441
  useCursor,
408
442
  useMatterContext,
443
+ useOverlayPass,
409
444
  useResize,
410
445
  useScroll,
411
446
  useShaderMaterial,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/MatterScene.tsx","../src/matter-context.ts","../src/useMatterContext.ts","../src/useShaderMaterial.ts","../src/useCursor.ts","../src/useResize.ts","../src/useScroll.ts","../src/useAnimatableUniform.ts","../src/FallbackBoundary.tsx","../src/useStaticHint.ts","../src/MatterMonitor.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'\nimport { Scene, OrthographicCamera } from 'three'\nimport {\n createRenderer,\n MatterScheduler,\n createVisibilityWatcher,\n createIntersectionWatcher,\n} from '@lovo/matter'\nimport { MatterContext, type MatterContextValue } from './matter-context.js'\n\nexport interface MatterSceneProps {\n children?: ReactNode\n /** Rendered server-side and during WebGPU init. Default: empty. */\n fallback?: ReactNode\n className?: string\n style?: CSSProperties\n /** Cap on devicePixelRatio. Default: 2. */\n maxDPR?: number\n}\n\nconst defaultStyle: CSSProperties = {\n position: 'absolute',\n inset: 0,\n display: 'block',\n width: '100%',\n height: '100%',\n}\n\n/**\n * Owns a canvas, a Three.js renderer (WebGPU + WebGL2 fallback), an\n * orthographic camera covering the canvas, an empty Scene, and a\n * MatterScheduler. Children consume these via useMatterContext().\n */\nexport function MatterScene(props: MatterSceneProps) {\n const { children, fallback, className, style, maxDPR } = props\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const [ctx, setCtx] = useState<MatterContextValue | null>(null)\n const [error, setError] = useState<Error | null>(null)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n let cancelled = false\n let cleanup: (() => void) | null = null\n\n const setup = async () => {\n try {\n const renderer = await createRenderer(canvas, { maxDPR })\n if (cancelled) {\n renderer.dispose()\n return\n }\n const scene = new Scene()\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10)\n camera.position.z = 1\n const scheduler = new MatterScheduler()\n\n scheduler.add(() => renderer.three.render(scene, camera))\n scheduler.start()\n\n const visibility = createVisibilityWatcher()\n const intersection = createIntersectionWatcher(canvas)\n\n const updatePauseState = () => {\n const shouldRun = visibility.isVisible() && intersection.isInView()\n if (shouldRun) scheduler.resume()\n else scheduler.pause()\n }\n updatePauseState()\n\n const unsubVisibility = visibility.subscribe(updatePauseState)\n const unsubIntersection = intersection.subscribe(updatePauseState)\n\n const onResize = () => renderer.resize()\n window.addEventListener('resize', onResize)\n\n cleanup = () => {\n unsubVisibility()\n unsubIntersection()\n visibility.dispose()\n intersection.dispose()\n window.removeEventListener('resize', onResize)\n scheduler.dispose()\n renderer.dispose()\n }\n\n setCtx({ renderer, scene, camera, scheduler })\n } catch (err) {\n if (cancelled) return\n const e = err instanceof Error ? err : new Error(String(err))\n console.error('[MatterScene] renderer init failed:', e)\n setError(e)\n }\n }\n\n void setup()\n return () => {\n cancelled = true\n cleanup?.()\n cleanup = null\n setCtx(null)\n }\n }, [maxDPR])\n\n return (\n <div className={className} style={{ ...defaultStyle, ...style }}>\n <canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />\n {error ? (\n <div\n style={{\n position: 'absolute',\n inset: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '1rem',\n color: '#fff',\n background: 'rgba(120, 30, 30, 0.85)',\n font: '0.85rem ui-monospace, monospace',\n whiteSpace: 'pre-wrap',\n textAlign: 'center',\n }}\n >\n MatterScene init failed:\n {'\\n'}\n {error.message}\n </div>\n ) : ctx ? (\n <MatterContext.Provider value={ctx}>{children}</MatterContext.Provider>\n ) : (\n (fallback ?? null)\n )}\n </div>\n )\n}\n","import { createContext } from 'react'\nimport type { Scene, Camera } from 'three'\nimport type { MatterRenderer, MatterScheduler } from '@lovo/matter'\n\nexport interface MatterContextValue {\n renderer: MatterRenderer\n scene: Scene\n camera: Camera\n scheduler: MatterScheduler\n}\n\nexport const MatterContext = createContext<MatterContextValue | null>(null)\n","import { useContext } from 'react'\nimport { MatterContext, type MatterContextValue } from './matter-context.js'\n\n/**\n * Read the matter scene context. Returns null when called outside a\n * <MatterScene>; useShaderMaterial and similar hooks check this and\n * auto-provision a scene if missing (auto-wrap behavior).\n */\nexport function useMatterContext(): MatterContextValue | null {\n return useContext(MatterContext)\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { MeshBasicNodeMaterial } from 'three/webgpu'\nimport type { Node } from 'three/webgpu'\nimport type { ShaderNodeObject } from 'three/tsl'\n\n/** A TSL fragment that produces a color. Accept any Node or TSL-wrapped node. */\nexport type ColorTSL = Node | ShaderNodeObject<Node>\n\n/**\n * Bind a TSL color expression to a NodeMaterial. Returns the material;\n * caller is responsible for adding it to a mesh and disposing when done.\n *\n * The TSL fragment is computed once via `useMemo` and re-applied if the\n * factory function changes. For dynamic uniforms, mutate `.value` on the\n * uniform nodes — don't recreate the TSL fragment per render.\n */\nexport function useShaderMaterial(build: () => ColorTSL): MeshBasicNodeMaterial {\n const material = useMemo(() => {\n const m = new MeshBasicNodeMaterial()\n m.colorNode = build() as Node\n return m\n }, [build])\n\n useEffect(() => {\n return () => material.dispose()\n }, [material])\n\n return material\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { CursorInput, type CursorInputOptions, type Vec2 } from '@lovo/matter'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport interface CursorSignal {\n /** Current smoothed cursor position (Vec2 in 0..1 viewport space). */\n get(): Vec2\n /** Subscribe to change events. Returns unsubscribe. */\n on(event: 'change', cb: (value: Vec2) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect\n// has created the real CursorInput. Calling .on returns an unsub no-op.\nconst STUB_SIGNAL: CursorSignal = {\n get: () => [0.5, 0.5] as const,\n on: () => () => undefined,\n}\n\n/**\n * React wrapper for CursorInput. Auto-attaches to the parent <MatterScene>'s\n * scheduler if available; otherwise creates a free-running rAF tick.\n *\n * Lifecycle is in a single effect so React 19 Strict Mode's intentional\n * mount→unmount→mount cycle creates a *fresh* CursorInput per real mount\n * instead of disposing a long-lived one (which would silently break the\n * window mousemove listener and the smoothing tick).\n */\nexport function useCursor(opts: CursorInputOptions = {}): CursorSignal {\n const ctx = useMatterContext()\n const [input, setInput] = useState<CursorInput | null>(null)\n\n useEffect(() => {\n // Plumb the parent <MatterScene>'s canvas as the cursor's normalization\n // element. Without this, cursor coords are viewport-normalized — fine for\n // a full-page scene but visibly offset when the canvas sits inside a\n // smaller wrapper (e.g., 70vh hero). DotField's cell tiling makes the\n // mismatch obvious; LinearGradient mostly gets away with it. Caller can\n // override by passing `opts.element` explicitly.\n const canvas = ctx?.renderer.three.domElement\n const elementOpt = opts.element ?? (canvas instanceof HTMLElement ? canvas : undefined)\n const fresh = new CursorInput({ ...opts, element: elementOpt })\n setInput(fresh)\n\n let detach: (() => void) | null = null\n if (ctx?.scheduler) {\n const client = ({ delta }: { delta: number }) => fresh.tick(delta)\n ctx.scheduler.add(client)\n detach = () => ctx.scheduler.remove(client)\n } else {\n let raf: number | null = null\n let lastNow = performance.now()\n const loop = (now: number) => {\n const delta = (now - lastNow) / 1000\n lastNow = now\n fresh.tick(delta)\n raf = requestAnimationFrame(loop)\n }\n raf = requestAnimationFrame(loop)\n detach = () => {\n if (raf !== null) cancelAnimationFrame(raf)\n }\n }\n\n return () => {\n detach?.()\n fresh.dispose()\n setInput(null)\n }\n // We intentionally only re-create on ctx change, not opts (which is a\n // fresh object literal each render). Smoothing tweaks during dev are\n // applied by remounting the parent component.\n // oxlint-disable-next-line react/exhaustive-deps\n }, [ctx])\n\n return input ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport type ResizeValue = readonly [width: number, height: number, dpr: number]\n\nexport interface ResizeSignal {\n /** Current size in CSS pixels + devicePixelRatio. */\n get(): ResizeValue\n on(event: 'change', cb: (value: ResizeValue) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect has\n// observed the canvas. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ResizeSignal = {\n get: () => [0, 0, 1] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track the parent <MatterScene>'s canvas size + DPR. Exposes a MatterSignal\n * that components can pass into a TSL uniform to make pixel-aware effects\n * (e.g., DotField's pixel-spacing math).\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle creates a fresh ResizeObserver per real mount\n * (CLAUDE.md gotcha #14).\n *\n * Falls back to the stub signal until the parent context is ready.\n */\nexport function useResize(): ResizeSignal {\n const ctx = useMatterContext()\n const [signal, setSignal] = useState<ResizeSignal | null>(null)\n\n useEffect(() => {\n if (!ctx) return undefined\n\n const canvas = ctx.renderer.three.domElement\n if (!(canvas instanceof HTMLCanvasElement)) return undefined\n\n let value: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n const listeners = new Set<(v: ResizeValue) => void>()\n const fresh: ResizeSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n const emit = () => {\n const next: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n if (next[0] === value[0] && next[1] === value[1] && next[2] === value[2]) return\n value = next\n for (const cb of listeners) cb(next)\n }\n\n const observer = new ResizeObserver(emit)\n observer.observe(canvas)\n\n // Cross-browser DPR-change watch. matchMedia(`(resolution: <dpr>dppx)`)\n // matches at the *current* DPR; when the user zooms the page the query\n // stops matching, fires `change`, and we re-arm the watch at the new DPR.\n // We track the current MQL + handler so we can fully detach in cleanup\n // (the handler is captured by the listener — passing a fresh closure to\n // removeEventListener wouldn't actually unregister it).\n let mql: MediaQueryList | null = null\n let mqlHandler: (() => void) | null = null\n const setupDprWatch = () => {\n if (typeof window === 'undefined') return\n const dpr = window.devicePixelRatio\n const next = window.matchMedia(`(resolution: ${dpr}dppx)`)\n const handler = () => {\n emit()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n setupDprWatch()\n }\n next.addEventListener('change', handler)\n mql = next\n mqlHandler = handler\n }\n setupDprWatch()\n\n return () => {\n observer.disconnect()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n mql = null\n mqlHandler = null\n listeners.clear()\n setSignal(null)\n }\n }, [ctx])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\nexport type ScrollValue = readonly [scrollY: number, progress: number]\n\nexport interface ScrollSignal {\n /** Current scroll Y (px) and normalized progress in [0,1]. */\n get(): ScrollValue\n on(event: 'change', cb: (value: ScrollValue) => void): () => void\n}\n\n// Inert stub returned during SSR + on the first client render before the\n// lifecycle effect attaches. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ScrollSignal = {\n get: () => [0, 0] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track window scroll position. Exposes a MatterSignal of `[scrollY, progress]`\n * where `progress` is `scrollY / max(documentHeight - innerHeight, 1)` clamped\n * to [0, 1]. Listener is rAF-throttled and `passive: true` so it never blocks\n * scrolling.\n *\n * No v1 Tier 1 component consumes this hook; it ships so users can pass\n * `inputs={{ scroll: useScroll() }}` to any Matter component.\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle in dev creates a fresh listener pair per real\n * mount and tears down cleanly on each pseudo-unmount (CLAUDE.md gotcha #14).\n *\n * **Known limitation (v1):** `progress` is computed against whichever\n * `documentHeight` was current when the last scroll fired. If the page grows\n * after mount (async content, font load reflow, expanding panels) without\n * the user scrolling, the denominator goes stale. A future ResizeObserver/\n * MutationObserver pass would close the gap; deferred until a v1 component\n * consumes scroll input.\n */\nexport function useScroll(): ScrollSignal {\n const [signal, setSignal] = useState<ScrollSignal | null>(null)\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined\n\n const compute = (): ScrollValue => {\n const y = window.scrollY\n // For pages shorter than the viewport, `documentHeight - innerHeight` is\n // <= 0; clamp to 1 to avoid div-by-zero. Progress stays at 0 in that\n // case because scrollY is also 0.\n const max = Math.max(document.documentElement.scrollHeight - window.innerHeight, 1)\n const progress = Math.max(0, Math.min(1, y / max))\n return [y, progress]\n }\n\n let value: ScrollValue = compute()\n const listeners = new Set<(v: ScrollValue) => void>()\n const fresh: ScrollSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n let rafPending = false\n const onScroll = () => {\n if (rafPending) return\n rafPending = true\n requestAnimationFrame(() => {\n rafPending = false\n const next = compute()\n if (next[0] === value[0] && next[1] === value[1]) return\n value = next\n for (const cb of listeners) cb(next)\n })\n }\n window.addEventListener('scroll', onScroll, { passive: true })\n\n return () => {\n window.removeEventListener('scroll', onScroll)\n listeners.clear()\n setSignal(null)\n }\n }, [])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { uniform } from 'three/tsl'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport type { Node } from 'three/webgpu'\n\nexport interface MatterSignal<T> {\n get(): T\n on(event: 'change', cb: (value: T) => void): () => void\n}\n\nexport type AnimatableProp<T> = T | MatterSignal<T>\n\nconst isSignal = <T>(value: AnimatableProp<T>): value is MatterSignal<T> => {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as MatterSignal<T>).get === 'function' &&\n typeof (value as MatterSignal<T>).on === 'function'\n )\n}\n\n/**\n * Bind an AnimatableProp<T> to a TSL uniform. Plain values create a\n * static uniform that updates only when the prop changes (React render\n * path). Signals subscribe via .on('change') and write into the uniform\n * imperatively without re-rendering.\n */\nexport function useAnimatableUniform<T>(value: AnimatableProp<T>): ShaderNodeObject<Node> {\n // Create the uniform once with the initial value; subsequent updates flow\n // through the effect below (either via signal subscription or direct write).\n const uniformNode = useMemo(() => {\n const initial = isSignal(value) ? value.get() : value\n return uniform(initial) as unknown as ShaderNodeObject<Node>\n // oxlint-disable-next-line react/exhaustive-deps\n }, [])\n\n useEffect(() => {\n if (isSignal(value)) {\n const unsub = value.on('change', (next) => {\n ;(uniformNode as unknown as { value: T }).value = next\n })\n return unsub\n }\n ;(uniformNode as unknown as { value: T }).value = value\n return undefined\n }, [value, uniformNode])\n\n return uniformNode\n}\n","'use client'\n\nimport { useEffect, useState, type ReactNode } from 'react'\n\nexport interface FallbackBoundaryProps {\n /** Rendered until WebGPU/WebGL is available on the client. */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Render `fallback` until the component mounts on the client. Gates the\n * children behind client-only mounting so SSR/no-WebGPU users see a\n * sensible static placeholder rather than a flash of nothing.\n */\nexport function FallbackBoundary({ fallback, children }: FallbackBoundaryProps) {\n const [mounted, setMounted] = useState(false)\n useEffect(() => {\n setMounted(true)\n }, [])\n return <>{mounted ? children : (fallback ?? null)}</>\n}\n","'use client'\n\nimport { useEffect } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\n/**\n * Opt a component out of the rAF loop while it has no dynamic uniforms.\n *\n * When `hint` is true, the scheduler runs one final flush tick (so any\n * uniform changes since the last frame are rendered) and then halts the\n * rAF loop until either `hint` becomes false or another component in the\n * same scene calls `scheduler.requestRender()`.\n *\n * Use for components whose animation is fully derived from props that don't\n * include `time`, e.g. `<LinearGradient speed={0}>` with no `interactive`.\n */\nexport function useStaticHint(hint: boolean): void {\n const ctx = useMatterContext()\n useEffect(() => {\n if (!ctx) return\n ctx.scheduler.setIdle(hint)\n return () => ctx.scheduler.setIdle(false)\n }, [ctx, hint])\n}\n","'use client'\n\nimport { useEffect, useRef, useState, useContext, type CSSProperties } from 'react'\nimport { MatterContext } from './matter-context.js'\n\nexport type MonitorAnchor = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\nconst anchorStyle: Record<MonitorAnchor, CSSProperties> = {\n 'top-left': { top: 8, left: 8 },\n 'top-right': { top: 8, right: 8 },\n 'bottom-left': { bottom: 8, left: 8 },\n 'bottom-right': { bottom: 8, right: 8 },\n}\n\nconst baseStyle: CSSProperties = {\n position: 'absolute',\n zIndex: 10,\n padding: '6px 8px',\n borderRadius: 6,\n background: 'rgba(0, 0, 0, 0.6)',\n color: '#fff',\n font: '11px ui-monospace, monospace',\n lineHeight: 1.4,\n pointerEvents: 'none',\n whiteSpace: 'pre',\n}\n\nexport interface MatterMonitorProps {\n anchor?: MonitorAnchor\n}\n\n/**\n * Dev-only overlay that displays the current scene's FPS, tick count, and\n * paused/idle state. Reads from the surrounding `<MatterScene>` via context\n * and subscribes to its scheduler. Renders nothing useful if mounted outside\n * a scene.\n */\nexport function MatterMonitor({ anchor = 'top-right' }: MatterMonitorProps) {\n const ctx = useContext(MatterContext)\n const [stats, setStats] = useState({ fps: 0, ticks: 0, frames: 0 })\n const ticksRef = useRef(0)\n const fpsAccumRef = useRef({ frames: 0, lastSampleAt: 0, fps: 0 })\n\n useEffect(() => {\n if (!ctx) return\n const client = (tick: { now: number }) => {\n ticksRef.current += 1\n const acc = fpsAccumRef.current\n acc.frames += 1\n if (acc.lastSampleAt === 0) acc.lastSampleAt = tick.now\n const dt = tick.now - acc.lastSampleAt\n if (dt >= 500) {\n acc.fps = Math.round((acc.frames * 1000) / dt)\n acc.frames = 0\n acc.lastSampleAt = tick.now\n }\n setStats({ fps: acc.fps, ticks: ticksRef.current, frames: acc.frames })\n }\n ctx.scheduler.add(client)\n return () => ctx.scheduler.remove(client)\n }, [ctx])\n\n if (!ctx) {\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n no scene\n </div>\n )\n }\n\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n <span data-testid=\"matter-monitor-fps\">fps: {stats.fps || '—'}</span>\n {'\\n'}\n <span data-testid=\"matter-monitor-ticks\">ticks: {stats.ticks}</span>\n </div>\n )\n}\n"],"mappings":"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","names":["useEffect","useEffect","useState","useState","useEffect","useEffect","useState","STUB_SIGNAL","useState","useEffect","useEffect","useState","STUB_SIGNAL","useEffect","useMemo","useEffect","useState","jsx","useEffect","useEffect","useEffect","useRef","useState","useContext","jsx","jsxs","useContext","useState","useRef","useEffect"]}
1
+ {"version":3,"sources":["../src/MatterScene.tsx","../src/matter-context.ts","../src/useMatterContext.ts","../src/useShaderMaterial.ts","../src/useCursor.ts","../src/useResize.ts","../src/useScroll.ts","../src/useAnimatableUniform.ts","../src/useOverlayPass.ts","../src/FallbackBoundary.tsx","../src/useStaticHint.ts","../src/MatterMonitor.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'\nimport { Scene, OrthographicCamera } from 'three'\nimport { PostProcessing } from 'three/webgpu'\nimport type { Node } from 'three/webgpu'\nimport { pass } from 'three/tsl'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport {\n createRenderer,\n MatterScheduler,\n createVisibilityWatcher,\n createIntersectionWatcher,\n} from '@lovo/matter'\nimport { MatterContext, type MatterContextValue, type OverlayTransform } from './matter-context.js'\n\nexport interface MatterSceneProps {\n children?: ReactNode\n /** Rendered server-side and during WebGPU init. Default: empty. */\n fallback?: ReactNode\n className?: string\n style?: CSSProperties\n /** Cap on devicePixelRatio. Default: 2. */\n maxDPR?: number\n}\n\nconst defaultStyle: CSSProperties = {\n position: 'absolute',\n inset: 0,\n display: 'block',\n width: '100%',\n height: '100%',\n}\n\n/**\n * Owns a canvas, a Three.js renderer (WebGPU + WebGL2 fallback), an\n * orthographic camera covering the canvas, an empty Scene, and a\n * MatterScheduler. Children consume these via useMatterContext().\n */\nexport function MatterScene(props: MatterSceneProps) {\n const { children, fallback, className, style, maxDPR } = props\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const [ctx, setCtx] = useState<MatterContextValue | null>(null)\n const [error, setError] = useState<Error | null>(null)\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n let cancelled = false\n let cleanup: (() => void) | null = null\n\n const setup = async () => {\n try {\n const renderer = await createRenderer(canvas, { maxDPR })\n if (cancelled) {\n renderer.dispose()\n return\n }\n const scene = new Scene()\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10)\n camera.position.z = 1\n const postProcessing = new PostProcessing(renderer.three)\n const scheduler = new MatterScheduler()\n\n const overlays = new Map<symbol, OverlayTransform>()\n\n // Allocate the base PassNode once per setup so rebuilds reuse the same\n // node identity instead of churning a fresh one (and a fresh render\n // target binding) on every register/unregister.\n const basePass = pass(scene, camera) as unknown as ShaderNodeObject<Node>\n\n const rebuildOutputNode = () => {\n const transforms = Array.from(overlays.values())\n postProcessing.outputNode = transforms.reduce(\n (node, transform) => transform(node),\n basePass,\n )\n postProcessing.needsUpdate = true\n }\n\n rebuildOutputNode() // initial: just basePass, no overlays\n\n const registerOverlay = (transform: OverlayTransform): (() => void) => {\n const key = Symbol('overlay')\n overlays.set(key, transform)\n rebuildOutputNode()\n return () => {\n overlays.delete(key)\n rebuildOutputNode()\n }\n }\n\n scheduler.add(() => postProcessing.render())\n scheduler.start()\n\n const visibility = createVisibilityWatcher()\n const intersection = createIntersectionWatcher(canvas)\n\n const updatePauseState = () => {\n const shouldRun = visibility.isVisible() && intersection.isInView()\n if (shouldRun) scheduler.resume()\n else scheduler.pause()\n }\n updatePauseState()\n\n const unsubVisibility = visibility.subscribe(updatePauseState)\n const unsubIntersection = intersection.subscribe(updatePauseState)\n\n const onResize = () => renderer.resize()\n window.addEventListener('resize', onResize)\n\n cleanup = () => {\n unsubVisibility()\n unsubIntersection()\n visibility.dispose()\n intersection.dispose()\n window.removeEventListener('resize', onResize)\n scheduler.dispose()\n renderer.dispose()\n }\n\n setCtx({ renderer, scene, camera, scheduler, registerOverlay })\n } catch (err) {\n if (cancelled) return\n const e = err instanceof Error ? err : new Error(String(err))\n console.error('[MatterScene] renderer init failed:', e)\n setError(e)\n }\n }\n\n void setup()\n return () => {\n cancelled = true\n cleanup?.()\n cleanup = null\n setCtx(null)\n }\n }, [maxDPR])\n\n return (\n <div className={className} style={{ ...defaultStyle, ...style }}>\n <canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />\n {error ? (\n <div\n style={{\n position: 'absolute',\n inset: 0,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: '1rem',\n color: '#fff',\n background: 'rgba(120, 30, 30, 0.85)',\n font: '0.85rem ui-monospace, monospace',\n whiteSpace: 'pre-wrap',\n textAlign: 'center',\n }}\n >\n MatterScene init failed:\n {'\\n'}\n {error.message}\n </div>\n ) : ctx ? (\n <MatterContext.Provider value={ctx}>{children}</MatterContext.Provider>\n ) : (\n (fallback ?? null)\n )}\n </div>\n )\n}\n","import { createContext } from 'react'\nimport type { Scene, Camera } from 'three'\nimport type { Node } from 'three/webgpu'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport type { MatterRenderer, MatterScheduler } from '@lovo/matter'\n\nexport type OverlayTransform = (input: ShaderNodeObject<Node>) => ShaderNodeObject<Node>\n\nexport interface MatterContextValue {\n renderer: MatterRenderer\n scene: Scene\n camera: Camera\n scheduler: MatterScheduler\n registerOverlay: (transform: OverlayTransform) => () => void\n}\n\nexport const MatterContext = createContext<MatterContextValue | null>(null)\n","import { useContext } from 'react'\nimport { MatterContext, type MatterContextValue } from './matter-context.js'\n\n/**\n * Read the matter scene context. Returns null when called outside a\n * <MatterScene>; useShaderMaterial and similar hooks check this and\n * auto-provision a scene if missing (auto-wrap behavior).\n */\nexport function useMatterContext(): MatterContextValue | null {\n return useContext(MatterContext)\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { MeshBasicNodeMaterial } from 'three/webgpu'\nimport type { Node } from 'three/webgpu'\nimport type { ShaderNodeObject } from 'three/tsl'\n\n/** A TSL fragment that produces a color. Accept any Node or TSL-wrapped node. */\nexport type ColorTSL = Node | ShaderNodeObject<Node>\n\n/**\n * Bind a TSL color expression to a NodeMaterial. Returns the material;\n * caller is responsible for adding it to a mesh and disposing when done.\n *\n * The TSL fragment is computed once via `useMemo` and re-applied if the\n * factory function changes. For dynamic uniforms, mutate `.value` on the\n * uniform nodes — don't recreate the TSL fragment per render.\n */\nexport function useShaderMaterial(build: () => ColorTSL): MeshBasicNodeMaterial {\n const material = useMemo(() => {\n const m = new MeshBasicNodeMaterial()\n m.colorNode = build() as Node\n return m\n }, [build])\n\n useEffect(() => {\n return () => material.dispose()\n }, [material])\n\n return material\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { CursorInput, type CursorInputOptions, type Vec2 } from '@lovo/matter'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport interface CursorSignal {\n /** Current smoothed cursor position (Vec2 in 0..1 viewport space). */\n get(): Vec2\n /** Subscribe to change events. Returns unsubscribe. */\n on(event: 'change', cb: (value: Vec2) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect\n// has created the real CursorInput. Calling .on returns an unsub no-op.\nconst STUB_SIGNAL: CursorSignal = {\n get: () => [0.5, 0.5] as const,\n on: () => () => undefined,\n}\n\n/**\n * React wrapper for CursorInput. Auto-attaches to the parent <MatterScene>'s\n * scheduler if available; otherwise creates a free-running rAF tick.\n *\n * Lifecycle is in a single effect so React 19 Strict Mode's intentional\n * mount→unmount→mount cycle creates a *fresh* CursorInput per real mount\n * instead of disposing a long-lived one (which would silently break the\n * window mousemove listener and the smoothing tick).\n */\nexport function useCursor(opts: CursorInputOptions = {}): CursorSignal {\n const ctx = useMatterContext()\n const [input, setInput] = useState<CursorInput | null>(null)\n\n useEffect(() => {\n // Plumb the parent <MatterScene>'s canvas as the cursor's normalization\n // element. Without this, cursor coords are viewport-normalized — fine for\n // a full-page scene but visibly offset when the canvas sits inside a\n // smaller wrapper (e.g., 70vh hero). DotField's cell tiling makes the\n // mismatch obvious; LinearGradient mostly gets away with it. Caller can\n // override by passing `opts.element` explicitly.\n const canvas = ctx?.renderer.three.domElement\n const elementOpt = opts.element ?? (canvas instanceof HTMLElement ? canvas : undefined)\n const fresh = new CursorInput({ ...opts, element: elementOpt })\n setInput(fresh)\n\n let detach: (() => void) | null = null\n if (ctx?.scheduler) {\n const client = ({ delta }: { delta: number }) => fresh.tick(delta)\n ctx.scheduler.add(client)\n detach = () => ctx.scheduler.remove(client)\n } else {\n let raf: number | null = null\n let lastNow = performance.now()\n const loop = (now: number) => {\n const delta = (now - lastNow) / 1000\n lastNow = now\n fresh.tick(delta)\n raf = requestAnimationFrame(loop)\n }\n raf = requestAnimationFrame(loop)\n detach = () => {\n if (raf !== null) cancelAnimationFrame(raf)\n }\n }\n\n return () => {\n detach?.()\n fresh.dispose()\n setInput(null)\n }\n // We intentionally only re-create on ctx change, not opts (which is a\n // fresh object literal each render). Smoothing tweaks during dev are\n // applied by remounting the parent component.\n // oxlint-disable-next-line react/exhaustive-deps\n }, [ctx])\n\n return input ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\nexport type ResizeValue = readonly [width: number, height: number, dpr: number]\n\nexport interface ResizeSignal {\n /** Current size in CSS pixels + devicePixelRatio. */\n get(): ResizeValue\n on(event: 'change', cb: (value: ResizeValue) => void): () => void\n}\n\n// Inert stub returned on the first render before the lifecycle effect has\n// observed the canvas. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ResizeSignal = {\n get: () => [0, 0, 1] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track the parent <MatterScene>'s canvas size + DPR. Exposes a MatterSignal\n * that components can pass into a TSL uniform to make pixel-aware effects\n * (e.g., DotField's pixel-spacing math).\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle creates a fresh ResizeObserver per real mount\n * (CLAUDE.md gotcha #14).\n *\n * Falls back to the stub signal until the parent context is ready.\n */\nexport function useResize(): ResizeSignal {\n const ctx = useMatterContext()\n const [signal, setSignal] = useState<ResizeSignal | null>(null)\n\n useEffect(() => {\n if (!ctx) return undefined\n\n const canvas = ctx.renderer.three.domElement\n if (!(canvas instanceof HTMLCanvasElement)) return undefined\n\n let value: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n const listeners = new Set<(v: ResizeValue) => void>()\n const fresh: ResizeSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n const emit = () => {\n const next: ResizeValue = [\n canvas.clientWidth,\n canvas.clientHeight,\n typeof window !== 'undefined' ? window.devicePixelRatio : 1,\n ]\n if (next[0] === value[0] && next[1] === value[1] && next[2] === value[2]) return\n value = next\n for (const cb of listeners) cb(next)\n }\n\n const observer = new ResizeObserver(emit)\n observer.observe(canvas)\n\n // Cross-browser DPR-change watch. matchMedia(`(resolution: <dpr>dppx)`)\n // matches at the *current* DPR; when the user zooms the page the query\n // stops matching, fires `change`, and we re-arm the watch at the new DPR.\n // We track the current MQL + handler so we can fully detach in cleanup\n // (the handler is captured by the listener — passing a fresh closure to\n // removeEventListener wouldn't actually unregister it).\n let mql: MediaQueryList | null = null\n let mqlHandler: (() => void) | null = null\n const setupDprWatch = () => {\n if (typeof window === 'undefined') return\n const dpr = window.devicePixelRatio\n const next = window.matchMedia(`(resolution: ${dpr}dppx)`)\n const handler = () => {\n emit()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n setupDprWatch()\n }\n next.addEventListener('change', handler)\n mql = next\n mqlHandler = handler\n }\n setupDprWatch()\n\n return () => {\n observer.disconnect()\n if (mql && mqlHandler) mql.removeEventListener('change', mqlHandler)\n mql = null\n mqlHandler = null\n listeners.clear()\n setSignal(null)\n }\n }, [ctx])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\nexport type ScrollValue = readonly [scrollY: number, progress: number]\n\nexport interface ScrollSignal {\n /** Current scroll Y (px) and normalized progress in [0,1]. */\n get(): ScrollValue\n on(event: 'change', cb: (value: ScrollValue) => void): () => void\n}\n\n// Inert stub returned during SSR + on the first client render before the\n// lifecycle effect attaches. Subscribing to it returns a no-op unsubscribe.\nconst STUB_SIGNAL: ScrollSignal = {\n get: () => [0, 0] as const,\n on: () => () => undefined,\n}\n\n/**\n * Track window scroll position. Exposes a MatterSignal of `[scrollY, progress]`\n * where `progress` is `scrollY / max(documentHeight - innerHeight, 1)` clamped\n * to [0, 1]. Listener is rAF-throttled and `passive: true` so it never blocks\n * scrolling.\n *\n * No v1 Tier 1 component consumes this hook; it ships so users can pass\n * `inputs={{ scroll: useScroll() }}` to any Matter component.\n *\n * Strict-Mode-safe: lifecycle is in one effect, so React 19's intentional\n * mount→unmount→mount cycle in dev creates a fresh listener pair per real\n * mount and tears down cleanly on each pseudo-unmount (CLAUDE.md gotcha #14).\n *\n * **Known limitation (v1):** `progress` is computed against whichever\n * `documentHeight` was current when the last scroll fired. If the page grows\n * after mount (async content, font load reflow, expanding panels) without\n * the user scrolling, the denominator goes stale. A future ResizeObserver/\n * MutationObserver pass would close the gap; deferred until a v1 component\n * consumes scroll input.\n */\nexport function useScroll(): ScrollSignal {\n const [signal, setSignal] = useState<ScrollSignal | null>(null)\n\n useEffect(() => {\n if (typeof window === 'undefined') return undefined\n\n const compute = (): ScrollValue => {\n const y = window.scrollY\n // For pages shorter than the viewport, `documentHeight - innerHeight` is\n // <= 0; clamp to 1 to avoid div-by-zero. Progress stays at 0 in that\n // case because scrollY is also 0.\n const max = Math.max(document.documentElement.scrollHeight - window.innerHeight, 1)\n const progress = Math.max(0, Math.min(1, y / max))\n return [y, progress]\n }\n\n let value: ScrollValue = compute()\n const listeners = new Set<(v: ScrollValue) => void>()\n const fresh: ScrollSignal = {\n get: () => value,\n on: (_event, cb) => {\n listeners.add(cb)\n return () => {\n listeners.delete(cb)\n }\n },\n }\n setSignal(fresh)\n\n let rafPending = false\n const onScroll = () => {\n if (rafPending) return\n rafPending = true\n requestAnimationFrame(() => {\n rafPending = false\n const next = compute()\n if (next[0] === value[0] && next[1] === value[1]) return\n value = next\n for (const cb of listeners) cb(next)\n })\n }\n window.addEventListener('scroll', onScroll, { passive: true })\n\n return () => {\n window.removeEventListener('scroll', onScroll)\n listeners.clear()\n setSignal(null)\n }\n }, [])\n\n return signal ?? STUB_SIGNAL\n}\n","'use client'\n\nimport { useEffect, useMemo } from 'react'\nimport { uniform } from 'three/tsl'\nimport type { ShaderNodeObject } from 'three/tsl'\nimport type { Node } from 'three/webgpu'\n\nexport interface MatterSignal<T> {\n get(): T\n on(event: 'change', cb: (value: T) => void): () => void\n}\n\nexport type AnimatableProp<T> = T | MatterSignal<T>\n\nconst isSignal = <T>(value: AnimatableProp<T>): value is MatterSignal<T> => {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as MatterSignal<T>).get === 'function' &&\n typeof (value as MatterSignal<T>).on === 'function'\n )\n}\n\n/**\n * Bind an AnimatableProp<T> to a TSL uniform. Plain values create a\n * static uniform that updates only when the prop changes (React render\n * path). Signals subscribe via .on('change') and write into the uniform\n * imperatively without re-rendering.\n */\nexport function useAnimatableUniform<T>(value: AnimatableProp<T>): ShaderNodeObject<Node> {\n // Create the uniform once with the initial value; subsequent updates flow\n // through the effect below (either via signal subscription or direct write).\n const uniformNode = useMemo(() => {\n const initial = isSignal(value) ? value.get() : value\n return uniform(initial) as unknown as ShaderNodeObject<Node>\n // oxlint-disable-next-line react/exhaustive-deps\n }, [])\n\n useEffect(() => {\n if (isSignal(value)) {\n const unsub = value.on('change', (next) => {\n ;(uniformNode as unknown as { value: T }).value = next\n })\n return unsub\n }\n ;(uniformNode as unknown as { value: T }).value = value\n return undefined\n }, [value, uniformNode])\n\n return uniformNode\n}\n","'use client'\n\nimport { useEffect, type DependencyList } from 'react'\nimport type { OverlayTransform } from './matter-context.js'\nimport { useMatterContext } from './useMatterContext.js'\n\n/**\n * Register a TSL transform as an overlay pass on the parent <MatterScene>.\n *\n * The transform takes the \"color so far\" — base scene + any earlier\n * overlays as a TSL vec4 node — and returns a modified vec4. Registration\n * happens on mount; unregistration on unmount. The hook re-registers\n * whenever any value in `deps` changes (useEffect semantics): use this\n * for structural changes (e.g., a `mode: 'additive' | 'subtractive'`\n * toggle) that swap the transform function itself. Uniforms captured\n * inside the transform mutate in place, so uniform value changes do\n * NOT need to be in deps.\n *\n * When called outside a <MatterScene> provider, this hook is a no-op.\n * Matches the existing useMatterContext convention.\n */\nexport function useOverlayPass(transform: OverlayTransform, deps: DependencyList): void {\n const ctx = useMatterContext()\n\n useEffect(() => {\n if (!ctx) return\n const unregister = ctx.registerOverlay(transform)\n return unregister\n // The transform captures the latest values via the deps array; we re-register\n // when deps change. ctx is included so a remounted MatterScene re-attaches.\n // oxlint-disable-next-line react/exhaustive-deps\n }, [ctx, ...deps])\n}\n","'use client'\n\nimport { useEffect, useState, type ReactNode } from 'react'\n\nexport interface FallbackBoundaryProps {\n /** Rendered until WebGPU/WebGL is available on the client. */\n fallback?: ReactNode\n children: ReactNode\n}\n\n/**\n * Render `fallback` until the component mounts on the client. Gates the\n * children behind client-only mounting so SSR/no-WebGPU users see a\n * sensible static placeholder rather than a flash of nothing.\n */\nexport function FallbackBoundary({ fallback, children }: FallbackBoundaryProps) {\n const [mounted, setMounted] = useState(false)\n useEffect(() => {\n setMounted(true)\n }, [])\n return <>{mounted ? children : (fallback ?? null)}</>\n}\n","'use client'\n\nimport { useEffect } from 'react'\nimport { useMatterContext } from './useMatterContext.js'\n\n/**\n * Opt a component out of the rAF loop while it has no dynamic uniforms.\n *\n * When `hint` is true, the scheduler runs one final flush tick (so any\n * uniform changes since the last frame are rendered) and then halts the\n * rAF loop until either `hint` becomes false or another component in the\n * same scene calls `scheduler.requestRender()`.\n *\n * Use for components whose animation is fully derived from props that don't\n * include `time`, e.g. `<LinearGradient speed={0}>` with no `interactive`.\n */\nexport function useStaticHint(hint: boolean): void {\n const ctx = useMatterContext()\n useEffect(() => {\n if (!ctx) return\n ctx.scheduler.setIdle(hint)\n return () => ctx.scheduler.setIdle(false)\n }, [ctx, hint])\n}\n","'use client'\n\nimport { useEffect, useRef, useState, useContext, type CSSProperties } from 'react'\nimport { MatterContext } from './matter-context.js'\n\nexport type MonitorAnchor = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n\nconst anchorStyle: Record<MonitorAnchor, CSSProperties> = {\n 'top-left': { top: 8, left: 8 },\n 'top-right': { top: 8, right: 8 },\n 'bottom-left': { bottom: 8, left: 8 },\n 'bottom-right': { bottom: 8, right: 8 },\n}\n\nconst baseStyle: CSSProperties = {\n position: 'absolute',\n zIndex: 10,\n padding: '6px 8px',\n borderRadius: 6,\n background: 'rgba(0, 0, 0, 0.6)',\n color: '#fff',\n font: '11px ui-monospace, monospace',\n lineHeight: 1.4,\n pointerEvents: 'none',\n whiteSpace: 'pre',\n}\n\nexport interface MatterMonitorProps {\n anchor?: MonitorAnchor\n}\n\n/**\n * Dev-only overlay that displays the current scene's FPS, tick count, and\n * paused/idle state. Reads from the surrounding `<MatterScene>` via context\n * and subscribes to its scheduler. Renders nothing useful if mounted outside\n * a scene.\n */\nexport function MatterMonitor({ anchor = 'top-right' }: MatterMonitorProps) {\n const ctx = useContext(MatterContext)\n const [stats, setStats] = useState({ fps: 0, ticks: 0, frames: 0 })\n const ticksRef = useRef(0)\n const fpsAccumRef = useRef({ frames: 0, lastSampleAt: 0, fps: 0 })\n\n useEffect(() => {\n if (!ctx) return\n const client = (tick: { now: number }) => {\n ticksRef.current += 1\n const acc = fpsAccumRef.current\n acc.frames += 1\n if (acc.lastSampleAt === 0) acc.lastSampleAt = tick.now\n const dt = tick.now - acc.lastSampleAt\n if (dt >= 500) {\n acc.fps = Math.round((acc.frames * 1000) / dt)\n acc.frames = 0\n acc.lastSampleAt = tick.now\n }\n setStats({ fps: acc.fps, ticks: ticksRef.current, frames: acc.frames })\n }\n ctx.scheduler.add(client)\n return () => ctx.scheduler.remove(client)\n }, [ctx])\n\n if (!ctx) {\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n no scene\n </div>\n )\n }\n\n return (\n <div data-testid=\"matter-monitor\" style={{ ...baseStyle, ...anchorStyle[anchor] }}>\n <span data-testid=\"matter-monitor-fps\">fps: {stats.fps || '—'}</span>\n {'\\n'}\n <span data-testid=\"matter-monitor-ticks\">ticks: {stats.ticks}</span>\n </div>\n )\n}\n"],"mappings":"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","names":["useEffect","useEffect","useState","useState","useEffect","useEffect","useState","STUB_SIGNAL","useState","useEffect","useEffect","useState","STUB_SIGNAL","useEffect","useMemo","useEffect","useEffect","useEffect","useState","jsx","useEffect","useEffect","useEffect","useRef","useState","useContext","jsx","jsxs","useContext","useState","useRef","useEffect"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovo/matter-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "React binding for Matter — MatterScene, useShaderMaterial, input hooks.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -44,9 +44,9 @@
44
44
  "access": "public"
45
45
  },
46
46
  "peerDependencies": {
47
+ "@lovo/matter": ">=0.2.0",
47
48
  "react": "^19.0.0",
48
- "three": "^0.170.0",
49
- "@lovo/matter": "^0.2.0"
49
+ "three": "^0.170.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@testing-library/dom": "^10.4.1",
@@ -62,7 +62,7 @@
62
62
  "typescript": "^5.6.0",
63
63
  "vitest": "npm:@voidzero-dev/vite-plus-test@latest",
64
64
  "vite-plus": "latest",
65
- "@lovo/matter": "0.2.0",
65
+ "@lovo/matter": "0.3.0",
66
66
  "@matter/tsconfig": "0.0.0"
67
67
  },
68
68
  "scripts": {