@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.
@@ -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 meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
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: 4,
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
- for (const [el, mesh] of this.meshMap.entries()) {
188
- if (!activeElements.has(el)) {
189
- this.scene.remove(mesh);
190
-
191
- mesh.geometry.dispose();
192
- if (mesh.material instanceof THREE.Material) mesh.material.dispose();
193
- this.meshMap.delete(el);
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
- private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
199
- activeElements.add(node.element);
243
+ // 탐색 후 완성된 Scene node 이용하여 mesh를 만들거나 조정
244
+ // => 이 과정에서 scene node에 있는 ele를 넣은 hash => activeElements
245
+ // => 이후 activeElements를 이용하여 mesh를 정리!!!+ map에서도 삭제
200
246
 
201
- let mesh = this.meshMap.get(node.element);
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
- node.isTraveler ? this.renderTarget?.texture : undefined,
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.meshMap.set(node.element, mesh);
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) mesh.layers.enable(28);
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, activeElements);
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
- let textMesh = parentMesh.children.find(
237
- (c) => c.name === "TEXT_CHILD",
238
- ) as THREE.Mesh;
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
- if (textMesh) {
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
- if (textMesh) {
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
- const textCenterX = node.rect.x + node.rect.width / 2;
279
- const textCenterY = node.rect.y + node.rect.height / 2;
280
-
281
- const offsetX = textCenterX - parentCenterX;
282
- const offsetY = -(textCenterY - parentCenterY);
283
-
284
- textMesh.position.set(offsetX, offsetY, 0.005);
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
- localX - canvasWidth / 2 + rect.width / 2,
308
- -localY + canvasHeight / 2 - rect.height / 2,
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 : undefined,
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
- const travelers: THREE.Mesh[] = [];
331
- for (const mesh of this.meshMap.values()) {
332
- if ((mesh.layers.mask & (1 << 28)) !== 0) {
333
- travelers.push(mesh);
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
- const width = traveler.scale.x;
365
- const height = traveler.scale.y;
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 = (height * this.qualityFactor) / pixelRatio;
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(28);
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
+ }
@@ -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
  }
@@ -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 {
@@ -1,3 +1,4 @@
1
1
  export * from './config';
2
2
  export * from './common';
3
- export * from './flags';
3
+ export * from './flags';
4
+ export * from './animate';