@mirage-engine/core 0.3.2 → 0.3.4

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.
@@ -25,8 +25,14 @@ export class Renderer {
25
25
  private renderOrder: number = 0;
26
26
  public qualityFactor: number = 2;
27
27
  private mode: MirageMode = "overlay";
28
+ private canvasSize: "viewport" | "document" = "viewport";
28
29
  private clipArea: travelerClipArea = 1;
29
30
  public targetLayer: number | LayerTarget = "base";
31
+ private readonly overscan: number = 200;
32
+
33
+ private get isViewport(): boolean {
34
+ return this.mode === "overlay" && this.canvasSize === "viewport";
35
+ }
30
36
 
31
37
  private target: HTMLElement;
32
38
  private mountContainer: HTMLElement;
@@ -52,22 +58,24 @@ export class Renderer {
52
58
  this.mountContainer = mountContainer;
53
59
  this.registry = registry;
54
60
 
61
+ this.mode = config.mode ?? "overlay";
62
+ this.canvasSize = (config as any).canvasSize ?? "viewport";
63
+ this.clipArea = config.travelerClipArea ?? 1;
64
+ this.targetLayer = config.layer ?? "base";
65
+
55
66
  this.textureManager = new TextureLifecycleManager((el, texture) => {
56
67
  const mesh = this.registry.get(el);
57
68
  if (mesh && mesh.material instanceof THREE.ShaderMaterial) {
58
69
  Painter.forceUpdateUniforms(mesh.material, { texture: texture });
59
70
  }
60
- });
71
+ }, this.isViewport);
61
72
 
62
- this.mode = config.mode ?? "overlay";
63
- this.clipArea = config.travelerClipArea ?? 1;
64
- this.targetLayer = config.layer ?? "base";
65
73
  this.canvas = document.createElement("canvas");
66
74
  this.scene = new THREE.Scene();
67
75
 
68
76
  this.targetRect = this.target.getBoundingClientRect();
69
- const width = this.targetRect.width;
70
- const height = this.targetRect.height;
77
+ const width = this.isViewport ? window.innerWidth + this.overscan * 2 : this.targetRect.width;
78
+ const height = this.isViewport ? window.innerHeight + this.overscan * 2 : this.targetRect.height;
71
79
 
72
80
  // target duplicate mode
73
81
  // const width = target.parentElement!.clientWidth;
@@ -113,10 +121,12 @@ export class Renderer {
113
121
 
114
122
  public createRenderTarget() {
115
123
  for (let i = 0; i < ATTR_TRAVEL.MAX_LAYERS; i++) {
124
+ const width = this.isViewport ? window.innerWidth + this.overscan * 2 : this.targetRect.width;
125
+ const height = this.isViewport ? window.innerHeight + this.overscan * 2 : this.targetRect.height;
116
126
  this.renderTargets.push(
117
127
  new THREE.WebGLRenderTarget(
118
- this.targetRect.width * this.qualityFactor,
119
- this.targetRect.height * this.qualityFactor,
128
+ width * this.qualityFactor,
129
+ height * this.qualityFactor,
120
130
  {
121
131
  minFilter: THREE.LinearFilter,
122
132
  magFilter: THREE.LinearFilter,
@@ -160,8 +170,10 @@ export class Renderer {
160
170
  }
161
171
 
162
172
  private updateCanvasLayout() {
163
- this.canvas.style.width = `${this.targetRect.width}px`;
164
- this.canvas.style.height = `${this.targetRect.height}px`;
173
+ const width = this.isViewport ? window.innerWidth + this.overscan * 2 : this.targetRect.width;
174
+ const height = this.isViewport ? window.innerHeight + this.overscan * 2 : this.targetRect.height;
175
+ this.canvas.style.width = `${width}px`;
176
+ this.canvas.style.height = `${height}px`;
165
177
 
166
178
  if (this.mode === "duplicate") {
167
179
  this.canvas.style.position = "";
@@ -169,9 +181,9 @@ export class Renderer {
169
181
  this.canvas.style.left = "";
170
182
  this.canvas.style.display = "block";
171
183
  } else {
172
- this.canvas.style.position = "absolute";
173
- this.canvas.style.top = `${this.target.offsetTop}px`;
174
- this.canvas.style.left = `${this.target.offsetLeft}px`;
184
+ this.canvas.style.position = this.isViewport ? "fixed" : "absolute";
185
+ this.canvas.style.top = this.isViewport ? `-${this.overscan}px` : `${this.target.offsetTop}px`;
186
+ this.canvas.style.left = this.isViewport ? `-${this.overscan}px` : `${this.target.offsetLeft}px`;
175
187
  this.canvas.style.display = "block";
176
188
  }
177
189
  }
@@ -222,10 +234,14 @@ export class Renderer {
222
234
 
223
235
  public syncScene(graphNode: SceneNode, pendingDeletions: Set<HTMLElement>) {
224
236
  const newRect = this.target.getBoundingClientRect();
237
+ const newWidth = this.isViewport ? window.innerWidth + this.overscan * 2 : newRect.width;
238
+ const newHeight = this.isViewport ? window.innerHeight + this.overscan * 2 : newRect.height;
239
+ const oldWidth = this.isViewport ? this.canvas.clientWidth : this.targetRect.width;
240
+ const oldHeight = this.isViewport ? this.canvas.clientHeight : this.targetRect.height;
225
241
 
226
242
  const isResized =
227
- Math.abs(newRect.width - this.targetRect.width) > 0.1 ||
228
- Math.abs(newRect.height - this.targetRect.height) > 0.1;
243
+ Math.abs(newWidth - oldWidth) > 0.1 ||
244
+ Math.abs(newHeight - oldHeight) > 0.1;
229
245
 
230
246
  const isMoved =
231
247
  this.mode === "overlay" &&
@@ -234,7 +250,7 @@ export class Renderer {
234
250
 
235
251
  if (isResized) {
236
252
  this.targetRect = newRect;
237
- this.setSize(this.targetRect.width, this.targetRect.height);
253
+ this.setSize(newWidth, newHeight);
238
254
 
239
255
  this.updateCanvasLayout();
240
256
  } else if (isMoved) {
@@ -300,6 +316,16 @@ export class Renderer {
300
316
  private reconcileNode(node: SceneNode) {
301
317
  let mesh = this.registry.get(node.element) as THREE.Mesh | undefined;
302
318
 
319
+ const currentShaderHash = JSON.stringify(node.shaderHooks || null);
320
+
321
+ if (mesh && mesh.userData.shaderHash !== currentShaderHash) {
322
+ this.scene.remove(mesh);
323
+ mesh.geometry.dispose();
324
+ if (mesh.material instanceof THREE.Material) mesh.material.dispose();
325
+ this.registry.remove(node.element);
326
+ mesh = undefined;
327
+ }
328
+
303
329
  if (!mesh) {
304
330
  const geometry = new THREE.PlaneGeometry(1, 1);
305
331
  const initialTexture = node.isTraveler
@@ -324,6 +350,8 @@ export class Renderer {
324
350
 
325
351
  this.registry.register(node.element, mesh);
326
352
  mesh.userData.baseMaterial = material;
353
+ mesh.userData.domElement = node.element;
354
+ mesh.userData.shaderHash = currentShaderHash;
327
355
  }
328
356
 
329
357
 
@@ -483,7 +511,8 @@ export class Renderer {
483
511
  mesh.scale.set(rect.width, rect.height, 1);
484
512
 
485
513
  mesh.userData.domRect = {
486
- ...rect,
514
+ x: rect.x,
515
+ y: rect.y,
487
516
  width: rect.width,
488
517
  height: rect.height,
489
518
  };
@@ -494,11 +523,16 @@ export class Renderer {
494
523
  const targetPageX = this.targetRect.left + window.scrollX;
495
524
  const targetPageY = this.targetRect.top + window.scrollY;
496
525
 
497
- const localX = rect.x - targetPageX;
498
- const localY = rect.y - targetPageY;
499
-
500
- const baseX = localX - canvasWidth / 2 + rect.width / 2;
501
- const baseY = -localY + canvasHeight / 2 - rect.height / 2;
526
+ let baseX: number, baseY: number;
527
+ if (this.isViewport) {
528
+ baseX = rect.x - window.innerWidth / 2 + rect.width / 2;
529
+ baseY = -rect.y + window.innerHeight / 2 - rect.height / 2;
530
+ } else {
531
+ const localX = rect.x - targetPageX;
532
+ const localY = rect.y - targetPageY;
533
+ baseX = localX - canvasWidth / 2 + rect.width / 2;
534
+ baseY = -localY + canvasHeight / 2 - rect.height / 2;
535
+ }
502
536
 
503
537
  mesh.position.set(
504
538
  baseX,
@@ -571,10 +605,16 @@ export class Renderer {
571
605
  }
572
606
 
573
607
  const nativeMesh = mesh.userData.nativeMesh as THREE.Mesh;
574
- const nativeLocalX = node.nativeRect.x - targetPageX;
575
- const nativeLocalY = node.nativeRect.y - targetPageY;
576
- const nativeBaseX = nativeLocalX - canvasWidth / 2 + node.nativeRect.width / 2;
577
- const nativeBaseY = -nativeLocalY + canvasHeight / 2 - node.nativeRect.height / 2;
608
+ let nativeBaseX: number, nativeBaseY: number;
609
+ if (this.isViewport) {
610
+ nativeBaseX = node.nativeRect.x - window.innerWidth / 2 + node.nativeRect.width / 2;
611
+ nativeBaseY = -node.nativeRect.y + window.innerHeight / 2 - node.nativeRect.height / 2;
612
+ } else {
613
+ const nativeLocalX = node.nativeRect.x - targetPageX;
614
+ const nativeLocalY = node.nativeRect.y - targetPageY;
615
+ nativeBaseX = nativeLocalX - canvasWidth / 2 + node.nativeRect.width / 2;
616
+ nativeBaseY = -nativeLocalY + canvasHeight / 2 - node.nativeRect.height / 2;
617
+ }
578
618
 
579
619
  nativeMesh.scale.set(node.nativeRect.width, node.nativeRect.height, 1);
580
620
  nativeMesh.position.set(
@@ -661,8 +701,8 @@ export class Renderer {
661
701
  this.camera.layers.set(targetLayer);
662
702
 
663
703
  const vector = new THREE.Vector3();
664
- const canvasWidth = this.targetRect.width;
665
- const canvasHeight = this.targetRect.height;
704
+ const canvasWidth = this.isViewport ? window.innerWidth + this.overscan * 2 : this.targetRect.width;
705
+ const canvasHeight = this.isViewport ? window.innerHeight + this.overscan * 2 : this.targetRect.height;
666
706
 
667
707
  const pixelRatio = this.renderer.getPixelRatio();
668
708
 
@@ -719,5 +759,72 @@ export class Renderer {
719
759
  this.renderer.render(this.scene, this.camera);
720
760
  }
721
761
 
722
-
762
+ public syncMeshesByDOM() {
763
+ // If not in viewport mode, we must account for page scroll in absolute positions.
764
+ const targetPageX = this.targetRect.left + window.scrollX;
765
+ const targetPageY = this.targetRect.top + window.scrollY;
766
+
767
+ const pixelRatio = this.renderer.getPixelRatio();
768
+ const canvasWidth = this.renderer.domElement.width / pixelRatio;
769
+ const canvasHeight = this.renderer.domElement.height / pixelRatio;
770
+
771
+ this.scene.children.forEach((child) => {
772
+ const mesh = child as THREE.Mesh;
773
+ if (!mesh.userData || !mesh.userData.domElement) return;
774
+
775
+ const element = mesh.userData.domElement as HTMLElement;
776
+ let rect: DOMRect;
777
+ if (element.nodeType === Node.TEXT_NODE) {
778
+ const range = document.createRange();
779
+ range.selectNode(element);
780
+ rect = range.getBoundingClientRect();
781
+ } else {
782
+ rect = element.getBoundingClientRect();
783
+ }
784
+ const cached = mesh.userData.domRect;
785
+
786
+ // Update if position or size changed by more than 0.5px
787
+ if (
788
+ !cached ||
789
+ Math.abs(rect.x - cached.x) > 0.5 ||
790
+ Math.abs(rect.y - cached.y) > 0.5 ||
791
+ Math.abs(rect.width - cached.width) > 0.5 ||
792
+ Math.abs(rect.height - cached.height) > 0.5
793
+ ) {
794
+ mesh.userData.domRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
795
+
796
+ let baseX: number, baseY: number;
797
+ if (this.isViewport) {
798
+ baseX = rect.x - window.innerWidth / 2 + rect.width / 2;
799
+ baseY = -rect.y + window.innerHeight / 2 - rect.height / 2;
800
+ } else {
801
+ const localX = rect.x - targetPageX;
802
+ const localY = rect.y - targetPageY;
803
+ baseX = localX - canvasWidth / 2 + rect.width / 2;
804
+ baseY = -localY + canvasHeight / 2 - rect.height / 2;
805
+ }
806
+
807
+ // Apply new position and scale
808
+ mesh.position.setX(baseX);
809
+ mesh.position.setY(baseY);
810
+ mesh.scale.set(rect.width, rect.height, 1);
811
+
812
+ // Update uniforms so shader-based drawing (like border-radius) doesn't stretch
813
+ if (mesh.material instanceof THREE.ShaderMaterial) {
814
+ Painter.forceUpdateUniforms(mesh.material, {
815
+ width: rect.width,
816
+ height: rect.height,
817
+ });
818
+ }
819
+
820
+ // Update native mesh if exists
821
+ if (mesh.userData.nativeMesh) {
822
+ const nativeMesh = mesh.userData.nativeMesh as THREE.Mesh;
823
+ nativeMesh.position.setX(baseX);
824
+ nativeMesh.position.setY(baseY);
825
+ nativeMesh.scale.set(rect.width, rect.height, 1);
826
+ }
827
+ }
828
+ });
829
+ }
723
830
  }
@@ -6,27 +6,29 @@ export type TextureReadyCallback = (
6
6
  ) => void;
7
7
 
8
8
  export class TextureLifecycleManager {
9
- private observer: IntersectionObserver;
9
+ private observer?: IntersectionObserver;
10
10
  private textures: WeakMap<HTMLElement, THREE.Texture> = new WeakMap();
11
11
  private loadStatus: WeakMap<HTMLElement, boolean> = new WeakMap();
12
12
  private elementUrls: WeakMap<HTMLElement, string> = new WeakMap();
13
13
  private onUpdate: TextureReadyCallback;
14
14
 
15
- constructor(onUpdate: TextureReadyCallback) {
15
+ constructor(onUpdate: TextureReadyCallback, enableCulling: boolean = true) {
16
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);
17
+ if (enableCulling) {
18
+ this.observer = new IntersectionObserver(
19
+ (entries) => {
20
+ for (const entry of entries) {
21
+ const el = entry.target as HTMLElement;
22
+ if (entry.isIntersecting) {
23
+ this.loadTexture(el);
24
+ } else {
25
+ this.disposeTexture(el);
26
+ }
25
27
  }
26
- }
27
- },
28
- { rootMargin: "300px" },
29
- );
28
+ },
29
+ { rootMargin: "300px" },
30
+ );
31
+ }
30
32
  }
31
33
 
32
34
  public register(element: HTMLElement, url: string) {
@@ -35,13 +37,17 @@ export class TextureLifecycleManager {
35
37
  if (currentUrl !== url) {
36
38
  this.elementUrls.set(element, url);
37
39
  // Restart observation to trigger intersecting check
38
- this.observer.unobserve(element);
39
- this.observer.observe(element);
40
+ if (this.observer) {
41
+ this.observer.unobserve(element);
42
+ this.observer.observe(element);
43
+ } else {
44
+ this.loadTexture(element);
45
+ }
40
46
  }
41
47
  }
42
48
 
43
49
  public unregister(element: HTMLElement) {
44
- if (element.nodeType === Node.ELEMENT_NODE) {
50
+ if (element.nodeType === Node.ELEMENT_NODE && this.observer) {
45
51
  this.observer.unobserve(element);
46
52
  }
47
53
  this.disposeTexture(element);
@@ -118,7 +124,9 @@ export class TextureLifecycleManager {
118
124
  }
119
125
 
120
126
  public disposeAll() {
121
- this.observer.disconnect();
127
+ if (this.observer) {
128
+ this.observer.disconnect();
129
+ }
122
130
  // Assuming WeakMap handles GC of remaining textures, but in WebGL we must explicitly delete.
123
131
  // However, WeakMap is not iterable, so we can't easily iterate and delete.
124
132
  // The renderer should unregister nodes upon pendingDeletions.
@@ -11,6 +11,7 @@ export const ATTR_DOM = {
11
11
  KEY: "mirageDom",
12
12
  VALUES: {
13
13
  HIDE: "hide",
14
+ SHOW: "show",
14
15
  },
15
16
  } as const;
16
17
 
@@ -25,6 +25,15 @@ interface BaseConfig {
25
25
 
26
26
  export interface OverlayConfig extends BaseConfig {
27
27
  mode?: "overlay";
28
+ /**
29
+ * Defines the render size of the canvas and camera behavior.
30
+ * 'viewport' (default) is optimized for long scrollable pages by allocating canvas size
31
+ * to the current screen viewport only, significantly improving performance (60fps).
32
+ * 'document' allocates the canvas size to match the full height of the target element,
33
+ * which can cause performance drops on very tall documents but might be necessary
34
+ * for specific visual needs.
35
+ */
36
+ canvasSize?: "viewport" | "document";
28
37
  }
29
38
 
30
39
  export interface DuplicateConfig extends BaseConfig {