@mrigx/core 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 +26 -0
- package/src/AtlasManager.ts +125 -0
- package/src/DomRasterSystem.ts +113 -0
- package/src/MrigEngine.ts +834 -0
- package/src/WebGPUManager.ts +65 -0
- package/src/index.ts +5 -0
- package/src/shaders.ts +772 -0
- package/src/types.ts +38 -0
- package/src/utils.ts +26 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +35 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrigx/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/core.umd.js",
|
|
6
|
+
"module": "./dist/core.es.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/core.es.js",
|
|
12
|
+
"require": "./dist/core.umd.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "vite build"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"html-to-image": "^1.11.13"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "~5.7.2",
|
|
23
|
+
"vite": "^6.0.0",
|
|
24
|
+
"vite-plugin-dts": "^4.5.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { MrigEngine } from './MrigEngine';
|
|
2
|
+
|
|
3
|
+
export interface AtlasRegion {
|
|
4
|
+
id: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
uv: [number, number, number, number]; // [uMin, vMin, uMax, vMax]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages a large WebGPU texture atlas for dynamically caching UI components.
|
|
14
|
+
* This allows the GPU to instantly translate components without re-rasterizing the DOM.
|
|
15
|
+
*/
|
|
16
|
+
export class AtlasManager {
|
|
17
|
+
private engine: MrigEngine;
|
|
18
|
+
public texture: GPUTexture | null = null;
|
|
19
|
+
public textureSize = 4096; // 4K atlas holds a LOT of UI components
|
|
20
|
+
|
|
21
|
+
private regions = new Map<string, AtlasRegion>();
|
|
22
|
+
|
|
23
|
+
// Simple Shelf Packing State
|
|
24
|
+
private currentX = 0;
|
|
25
|
+
private currentY = 0;
|
|
26
|
+
private shelfHeight = 0;
|
|
27
|
+
|
|
28
|
+
constructor(engine: MrigEngine) {
|
|
29
|
+
this.engine = engine;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public init() {
|
|
33
|
+
const device = this.engine.gpuManager.device;
|
|
34
|
+
if (!device) return;
|
|
35
|
+
|
|
36
|
+
this.texture = device.createTexture({
|
|
37
|
+
size: [this.textureSize, this.textureSize, 1],
|
|
38
|
+
format: 'rgba8unorm',
|
|
39
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Patches a rasterized component directly into a sub-region of the atlas texture.
|
|
45
|
+
* If the component is new or changed size, it dynamically allocates a new shelf slot.
|
|
46
|
+
*/
|
|
47
|
+
public uploadSprite(id: string, sourceCanvas: HTMLCanvasElement | ImageBitmap): AtlasRegion | null {
|
|
48
|
+
if (!this.texture || !this.engine.gpuManager.device) return null;
|
|
49
|
+
|
|
50
|
+
const width = sourceCanvas.width;
|
|
51
|
+
const height = sourceCanvas.height;
|
|
52
|
+
|
|
53
|
+
let region: AtlasRegion | null = this.regions.get(id) || null;
|
|
54
|
+
|
|
55
|
+
// If it's a new sprite or the dimensions changed, we must allocate a new slot
|
|
56
|
+
if (!region || region.width !== width || region.height !== height) {
|
|
57
|
+
region = this.allocate(id, width, height);
|
|
58
|
+
if (!region) {
|
|
59
|
+
console.warn('Mrig Atlas is full! Cannot allocate sprite.');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Zero-overhead GPU memory patch. Replaces ONLY the pixels of this specific component.
|
|
65
|
+
this.engine.gpuManager.device.queue.copyExternalImageToTexture(
|
|
66
|
+
{ source: sourceCanvas },
|
|
67
|
+
{ texture: this.texture, origin: [region.x, region.y, 0] },
|
|
68
|
+
[width, height, 1]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return region;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Standard 2D shelf-packing algorithm to tightly pack UI sprites into the atlas.
|
|
76
|
+
*/
|
|
77
|
+
private allocate(id: string, width: number, height: number): AtlasRegion | null {
|
|
78
|
+
// If it doesn't fit on the current shelf, move to the next shelf down
|
|
79
|
+
if (this.currentX + width > this.textureSize) {
|
|
80
|
+
this.currentX = 0;
|
|
81
|
+
this.currentY += this.shelfHeight;
|
|
82
|
+
this.shelfHeight = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If it still doesn't fit vertically, the atlas is full
|
|
86
|
+
if (this.currentY + height > this.textureSize) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const region: AtlasRegion = {
|
|
91
|
+
id,
|
|
92
|
+
x: this.currentX,
|
|
93
|
+
y: this.currentY,
|
|
94
|
+
width,
|
|
95
|
+
height,
|
|
96
|
+
uv: [
|
|
97
|
+
this.currentX / this.textureSize,
|
|
98
|
+
this.currentY / this.textureSize,
|
|
99
|
+
(this.currentX + width) / this.textureSize,
|
|
100
|
+
(this.currentY + height) / this.textureSize
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Expand the shelf height if this sprite is taller
|
|
105
|
+
this.shelfHeight = Math.max(this.shelfHeight, height);
|
|
106
|
+
// Move the cursor right
|
|
107
|
+
this.currentX += width;
|
|
108
|
+
|
|
109
|
+
this.regions.set(id, region);
|
|
110
|
+
return region;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public getRegion(id: string): AtlasRegion | undefined {
|
|
114
|
+
return this.regions.get(id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public destroy() {
|
|
118
|
+
if (this.texture) {
|
|
119
|
+
this.texture.destroy();
|
|
120
|
+
this.texture = null;
|
|
121
|
+
}
|
|
122
|
+
this.regions.clear();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file DomRasterSystem.ts
|
|
3
|
+
* @description Rasterizes the DOM into a WebGPU texture to allow the optical engine to distort the native HTML contents.
|
|
4
|
+
* Instrumented with profiling to diagnose main-thread bottlenecks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { toCanvas } from 'html-to-image';
|
|
8
|
+
import { MrigEngine } from './MrigEngine';
|
|
9
|
+
|
|
10
|
+
export class DomRasterSystem {
|
|
11
|
+
private engine: MrigEngine;
|
|
12
|
+
private targetEl: HTMLElement | null = null;
|
|
13
|
+
private isRasterizing = false;
|
|
14
|
+
private pendingRasterize = false;
|
|
15
|
+
private listeners: (() => void)[] = [];
|
|
16
|
+
|
|
17
|
+
public onRasterizeComplete(cb: () => void) {
|
|
18
|
+
this.listeners.push(cb);
|
|
19
|
+
return () => { this.listeners = this.listeners.filter(l => l !== cb); };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(engine: MrigEngine) {
|
|
23
|
+
this.engine = engine;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public setTarget(el: HTMLElement) {
|
|
27
|
+
this.targetEl = el;
|
|
28
|
+
this.rasterize();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async rasterize() {
|
|
32
|
+
if (!this.targetEl || !this.engine.gpuManager.device) return;
|
|
33
|
+
|
|
34
|
+
if (this.isRasterizing) {
|
|
35
|
+
this.pendingRasterize = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.isRasterizing = true;
|
|
40
|
+
this.pendingRasterize = false;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const dpr = window.devicePixelRatio || 1;
|
|
44
|
+
const targetWidth = document.documentElement.scrollWidth || window.innerWidth;
|
|
45
|
+
const targetHeight = document.documentElement.scrollHeight || window.innerHeight;
|
|
46
|
+
|
|
47
|
+
const startCanvas = performance.now();
|
|
48
|
+
const canvas = await toCanvas(this.targetEl, {
|
|
49
|
+
backgroundColor: 'transparent',
|
|
50
|
+
width: targetWidth,
|
|
51
|
+
height: targetHeight,
|
|
52
|
+
pixelRatio: dpr,
|
|
53
|
+
style: {
|
|
54
|
+
opacity: '1',
|
|
55
|
+
pointerEvents: 'none',
|
|
56
|
+
'--theme-color': getComputedStyle(document.documentElement).getPropertyValue('--theme-color').trim() || '#ffffff',
|
|
57
|
+
'--color-white': getComputedStyle(document.documentElement).getPropertyValue('--theme-color').trim() || '#ffffff',
|
|
58
|
+
},
|
|
59
|
+
skipFonts: true,
|
|
60
|
+
filter: (node) => {
|
|
61
|
+
if (node instanceof HTMLElement && node.id === 'mrig-transition-overlay') return false;
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const durationCanvas = performance.now() - startCanvas;
|
|
66
|
+
|
|
67
|
+
const cWidth = canvas.width;
|
|
68
|
+
const cHeight = canvas.height;
|
|
69
|
+
const pixelCount = cWidth * cHeight;
|
|
70
|
+
|
|
71
|
+
const startTexture = performance.now();
|
|
72
|
+
const newTexture = this.engine.gpuManager.device.createTexture({
|
|
73
|
+
size: [cWidth, cHeight, 1],
|
|
74
|
+
format: 'rgba8unorm',
|
|
75
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
76
|
+
});
|
|
77
|
+
const durationTexture = performance.now() - startTexture;
|
|
78
|
+
|
|
79
|
+
const startUpload = performance.now();
|
|
80
|
+
this.engine.gpuManager.device.queue.copyExternalImageToTexture(
|
|
81
|
+
{ source: canvas },
|
|
82
|
+
{ texture: newTexture },
|
|
83
|
+
[cWidth, cHeight, 1]
|
|
84
|
+
);
|
|
85
|
+
const durationUpload = performance.now() - startUpload;
|
|
86
|
+
|
|
87
|
+
console.log(
|
|
88
|
+
'[Mrig Profiler] Full Page Snapshot\n' +
|
|
89
|
+
'- Dimensions: ' + cWidth + 'x' + cHeight + ' (' + (pixelCount / 1000000).toFixed(2) + 'M pixels)\n' +
|
|
90
|
+
'- toCanvas: ' + durationCanvas.toFixed(2) + 'ms\n' +
|
|
91
|
+
'- createTexture: ' + durationTexture.toFixed(2) + 'ms\n' +
|
|
92
|
+
'- uploadTexture: ' + durationUpload.toFixed(2) + 'ms\n' +
|
|
93
|
+
'- Total time: ' + (durationCanvas + durationTexture + durationUpload).toFixed(2) + 'ms'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
this.engine.updateDomTexture(newTexture);
|
|
97
|
+
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('Mrig DOM Rasterization failed:', e);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.isRasterizing = false;
|
|
103
|
+
this.listeners.forEach(cb => cb());
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
window.dispatchEvent(new CustomEvent('mrig-raster-complete'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.pendingRasterize) {
|
|
109
|
+
setTimeout(() => this.rasterize(), 33);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|