@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.
@@ -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
+ }
@@ -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(this.target, config, mountContainer);
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
  }
@@ -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
- constructor(target: HTMLElement, renderer: Renderer, config: CoreConfig) {
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
- mutation.attributeName === "style" ||
70
- mutation.attributeName === "class"
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
  }
@@ -16,16 +16,119 @@ import { FilterConfig } from "../types/config";
16
16
 
17
17
  // Helper function: getTextNodeRect, isValidTextNode, isLeafTextElement, extractTextStyles
18
18
 
19
- function getTextNodeRect(textNode: Text) {
20
- const range = document.createRange();
21
- range.selectNodeContents(textNode);
22
- const rect = range.getBoundingClientRect();
23
- return {
24
- left: rect.left,
25
- top: rect.top,
26
- width: rect.width,
27
- height: rect.height,
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, " ").trim();
171
+ const normalizedText = textNode.textContent.replace(/\s+/g, " ");
68
172
  if (normalizedText.length === 0) return null;
69
173
 
70
- const rect = getTextNodeRect(textNode);
174
+ const textLines = extractTextLines(textNode);
71
175
 
72
- // size check
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
- // Create SceneNode for text node
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: rect.left + window.scrollX,
87
- y: rect.top + window.scrollY,
88
- width: rect.width,
89
- height: rect.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
- opacity: parseFloat(computed.opacity),
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
- opacity: parseFloat(computed.opacity),
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
  };