@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.
- package/CHANGELOG.md +12 -0
- package/dist/mirage-engine.js +830 -797
- package/dist/mirage-engine.umd.js +16 -16
- package/dist/src/animation/Animator.d.ts +0 -2
- package/dist/src/renderer/Renderer.d.ts +4 -0
- package/dist/src/store/TextureLifecycleManager.d.ts +2 -2
- package/dist/src/types/attributes.d.ts +1 -0
- package/dist/src/types/config.d.ts +9 -0
- package/package.json +1 -1
- package/src/animation/Animator.ts +1 -85
- package/src/core/Syncer.ts +2 -5
- package/src/renderer/Renderer.ts +136 -29
- package/src/store/TextureLifecycleManager.ts +26 -18
- package/src/types/attributes.ts +1 -0
- package/src/types/config.ts +9 -0
package/src/renderer/Renderer.ts
CHANGED
|
@@ -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
|
-
|
|
119
|
-
|
|
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.
|
|
164
|
-
this.
|
|
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(
|
|
228
|
-
Math.abs(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
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
|
|
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.
|
package/src/types/attributes.ts
CHANGED
package/src/types/config.ts
CHANGED
|
@@ -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 {
|