@mirage-engine/core 0.0.2
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/mirage-engine.js +487 -0
- package/dist/mirage-engine.umd.js +55 -0
- package/dist/src/core/Engine.d.ts +11 -0
- package/dist/src/core/Syncer.d.ts +20 -0
- package/dist/src/dom/Extractor.d.ts +3 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/renderer/Renderer.d.ts +27 -0
- package/dist/src/types/common.d.ts +20 -0
- package/dist/src/types/config.d.ts +18 -0
- package/dist/src/types/flags.d.ts +6 -0
- package/dist/src/types/index.d.ts +3 -0
- package/dist/src/utils/math.d.ts +13 -0
- package/dist/vite.config.d.ts +2 -0
- package/package.json +26 -0
- package/src/core/Engine.ts +44 -0
- package/src/core/Syncer.ts +148 -0
- package/src/dom/Extractor.ts +153 -0
- package/src/index.ts +2 -0
- package/src/renderer/Renderer.ts +308 -0
- package/src/types/common.ts +28 -0
- package/src/types/config.ts +21 -0
- package/src/types/flags.ts +7 -0
- package/src/types/index.ts +3 -0
- package/src/utils/math.ts +23 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import {
|
|
3
|
+
SceneNode,
|
|
4
|
+
DIRTY_CONTENT,
|
|
5
|
+
TextQuality,
|
|
6
|
+
CoreConfig,
|
|
7
|
+
MirageMode,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { Painter } from "@mirage-engine/painter";
|
|
10
|
+
|
|
11
|
+
export class Renderer {
|
|
12
|
+
public readonly canvas: HTMLCanvasElement;
|
|
13
|
+
private readonly scene: THREE.Scene;
|
|
14
|
+
private readonly camera: THREE.OrthographicCamera;
|
|
15
|
+
private readonly renderer: THREE.WebGLRenderer;
|
|
16
|
+
private renderOrder: number = 0;
|
|
17
|
+
private textQualityFactor: number = 2;
|
|
18
|
+
private mode: MirageMode = "overlay";
|
|
19
|
+
private customZIndex: string = "9999";
|
|
20
|
+
|
|
21
|
+
private target: HTMLElement;
|
|
22
|
+
private mountContainer: HTMLElement;
|
|
23
|
+
private targetRect: DOMRect;
|
|
24
|
+
|
|
25
|
+
private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
target: HTMLElement,
|
|
29
|
+
config: CoreConfig,
|
|
30
|
+
mountContainer: HTMLElement,
|
|
31
|
+
) {
|
|
32
|
+
this.target = target;
|
|
33
|
+
this.mountContainer = mountContainer;
|
|
34
|
+
|
|
35
|
+
this.mode = config.mode ?? "overlay";
|
|
36
|
+
|
|
37
|
+
if (config.style?.zIndex) {
|
|
38
|
+
this.customZIndex = config.style.zIndex;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.canvas = document.createElement("canvas");
|
|
42
|
+
this.scene = new THREE.Scene();
|
|
43
|
+
|
|
44
|
+
this.targetRect = this.target.getBoundingClientRect();
|
|
45
|
+
const width = this.targetRect.width;
|
|
46
|
+
const height = this.targetRect.height;
|
|
47
|
+
|
|
48
|
+
// target duplicate mode
|
|
49
|
+
// const width = target.parentElement!.clientWidth;
|
|
50
|
+
// const height = target.parentElement!.clientHeight;
|
|
51
|
+
|
|
52
|
+
this.camera = new THREE.OrthographicCamera(
|
|
53
|
+
width / -2,
|
|
54
|
+
width / 2,
|
|
55
|
+
height / 2,
|
|
56
|
+
height / -2,
|
|
57
|
+
1,
|
|
58
|
+
1000,
|
|
59
|
+
);
|
|
60
|
+
this.camera.position.z = 100;
|
|
61
|
+
|
|
62
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
63
|
+
canvas: this.canvas,
|
|
64
|
+
alpha: true,
|
|
65
|
+
antialias: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
69
|
+
this.renderer.setSize(width, height);
|
|
70
|
+
|
|
71
|
+
this.applyTextQuality(config.textQuality ?? "medium");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private applyTextQuality(quality: TextQuality) {
|
|
75
|
+
if (typeof quality === "number") {
|
|
76
|
+
this.textQualityFactor = Math.max(0.1, quality);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
switch (quality) {
|
|
80
|
+
case "low":
|
|
81
|
+
this.textQualityFactor = 1;
|
|
82
|
+
break;
|
|
83
|
+
case "high":
|
|
84
|
+
this.textQualityFactor = 4;
|
|
85
|
+
break;
|
|
86
|
+
case "medium":
|
|
87
|
+
default:
|
|
88
|
+
this.textQualityFactor = 2;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public mount() {
|
|
94
|
+
this.mountContainer.appendChild(this.canvas);
|
|
95
|
+
|
|
96
|
+
this.canvas.style.zIndex = this.customZIndex;
|
|
97
|
+
this.canvas.style.pointerEvents = this.mode === "overlay" ? "none" : "auto";
|
|
98
|
+
|
|
99
|
+
this.updateCanvasLayout();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private updateCanvasLayout() {
|
|
103
|
+
this.canvas.style.width = `${this.targetRect.width}px`;
|
|
104
|
+
this.canvas.style.height = `${this.targetRect.height}px`;
|
|
105
|
+
|
|
106
|
+
if (this.mode === "duplicate") {
|
|
107
|
+
this.canvas.style.position = "";
|
|
108
|
+
this.canvas.style.top = "";
|
|
109
|
+
this.canvas.style.left = "";
|
|
110
|
+
|
|
111
|
+
this.canvas.style.display = "block";
|
|
112
|
+
} else {
|
|
113
|
+
this.canvas.style.position = "absolute";
|
|
114
|
+
this.canvas.style.top = `${this.target.offsetTop}px`;
|
|
115
|
+
this.canvas.style.left = `${this.target.offsetLeft}px`;
|
|
116
|
+
this.canvas.style.display = "block";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public dispose() {
|
|
121
|
+
this.renderer.dispose();
|
|
122
|
+
this.canvas.remove();
|
|
123
|
+
// TODO: Scene 내부 Mesh들도 순회하며 dispose
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public setSize(width: number, height: number) {
|
|
127
|
+
this.renderer.setSize(width, height);
|
|
128
|
+
|
|
129
|
+
this.camera.left = width / -2;
|
|
130
|
+
this.camera.right = width / 2;
|
|
131
|
+
this.camera.top = height / 2;
|
|
132
|
+
this.camera.bottom = height / -2;
|
|
133
|
+
|
|
134
|
+
this.camera.updateProjectionMatrix();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public syncScene(graphNode: SceneNode) {
|
|
138
|
+
const newRect = this.target.getBoundingClientRect();
|
|
139
|
+
|
|
140
|
+
const isResized =
|
|
141
|
+
Math.abs(newRect.width - this.targetRect.width) > 0.1 ||
|
|
142
|
+
Math.abs(newRect.height - this.targetRect.height) > 0.1;
|
|
143
|
+
|
|
144
|
+
const isMoved =
|
|
145
|
+
this.mode === "overlay" &&
|
|
146
|
+
(Math.abs(newRect.top - this.targetRect.top) > 0.1 ||
|
|
147
|
+
Math.abs(newRect.left - this.targetRect.left) > 0.1);
|
|
148
|
+
|
|
149
|
+
if (isResized) {
|
|
150
|
+
this.targetRect = newRect;
|
|
151
|
+
this.renderer.setSize(this.targetRect.width, this.targetRect.height);
|
|
152
|
+
|
|
153
|
+
this.camera.left = this.targetRect.width / -2;
|
|
154
|
+
this.camera.right = this.targetRect.width / 2;
|
|
155
|
+
this.camera.top = this.targetRect.height / 2;
|
|
156
|
+
this.camera.bottom = this.targetRect.height / -2;
|
|
157
|
+
this.camera.updateProjectionMatrix();
|
|
158
|
+
|
|
159
|
+
this.updateCanvasLayout();
|
|
160
|
+
} else if (isMoved) {
|
|
161
|
+
this.targetRect = newRect;
|
|
162
|
+
this.updateCanvasLayout();
|
|
163
|
+
} else {
|
|
164
|
+
this.targetRect = newRect;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.renderOrder = 0;
|
|
168
|
+
|
|
169
|
+
const activeElements = new Set<HTMLElement>();
|
|
170
|
+
|
|
171
|
+
this.reconcileNode(graphNode, activeElements);
|
|
172
|
+
|
|
173
|
+
for (const [el, mesh] of this.meshMap.entries()) {
|
|
174
|
+
if (!activeElements.has(el)) {
|
|
175
|
+
this.scene.remove(mesh);
|
|
176
|
+
|
|
177
|
+
// Garbage Collection
|
|
178
|
+
mesh.geometry.dispose();
|
|
179
|
+
if (mesh.material instanceof THREE.Material) mesh.material.dispose();
|
|
180
|
+
this.meshMap.delete(el);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
|
|
186
|
+
activeElements.add(node.element);
|
|
187
|
+
|
|
188
|
+
let mesh = this.meshMap.get(node.element);
|
|
189
|
+
if (!mesh) {
|
|
190
|
+
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
191
|
+
const material = Painter.create(
|
|
192
|
+
"BOX",
|
|
193
|
+
node.styles,
|
|
194
|
+
"",
|
|
195
|
+
node.rect.width,
|
|
196
|
+
node.rect.height,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
mesh = new THREE.Mesh(geometry, material);
|
|
200
|
+
if (node.type === "TEXT") mesh.name = "BG_MESH";
|
|
201
|
+
|
|
202
|
+
this.scene.add(mesh);
|
|
203
|
+
this.meshMap.set(node.element, mesh);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
mesh.userData.domRect = node.rect;
|
|
207
|
+
|
|
208
|
+
this.updateMeshProperties(mesh, node);
|
|
209
|
+
|
|
210
|
+
if (node.type === "BOX") {
|
|
211
|
+
for (const child of node.children) {
|
|
212
|
+
this.reconcileNode(child, activeElements);
|
|
213
|
+
}
|
|
214
|
+
} else if (node.type === "TEXT") {
|
|
215
|
+
this.reconcileTextChild(mesh, node);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private reconcileTextChild(parentMesh: THREE.Mesh, node: SceneNode) {
|
|
220
|
+
let textMesh = parentMesh.children.find(
|
|
221
|
+
(c) => c.name === "TEXT_CHILD",
|
|
222
|
+
) as THREE.Mesh;
|
|
223
|
+
|
|
224
|
+
const currentStyleHash = JSON.stringify(node.textStyles);
|
|
225
|
+
const cachedStyleHash = textMesh?.userData?.styleHash;
|
|
226
|
+
const isDirty =
|
|
227
|
+
!textMesh ||
|
|
228
|
+
node.dirtyMask & DIRTY_CONTENT ||
|
|
229
|
+
currentStyleHash !== cachedStyleHash;
|
|
230
|
+
|
|
231
|
+
if (isDirty) {
|
|
232
|
+
if (textMesh) {
|
|
233
|
+
(textMesh.material as THREE.MeshBasicMaterial).map?.dispose();
|
|
234
|
+
textMesh.geometry.dispose();
|
|
235
|
+
parentMesh.remove(textMesh);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const material = Painter.create(
|
|
239
|
+
"TEXT",
|
|
240
|
+
node.textStyles!,
|
|
241
|
+
node.textContent || "",
|
|
242
|
+
node.rect.width,
|
|
243
|
+
node.rect.height,
|
|
244
|
+
this.textQualityFactor,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
248
|
+
textMesh = new THREE.Mesh(geometry, material);
|
|
249
|
+
|
|
250
|
+
textMesh.name = "TEXT_CHILD";
|
|
251
|
+
textMesh.userData = { styleHash: currentStyleHash };
|
|
252
|
+
|
|
253
|
+
textMesh.position.z = 0.005;
|
|
254
|
+
parentMesh.add(textMesh);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (textMesh) {
|
|
258
|
+
const parentRect = parentMesh.userData.domRect;
|
|
259
|
+
const parentCenterX = parentRect.x + parentRect.width / 2;
|
|
260
|
+
const parentCenterY = parentRect.y + parentRect.height / 2;
|
|
261
|
+
|
|
262
|
+
const textCenterX = node.rect.x + node.rect.width / 2;
|
|
263
|
+
const textCenterY = node.rect.y + node.rect.height / 2;
|
|
264
|
+
|
|
265
|
+
const offsetX = textCenterX - parentCenterX;
|
|
266
|
+
const offsetY = -(textCenterY - parentCenterY);
|
|
267
|
+
|
|
268
|
+
textMesh.position.set(offsetX, offsetY, 0.005);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private updateMeshProperties(mesh: THREE.Mesh, node: SceneNode) {
|
|
273
|
+
const { rect, styles } = node;
|
|
274
|
+
|
|
275
|
+
const pixelRatio = this.renderer.getPixelRatio();
|
|
276
|
+
const canvasWidth = this.renderer.domElement.width / pixelRatio;
|
|
277
|
+
const canvasHeight = this.renderer.domElement.height / pixelRatio;
|
|
278
|
+
|
|
279
|
+
mesh.scale.set(rect.width, rect.height, 1);
|
|
280
|
+
|
|
281
|
+
const Z_MICRO_OFFSET = 0.001;
|
|
282
|
+
this.renderOrder++;
|
|
283
|
+
|
|
284
|
+
const targetPageX = this.targetRect.left + window.scrollX;
|
|
285
|
+
const targetPageY = this.targetRect.top + window.scrollY;
|
|
286
|
+
|
|
287
|
+
const localX = rect.x - targetPageX;
|
|
288
|
+
const localY = rect.y - targetPageY;
|
|
289
|
+
|
|
290
|
+
mesh.position.set(
|
|
291
|
+
localX - canvasWidth / 2 + rect.width / 2,
|
|
292
|
+
-localY + canvasHeight / 2 - rect.height / 2,
|
|
293
|
+
styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
|
|
294
|
+
);
|
|
295
|
+
Painter.update(
|
|
296
|
+
mesh.material as THREE.Material,
|
|
297
|
+
"BOX",
|
|
298
|
+
node.styles,
|
|
299
|
+
"",
|
|
300
|
+
node.rect.width,
|
|
301
|
+
node.rect.height,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public render() {
|
|
306
|
+
this.renderer.render(this.scene, this.camera);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TextStyles, BoxStyles } from "@mirage-engine/painter";
|
|
2
|
+
|
|
3
|
+
export type NodeType = "BOX" | "TEXT"
|
|
4
|
+
|
|
5
|
+
export interface NodeRect {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export interface SceneNode {
|
|
15
|
+
id: string;
|
|
16
|
+
type: NodeType;
|
|
17
|
+
element: HTMLElement;
|
|
18
|
+
|
|
19
|
+
rect: NodeRect;
|
|
20
|
+
styles: BoxStyles;
|
|
21
|
+
|
|
22
|
+
textContent?: string;
|
|
23
|
+
textStyles?: TextStyles;
|
|
24
|
+
|
|
25
|
+
dirtyMask: number;
|
|
26
|
+
children: SceneNode[];
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type TextQuality = "low" | "medium" | "high" | number;
|
|
2
|
+
export type MirageMode = "overlay" | "duplicate";
|
|
3
|
+
|
|
4
|
+
interface BaseConfig {
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
textQuality?: TextQuality;
|
|
7
|
+
style?: {
|
|
8
|
+
zIndex?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OverlayConfig extends BaseConfig {
|
|
13
|
+
mode?: "overlay";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DuplicateConfig extends BaseConfig {
|
|
17
|
+
mode: "duplicate";
|
|
18
|
+
container?: HTMLElement;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CoreConfig = OverlayConfig | DuplicateConfig;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//Bitmask Flags
|
|
2
|
+
export const DIRTY_NONE = 0;
|
|
3
|
+
export const DIRTY_RECT = 1 << 0; // 1: x, y, width, height (00000001)
|
|
4
|
+
export const DIRTY_STYLE = 1 << 1; // 2: style (00000010)
|
|
5
|
+
export const DIRTY_ZINDEX = 1 << 2; // 4: zIndex (00000100)
|
|
6
|
+
export const DIRTY_STRUCTURE = 1 << 3; // 8: childNode add / delete (00001000)
|
|
7
|
+
export const DIRTY_CONTENT = 1 << 4; // 16: text content changed (00010000)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts DOM coordinates (Top-Left 0,0) to WebGL coordinates (Center 0,0).
|
|
3
|
+
* @param x - The x coordinate of the DOM element (Left)
|
|
4
|
+
* @param y - The y coordinate of the DOM element (Top)
|
|
5
|
+
* @param width - The width of the DOM element
|
|
6
|
+
* @param height - The height of the DOM element
|
|
7
|
+
* @param canvasWidth - The total width of the canvas
|
|
8
|
+
* @param canvasHeight - The total height of the canvas
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function domToWebGL(
|
|
12
|
+
x: number,
|
|
13
|
+
y: number,
|
|
14
|
+
width: number,
|
|
15
|
+
height: number,
|
|
16
|
+
canvasWidth: number,
|
|
17
|
+
canvasHeight: number
|
|
18
|
+
) {
|
|
19
|
+
const xGL = x - canvasWidth / 2 + width / 2;
|
|
20
|
+
const yGL = -(y - canvasHeight / 2 + height / 2);
|
|
21
|
+
|
|
22
|
+
return { x: xGL, y: yGL };
|
|
23
|
+
}
|
package/tsconfig.json
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import dts from "vite-plugin-dts";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
build: {
|
|
7
|
+
target: "es2015",
|
|
8
|
+
lib: {
|
|
9
|
+
entry: path.resolve(__dirname, "src/index.ts"),
|
|
10
|
+
name: "MirageEngine",
|
|
11
|
+
fileName: (format) =>
|
|
12
|
+
`mirage-engine.${format === "es" ? "js" : "umd.js"}`,
|
|
13
|
+
},
|
|
14
|
+
rollupOptions: {
|
|
15
|
+
external: ["three"],
|
|
16
|
+
output: {
|
|
17
|
+
globals: {
|
|
18
|
+
three: "THREE",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
plugins: [
|
|
24
|
+
dts({
|
|
25
|
+
insertTypesEntry: true,
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
resolve: {
|
|
29
|
+
alias: {
|
|
30
|
+
"@": path.resolve(__dirname, "./src"),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|