@mrigx/react 0.1.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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@mrigx/react",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/react.umd.js",
6
+ "module": "./dist/react.es.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/react.es.js",
12
+ "require": "./dist/react.umd.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "vite build"
17
+ },
18
+ "dependencies": {
19
+ "@mrigx/core": "*"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "~5.7.2",
27
+ "vite": "^6.0.0",
28
+ "vite-plugin-dts": "^4.5.0",
29
+ "@vitejs/plugin-react": "^4.3.4"
30
+ }
31
+ }
@@ -0,0 +1,95 @@
1
+ import { createContext, useContext, useState, useEffect, useRef } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { MrigEngine } from "@mrigx/core";
4
+ import { MrigCanvas } from './MrigCanvas';
5
+ import { DomRasterSystem } from "@mrigx/core";
6
+
7
+ const EngineContext = createContext<MrigEngine | null>(null);
8
+
9
+ export const useEngine = () => {
10
+ return useContext(EngineContext);
11
+ };
12
+
13
+ export function useIsRasterizing() {
14
+ const engine = useEngine();
15
+ const [isRasterizing, setIsRasterizing] = useState(false);
16
+
17
+ useEffect(() => {
18
+ if (engine) {
19
+ return engine.onRasterStateChange((state) => setIsRasterizing(state));
20
+ }
21
+ }, [engine]);
22
+
23
+ return isRasterizing;
24
+ }
25
+
26
+ export function MrigProvider({ children, className = '' }: { children: ReactNode, className?: string }) {
27
+ const [engine] = useState(() => new MrigEngine());
28
+ const domRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ return () => {
32
+ engine.destroy();
33
+ };
34
+ }, [engine]);
35
+
36
+ useEffect(() => {
37
+ if (engine && domRef.current) {
38
+ const rasterSystem = new DomRasterSystem(engine);
39
+ rasterSystem.setTarget(domRef.current);
40
+
41
+ document.fonts.ready.then(() => {
42
+ rasterSystem.rasterize();
43
+ setTimeout(() => rasterSystem.rasterize(), 100);
44
+ setTimeout(() => rasterSystem.rasterize(), 500);
45
+ });
46
+
47
+ let timeoutId: number;
48
+ const handleResize = () => {
49
+ clearTimeout(timeoutId);
50
+ timeoutId = window.setTimeout(() => rasterSystem.rasterize(), 64);
51
+ };
52
+
53
+ window.addEventListener('resize', handleResize);
54
+
55
+ const observer = new MutationObserver((mutations) => {
56
+ // Skip full-screen rasterize if ALL mutations happened inside a dynamic component (handled by Atlas)
57
+ const isOnlyDynamic = mutations.every(m => {
58
+ let target = m.target as HTMLElement;
59
+ if (target.nodeType === Node.TEXT_NODE) target = target.parentElement as HTMLElement;
60
+ if (target && target.id === 'mrig-transition-overlay') return true;
61
+ const isDynamic = target && target.closest && target.closest('[data-mrig-dynamic="true"]');
62
+ return isDynamic;
63
+ });
64
+
65
+ if (isOnlyDynamic) return; // Atlas handles this, don't freeze the screen!
66
+
67
+ clearTimeout(timeoutId);
68
+ timeoutId = window.setTimeout(() => rasterSystem.rasterize(), 32);
69
+ });
70
+ observer.observe(domRef.current, { childList: true, subtree: true, characterData: true, attributes: true });
71
+
72
+ return () => {
73
+ window.removeEventListener('resize', handleResize);
74
+ observer.disconnect();
75
+ clearTimeout(timeoutId);
76
+ };
77
+ }
78
+ }, [engine]);
79
+
80
+ return (
81
+ <EngineContext.Provider value={engine}>
82
+ <MrigCanvas />
83
+ <div id="mrig-app-root" ref={domRef} style={{ pointerEvents: 'auto', opacity: 0.001 }} className={className}>
84
+ {children}
85
+ </div>
86
+ </EngineContext.Provider>
87
+ );
88
+ }
89
+
90
+
91
+
92
+
93
+
94
+
95
+
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @file MrigCanvas.tsx
3
+ * @description The fixed full-screen WebGPU canvas that renders the optical field.
4
+ */
5
+
6
+ import { useEffect, useRef } from 'react';
7
+ import { useEngine } from './EngineContext';
8
+
9
+ /**
10
+ * A full-screen fixed canvas component that initializes the WebGPU engine.
11
+ * Place this at the root of your application, behind the DOM.
12
+ */
13
+ export function MrigCanvas() {
14
+ const canvasRef = useRef<HTMLCanvasElement>(null);
15
+ const engine = useEngine();
16
+
17
+ useEffect(() => {
18
+ if (canvasRef.current && engine) {
19
+ const dpr = window.devicePixelRatio || 1;
20
+ canvasRef.current.width = window.innerWidth * dpr;
21
+ canvasRef.current.height = window.innerHeight * dpr;
22
+
23
+ engine.init(canvasRef.current).catch(err => {
24
+ console.error("MrigCanvas Failed to initialize WebGPU:", err);
25
+ });
26
+
27
+ const handleResize = () => {
28
+ if (!canvasRef.current || !engine) return;
29
+ canvasRef.current.width = window.innerWidth * dpr;
30
+ canvasRef.current.height = window.innerHeight * dpr;
31
+ // The engine could implement a resize method to recreate textures,
32
+ // for simplicity we reload or re-init in a production robust version.
33
+ };
34
+
35
+ window.addEventListener('resize', handleResize);
36
+ return () => window.removeEventListener('resize', handleResize);
37
+ }
38
+ }, [engine]);
39
+
40
+ return (
41
+ <canvas
42
+ ref={canvasRef}
43
+ style={{
44
+ position: 'fixed',
45
+ top: 0,
46
+ left: 0,
47
+ width: '100vw',
48
+ height: '100vh',
49
+ pointerEvents: 'none',
50
+ zIndex: 0,
51
+ }}
52
+ />
53
+ );
54
+ }
55
+
56
+
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './EngineContext';
2
+ export * from './MrigCanvas';
3
+ export * from './useMrigSurface';
4
+ export * from './useMrigInteraction';
5
+ export * from './styles';
package/src/styles.ts ADDED
@@ -0,0 +1,77 @@
1
+ const STYLES = `
2
+ .mrig-optical-button {
3
+ background-color: transparent;
4
+ transition: background-color 0.2s, opacity 0.2s;
5
+ outline: none;
6
+ }
7
+ .mrig-optical-button:hover:not(:disabled) {
8
+ background-color: rgba(255, 255, 255, 0.05);
9
+ }
10
+ .mrig-optical-button:active:not(:disabled) {
11
+ background-color: rgba(255, 255, 255, 0.1);
12
+ }
13
+ .mrig-optical-button:focus-visible {
14
+ outline: 2px solid rgba(255, 255, 255, 0.4);
15
+ outline-offset: 2px;
16
+ }
17
+ .mrig-optical-button:disabled {
18
+ opacity: 0.5;
19
+ cursor: not-allowed;
20
+ }
21
+ .mrig-optical-icon-button {
22
+ background-color: rgba(255, 255, 255, 0.02);
23
+ transition: background-color 0.2s, opacity 0.2s;
24
+ outline: none;
25
+ }
26
+ .mrig-optical-icon-button:hover:not(:disabled) {
27
+ background-color: rgba(255, 255, 255, 0.08);
28
+ }
29
+ .mrig-optical-icon-button:active:not(:disabled) {
30
+ background-color: rgba(255, 255, 255, 0.12);
31
+ }
32
+ .mrig-optical-icon-button:focus-visible {
33
+ outline: 2px solid rgba(255, 255, 255, 0.4);
34
+ outline-offset: 2px;
35
+ }
36
+ .mrig-optical-icon-button:disabled {
37
+ opacity: 0.5;
38
+ cursor: not-allowed;
39
+ }
40
+ .mrig-optical-input {
41
+ border: 1px solid rgba(255, 255, 255, 0.15);
42
+ background-color: rgba(255, 255, 255, 0.02);
43
+ transition: border-color 0.2s, background-color 0.2s;
44
+ outline: none;
45
+ }
46
+ .mrig-optical-input:focus {
47
+ border-color: rgba(255, 255, 255, 0.4);
48
+ }
49
+ .mrig-optical-input:disabled {
50
+ opacity: 0.5;
51
+ cursor: not-allowed;
52
+ }
53
+
54
+ [data-mrig-dynamic="true"] {
55
+ /* Expand the invisible hit area by 10px in all directions to absorb GPU buoyancy wobble */
56
+ position: relative;
57
+ }
58
+ [data-mrig-dynamic="true"]::after {
59
+ content: '';
60
+ position: absolute;
61
+ top: -10px;
62
+ left: -10px;
63
+ right: -10px;
64
+ bottom: -10px;
65
+ z-index: 1;
66
+ }
67
+ `;
68
+
69
+ let injected = false;
70
+ export function injectMrigStyles() {
71
+ if (typeof window === 'undefined' || injected) return;
72
+ const style = document.createElement('style');
73
+ style.id = 'mrig-component-styles';
74
+ style.innerHTML = STYLES;
75
+ document.head.appendChild(style);
76
+ injected = true;
77
+ }
@@ -0,0 +1,41 @@
1
+ import { useCallback } from 'react';
2
+ import type { MouseEvent, TouchEvent } from 'react';
3
+ import { useEngine } from './EngineContext';
4
+
5
+ export function useMrigInteraction() {
6
+ const engine = useEngine();
7
+
8
+ const pulseCenter = useCallback((element: HTMLElement | null, strength: number = 0.5) => {
9
+ if (!engine || !element) return;
10
+ const rect = element.getBoundingClientRect();
11
+ const x = (rect.left + rect.width / 2) / window.innerWidth;
12
+ const y = (rect.top + rect.height / 2) / window.innerHeight;
13
+ engine.triggerDisturbance(x, y, strength);
14
+ }, [engine]);
15
+
16
+ const pulsePointer = useCallback((event: MouseEvent | TouchEvent, strength: number = 0.5) => {
17
+ if (!engine) return;
18
+ let clientX, clientY;
19
+ if ('touches' in event) {
20
+ if (event.touches.length === 0) return;
21
+ clientX = event.touches[0].clientX;
22
+ clientY = event.touches[0].clientY;
23
+ } else {
24
+ clientX = (event as MouseEvent).clientX;
25
+ clientY = (event as MouseEvent).clientY;
26
+ }
27
+ const x = clientX / window.innerWidth;
28
+ const y = clientY / window.innerHeight;
29
+ engine.triggerDisturbance(x, y, strength);
30
+ }, [engine]);
31
+
32
+ const triggerRipple = useCallback((clientX: number, clientY: number, strength: number = 0.5, radius: number = 0.04) => {
33
+ if (!engine) return;
34
+ const x = clientX / window.innerWidth;
35
+ const y = clientY / window.innerHeight;
36
+ engine.triggerDisturbance(x, y, strength, radius);
37
+ }, [engine]);
38
+
39
+ return { pulseCenter, pulsePointer, triggerRipple };
40
+ }
41
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @file useMrigSurface.ts
3
+ * @description React hook to register a DOM element as an optical surface within the engine.
4
+ */
5
+
6
+ import { useLayoutEffect, useRef, useId, useCallback } from 'react';
7
+ import type { Ref } from 'react';
8
+ import { useEngine } from './EngineContext';
9
+ import type { SurfaceConfig } from "@mrigx/core";
10
+
11
+ export function useMrigSurface<T extends HTMLElement>(
12
+ config: SurfaceConfig,
13
+ forwardedRef?: Ref<T>
14
+ ) {
15
+ const internalRef = useRef<T>(null);
16
+ const engine = useEngine();
17
+ const id = useId();
18
+
19
+ const { material, materialStrength, borderRadius, isDynamic } = config;
20
+
21
+ useLayoutEffect(() => {
22
+ if (!engine || !internalRef.current) return;
23
+
24
+ const el = internalRef.current;
25
+ if (isDynamic) el.setAttribute('data-mrig-dynamic', 'true');
26
+
27
+ let timeoutId: any;
28
+ const updateBounds = () => {
29
+ clearTimeout(timeoutId);
30
+ timeoutId = setTimeout(() => {
31
+ if (!internalRef.current) return;
32
+ const rect = el.getBoundingClientRect();
33
+ const canvasWidth = window.innerWidth;
34
+ const canvasHeight = window.innerHeight;
35
+
36
+ engine.registerSurface(id, {
37
+ x: rect.left / canvasWidth,
38
+ y: rect.top / canvasHeight,
39
+ width: rect.width / canvasWidth,
40
+ height: rect.height / canvasHeight,
41
+ config: { material, materialStrength, borderRadius, isDynamic }
42
+ }, el);
43
+ }, 120); // Debounce rasterization of dynamic components to prevent main-thread choking
44
+ };
45
+
46
+ updateBounds();
47
+
48
+ window.addEventListener('resize', updateBounds);
49
+ window.addEventListener('scroll', updateBounds, { passive: true });
50
+
51
+ const observer = new ResizeObserver(updateBounds);
52
+ observer.observe(el);
53
+
54
+ let mutObserver: MutationObserver | null = null;
55
+ if (isDynamic) {
56
+ mutObserver = new MutationObserver(() => {
57
+ updateBounds();
58
+ });
59
+ mutObserver.observe(el, { childList: true, subtree: true, attributes: true, characterData: true });
60
+ }
61
+
62
+ return () => {
63
+ window.removeEventListener('resize', updateBounds);
64
+ window.removeEventListener('scroll', updateBounds);
65
+ observer.disconnect();
66
+ if (mutObserver) mutObserver.disconnect();
67
+ engine.unregisterSurface(id);
68
+ };
69
+ }, [engine, id, material, materialStrength, borderRadius, isDynamic]);
70
+
71
+ return useCallback((node: T | null) => {
72
+ internalRef.current = node;
73
+ if (typeof forwardedRef === 'function') {
74
+ forwardedRef(node);
75
+ } else if (forwardedRef) {
76
+ (forwardedRef as React.MutableRefObject<T | null>).current = node;
77
+ }
78
+ }, [forwardedRef]);
79
+ }
80
+
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": [
6
+ "ES2020",
7
+ "DOM",
8
+ "DOM.Iterable"
9
+ ],
10
+ "module": "ESNext",
11
+ "skipLibCheck": true,
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "baseUrl": "."
23
+ },
24
+ "include": [
25
+ "src"
26
+ ]
27
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import dts from 'vite-plugin-dts';
4
+ import { resolve } from 'path';
5
+ import url from 'url';
6
+
7
+ const dirname = url.fileURLToPath(new URL('.', import.meta.url));
8
+
9
+ export default defineConfig({
10
+ publicDir: false,
11
+ plugins: [
12
+ react(),
13
+ dts({
14
+ include: ['src/**/*'],
15
+ outDir: 'dist',
16
+ insertTypesEntry: true,
17
+ tsconfigPath: './tsconfig.json'
18
+ })
19
+ ],
20
+ build: {
21
+ outDir: 'dist',
22
+ emptyOutDir: true,
23
+ lib: {
24
+ entry: resolve(dirname, 'src/index.ts'),
25
+ name: 'MrigReact',
26
+ fileName: (format) => 'react.' + format + '.js'
27
+ },
28
+ rollupOptions: {
29
+ external: ['react', 'react-dom', '@mrigx/core'],
30
+ output: {
31
+ globals: {
32
+ 'react': 'React',
33
+ 'react-dom': 'ReactDOM',
34
+ '@mrigx/core': 'MrigCore'
35
+ }
36
+ }
37
+ }
38
+ }
39
+ });