@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.
@@ -11,21 +11,124 @@ import {
11
11
  ALLOWED_FILTERS,
12
12
  } from "../types";
13
13
 
14
- import { BoxStyles, TextStyles } from "@mirage-engine/painter";
14
+ import { BoxStyles, TextStyles, ShaderHooks } from "@mirage-engine/painter";
15
15
  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;
@@ -173,6 +301,14 @@ export function extractSceneGraph(
173
301
  }
174
302
  }
175
303
 
304
+ const shaderData = element.dataset.mirageShader;
305
+ let shaderHooks: ShaderHooks | undefined;
306
+ if (shaderData) {
307
+ shaderHooks = JSON.parse(shaderData) as ShaderHooks;
308
+ // [TODO] object 형태로 shader hooks 받기
309
+ // [TODO] 변수형태로 저장해서 return scene node에 넣어주기
310
+ }
311
+
176
312
  const rect = element.getBoundingClientRect();
177
313
  const computed = window.getComputedStyle(element);
178
314
 
@@ -181,6 +317,7 @@ export function extractSceneGraph(
181
317
  return null;
182
318
  }
183
319
 
320
+ // [TODO] dataset 방식으로 변경
184
321
  let id = element.getAttribute("data-mid");
185
322
  if (!id) {
186
323
  id = Math.random().toString(36).substring(2, 11);
@@ -188,13 +325,29 @@ export function extractSceneGraph(
188
325
  }
189
326
 
190
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
+
191
340
  const styles: BoxStyles = {
192
341
  backgroundColor: computed.backgroundColor,
193
- opacity: parseFloat(computed.opacity),
342
+ backgroundImage: computed.backgroundImage,
343
+ opacity:
344
+ element.dataset.mirageDom === "hide" ? 1 : parseFloat(computed.opacity),
194
345
  zIndex: isNaN(zIndex) ? 0 : zIndex,
195
346
  borderRadius: computed.borderRadius,
196
347
  borderColor: computed.borderColor,
197
348
  borderWidth: computed.borderWidth,
349
+ imageSrc,
350
+ isTraveler,
198
351
  };
199
352
 
200
353
  let textContent: string | undefined;
@@ -231,6 +384,8 @@ export function extractSceneGraph(
231
384
  dirtyMask: initialMask,
232
385
  visibility: visibleFlag,
233
386
  isTraveler: isTraveler,
387
+ isFixed: computed.position === "fixed",
234
388
  children,
389
+ shaderHooks,
235
390
  };
236
391
  }
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module "*?raw" {
2
+ const content: string;
3
+ export default content;
4
+ }