@mirage-engine/core 0.2.1 → 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 +8 -0
- package/dist/mirage-engine.js +1020 -431
- 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 +8 -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 +5 -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 +165 -20
- package/src/env.d.ts +4 -0
- package/src/renderer/Renderer.ts +186 -74
- 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 +2 -0
- package/src/types/index.ts +2 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { StyleData } from "../types";
|
|
2
|
+
import { MeshRegistry } from "../store/MeshRegistry";
|
|
3
|
+
import { Painter, parseColor } from "@mirage-engine/painter";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
|
|
6
|
+
export function animateMeshByData(
|
|
7
|
+
registry: MeshRegistry,
|
|
8
|
+
data: Map<HTMLElement, StyleData>,
|
|
9
|
+
) {
|
|
10
|
+
if (data.size === 0) return;
|
|
11
|
+
|
|
12
|
+
data.forEach((styleData, element) => {
|
|
13
|
+
const mesh = registry.get(element);
|
|
14
|
+
if (!mesh || !mesh.userData.basePosition) return;
|
|
15
|
+
|
|
16
|
+
let { x: baseX, y: baseY } = mesh.userData.basePosition;
|
|
17
|
+
const { width: baseW, height: baseH } = mesh.userData.baseSize;
|
|
18
|
+
|
|
19
|
+
const currentW = styleData.width ?? baseW;
|
|
20
|
+
const currentH = styleData.height ?? baseH;
|
|
21
|
+
const widthDiff = currentW - baseW;
|
|
22
|
+
const heightDiff = currentH - baseH;
|
|
23
|
+
|
|
24
|
+
// --- Capture and Lock Transform Origin (Performance & Accuracy) ---
|
|
25
|
+
// We capture the origin on the very first frame of change (> 0.1px)
|
|
26
|
+
// to ensure perfect sync from the start of the first animation.
|
|
27
|
+
if (
|
|
28
|
+
styleData.width !== undefined &&
|
|
29
|
+
mesh.userData.originRatioX === undefined &&
|
|
30
|
+
Math.abs(widthDiff) > 0.1
|
|
31
|
+
) {
|
|
32
|
+
const rect = element.getBoundingClientRect();
|
|
33
|
+
const tx = styleData.x ?? 0;
|
|
34
|
+
const baseTx = mesh.userData.baseTransform?.x ?? 0;
|
|
35
|
+
const scrollOffsetX = mesh.userData.isFixed ? 0 : window.scrollX;
|
|
36
|
+
// rect.left already includes GSAP transform (tx).
|
|
37
|
+
// We subtract tx to strip the animation, then add baseTx to match baseDOM's initial state.
|
|
38
|
+
const currentPageX = rect.left + scrollOffsetX - tx + baseTx;
|
|
39
|
+
const deltaX = currentPageX - mesh.userData.baseDOM.x;
|
|
40
|
+
|
|
41
|
+
let ratioX = -deltaX / widthDiff;
|
|
42
|
+
if (Math.abs(ratioX) < 0.05) ratioX = 0.0;
|
|
43
|
+
else if (Math.abs(ratioX - 1.0) < 0.05) ratioX = 1.0;
|
|
44
|
+
else if (Math.abs(ratioX - 0.5) < 0.05) ratioX = 0.5;
|
|
45
|
+
|
|
46
|
+
mesh.userData.originRatioX = ratioX;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
styleData.height !== undefined &&
|
|
51
|
+
mesh.userData.originRatioY === undefined &&
|
|
52
|
+
Math.abs(heightDiff) > 0.1
|
|
53
|
+
) {
|
|
54
|
+
const rect = element.getBoundingClientRect();
|
|
55
|
+
const ty = styleData.y ?? 0;
|
|
56
|
+
const baseTy = mesh.userData.baseTransform?.y ?? 0;
|
|
57
|
+
const scrollOffsetY = mesh.userData.isFixed ? 0 : window.scrollY;
|
|
58
|
+
const currentPageY = rect.top + scrollOffsetY - ty + baseTy;
|
|
59
|
+
const deltaY = currentPageY - mesh.userData.baseDOM.y;
|
|
60
|
+
|
|
61
|
+
let ratioY = -deltaY / heightDiff;
|
|
62
|
+
if (Math.abs(ratioY) < 0.05) ratioY = 0.0;
|
|
63
|
+
else if (Math.abs(ratioY - 1.0) < 0.05) ratioY = 1.0;
|
|
64
|
+
else if (Math.abs(ratioY - 0.5) < 0.05) ratioY = 0.5;
|
|
65
|
+
|
|
66
|
+
mesh.userData.originRatioY = ratioY;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Calculate Mesh Position using Measured Origin ---
|
|
70
|
+
const ratioX = mesh.userData.originRatioX ?? 0.5;
|
|
71
|
+
const ratioY = mesh.userData.originRatioY ?? 0.5;
|
|
72
|
+
|
|
73
|
+
// Since Three.js meshes expand from center, we offset the center based on the locked origin ratio
|
|
74
|
+
const moveX = widthDiff * (0.5 - ratioX);
|
|
75
|
+
const moveY = heightDiff * (0.5 - ratioY);
|
|
76
|
+
|
|
77
|
+
// --- Calculate Delta Movement ---
|
|
78
|
+
// Extract baseTransform stored during syncScene
|
|
79
|
+
const baseTx = mesh.userData.baseTransform?.x ?? 0;
|
|
80
|
+
const baseTy = mesh.userData.baseTransform?.y ?? 0;
|
|
81
|
+
|
|
82
|
+
// We only want to apply the DIFFERENCE in transform since the base extraction.
|
|
83
|
+
const deltaTx = (styleData.x ?? baseTx) - baseTx;
|
|
84
|
+
const deltaTy = (styleData.y ?? baseTy) - baseTy;
|
|
85
|
+
|
|
86
|
+
mesh.position.setX(baseX + moveX + deltaTx);
|
|
87
|
+
mesh.position.setY(baseY - (moveY + deltaTy));
|
|
88
|
+
mesh.scale.set(currentW, currentH, 1);
|
|
89
|
+
|
|
90
|
+
Painter.forceUpdateUniforms(mesh.material as THREE.ShaderMaterial, {
|
|
91
|
+
backgroundColor: styleData.backgroundColor,
|
|
92
|
+
backgroundImage: styleData.backgroundImage,
|
|
93
|
+
opacity: styleData.opacity,
|
|
94
|
+
borderRadius: styleData.borderRadius ?? mesh.userData.baseStyles?.borderRadius,
|
|
95
|
+
width: currentW,
|
|
96
|
+
height: currentH,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function animateMeshByAttribute(
|
|
101
|
+
_target: HTMLElement,
|
|
102
|
+
_options: { duration: number; easing?: string },
|
|
103
|
+
) {}
|
|
104
|
+
|
|
105
|
+
export function updateFixedMeshesScroll(
|
|
106
|
+
fixedMeshes: Set<THREE.Mesh>,
|
|
107
|
+
currentScrollX: number,
|
|
108
|
+
currentScrollY: number,
|
|
109
|
+
) {
|
|
110
|
+
fixedMeshes.forEach((mesh) => {
|
|
111
|
+
if (mesh.userData.isFixed && mesh.userData.initialScroll && mesh.userData.originalBasePosition) {
|
|
112
|
+
const offsetX = currentScrollX - mesh.userData.initialScroll.x;
|
|
113
|
+
const offsetY = currentScrollY - mesh.userData.initialScroll.y;
|
|
114
|
+
|
|
115
|
+
mesh.userData.basePosition.x = mesh.userData.originalBasePosition.x + offsetX;
|
|
116
|
+
mesh.userData.basePosition.y = mesh.userData.originalBasePosition.y - offsetY;
|
|
117
|
+
|
|
118
|
+
mesh.position.x = mesh.userData.basePosition.x;
|
|
119
|
+
mesh.position.y = mesh.userData.basePosition.y;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function extractFromStyle(style: CSSStyleDeclaration): StyleData {
|
|
125
|
+
const styleObject: StyleData = {};
|
|
126
|
+
|
|
127
|
+
if (style.opacity) {
|
|
128
|
+
styleObject.opacity = parseFloat(style.opacity);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)") {
|
|
132
|
+
const parsed = parseColor(style.backgroundColor);
|
|
133
|
+
styleObject.backgroundColor = [parsed.color.r, parsed.color.g, parsed.color.b];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (style.backgroundImage) {
|
|
137
|
+
styleObject.backgroundImage = style.backgroundImage;
|
|
138
|
+
} else if (style.background) {
|
|
139
|
+
styleObject.backgroundImage = style.background;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (style.borderRadius) {
|
|
143
|
+
styleObject.borderRadius = parseFloat(style.borderRadius);
|
|
144
|
+
}
|
|
145
|
+
if (style.width) {
|
|
146
|
+
styleObject.width = parseFloat(style.width);
|
|
147
|
+
}
|
|
148
|
+
if (style.height) {
|
|
149
|
+
styleObject.height = parseFloat(style.height);
|
|
150
|
+
}
|
|
151
|
+
if (style.transform && style.transform !== "none") {
|
|
152
|
+
const matrix = new DOMMatrix(style.transform);
|
|
153
|
+
|
|
154
|
+
styleObject.x = matrix.m41;
|
|
155
|
+
styleObject.y = matrix.m42;
|
|
156
|
+
styleObject.z = matrix.m43;
|
|
157
|
+
|
|
158
|
+
// [TODO] direct matrix data pipeline to webGL
|
|
159
|
+
// styleObject.matrix = matrix.toFloat32Array();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return styleObject;
|
|
163
|
+
}
|
package/src/core/Engine.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { CoreConfig } from "../types";
|
|
2
2
|
import { Renderer } from "../renderer/Renderer";
|
|
3
3
|
import { Syncer } from "./Syncer";
|
|
4
|
+
import { MeshRegistry } from "../store/MeshRegistry";
|
|
4
5
|
|
|
5
6
|
export class Engine {
|
|
6
7
|
private renderer: Renderer;
|
|
7
8
|
private syncer: Syncer;
|
|
8
9
|
private target: HTMLElement;
|
|
10
|
+
private registry: MeshRegistry;
|
|
9
11
|
|
|
10
12
|
constructor(target: HTMLElement, config: CoreConfig) {
|
|
11
13
|
this.target = target;
|
|
14
|
+
this.registry = new MeshRegistry();
|
|
15
|
+
|
|
16
|
+
if (!document.getElementById("mirage-engine-styles")) {
|
|
17
|
+
const style = document.createElement("style");
|
|
18
|
+
style.id = "mirage-engine-styles";
|
|
19
|
+
style.textContent = `
|
|
20
|
+
[data-mirage-dom="hide"] {
|
|
21
|
+
opacity: 0 !important;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
document.head.appendChild(style);
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
let mountContainer: HTMLElement | undefined;
|
|
14
28
|
|
|
@@ -23,11 +37,14 @@ export class Engine {
|
|
|
23
37
|
throw new Error("[Mirage] Cannot find a container (parent or option).");
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
this.renderer = new Renderer(
|
|
27
|
-
|
|
40
|
+
this.renderer = new Renderer(
|
|
41
|
+
this.target,
|
|
42
|
+
config,
|
|
43
|
+
mountContainer,
|
|
44
|
+
this.registry,
|
|
45
|
+
);
|
|
28
46
|
this.renderer.mount();
|
|
29
|
-
|
|
30
|
-
this.syncer = new Syncer(this.target, this.renderer, config);
|
|
47
|
+
this.syncer = new Syncer(this.target, this.renderer, this.registry, config);
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
public start() {
|
|
@@ -41,4 +58,39 @@ export class Engine {
|
|
|
41
58
|
this.syncer.stop();
|
|
42
59
|
this.renderer.dispose();
|
|
43
60
|
}
|
|
61
|
+
|
|
62
|
+
public test() {
|
|
63
|
+
const boxMesh = this.registry.get(
|
|
64
|
+
document.querySelector("#box2") as HTMLElement,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (!boxMesh) return;
|
|
68
|
+
|
|
69
|
+
const keys: { [key: string]: boolean } = {
|
|
70
|
+
ArrowRight: false,
|
|
71
|
+
ArrowLeft: false,
|
|
72
|
+
ArrowUp: false,
|
|
73
|
+
ArrowDown: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
window.addEventListener("keydown", (e) => {
|
|
77
|
+
if (keys[e.key] !== undefined) keys[e.key] = true;
|
|
78
|
+
});
|
|
79
|
+
window.addEventListener("keyup", (e) => {
|
|
80
|
+
if (keys[e.key] !== undefined) keys[e.key] = false;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const moveStep = 2;
|
|
84
|
+
|
|
85
|
+
const animate = () => {
|
|
86
|
+
requestAnimationFrame(animate);
|
|
87
|
+
|
|
88
|
+
if (keys.ArrowRight) boxMesh.position.setX(boxMesh.position.x + moveStep);
|
|
89
|
+
if (keys.ArrowLeft) boxMesh.position.setX(boxMesh.position.x - moveStep);
|
|
90
|
+
if (keys.ArrowUp) boxMesh.position.setY(boxMesh.position.y + moveStep);
|
|
91
|
+
if (keys.ArrowDown) boxMesh.position.setY(boxMesh.position.y - moveStep);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
animate();
|
|
95
|
+
}
|
|
44
96
|
}
|
package/src/core/Syncer.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { CoreConfig } from "../types/config";
|
|
2
2
|
import { Renderer } from "../renderer/Renderer";
|
|
3
|
+
import { MeshRegistry } from "../store/MeshRegistry";
|
|
3
4
|
import { extractSceneGraph } from "../dom/Extractor";
|
|
4
5
|
import {
|
|
5
6
|
DIRTY_NONE,
|
|
6
7
|
DIRTY_RECT,
|
|
7
8
|
DIRTY_STRUCTURE,
|
|
9
|
+
DIRTY_CONTENT
|
|
10
|
+
,
|
|
8
11
|
DIRTY_STYLE,
|
|
9
12
|
USER_LAYER,
|
|
10
13
|
SYSTEM_LAYER,
|
|
11
14
|
Visibility,
|
|
15
|
+
StyleData,
|
|
12
16
|
} from "../types";
|
|
17
|
+
import { extractFromStyle, animateMeshByData, updateFixedMeshesScroll } from "../animation/Animator";
|
|
13
18
|
|
|
14
19
|
interface InternalResizeConfig {
|
|
15
20
|
enabled: boolean;
|
|
@@ -21,9 +26,12 @@ interface InternalResizeConfig {
|
|
|
21
26
|
export class Syncer {
|
|
22
27
|
private target: HTMLElement;
|
|
23
28
|
private renderer: Renderer;
|
|
29
|
+
private registry: MeshRegistry;
|
|
24
30
|
private filter: CoreConfig["filter"];
|
|
25
31
|
|
|
26
32
|
private observer: MutationObserver;
|
|
33
|
+
private pendingDeletions: Set<HTMLElement> = new Set();
|
|
34
|
+
private pendingStyles: Map<HTMLElement, StyleData> = new Map();
|
|
27
35
|
|
|
28
36
|
private isDomDirty: boolean = false;
|
|
29
37
|
private isRunning: boolean = false;
|
|
@@ -38,9 +46,19 @@ export class Syncer {
|
|
|
38
46
|
private resizeTimer: number | null = null;
|
|
39
47
|
private isResizing: boolean = false;
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
private lastScrollX: number = 0;
|
|
50
|
+
private lastScrollY: number = 0;
|
|
51
|
+
private scrollTimer: number | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
target: HTMLElement,
|
|
55
|
+
renderer: Renderer,
|
|
56
|
+
registry: MeshRegistry,
|
|
57
|
+
config: CoreConfig,
|
|
58
|
+
) {
|
|
42
59
|
this.target = target;
|
|
43
60
|
this.renderer = renderer;
|
|
61
|
+
this.registry = registry;
|
|
44
62
|
this.filter = config.filter;
|
|
45
63
|
|
|
46
64
|
// Resize Debounce Configuration
|
|
@@ -64,13 +82,28 @@ export class Syncer {
|
|
|
64
82
|
for (const mutation of mutations) {
|
|
65
83
|
if (mutation.type === "childList") {
|
|
66
84
|
currentMask |= DIRTY_STRUCTURE;
|
|
85
|
+
if (mutation.removedNodes.length > 0) {
|
|
86
|
+
mutation.removedNodes.forEach((node) => {
|
|
87
|
+
if (node instanceof HTMLElement) {
|
|
88
|
+
this.pendingDeletions.add(node);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
67
92
|
} else if (mutation.type === "attributes") {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
mutation.
|
|
71
|
-
|
|
93
|
+
if (mutation.attributeName === "style") {
|
|
94
|
+
currentMask |= DIRTY_RECT | DIRTY_STYLE;
|
|
95
|
+
const target = mutation.target as HTMLElement;
|
|
96
|
+
|
|
97
|
+
const extractedStyleData = extractFromStyle(target.style);
|
|
98
|
+
// console.log(extractedStyleData);
|
|
99
|
+
this.pendingStyles.set(target, extractedStyleData);
|
|
100
|
+
} else if (mutation.attributeName === "class") {
|
|
72
101
|
currentMask |= DIRTY_RECT | DIRTY_STYLE;
|
|
73
102
|
}
|
|
103
|
+
// else if (mutation.attributeName === "data-mirage") {
|
|
104
|
+
// }
|
|
105
|
+
} else if (mutation.type === "characterData") {
|
|
106
|
+
currentMask |= DIRTY_CONTENT | DIRTY_RECT;
|
|
74
107
|
}
|
|
75
108
|
}
|
|
76
109
|
|
|
@@ -80,7 +113,6 @@ export class Syncer {
|
|
|
80
113
|
// Structural Change detected
|
|
81
114
|
if (currentMask & DIRTY_STRUCTURE) {
|
|
82
115
|
this.clearTimers();
|
|
83
|
-
console.log("Structural Change detected");
|
|
84
116
|
this.isDomDirty = true;
|
|
85
117
|
return;
|
|
86
118
|
}
|
|
@@ -109,7 +141,6 @@ export class Syncer {
|
|
|
109
141
|
|
|
110
142
|
this.target.addEventListener("transitionend", this.onTransitionFinished);
|
|
111
143
|
this.target.addEventListener("animationend", this.onTransitionFinished);
|
|
112
|
-
|
|
113
144
|
window.addEventListener("resize", this.onWindowResize);
|
|
114
145
|
|
|
115
146
|
this.forceUpdateScene();
|
|
@@ -138,17 +169,18 @@ export class Syncer {
|
|
|
138
169
|
clearTimeout(this.cssTimer);
|
|
139
170
|
this.cssTimer = null;
|
|
140
171
|
}
|
|
172
|
+
if (this.scrollTimer) {
|
|
173
|
+
clearTimeout(this.scrollTimer);
|
|
174
|
+
this.scrollTimer = null;
|
|
175
|
+
}
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
private onTransitionFinished = (e: Event) => {
|
|
144
179
|
if (!this.target.contains(e.target as Node)) return;
|
|
145
|
-
|
|
146
180
|
if (this.mutationTimer !== null) return;
|
|
147
|
-
|
|
148
181
|
if (this.cssTimer) clearTimeout(this.cssTimer);
|
|
149
|
-
|
|
182
|
+
if (this.pendingStyles.size != 0) return;
|
|
150
183
|
this.pendingMask |= DIRTY_RECT | DIRTY_STYLE;
|
|
151
|
-
|
|
152
184
|
this.cssTimer = window.setTimeout(() => {
|
|
153
185
|
this.isDomDirty = true;
|
|
154
186
|
this.cssTimer = null;
|
|
@@ -160,17 +192,13 @@ export class Syncer {
|
|
|
160
192
|
this.isDomDirty = true;
|
|
161
193
|
return;
|
|
162
194
|
}
|
|
163
|
-
|
|
164
195
|
if (!this.isResizing) {
|
|
165
196
|
this.isResizing = true;
|
|
166
197
|
if (this.resizeConfig.onStart) this.resizeConfig.onStart();
|
|
167
198
|
}
|
|
168
|
-
|
|
169
199
|
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
170
|
-
|
|
171
200
|
this.resizeTimer = window.setTimeout(() => {
|
|
172
201
|
this.isDomDirty = true;
|
|
173
|
-
|
|
174
202
|
if (this.resizeConfig.onEnd) this.resizeConfig.onEnd();
|
|
175
203
|
this.isResizing = false;
|
|
176
204
|
this.resizeTimer = null;
|
|
@@ -179,6 +207,9 @@ export class Syncer {
|
|
|
179
207
|
|
|
180
208
|
private forceUpdateScene() {
|
|
181
209
|
this.isDomDirty = false;
|
|
210
|
+
|
|
211
|
+
this.lastScrollX = window.scrollX;
|
|
212
|
+
this.lastScrollY = window.scrollY;
|
|
182
213
|
|
|
183
214
|
const discoveredTraveler =
|
|
184
215
|
document.querySelector("[data-mirage-travel='traveler']") !== null;
|
|
@@ -197,7 +228,8 @@ export class Syncer {
|
|
|
197
228
|
);
|
|
198
229
|
|
|
199
230
|
if (sceneGraph) {
|
|
200
|
-
this.renderer.syncScene(sceneGraph);
|
|
231
|
+
this.renderer.syncScene(sceneGraph, this.pendingDeletions);
|
|
232
|
+
this.pendingDeletions.clear();
|
|
201
233
|
}
|
|
202
234
|
|
|
203
235
|
this.pendingMask = DIRTY_NONE;
|
|
@@ -210,7 +242,30 @@ export class Syncer {
|
|
|
210
242
|
this.forceUpdateScene();
|
|
211
243
|
}
|
|
212
244
|
|
|
245
|
+
const currentScrollX = window.scrollX;
|
|
246
|
+
const currentScrollY = window.scrollY;
|
|
247
|
+
if (currentScrollX !== this.lastScrollX || currentScrollY !== this.lastScrollY) {
|
|
248
|
+
updateFixedMeshesScroll(this.renderer.fixedMeshes, currentScrollX, currentScrollY);
|
|
249
|
+
|
|
250
|
+
this.lastScrollX = currentScrollX;
|
|
251
|
+
this.lastScrollY = currentScrollY;
|
|
252
|
+
|
|
253
|
+
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
|
254
|
+
this.scrollTimer = window.setTimeout(() => {
|
|
255
|
+
this.pendingMask |= DIRTY_RECT;
|
|
256
|
+
this.isDomDirty = true;
|
|
257
|
+
this.scrollTimer = null;
|
|
258
|
+
}, 150);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this.pendingStyles.size > 0) {
|
|
262
|
+
animateMeshByData(this.registry, this.pendingStyles);
|
|
263
|
+
this.pendingStyles.clear();
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
this.renderer.render();
|
|
214
267
|
requestAnimationFrame(this.renderLoop);
|
|
215
268
|
};
|
|
269
|
+
|
|
270
|
+
// [ThinkPoint] change call back pattern when after
|
|
216
271
|
}
|
package/src/dom/Extractor.ts
CHANGED
|
@@ -16,16 +16,119 @@ import { FilterConfig } from "../types/config";
|
|
|
16
16
|
|
|
17
17
|
// Helper function: getTextNodeRect, isValidTextNode, isLeafTextElement, extractTextStyles
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
function extractTextLines(textNode: Text): { text: string; rect: { left: number; top: number; width: number; height: number } }[] {
|
|
20
|
+
const text = textNode.textContent || "";
|
|
21
|
+
const lines: { text: string; rect: { left: number; top: number; width: number; height: number } }[] = [];
|
|
22
|
+
|
|
23
|
+
let currentLineText = "";
|
|
24
|
+
let currentLineRect: { left: number; top: number; right: number; bottom: number } | null = null;
|
|
25
|
+
let currentTop = -1;
|
|
26
|
+
|
|
27
|
+
const processChunk = (chunkText: string, offset: number) => {
|
|
28
|
+
for (let i = 0; i < chunkText.length; i++) {
|
|
29
|
+
const char = chunkText[i];
|
|
30
|
+
const range = document.createRange();
|
|
31
|
+
range.setStart(textNode, offset + i);
|
|
32
|
+
range.setEnd(textNode, offset + i + 1);
|
|
33
|
+
const rect = range.getBoundingClientRect();
|
|
34
|
+
|
|
35
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
36
|
+
currentLineText += char;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (currentTop === -1 || Math.abs(rect.top - currentTop) > rect.height / 2) {
|
|
41
|
+
if (currentLineText && currentLineRect) {
|
|
42
|
+
lines.push({
|
|
43
|
+
text: currentLineText,
|
|
44
|
+
rect: {
|
|
45
|
+
left: currentLineRect.left,
|
|
46
|
+
top: currentLineRect.top,
|
|
47
|
+
width: currentLineRect.right - currentLineRect.left,
|
|
48
|
+
height: currentLineRect.bottom - currentLineRect.top,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
currentLineText = char;
|
|
53
|
+
currentLineRect = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
|
|
54
|
+
currentTop = rect.top;
|
|
55
|
+
} else {
|
|
56
|
+
currentLineText += char;
|
|
57
|
+
if (currentLineRect) {
|
|
58
|
+
currentLineRect.left = Math.min(currentLineRect.left, rect.left);
|
|
59
|
+
currentLineRect.top = Math.min(currentLineRect.top, rect.top);
|
|
60
|
+
currentLineRect.right = Math.max(currentLineRect.right, rect.right);
|
|
61
|
+
currentLineRect.bottom = Math.max(currentLineRect.bottom, rect.bottom);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
28
65
|
};
|
|
66
|
+
|
|
67
|
+
const tokens = text.match(/[^\s\-]+\-?|\-|\s+/g) || [];
|
|
68
|
+
let currentOffset = 0;
|
|
69
|
+
|
|
70
|
+
for (const token of tokens) {
|
|
71
|
+
const range = document.createRange();
|
|
72
|
+
range.setStart(textNode, currentOffset);
|
|
73
|
+
range.setEnd(textNode, currentOffset + token.length);
|
|
74
|
+
const rects = range.getClientRects();
|
|
75
|
+
|
|
76
|
+
if (rects.length > 1) {
|
|
77
|
+
// Token wraps across lines (e.g. word-break: break-all)
|
|
78
|
+
// Fallback to precise character-by-character iteration for this token
|
|
79
|
+
processChunk(token, currentOffset);
|
|
80
|
+
} else {
|
|
81
|
+
// Token is on a single line. Process it as a single block!
|
|
82
|
+
const rect = rects.length === 1 ? rects[0] : range.getBoundingClientRect();
|
|
83
|
+
|
|
84
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
85
|
+
currentLineText += token;
|
|
86
|
+
currentOffset += token.length;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (currentTop === -1 || Math.abs(rect.top - currentTop) > rect.height / 2) {
|
|
91
|
+
if (currentLineText && currentLineRect) {
|
|
92
|
+
lines.push({
|
|
93
|
+
text: currentLineText,
|
|
94
|
+
rect: {
|
|
95
|
+
left: currentLineRect.left,
|
|
96
|
+
top: currentLineRect.top,
|
|
97
|
+
width: currentLineRect.right - currentLineRect.left,
|
|
98
|
+
height: currentLineRect.bottom - currentLineRect.top,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
currentLineText = token;
|
|
103
|
+
currentLineRect = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
|
|
104
|
+
currentTop = rect.top;
|
|
105
|
+
} else {
|
|
106
|
+
currentLineText += token;
|
|
107
|
+
if (currentLineRect) {
|
|
108
|
+
currentLineRect.left = Math.min(currentLineRect.left, rect.left);
|
|
109
|
+
currentLineRect.top = Math.min(currentLineRect.top, rect.top);
|
|
110
|
+
currentLineRect.right = Math.max(currentLineRect.right, rect.right);
|
|
111
|
+
currentLineRect.bottom = Math.max(currentLineRect.bottom, rect.bottom);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
currentOffset += token.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (currentLineText && currentLineRect) {
|
|
120
|
+
lines.push({
|
|
121
|
+
text: currentLineText,
|
|
122
|
+
rect: {
|
|
123
|
+
left: currentLineRect.left,
|
|
124
|
+
top: currentLineRect.top,
|
|
125
|
+
width: currentLineRect.right - currentLineRect.left,
|
|
126
|
+
height: currentLineRect.bottom - currentLineRect.top,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.filter(line => line.text.trim().length > 0 && line.rect.width > 0 && line.rect.height > 0);
|
|
29
132
|
}
|
|
30
133
|
|
|
31
134
|
function extractTextStyles(computed: CSSStyleDeclaration): TextStyles {
|
|
@@ -40,6 +143,7 @@ function extractTextStyles(computed: CSSStyleDeclaration): TextStyles {
|
|
|
40
143
|
}
|
|
41
144
|
return {
|
|
42
145
|
font: `${computed.fontStyle} ${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`,
|
|
146
|
+
fontSize: computed.fontSize,
|
|
43
147
|
color: computed.color,
|
|
44
148
|
textAlign: (computed.textAlign as CanvasTextAlign) || "start",
|
|
45
149
|
textBaseline: "alphabetic",
|
|
@@ -64,47 +168,71 @@ export function extractSceneGraph(
|
|
|
64
168
|
const textNode = sourceNode as Text;
|
|
65
169
|
// empthy text check
|
|
66
170
|
if (!textNode.textContent || !textNode.textContent.trim()) return null;
|
|
67
|
-
const normalizedText = textNode.textContent.replace(/\s+/g, " ")
|
|
171
|
+
const normalizedText = textNode.textContent.replace(/\s+/g, " ");
|
|
68
172
|
if (normalizedText.length === 0) return null;
|
|
69
173
|
|
|
70
|
-
const
|
|
174
|
+
const textLines = extractTextLines(textNode);
|
|
71
175
|
|
|
72
|
-
|
|
73
|
-
if (rect.width === 0 || rect.height === 0) return null;
|
|
176
|
+
if (textLines.length === 0) return null;
|
|
74
177
|
|
|
75
178
|
// Cascading Styles
|
|
76
179
|
const parent = textNode.parentElement;
|
|
77
180
|
const computed = parent ? window.getComputedStyle(parent) : null;
|
|
78
181
|
if (!computed) return null;
|
|
79
182
|
|
|
80
|
-
//
|
|
183
|
+
// Calculate overall bounding box of the lines
|
|
184
|
+
const minX = Math.min(...textLines.map(l => l.rect.left));
|
|
185
|
+
const minY = Math.min(...textLines.map(l => l.rect.top));
|
|
186
|
+
const maxX = Math.max(...textLines.map(l => l.rect.left + l.rect.width));
|
|
187
|
+
const maxY = Math.max(...textLines.map(l => l.rect.top + l.rect.height));
|
|
188
|
+
|
|
189
|
+
// Create SceneNode for the text node
|
|
81
190
|
return {
|
|
82
191
|
id: Math.random().toString(36).substring(2, 9),
|
|
83
192
|
type: "TEXT",
|
|
84
193
|
element: textNode as unknown as HTMLElement,
|
|
85
194
|
rect: {
|
|
86
|
-
x:
|
|
87
|
-
y:
|
|
88
|
-
width:
|
|
89
|
-
height:
|
|
195
|
+
x: minX + window.scrollX,
|
|
196
|
+
y: minY + window.scrollY,
|
|
197
|
+
width: maxX - minX,
|
|
198
|
+
height: maxY - minY,
|
|
90
199
|
},
|
|
91
200
|
styles: {
|
|
92
201
|
backgroundColor: "transparent",
|
|
93
|
-
|
|
202
|
+
backgroundImage: "",
|
|
203
|
+
opacity:
|
|
204
|
+
parent && parent.dataset.mirageDom === "hide"
|
|
205
|
+
? 1
|
|
206
|
+
: parseFloat(computed.opacity),
|
|
94
207
|
zIndex: 0,
|
|
95
208
|
borderRadius: "0px",
|
|
96
209
|
borderColor: "transparent",
|
|
97
210
|
borderWidth: "0px",
|
|
211
|
+
isTraveler: false,
|
|
98
212
|
},
|
|
99
213
|
textContent: normalizedText,
|
|
214
|
+
textLines: textLines.map(l => ({
|
|
215
|
+
text: l.text.trim(),
|
|
216
|
+
rect: {
|
|
217
|
+
x: l.rect.left + window.scrollX,
|
|
218
|
+
y: l.rect.top + window.scrollY,
|
|
219
|
+
width: l.rect.width,
|
|
220
|
+
height: l.rect.height,
|
|
221
|
+
}
|
|
222
|
+
})),
|
|
100
223
|
textStyles: extractTextStyles(computed),
|
|
101
224
|
dirtyMask: initialMask,
|
|
102
225
|
visibility: inheritedFlow,
|
|
103
226
|
isTraveler: false,
|
|
227
|
+
isFixed: computed.position === "fixed",
|
|
104
228
|
children: [],
|
|
105
229
|
};
|
|
106
230
|
}
|
|
107
231
|
|
|
232
|
+
if (sourceNode.nodeType !== Node.ELEMENT_NODE) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
108
236
|
const element = sourceNode as HTMLElement;
|
|
109
237
|
// [[Filter]] data attribute based filtering
|
|
110
238
|
const filterData = element.dataset.mirageFilter;
|
|
@@ -197,13 +325,29 @@ export function extractSceneGraph(
|
|
|
197
325
|
}
|
|
198
326
|
|
|
199
327
|
const zIndex = parseInt(computed.zIndex);
|
|
328
|
+
// console.log(`${element.id}: ${computed.background}`);
|
|
329
|
+
// console.log(computed.backgroundImage);
|
|
330
|
+
let imageSrc: string | undefined;
|
|
331
|
+
if (element.tagName === "IMG") {
|
|
332
|
+
imageSrc = (element as HTMLImageElement).src;
|
|
333
|
+
} else if (computed.backgroundImage && computed.backgroundImage !== "none") {
|
|
334
|
+
const match = computed.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/);
|
|
335
|
+
if (match) {
|
|
336
|
+
imageSrc = match[1];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
200
340
|
const styles: BoxStyles = {
|
|
201
341
|
backgroundColor: computed.backgroundColor,
|
|
202
|
-
|
|
342
|
+
backgroundImage: computed.backgroundImage,
|
|
343
|
+
opacity:
|
|
344
|
+
element.dataset.mirageDom === "hide" ? 1 : parseFloat(computed.opacity),
|
|
203
345
|
zIndex: isNaN(zIndex) ? 0 : zIndex,
|
|
204
346
|
borderRadius: computed.borderRadius,
|
|
205
347
|
borderColor: computed.borderColor,
|
|
206
348
|
borderWidth: computed.borderWidth,
|
|
349
|
+
imageSrc,
|
|
350
|
+
isTraveler,
|
|
207
351
|
};
|
|
208
352
|
|
|
209
353
|
let textContent: string | undefined;
|
|
@@ -240,6 +384,7 @@ export function extractSceneGraph(
|
|
|
240
384
|
dirtyMask: initialMask,
|
|
241
385
|
visibility: visibleFlag,
|
|
242
386
|
isTraveler: isTraveler,
|
|
387
|
+
isFixed: computed.position === "fixed",
|
|
243
388
|
children,
|
|
244
389
|
shaderHooks,
|
|
245
390
|
};
|