@mirage-engine/core 0.2.0 → 0.2.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 +16 -0
- package/dist/mirage-engine.js +1016 -418
- package/dist/mirage-engine.umd.js +136 -72
- package/dist/src/animation/Animator.d.ts +11 -0
- package/dist/src/core/Engine.d.ts +2 -0
- package/dist/src/core/Syncer.d.ts +8 -1
- package/dist/src/renderer/Renderer.d.ts +9 -4
- package/dist/src/store/MeshRegistry.d.ts +9 -0
- package/dist/src/store/TextureLifecycleManager.d.ts +16 -0
- package/dist/src/types/animate.d.ts +19 -0
- package/dist/src/types/common.d.ts +7 -1
- package/dist/src/types/config.d.ts +4 -0
- package/dist/src/types/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/animation/Animator.ts +163 -0
- package/src/core/Engine.ts +56 -4
- package/src/core/Syncer.ts +71 -16
- package/src/dom/Extractor.ts +176 -21
- package/src/env.d.ts +4 -0
- package/src/renderer/Renderer.ts +206 -80
- package/src/store/MeshRegistry.ts +32 -0
- package/src/store/TextureLifecycleManager.ts +111 -0
- package/src/types/animate.ts +26 -0
- package/src/types/common.ts +4 -1
- package/src/types/config.ts +4 -0
- package/src/types/index.ts +2 -1
package/src/renderer/Renderer.ts
CHANGED
|
@@ -7,8 +7,11 @@ import {
|
|
|
7
7
|
MirageMode,
|
|
8
8
|
USER_LAYER,
|
|
9
9
|
SYSTEM_LAYER,
|
|
10
|
+
travelerClipArea,
|
|
10
11
|
} from "../types";
|
|
11
12
|
import { Painter } from "@mirage-engine/painter";
|
|
13
|
+
import { MeshRegistry } from "../store/MeshRegistry";
|
|
14
|
+
import { TextureLifecycleManager } from "../store/TextureLifecycleManager";
|
|
12
15
|
|
|
13
16
|
export class Renderer {
|
|
14
17
|
public readonly canvas: HTMLCanvasElement;
|
|
@@ -19,23 +22,38 @@ export class Renderer {
|
|
|
19
22
|
private renderOrder: number = 0;
|
|
20
23
|
private qualityFactor: number = 2;
|
|
21
24
|
private mode: MirageMode = "overlay";
|
|
25
|
+
private clipArea: travelerClipArea = 1;
|
|
22
26
|
|
|
23
27
|
private target: HTMLElement;
|
|
24
28
|
private mountContainer: HTMLElement;
|
|
29
|
+
private registry: MeshRegistry;
|
|
25
30
|
private targetRect: DOMRect;
|
|
26
31
|
|
|
27
|
-
private
|
|
32
|
+
private travelers: Set<THREE.Mesh> = new Set();
|
|
33
|
+
private textureManager: TextureLifecycleManager;
|
|
34
|
+
// private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
|
|
35
|
+
|
|
36
|
+
public readonly fixedMeshes: Set<THREE.Mesh> = new Set();
|
|
28
37
|
|
|
29
38
|
constructor(
|
|
30
39
|
target: HTMLElement,
|
|
31
40
|
config: CoreConfig,
|
|
32
41
|
mountContainer: HTMLElement,
|
|
42
|
+
registry: MeshRegistry,
|
|
33
43
|
) {
|
|
34
44
|
this.target = target;
|
|
35
45
|
this.mountContainer = mountContainer;
|
|
46
|
+
this.registry = registry;
|
|
47
|
+
|
|
48
|
+
this.textureManager = new TextureLifecycleManager((el, texture) => {
|
|
49
|
+
const mesh = this.registry.get(el);
|
|
50
|
+
if (mesh && mesh.material instanceof THREE.ShaderMaterial) {
|
|
51
|
+
Painter.forceUpdateUniforms(mesh.material, { texture: texture });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
36
54
|
|
|
37
55
|
this.mode = config.mode ?? "overlay";
|
|
38
|
-
|
|
56
|
+
this.clipArea = config.travelerClipArea ?? 1;
|
|
39
57
|
this.canvas = document.createElement("canvas");
|
|
40
58
|
this.scene = new THREE.Scene();
|
|
41
59
|
|
|
@@ -58,8 +76,6 @@ export class Renderer {
|
|
|
58
76
|
this.camera.position.z = 100;
|
|
59
77
|
this.camera.layers.set(0);
|
|
60
78
|
|
|
61
|
-
// [new]
|
|
62
|
-
// THREE.ColorManagement.enabled = false;
|
|
63
79
|
|
|
64
80
|
this.renderer = new THREE.WebGLRenderer({
|
|
65
81
|
canvas: this.canvas,
|
|
@@ -69,6 +85,11 @@ export class Renderer {
|
|
|
69
85
|
// premultipliedAlpha: true
|
|
70
86
|
});
|
|
71
87
|
|
|
88
|
+
|
|
89
|
+
THREE.ColorManagement.enabled = false;
|
|
90
|
+
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
91
|
+
|
|
92
|
+
|
|
72
93
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
73
94
|
this.renderer.setSize(width, height);
|
|
74
95
|
|
|
@@ -84,7 +105,7 @@ export class Renderer {
|
|
|
84
105
|
format: THREE.RGBAFormat,
|
|
85
106
|
stencilBuffer: false,
|
|
86
107
|
depthBuffer: true,
|
|
87
|
-
samples:
|
|
108
|
+
// samples: 0.7,
|
|
88
109
|
},
|
|
89
110
|
);
|
|
90
111
|
}
|
|
@@ -136,6 +157,7 @@ export class Renderer {
|
|
|
136
157
|
public dispose() {
|
|
137
158
|
this.renderer.dispose();
|
|
138
159
|
this.canvas.remove();
|
|
160
|
+
this.textureManager.disposeAll();
|
|
139
161
|
// TODO: Scene 내부 Mesh들도 순회하며 dispose
|
|
140
162
|
}
|
|
141
163
|
|
|
@@ -154,7 +176,7 @@ export class Renderer {
|
|
|
154
176
|
this.camera.updateProjectionMatrix();
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
public syncScene(graphNode: SceneNode) {
|
|
179
|
+
public syncScene(graphNode: SceneNode, pendingDeletions: Set<HTMLElement>) {
|
|
158
180
|
const newRect = this.target.getBoundingClientRect();
|
|
159
181
|
|
|
160
182
|
const isResized =
|
|
@@ -180,28 +202,59 @@ export class Renderer {
|
|
|
180
202
|
|
|
181
203
|
this.renderOrder = 0;
|
|
182
204
|
|
|
183
|
-
const activeElements = new Set<HTMLElement>();
|
|
184
|
-
|
|
185
|
-
this.reconcileNode(graphNode, activeElements);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
// const activeElements = new Set<HTMLElement>();
|
|
206
|
+
|
|
207
|
+
// this.reconcileNode(graphNode, activeElements);
|
|
208
|
+
this.reconcileNode(graphNode);
|
|
209
|
+
|
|
210
|
+
if (pendingDeletions.size > 0) {
|
|
211
|
+
pendingDeletions.forEach((el) => {
|
|
212
|
+
const meshToDestroy = this.registry.get(el) as THREE.Mesh | undefined;
|
|
213
|
+
if (meshToDestroy) {
|
|
214
|
+
this.scene.remove(meshToDestroy);
|
|
215
|
+
this.travelers.delete(meshToDestroy);
|
|
216
|
+
this.fixedMeshes.delete(meshToDestroy);
|
|
217
|
+
meshToDestroy.geometry.dispose();
|
|
218
|
+
meshToDestroy.traverse((child) => {
|
|
219
|
+
if (child instanceof THREE.Mesh) {
|
|
220
|
+
if (child.geometry) child.geometry.dispose();
|
|
221
|
+
if (child.material) {
|
|
222
|
+
if (Array.isArray(child.material)) {
|
|
223
|
+
child.material.forEach((mat) => mat.dispose());
|
|
224
|
+
} else {
|
|
225
|
+
child.material.dispose();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
this.registry.remove(el);
|
|
231
|
+
this.textureManager.unregister(el);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
195
234
|
}
|
|
235
|
+
|
|
236
|
+
// mesh.geometry.dispose();
|
|
237
|
+
// if (mesh.material instanceof THREE.Material) mesh.material.dispose();
|
|
238
|
+
// this.meshMap.delete(el);
|
|
239
|
+
// }
|
|
240
|
+
// }
|
|
196
241
|
}
|
|
197
242
|
|
|
198
|
-
|
|
199
|
-
|
|
243
|
+
// 탐색 후 완성된 Scene node를 이용하여 mesh를 만들거나 조정
|
|
244
|
+
// => 이 과정에서 scene node에 있는 ele를 넣은 hash => activeElements
|
|
245
|
+
// => 이후 activeElements를 이용하여 mesh를 정리!!!+ map에서도 삭제
|
|
200
246
|
|
|
201
|
-
|
|
247
|
+
// private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
|
|
248
|
+
private reconcileNode(node: SceneNode) {
|
|
249
|
+
// activeElements.add(node.element);
|
|
250
|
+
|
|
251
|
+
// let mesh = this.meshMap.get(node.element);
|
|
252
|
+
let mesh = this.registry.get(node.element) as THREE.Mesh | undefined;
|
|
202
253
|
if (!mesh) {
|
|
203
254
|
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
204
255
|
let material: THREE.MeshBasicMaterial | THREE.Material;
|
|
256
|
+
const initialTexture = node.isTraveler ? this.renderTarget?.texture : this.textureManager.get(node.element);
|
|
257
|
+
|
|
205
258
|
material = Painter.create(
|
|
206
259
|
"BOX",
|
|
207
260
|
node.styles,
|
|
@@ -209,23 +262,43 @@ export class Renderer {
|
|
|
209
262
|
node.rect.width,
|
|
210
263
|
node.rect.height,
|
|
211
264
|
this.qualityFactor,
|
|
212
|
-
|
|
265
|
+
initialTexture,
|
|
266
|
+
node.shaderHooks,
|
|
213
267
|
);
|
|
214
268
|
mesh = new THREE.Mesh(geometry, material);
|
|
215
269
|
if (node.type === "TEXT") mesh.name = "BG_MESH";
|
|
216
270
|
this.scene.add(mesh);
|
|
217
|
-
this.
|
|
271
|
+
this.registry.register(node.element, mesh);
|
|
218
272
|
}
|
|
219
273
|
|
|
274
|
+
// [Important] use whene mesh animating with js
|
|
220
275
|
mesh.userData.domRect = node.rect;
|
|
221
276
|
|
|
222
277
|
this.updateMeshProperties(mesh, node);
|
|
223
278
|
this.updateMeshLayers(mesh, node);
|
|
224
|
-
if (node.isTraveler)
|
|
279
|
+
if (node.isTraveler) {
|
|
280
|
+
mesh.layers.enable(28);
|
|
281
|
+
this.travelers.add(mesh);
|
|
282
|
+
} else {
|
|
283
|
+
mesh.layers.disable(28);
|
|
284
|
+
this.travelers.delete(mesh);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (node.isFixed) {
|
|
288
|
+
this.fixedMeshes.add(mesh);
|
|
289
|
+
} else {
|
|
290
|
+
this.fixedMeshes.delete(mesh);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (node.styles.imageSrc) {
|
|
294
|
+
this.textureManager.register(node.element, node.styles.imageSrc);
|
|
295
|
+
} else {
|
|
296
|
+
this.textureManager.unregister(node.element);
|
|
297
|
+
}
|
|
225
298
|
|
|
226
299
|
if (node.type === "BOX") {
|
|
227
300
|
for (const child of node.children) {
|
|
228
|
-
this.reconcileNode(child
|
|
301
|
+
this.reconcileNode(child);
|
|
229
302
|
}
|
|
230
303
|
} else if (node.type === "TEXT") {
|
|
231
304
|
this.reconcileTextChild(mesh, node);
|
|
@@ -233,55 +306,66 @@ export class Renderer {
|
|
|
233
306
|
}
|
|
234
307
|
|
|
235
308
|
private reconcileTextChild(parentMesh: THREE.Mesh, node: SceneNode) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const currentStyleHash = JSON.stringify(node.textStyles);
|
|
241
|
-
const cachedStyleHash = textMesh?.userData?.styleHash;
|
|
309
|
+
const lines = node.textLines || [{ text: node.textContent || "", rect: node.rect }];
|
|
310
|
+
const currentStyleHash = JSON.stringify(node.textStyles) + node.textContent + lines.map(l => l.text).join("|");
|
|
311
|
+
const cachedStyleHash = parentMesh.userData?.textChildStyleHash;
|
|
242
312
|
const isDirty =
|
|
243
|
-
!textMesh ||
|
|
244
313
|
node.dirtyMask & DIRTY_CONTENT ||
|
|
245
314
|
currentStyleHash !== cachedStyleHash;
|
|
246
315
|
|
|
247
316
|
if (isDirty) {
|
|
248
|
-
|
|
317
|
+
// Remove all existing TEXT_CHILD meshes
|
|
318
|
+
const existingChildren = parentMesh.children.filter(c => c.name.startsWith("TEXT_CHILD"));
|
|
319
|
+
existingChildren.forEach(child => {
|
|
320
|
+
const textMesh = child as THREE.Mesh;
|
|
249
321
|
(textMesh.material as THREE.MeshBasicMaterial).map?.dispose();
|
|
250
322
|
textMesh.geometry.dispose();
|
|
251
323
|
parentMesh.remove(textMesh);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const material = Painter.create(
|
|
255
|
-
"TEXT",
|
|
256
|
-
node.textStyles!,
|
|
257
|
-
node.textContent || "",
|
|
258
|
-
node.rect.width,
|
|
259
|
-
node.rect.height,
|
|
260
|
-
this.qualityFactor,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
264
|
-
textMesh = new THREE.Mesh(geometry, material);
|
|
265
|
-
|
|
266
|
-
textMesh.name = "TEXT_CHILD";
|
|
267
|
-
textMesh.userData = { styleHash: currentStyleHash };
|
|
268
|
-
this.updateMeshLayers(textMesh, node);
|
|
269
|
-
textMesh.position.z = 0.005;
|
|
270
|
-
parentMesh.add(textMesh);
|
|
271
|
-
}
|
|
324
|
+
});
|
|
272
325
|
|
|
273
|
-
|
|
274
|
-
const parentRect = parentMesh.userData.domRect;
|
|
326
|
+
const parentRect = node.rect;
|
|
275
327
|
const parentCenterX = parentRect.x + parentRect.width / 2;
|
|
276
328
|
const parentCenterY = parentRect.y + parentRect.height / 2;
|
|
277
329
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
330
|
+
lines.forEach((line, index) => {
|
|
331
|
+
const material = Painter.create(
|
|
332
|
+
"TEXT",
|
|
333
|
+
node.textStyles!,
|
|
334
|
+
line.text,
|
|
335
|
+
line.rect.width,
|
|
336
|
+
line.rect.height,
|
|
337
|
+
this.qualityFactor,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
341
|
+
const textMesh = new THREE.Mesh(geometry, material);
|
|
342
|
+
|
|
343
|
+
textMesh.name = `TEXT_CHILD_${index}`;
|
|
344
|
+
this.updateMeshLayers(textMesh, node);
|
|
345
|
+
|
|
346
|
+
// Parent is already scaled to node.rect.width/height.
|
|
347
|
+
// We counter-scale the child so its absolute size is exactly line.rect.width/height.
|
|
348
|
+
const scaleX = node.rect.width === 0 ? 1 : line.rect.width / node.rect.width;
|
|
349
|
+
const scaleY = node.rect.height === 0 ? 1 : line.rect.height / node.rect.height;
|
|
350
|
+
textMesh.scale.set(scaleX, scaleY, 1);
|
|
351
|
+
|
|
352
|
+
const textCenterX = line.rect.x + line.rect.width / 2;
|
|
353
|
+
const textCenterY = line.rect.y + line.rect.height / 2;
|
|
354
|
+
|
|
355
|
+
const offsetX = textCenterX - parentCenterX;
|
|
356
|
+
const offsetY = -(textCenterY - parentCenterY);
|
|
357
|
+
|
|
358
|
+
// Position must also be counter-scaled relative to parent's scale
|
|
359
|
+
textMesh.position.set(
|
|
360
|
+
node.rect.width === 0 ? 0 : offsetX / node.rect.width,
|
|
361
|
+
node.rect.height === 0 ? 0 : offsetY / node.rect.height,
|
|
362
|
+
0.005
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
parentMesh.add(textMesh);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
parentMesh.userData.textChildStyleHash = currentStyleHash;
|
|
285
369
|
}
|
|
286
370
|
}
|
|
287
371
|
|
|
@@ -303,11 +387,44 @@ export class Renderer {
|
|
|
303
387
|
const localX = rect.x - targetPageX;
|
|
304
388
|
const localY = rect.y - targetPageY;
|
|
305
389
|
|
|
390
|
+
const baseX = localX - canvasWidth / 2 + rect.width / 2;
|
|
391
|
+
const baseY = -localY + canvasHeight / 2 - rect.height / 2;
|
|
392
|
+
|
|
306
393
|
mesh.position.set(
|
|
307
|
-
|
|
308
|
-
|
|
394
|
+
baseX,
|
|
395
|
+
baseY,
|
|
309
396
|
styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
|
|
310
397
|
);
|
|
398
|
+
const pureDOM_X = rect.x; // 트랜스폼 오염 전 순수 좌표 (가정)
|
|
399
|
+
const pureDOM_Y = rect.y;
|
|
400
|
+
|
|
401
|
+
mesh.userData.basePosition = { x: baseX, y: baseY };
|
|
402
|
+
mesh.userData.originalBasePosition = { x: baseX, y: baseY };
|
|
403
|
+
mesh.userData.baseSize = { width: rect.width, height: rect.height };
|
|
404
|
+
// ✨ 추가: 역산을 위한 순수 DOM 초기 좌표 저장
|
|
405
|
+
mesh.userData.baseDOM = { x: pureDOM_X, y: pureDOM_Y };
|
|
406
|
+
mesh.userData.isFixed = node.isFixed;
|
|
407
|
+
mesh.userData.initialScroll = { x: window.scrollX, y: window.scrollY };
|
|
408
|
+
|
|
409
|
+
// ✨ 추가: 초기 transform 상태를 저장하여 애니메이션 시 delta 값만 적용하도록 함 (이중 적용 방지)
|
|
410
|
+
const targetEl = node.element.nodeType === Node.TEXT_NODE ? node.element.parentElement! : node.element;
|
|
411
|
+
const computed = window.getComputedStyle(targetEl);
|
|
412
|
+
let baseTx = 0, baseTy = 0;
|
|
413
|
+
if (computed.transform && computed.transform !== "none") {
|
|
414
|
+
const matrix = new DOMMatrix(computed.transform);
|
|
415
|
+
baseTx = matrix.m41;
|
|
416
|
+
baseTy = matrix.m42;
|
|
417
|
+
}
|
|
418
|
+
mesh.userData.baseTransform = { x: baseTx, y: baseTy };
|
|
419
|
+
|
|
420
|
+
// ✨ 추가: 애니메이션이 끝나고 씬이 갱신되면 비율 캐시 초기화!
|
|
421
|
+
delete mesh.userData.originRatioX;
|
|
422
|
+
delete mesh.userData.originRatioY;
|
|
423
|
+
// mesh.position.set(
|
|
424
|
+
// localX - canvasWidth / 2 + rect.width / 2,
|
|
425
|
+
// -localY + canvasHeight / 2 - rect.height / 2,
|
|
426
|
+
// styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
|
|
427
|
+
// );
|
|
311
428
|
Painter.update(
|
|
312
429
|
mesh.material as THREE.Material,
|
|
313
430
|
"BOX",
|
|
@@ -316,7 +433,7 @@ export class Renderer {
|
|
|
316
433
|
node.rect.width,
|
|
317
434
|
node.rect.height,
|
|
318
435
|
this.qualityFactor,
|
|
319
|
-
node.isTraveler ? this.renderTarget?.texture :
|
|
436
|
+
node.isTraveler ? this.renderTarget?.texture : this.textureManager.get(node.element),
|
|
320
437
|
);
|
|
321
438
|
}
|
|
322
439
|
|
|
@@ -327,13 +444,11 @@ export class Renderer {
|
|
|
327
444
|
}
|
|
328
445
|
|
|
329
446
|
private captureRenderTarget() {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
if (travelers.length === 0) return;
|
|
447
|
+
// [Problem] this method called on requestAnimationFrame
|
|
448
|
+
// => this logic travers all meshes every frame, need optimization
|
|
449
|
+
|
|
450
|
+
// if (travelers.length === 0) return;
|
|
451
|
+
if (this.travelers.size === 0) return;
|
|
337
452
|
|
|
338
453
|
const oldClearColor = new THREE.Color();
|
|
339
454
|
const oldClearAlpha = this.renderer.getClearAlpha();
|
|
@@ -354,23 +469,34 @@ export class Renderer {
|
|
|
354
469
|
|
|
355
470
|
const pixelRatio = this.renderer.getPixelRatio();
|
|
356
471
|
|
|
357
|
-
for (const traveler of travelers) {
|
|
472
|
+
for (const traveler of this.travelers) {
|
|
358
473
|
vector.setFromMatrixPosition(traveler.matrixWorld);
|
|
359
474
|
vector.project(this.camera);
|
|
360
475
|
|
|
361
476
|
const centerX = ((vector.x + 1) / 2) * canvasWidth;
|
|
362
477
|
const centerY = ((vector.y + 1) / 2) * canvasHeight;
|
|
363
478
|
|
|
364
|
-
|
|
365
|
-
|
|
479
|
+
let clipDiff = 0;
|
|
480
|
+
let clipRadito = 1;
|
|
481
|
+
if (typeof this.clipArea === "number") {
|
|
482
|
+
clipRadito = this.clipArea;
|
|
483
|
+
} else if (this.clipArea.endsWith("%")) {
|
|
484
|
+
clipRadito = parseFloat(this.clipArea) / 100;
|
|
485
|
+
} else if (this.clipArea.endsWith("px")) {
|
|
486
|
+
clipDiff = parseFloat(this.clipArea);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const width = traveler.scale.x * clipRadito + 0.5;
|
|
490
|
+
const height = traveler.scale.y * clipRadito + 0.5;
|
|
366
491
|
|
|
367
492
|
const localX = centerX - width / 2;
|
|
368
493
|
const localY = centerY - height / 2;
|
|
369
494
|
|
|
370
|
-
const scissorX = (localX * this.qualityFactor) / pixelRatio;
|
|
371
|
-
const scissorY = (localY * this.qualityFactor) / pixelRatio;
|
|
372
|
-
const scissorW = (width * this.qualityFactor) / pixelRatio;
|
|
373
|
-
const scissorH =
|
|
495
|
+
const scissorX = (localX * this.qualityFactor - clipDiff) / pixelRatio;
|
|
496
|
+
const scissorY = (localY * this.qualityFactor - clipDiff) / pixelRatio;
|
|
497
|
+
const scissorW = (width * this.qualityFactor + clipDiff * 2) / pixelRatio;
|
|
498
|
+
const scissorH =
|
|
499
|
+
(height * this.qualityFactor + clipDiff * 2) / pixelRatio;
|
|
374
500
|
|
|
375
501
|
this.renderer.setScissor(scissorX, scissorY, scissorW, scissorH);
|
|
376
502
|
this.renderer.render(this.scene, this.camera);
|
|
@@ -378,7 +504,7 @@ export class Renderer {
|
|
|
378
504
|
this.renderer.setScissorTest(false);
|
|
379
505
|
this.renderer.autoClear = true;
|
|
380
506
|
this.renderer.setRenderTarget(null);
|
|
381
|
-
this.camera.layers.set(
|
|
507
|
+
this.camera.layers.set(0);
|
|
382
508
|
this.renderer.setClearColor(oldClearColor, oldClearAlpha);
|
|
383
509
|
}
|
|
384
510
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
export class MeshRegistry {
|
|
4
|
+
private store: WeakMap<HTMLElement, THREE.Mesh>;
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.store = new WeakMap();
|
|
8
|
+
}
|
|
9
|
+
public register(element: HTMLElement, mesh: THREE.Mesh): void {
|
|
10
|
+
this.store.set(element, mesh);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public get(element: HTMLElement): THREE.Mesh | undefined {
|
|
14
|
+
return this.store.get(element);
|
|
15
|
+
}
|
|
16
|
+
// public register(element: HTMLElement, meshGroup: THREE.Group): void {
|
|
17
|
+
// this.store.set(element, meshGroup);
|
|
18
|
+
// }
|
|
19
|
+
|
|
20
|
+
// public get(element: HTMLElement): THREE.Group | undefined {
|
|
21
|
+
// return this.store.get(element);
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
public has(element: HTMLElement): boolean {
|
|
25
|
+
return this.store.has(element);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Optional
|
|
29
|
+
public remove(element: HTMLElement): void {
|
|
30
|
+
this.store.delete(element);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
export type TextureReadyCallback = (
|
|
4
|
+
element: HTMLElement,
|
|
5
|
+
texture: THREE.Texture | null,
|
|
6
|
+
) => void;
|
|
7
|
+
|
|
8
|
+
export class TextureLifecycleManager {
|
|
9
|
+
private observer: IntersectionObserver;
|
|
10
|
+
private textures: WeakMap<HTMLElement, THREE.Texture> = new WeakMap();
|
|
11
|
+
private loadStatus: WeakMap<HTMLElement, boolean> = new WeakMap();
|
|
12
|
+
private elementUrls: WeakMap<HTMLElement, string> = new WeakMap();
|
|
13
|
+
private onUpdate: TextureReadyCallback;
|
|
14
|
+
|
|
15
|
+
constructor(onUpdate: TextureReadyCallback) {
|
|
16
|
+
this.onUpdate = onUpdate;
|
|
17
|
+
this.observer = new IntersectionObserver(
|
|
18
|
+
(entries) => {
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const el = entry.target as HTMLElement;
|
|
21
|
+
if (entry.isIntersecting) {
|
|
22
|
+
this.loadTexture(el);
|
|
23
|
+
} else {
|
|
24
|
+
this.disposeTexture(el);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{ rootMargin: "300px" },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public register(element: HTMLElement, url: string) {
|
|
33
|
+
if (element.nodeType !== Node.ELEMENT_NODE) return;
|
|
34
|
+
const currentUrl = this.elementUrls.get(element);
|
|
35
|
+
if (currentUrl !== url) {
|
|
36
|
+
this.elementUrls.set(element, url);
|
|
37
|
+
// Restart observation to trigger intersecting check
|
|
38
|
+
this.observer.unobserve(element);
|
|
39
|
+
this.observer.observe(element);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public unregister(element: HTMLElement) {
|
|
44
|
+
if (element.nodeType === Node.ELEMENT_NODE) {
|
|
45
|
+
this.observer.unobserve(element);
|
|
46
|
+
}
|
|
47
|
+
this.disposeTexture(element);
|
|
48
|
+
this.elementUrls.delete(element);
|
|
49
|
+
this.loadStatus.delete(element);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async loadTexture(element: HTMLElement) {
|
|
53
|
+
if (this.loadStatus.get(element) || this.textures.has(element)) return;
|
|
54
|
+
|
|
55
|
+
const url = this.elementUrls.get(element);
|
|
56
|
+
if (!url) return;
|
|
57
|
+
|
|
58
|
+
this.loadStatus.set(element, true);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(url);
|
|
62
|
+
const blob = await response.blob();
|
|
63
|
+
const bitmap = await createImageBitmap(blob, { imageOrientation: "flipY" });
|
|
64
|
+
|
|
65
|
+
// Check if URL changed or element was unregistered while decoding
|
|
66
|
+
if (this.elementUrls.get(element) !== url) {
|
|
67
|
+
bitmap.close();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if element is no longer intersecting (debounced unobserve might have fired)
|
|
72
|
+
// Actually we just check if it's still observed and has the URL.
|
|
73
|
+
// A more robust check for intersection state might be needed, but for now we rely on disposeTexture.
|
|
74
|
+
|
|
75
|
+
const texture = new THREE.CanvasTexture(bitmap);
|
|
76
|
+
texture.needsUpdate = true;
|
|
77
|
+
this.textures.set(element, texture);
|
|
78
|
+
this.onUpdate(element, texture);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn("[MirageEngine] Failed to load texture:", url, e);
|
|
81
|
+
} finally {
|
|
82
|
+
// Re-check url, if it changed, we should probably trigger loadTexture again,
|
|
83
|
+
// but IntersectionObserver will handle it if it's still intersecting.
|
|
84
|
+
if (this.elementUrls.has(element)) {
|
|
85
|
+
this.loadStatus.set(element, false);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private disposeTexture(element: HTMLElement) {
|
|
91
|
+
const texture = this.textures.get(element);
|
|
92
|
+
if (texture) {
|
|
93
|
+
texture.dispose();
|
|
94
|
+
this.textures.delete(element);
|
|
95
|
+
this.onUpdate(element, null);
|
|
96
|
+
}
|
|
97
|
+
// Also cancel loading state if we had one
|
|
98
|
+
this.loadStatus.set(element, false);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public get(element: HTMLElement) {
|
|
102
|
+
return this.textures.get(element);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public disposeAll() {
|
|
106
|
+
this.observer.disconnect();
|
|
107
|
+
// Assuming WeakMap handles GC of remaining textures, but in WebGL we must explicitly delete.
|
|
108
|
+
// However, WeakMap is not iterable, so we can't easily iterate and delete.
|
|
109
|
+
// The renderer should unregister nodes upon pendingDeletions.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface StyleData {
|
|
2
|
+
// 1. Transform
|
|
3
|
+
x?: number;
|
|
4
|
+
y?: number;
|
|
5
|
+
z?: number;
|
|
6
|
+
scaleX?: number;
|
|
7
|
+
scaleY?: number;
|
|
8
|
+
scaleZ?: number;
|
|
9
|
+
rotateX?: number;
|
|
10
|
+
rotateY?: number;
|
|
11
|
+
rotateZ?: number;
|
|
12
|
+
|
|
13
|
+
// for future use
|
|
14
|
+
matrix?: Float32Array;
|
|
15
|
+
|
|
16
|
+
// 2. Material / Appearance
|
|
17
|
+
opacity?: number;
|
|
18
|
+
backgroundColor?: [number, number, number];
|
|
19
|
+
backgroundImage?: string;
|
|
20
|
+
borderRadius?: number | [number, number, number, number];
|
|
21
|
+
|
|
22
|
+
// 3. Layout / Depth
|
|
23
|
+
width?: number;
|
|
24
|
+
height?: number;
|
|
25
|
+
zIndex?: number;
|
|
26
|
+
}
|
package/src/types/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextStyles, BoxStyles } from "@mirage-engine/painter";
|
|
1
|
+
import { TextStyles, BoxStyles, ShaderHooks } from "@mirage-engine/painter";
|
|
2
2
|
import { Visibility } from "./flags";
|
|
3
3
|
|
|
4
4
|
export type NodeType = "BOX" | "TEXT";
|
|
@@ -20,12 +20,15 @@ export interface SceneNode {
|
|
|
20
20
|
|
|
21
21
|
textContent?: string;
|
|
22
22
|
textStyles?: TextStyles;
|
|
23
|
+
textLines?: { text: string; rect: NodeRect }[];
|
|
23
24
|
|
|
24
25
|
dirtyMask: number;
|
|
25
26
|
// TODO: SceneNode의 sort 로직 추가(아마도)
|
|
26
27
|
|
|
27
28
|
visibility: Visibility;
|
|
28
29
|
isTraveler: boolean;
|
|
30
|
+
isFixed: boolean;
|
|
31
|
+
shaderHooks?: ShaderHooks;
|
|
29
32
|
|
|
30
33
|
children: SceneNode[];
|
|
31
34
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type Quality = "low" | "medium" | "high" | number;
|
|
2
2
|
export type MirageMode = "overlay" | "duplicate";
|
|
3
|
+
export type PxUnit = `${number}px`;
|
|
4
|
+
export type PercentUnit = `${number}%`;
|
|
5
|
+
export type travelerClipArea = PxUnit | PercentUnit | number;
|
|
3
6
|
export interface FilterConfig {
|
|
4
7
|
includeTree?: string[];
|
|
5
8
|
excludeTree?: string[];
|
|
@@ -22,6 +25,7 @@ interface BaseConfig {
|
|
|
22
25
|
};
|
|
23
26
|
filter?: FilterConfig;
|
|
24
27
|
resizeDebounce?: boolean | ResizeConfig;
|
|
28
|
+
travelerClipArea? : travelerClipArea;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface OverlayConfig extends BaseConfig {
|
package/src/types/index.ts
CHANGED