@mirage-engine/core 0.2.1 → 0.3.0
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 +20 -0
- package/dist/mirage-engine.js +1443 -503
- package/dist/mirage-engine.umd.js +142 -72
- package/dist/src/animation/Animator.d.ts +6 -0
- package/dist/src/core/Engine.d.ts +5 -0
- package/dist/src/core/Syncer.d.ts +5 -16
- package/dist/src/dom/Extractor.d.ts +1 -2
- package/dist/src/renderer/Renderer.d.ts +14 -8
- 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/attributes.d.ts +63 -0
- package/dist/src/types/common.d.ts +9 -0
- package/dist/src/types/config.d.ts +2 -8
- package/dist/src/types/flags.d.ts +8 -7
- package/dist/src/types/index.d.ts +2 -0
- package/package.json +3 -2
- package/src/animation/Animator.ts +108 -0
- package/src/core/Engine.ts +69 -5
- package/src/core/Syncer.ts +49 -190
- package/src/dom/Extractor.ts +498 -64
- package/src/env.d.ts +4 -0
- package/src/renderer/Renderer.ts +430 -133
- package/src/store/MeshRegistry.ts +32 -0
- package/src/store/TextureLifecycleManager.ts +126 -0
- package/src/types/animate.ts +26 -0
- package/src/types/attributes.ts +57 -0
- package/src/types/common.ts +6 -0
- package/src/types/config.ts +5 -10
- package/src/types/flags.ts +20 -15
- package/src/types/index.ts +3 -1
package/src/dom/Extractor.ts
CHANGED
|
@@ -7,25 +7,171 @@ import {
|
|
|
7
7
|
SceneNode,
|
|
8
8
|
Visibility,
|
|
9
9
|
USER_LAYER,
|
|
10
|
-
|
|
10
|
+
SELECT_LAYER,
|
|
11
11
|
ALLOWED_FILTERS,
|
|
12
|
+
ATTR_DOM,
|
|
13
|
+
ATTR_FILTER,
|
|
14
|
+
ATTR_SELECT,
|
|
15
|
+
ATTR_TRAVEL,
|
|
16
|
+
ATTR_SHADER,
|
|
12
17
|
} from "../types";
|
|
13
18
|
|
|
14
19
|
import { BoxStyles, TextStyles, ShaderHooks } from "@mirage-engine/painter";
|
|
15
|
-
import { FilterConfig } from "../types/config";
|
|
16
20
|
|
|
17
21
|
// Helper function: getTextNodeRect, isValidTextNode, isLeafTextElement, extractTextStyles
|
|
18
22
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
width:
|
|
27
|
-
|
|
23
|
+
function extractTextLines(textNode: Text): {
|
|
24
|
+
text: string;
|
|
25
|
+
rect: { left: number; top: number; width: number; height: number };
|
|
26
|
+
}[] {
|
|
27
|
+
const text = textNode.textContent || "";
|
|
28
|
+
const lines: {
|
|
29
|
+
text: string;
|
|
30
|
+
rect: { left: number; top: number; width: number; height: number };
|
|
31
|
+
}[] = [];
|
|
32
|
+
|
|
33
|
+
let currentLineText = "";
|
|
34
|
+
let currentLineRect: {
|
|
35
|
+
left: number;
|
|
36
|
+
top: number;
|
|
37
|
+
right: number;
|
|
38
|
+
bottom: number;
|
|
39
|
+
} | null = null;
|
|
40
|
+
let currentTop = -1;
|
|
41
|
+
|
|
42
|
+
const processChunk = (chunkText: string, offset: number) => {
|
|
43
|
+
for (let i = 0; i < chunkText.length; i++) {
|
|
44
|
+
const char = chunkText[i];
|
|
45
|
+
const range = document.createRange();
|
|
46
|
+
range.setStart(textNode, offset + i);
|
|
47
|
+
range.setEnd(textNode, offset + i + 1);
|
|
48
|
+
const rect = range.getBoundingClientRect();
|
|
49
|
+
|
|
50
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
51
|
+
currentLineText += char;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
currentTop === -1 ||
|
|
57
|
+
Math.abs(rect.top - currentTop) > rect.height / 2
|
|
58
|
+
) {
|
|
59
|
+
if (currentLineText && currentLineRect) {
|
|
60
|
+
lines.push({
|
|
61
|
+
text: currentLineText,
|
|
62
|
+
rect: {
|
|
63
|
+
left: currentLineRect.left,
|
|
64
|
+
top: currentLineRect.top,
|
|
65
|
+
width: currentLineRect.right - currentLineRect.left,
|
|
66
|
+
height: currentLineRect.bottom - currentLineRect.top,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
currentLineText = char;
|
|
71
|
+
currentLineRect = {
|
|
72
|
+
left: rect.left,
|
|
73
|
+
top: rect.top,
|
|
74
|
+
right: rect.right,
|
|
75
|
+
bottom: rect.bottom,
|
|
76
|
+
};
|
|
77
|
+
currentTop = rect.top;
|
|
78
|
+
} else {
|
|
79
|
+
currentLineText += char;
|
|
80
|
+
if (currentLineRect) {
|
|
81
|
+
currentLineRect.left = Math.min(currentLineRect.left, rect.left);
|
|
82
|
+
currentLineRect.top = Math.min(currentLineRect.top, rect.top);
|
|
83
|
+
currentLineRect.right = Math.max(currentLineRect.right, rect.right);
|
|
84
|
+
currentLineRect.bottom = Math.max(
|
|
85
|
+
currentLineRect.bottom,
|
|
86
|
+
rect.bottom,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
28
91
|
};
|
|
92
|
+
|
|
93
|
+
const tokens = text.match(/[^\s\-]+\-?|\-|\s+/g) || [];
|
|
94
|
+
let currentOffset = 0;
|
|
95
|
+
|
|
96
|
+
for (const token of tokens) {
|
|
97
|
+
const range = document.createRange();
|
|
98
|
+
range.setStart(textNode, currentOffset);
|
|
99
|
+
range.setEnd(textNode, currentOffset + token.length);
|
|
100
|
+
const rects = range.getClientRects();
|
|
101
|
+
|
|
102
|
+
if (rects.length > 1) {
|
|
103
|
+
// Token wraps across lines (e.g. word-break: break-all)
|
|
104
|
+
// Fallback to precise character-by-character iteration for this token
|
|
105
|
+
processChunk(token, currentOffset);
|
|
106
|
+
} else {
|
|
107
|
+
// Token is on a single line. Process it as a single block!
|
|
108
|
+
const rect =
|
|
109
|
+
rects.length === 1 ? rects[0] : range.getBoundingClientRect();
|
|
110
|
+
|
|
111
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
112
|
+
currentLineText += token;
|
|
113
|
+
currentOffset += token.length;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
currentTop === -1 ||
|
|
119
|
+
Math.abs(rect.top - currentTop) > rect.height / 2
|
|
120
|
+
) {
|
|
121
|
+
if (currentLineText && currentLineRect) {
|
|
122
|
+
lines.push({
|
|
123
|
+
text: currentLineText,
|
|
124
|
+
rect: {
|
|
125
|
+
left: currentLineRect.left,
|
|
126
|
+
top: currentLineRect.top,
|
|
127
|
+
width: currentLineRect.right - currentLineRect.left,
|
|
128
|
+
height: currentLineRect.bottom - currentLineRect.top,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
currentLineText = token;
|
|
133
|
+
currentLineRect = {
|
|
134
|
+
left: rect.left,
|
|
135
|
+
top: rect.top,
|
|
136
|
+
right: rect.right,
|
|
137
|
+
bottom: rect.bottom,
|
|
138
|
+
};
|
|
139
|
+
currentTop = rect.top;
|
|
140
|
+
} else {
|
|
141
|
+
currentLineText += token;
|
|
142
|
+
if (currentLineRect) {
|
|
143
|
+
currentLineRect.left = Math.min(currentLineRect.left, rect.left);
|
|
144
|
+
currentLineRect.top = Math.min(currentLineRect.top, rect.top);
|
|
145
|
+
currentLineRect.right = Math.max(currentLineRect.right, rect.right);
|
|
146
|
+
currentLineRect.bottom = Math.max(
|
|
147
|
+
currentLineRect.bottom,
|
|
148
|
+
rect.bottom,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
currentOffset += token.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (currentLineText && currentLineRect) {
|
|
158
|
+
lines.push({
|
|
159
|
+
text: currentLineText,
|
|
160
|
+
rect: {
|
|
161
|
+
left: currentLineRect.left,
|
|
162
|
+
top: currentLineRect.top,
|
|
163
|
+
width: currentLineRect.right - currentLineRect.left,
|
|
164
|
+
height: currentLineRect.bottom - currentLineRect.top,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines.filter(
|
|
170
|
+
(line) =>
|
|
171
|
+
line.text.trim().length > 0 &&
|
|
172
|
+
line.rect.width > 0 &&
|
|
173
|
+
line.rect.height > 0,
|
|
174
|
+
);
|
|
29
175
|
}
|
|
30
176
|
|
|
31
177
|
function extractTextStyles(computed: CSSStyleDeclaration): TextStyles {
|
|
@@ -40,6 +186,7 @@ function extractTextStyles(computed: CSSStyleDeclaration): TextStyles {
|
|
|
40
186
|
}
|
|
41
187
|
return {
|
|
42
188
|
font: `${computed.fontStyle} ${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`,
|
|
189
|
+
fontSize: computed.fontSize,
|
|
43
190
|
color: computed.color,
|
|
44
191
|
textAlign: (computed.textAlign as CanvasTextAlign) || "start",
|
|
45
192
|
textBaseline: "alphabetic",
|
|
@@ -57,64 +204,124 @@ export function extractSceneGraph(
|
|
|
57
204
|
DIRTY_CONTENT |
|
|
58
205
|
DIRTY_STRUCTURE,
|
|
59
206
|
inheritedFlow: Visibility,
|
|
60
|
-
|
|
207
|
+
captureLayer: number = 1,
|
|
208
|
+
inheritedZIndex: number = 0,
|
|
209
|
+
qualityFactor: number = 2,
|
|
210
|
+
inheritedNativeLayer?: number,
|
|
211
|
+
inheritedNativeStyles?: any,
|
|
61
212
|
): SceneNode | null {
|
|
62
213
|
// Check text node
|
|
63
214
|
if (sourceNode.nodeType === Node.TEXT_NODE) {
|
|
64
215
|
const textNode = sourceNode as Text;
|
|
65
216
|
// empthy text check
|
|
66
217
|
if (!textNode.textContent || !textNode.textContent.trim()) return null;
|
|
67
|
-
const normalizedText = textNode.textContent.replace(/\s+/g, " ")
|
|
218
|
+
const normalizedText = textNode.textContent.replace(/\s+/g, " ");
|
|
68
219
|
if (normalizedText.length === 0) return null;
|
|
69
220
|
|
|
70
|
-
const
|
|
221
|
+
const textLines = extractTextLines(textNode);
|
|
71
222
|
|
|
72
|
-
|
|
73
|
-
if (rect.width === 0 || rect.height === 0) return null;
|
|
223
|
+
if (textLines.length === 0) return null;
|
|
74
224
|
|
|
75
225
|
// Cascading Styles
|
|
76
226
|
const parent = textNode.parentElement;
|
|
77
227
|
const computed = parent ? window.getComputedStyle(parent) : null;
|
|
78
228
|
if (!computed) return null;
|
|
79
229
|
|
|
80
|
-
//
|
|
230
|
+
// Calculate overall bounding box of the lines
|
|
231
|
+
const minX = Math.min(...textLines.map((l) => l.rect.left));
|
|
232
|
+
const minY = Math.min(...textLines.map((l) => l.rect.top));
|
|
233
|
+
const maxX = Math.max(...textLines.map((l) => l.rect.left + l.rect.width));
|
|
234
|
+
const maxY = Math.max(...textLines.map((l) => l.rect.top + l.rect.height));
|
|
235
|
+
|
|
236
|
+
// Create SceneNode for the text node
|
|
81
237
|
return {
|
|
82
238
|
id: Math.random().toString(36).substring(2, 9),
|
|
83
239
|
type: "TEXT",
|
|
84
240
|
element: textNode as unknown as HTMLElement,
|
|
85
241
|
rect: {
|
|
86
|
-
x:
|
|
87
|
-
y:
|
|
88
|
-
width:
|
|
89
|
-
height:
|
|
242
|
+
x: minX + window.scrollX,
|
|
243
|
+
y: minY + window.scrollY,
|
|
244
|
+
width: maxX - minX,
|
|
245
|
+
height: maxY - minY,
|
|
90
246
|
},
|
|
91
247
|
styles: {
|
|
92
248
|
backgroundColor: "transparent",
|
|
93
|
-
|
|
94
|
-
|
|
249
|
+
backgroundImage: "",
|
|
250
|
+
opacity:
|
|
251
|
+
parent && parent.dataset[ATTR_DOM.KEY] === ATTR_DOM.VALUES.HIDE
|
|
252
|
+
? 1
|
|
253
|
+
: parseFloat(computed.opacity),
|
|
254
|
+
zIndex:
|
|
255
|
+
(isNaN(parseInt(computed.zIndex)) ? 0 : parseInt(computed.zIndex)) +
|
|
256
|
+
inheritedZIndex,
|
|
95
257
|
borderRadius: "0px",
|
|
96
258
|
borderColor: "transparent",
|
|
97
259
|
borderWidth: "0px",
|
|
260
|
+
isTraveler: false,
|
|
98
261
|
},
|
|
99
262
|
textContent: normalizedText,
|
|
263
|
+
textLines: textLines.map((l) => ({
|
|
264
|
+
text: l.text.trim(),
|
|
265
|
+
rect: {
|
|
266
|
+
x: l.rect.left + window.scrollX,
|
|
267
|
+
y: l.rect.top + window.scrollY,
|
|
268
|
+
width: l.rect.width,
|
|
269
|
+
height: l.rect.height,
|
|
270
|
+
},
|
|
271
|
+
})),
|
|
100
272
|
textStyles: extractTextStyles(computed),
|
|
101
273
|
dirtyMask: initialMask,
|
|
102
274
|
visibility: inheritedFlow,
|
|
103
275
|
isTraveler: false,
|
|
276
|
+
captureLayer,
|
|
277
|
+
isFixed: computed.position === "fixed",
|
|
278
|
+
nativeLayer: inheritedNativeLayer,
|
|
279
|
+
nativeStyles: inheritedNativeStyles
|
|
280
|
+
? {
|
|
281
|
+
backgroundColor: "transparent",
|
|
282
|
+
backgroundImage: "",
|
|
283
|
+
opacity:
|
|
284
|
+
parent && parent.dataset[ATTR_DOM.KEY] === ATTR_DOM.VALUES.HIDE
|
|
285
|
+
? 1
|
|
286
|
+
: parseFloat(computed.opacity),
|
|
287
|
+
zIndex:
|
|
288
|
+
(isNaN(parseInt(computed.zIndex))
|
|
289
|
+
? 0
|
|
290
|
+
: parseInt(computed.zIndex)) + inheritedZIndex,
|
|
291
|
+
borderRadius: "0px",
|
|
292
|
+
borderColor: "transparent",
|
|
293
|
+
borderWidth: "0px",
|
|
294
|
+
isTraveler: false,
|
|
295
|
+
...extractTextStyles(computed),
|
|
296
|
+
...inheritedNativeStyles,
|
|
297
|
+
}
|
|
298
|
+
: undefined,
|
|
299
|
+
nativeRect: inheritedNativeStyles
|
|
300
|
+
? {
|
|
301
|
+
x: minX + window.scrollX,
|
|
302
|
+
y: minY + window.scrollY,
|
|
303
|
+
width: maxX - minX,
|
|
304
|
+
height: maxY - minY,
|
|
305
|
+
}
|
|
306
|
+
: undefined,
|
|
104
307
|
children: [],
|
|
105
308
|
};
|
|
106
309
|
}
|
|
107
310
|
|
|
311
|
+
if (sourceNode.nodeType !== Node.ELEMENT_NODE) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
108
315
|
const element = sourceNode as HTMLElement;
|
|
109
316
|
// [[Filter]] data attribute based filtering
|
|
110
|
-
const filterData = element.dataset.
|
|
317
|
+
const filterData = element.dataset[ATTR_FILTER.KEY];
|
|
111
318
|
let visibleFlow = inheritedFlow;
|
|
112
319
|
let visibleFlag = inheritedFlow;
|
|
113
320
|
if (filterData) {
|
|
114
321
|
const filterSet = new Set(filterData.split(/\s+/));
|
|
115
322
|
// error check
|
|
116
323
|
for (const token of filterSet) {
|
|
117
|
-
if (!ALLOWED_FILTERS.includes(token)) {
|
|
324
|
+
if (!ALLOWED_FILTERS.includes(token as any)) {
|
|
118
325
|
throw new Error(
|
|
119
326
|
`[MirageEngine] Invalid filter token: '${token}'. ` +
|
|
120
327
|
`Expected one of: 'include-tree', 'exclude-tree', 'include-self', 'exclude-self', 'end'.`,
|
|
@@ -122,58 +329,152 @@ export function extractSceneGraph(
|
|
|
122
329
|
}
|
|
123
330
|
}
|
|
124
331
|
|
|
125
|
-
if (filterSet.has(
|
|
332
|
+
if (filterSet.has(ATTR_FILTER.VALUES.END)) return null;
|
|
126
333
|
|
|
127
334
|
// error check
|
|
128
|
-
if (
|
|
335
|
+
if (
|
|
336
|
+
filterSet.has(ATTR_FILTER.VALUES.INCLUDE_TREE) &&
|
|
337
|
+
filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_TREE)
|
|
338
|
+
) {
|
|
129
339
|
throw new Error(
|
|
130
340
|
`[MirageEngine] Conflicting filters: 'include-tree' and 'exclude-tree' cannot be used together on the same element.`,
|
|
131
341
|
);
|
|
132
342
|
}
|
|
133
|
-
if (
|
|
343
|
+
if (
|
|
344
|
+
filterSet.has(ATTR_FILTER.VALUES.INCLUDE_SELF) &&
|
|
345
|
+
filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_SELF)
|
|
346
|
+
) {
|
|
134
347
|
throw new Error(
|
|
135
348
|
`[MirageEngine] Conflicting filters: 'include-self' and 'exclude-self' cannot be used together on the same element.`,
|
|
136
349
|
);
|
|
137
350
|
}
|
|
138
351
|
|
|
139
|
-
if (filterSet.has(
|
|
352
|
+
if (filterSet.has(ATTR_FILTER.VALUES.INCLUDE_TREE)) {
|
|
140
353
|
visibleFlow = (visibleFlow | USER_LAYER) as Visibility;
|
|
141
|
-
} else if (filterSet.has(
|
|
354
|
+
} else if (filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_TREE)) {
|
|
142
355
|
visibleFlow = (visibleFlow & ~USER_LAYER) as Visibility;
|
|
143
356
|
}
|
|
144
357
|
|
|
145
358
|
visibleFlag = visibleFlow;
|
|
146
359
|
|
|
147
|
-
if (filterSet.has(
|
|
360
|
+
if (filterSet.has(ATTR_FILTER.VALUES.INCLUDE_SELF)) {
|
|
148
361
|
visibleFlag = (visibleFlag | USER_LAYER) as Visibility;
|
|
149
|
-
} else if (filterSet.has(
|
|
362
|
+
} else if (filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_SELF)) {
|
|
150
363
|
visibleFlag = (visibleFlag & ~USER_LAYER) as Visibility;
|
|
151
364
|
}
|
|
152
365
|
}
|
|
153
366
|
|
|
154
|
-
// [[
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
367
|
+
// [[Select]] data attribute based selecting
|
|
368
|
+
const selectData = element.dataset[ATTR_SELECT.KEY];
|
|
369
|
+
if (selectData) {
|
|
370
|
+
const selectSet = new Set(selectData.split(/\s+/));
|
|
371
|
+
// error check
|
|
372
|
+
for (const token of selectSet) {
|
|
373
|
+
if (!ALLOWED_FILTERS.includes(token as any)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`[MirageEngine] Invalid select token: '${token}'. ` +
|
|
376
|
+
`Expected one of: 'include-tree', 'exclude-tree', 'include-self', 'exclude-self', 'end'.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (selectSet.has(ATTR_SELECT.VALUES.END)) return null;
|
|
162
382
|
|
|
163
|
-
|
|
383
|
+
if (
|
|
384
|
+
selectSet.has(ATTR_SELECT.VALUES.INCLUDE_TREE) &&
|
|
385
|
+
selectSet.has(ATTR_SELECT.VALUES.EXCLUDE_TREE)
|
|
386
|
+
) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`[MirageEngine] Conflicting selects: 'include-tree' and 'exclude-tree' cannot be used together on the same element.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
selectSet.has(ATTR_SELECT.VALUES.INCLUDE_SELF) &&
|
|
393
|
+
selectSet.has(ATTR_SELECT.VALUES.EXCLUDE_SELF)
|
|
394
|
+
) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`[MirageEngine] Conflicting selects: 'include-self' and 'exclude-self' cannot be used together on the same element.`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (selectSet.has(ATTR_SELECT.VALUES.INCLUDE_TREE)) {
|
|
401
|
+
visibleFlow = (visibleFlow | SELECT_LAYER) as Visibility;
|
|
402
|
+
} else if (selectSet.has(ATTR_SELECT.VALUES.EXCLUDE_TREE)) {
|
|
403
|
+
visibleFlow = (visibleFlow & ~SELECT_LAYER) as Visibility;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
visibleFlag = visibleFlow;
|
|
407
|
+
|
|
408
|
+
if (selectSet.has(ATTR_SELECT.VALUES.INCLUDE_SELF)) {
|
|
409
|
+
visibleFlag = (visibleFlag | SELECT_LAYER) as Visibility;
|
|
410
|
+
} else if (selectSet.has(ATTR_SELECT.VALUES.EXCLUDE_SELF)) {
|
|
411
|
+
visibleFlag = (visibleFlag & ~SELECT_LAYER) as Visibility;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
164
414
|
|
|
165
|
-
const travelData = element.dataset.
|
|
415
|
+
const travelData = element.dataset[ATTR_TRAVEL.KEY];
|
|
166
416
|
let isTraveler = false;
|
|
417
|
+
let nativeParsedStyles: any = inheritedNativeStyles
|
|
418
|
+
? { ...inheritedNativeStyles }
|
|
419
|
+
: {};
|
|
420
|
+
let nativeLayer: number | undefined = inheritedNativeLayer;
|
|
167
421
|
if (travelData) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
422
|
+
let explicitLayer = 1;
|
|
423
|
+
|
|
424
|
+
// '{' 부터 '}' 까지의 JSON 문자열 추출
|
|
425
|
+
const jsonStart = travelData.indexOf("{");
|
|
426
|
+
const jsonEnd = travelData.lastIndexOf("}");
|
|
427
|
+
|
|
428
|
+
let tokensPart = travelData;
|
|
429
|
+
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
|
|
430
|
+
tokensPart = travelData.substring(0, jsonStart).trim();
|
|
431
|
+
const jsonStr = travelData.substring(jsonStart, jsonEnd + 1);
|
|
432
|
+
try {
|
|
433
|
+
// 싱글 쿼트를 더블 쿼트로 변경하거나 키에 따옴표가 없는 객체를 지원하기 위해
|
|
434
|
+
// Function을 통한 안전한 객체 평가(또는 JSON.parse)를 사용
|
|
435
|
+
nativeParsedStyles = new Function("return " + jsonStr)();
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.warn(
|
|
438
|
+
`[MirageEngine] Failed to parse travel styles JSON: ${jsonStr}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const tokens = tokensPart.split(/\s+/);
|
|
444
|
+
|
|
445
|
+
let hasTravelToken = false;
|
|
446
|
+
|
|
447
|
+
// traveler 인 경우
|
|
448
|
+
if (tokens.includes(ATTR_TRAVEL.VALUES.TRAVELER)) {
|
|
172
449
|
isTraveler = true;
|
|
450
|
+
hasTravelToken = true;
|
|
451
|
+
const numToken = tokens.find((t) => !isNaN(parseInt(t, 10)));
|
|
452
|
+
if (numToken) {
|
|
453
|
+
explicitLayer = parseInt(numToken, 10);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// native 인 경우
|
|
457
|
+
else if (tokens.includes(ATTR_TRAVEL.VALUES.NATIVE)) {
|
|
458
|
+
isTraveler = false;
|
|
459
|
+
const numToken = tokens.find((t) => !isNaN(parseInt(t, 10)));
|
|
460
|
+
if (numToken) {
|
|
461
|
+
nativeLayer = parseInt(numToken, 10);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// traveler만 명시된 레이어에 맞게 captureLayer 컨텍스트를 업데이트해야 함 (계층적 여행)
|
|
466
|
+
if (hasTravelToken) {
|
|
467
|
+
const targetCaptureLayer = explicitLayer + 1;
|
|
468
|
+
if (targetCaptureLayer < captureLayer) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`[MirageEngine] Traveler layer (${explicitLayer}) cannot be smaller than inherited capture layer (${captureLayer - 1}).`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
captureLayer = Math.min(targetCaptureLayer, ATTR_TRAVEL.MAX_LAYERS + 1);
|
|
173
474
|
}
|
|
174
475
|
}
|
|
175
476
|
|
|
176
|
-
const shaderData = element.dataset.
|
|
477
|
+
const shaderData = element.dataset[ATTR_SHADER.KEY];
|
|
177
478
|
let shaderHooks: ShaderHooks | undefined;
|
|
178
479
|
if (shaderData) {
|
|
179
480
|
shaderHooks = JSON.parse(shaderData) as ShaderHooks;
|
|
@@ -196,33 +497,120 @@ export function extractSceneGraph(
|
|
|
196
497
|
element.setAttribute("data-mid", id);
|
|
197
498
|
}
|
|
198
499
|
|
|
199
|
-
const
|
|
200
|
-
const
|
|
500
|
+
const localZIndex = parseInt(computed.zIndex);
|
|
501
|
+
const effectiveZIndex =
|
|
502
|
+
(isNaN(localZIndex) ? 0 : localZIndex) + inheritedZIndex;
|
|
503
|
+
// console.log(`${element.id}: ${computed.background}`);
|
|
504
|
+
// console.log(computed.backgroundImage);
|
|
505
|
+
let imageSrc: string | undefined;
|
|
506
|
+
if (element.tagName === "IMG") {
|
|
507
|
+
imageSrc = (element as HTMLImageElement).src;
|
|
508
|
+
} else if (element.tagName.toLowerCase() === "svg") {
|
|
509
|
+
const clone = element.cloneNode(true) as SVGSVGElement;
|
|
510
|
+
|
|
511
|
+
const overrideColor = nativeParsedStyles?.color;
|
|
512
|
+
const overrideFill = nativeParsedStyles?.fill;
|
|
513
|
+
const overrideStroke = nativeParsedStyles?.stroke;
|
|
514
|
+
const overrideOpacity = nativeParsedStyles?.opacity;
|
|
515
|
+
|
|
516
|
+
const inlineSVGStyles = (orig: Element, cloned: Element) => {
|
|
517
|
+
const computed = window.getComputedStyle(orig);
|
|
518
|
+
const clonedHtml = cloned as HTMLElement;
|
|
519
|
+
|
|
520
|
+
const isCurrentColorFill = computed.fill === computed.color;
|
|
521
|
+
const isCurrentColorStroke = computed.stroke === computed.color;
|
|
522
|
+
|
|
523
|
+
const fill = overrideFill || (isCurrentColorFill ? overrideColor : undefined) || computed.fill;
|
|
524
|
+
if (fill && fill !== "none") clonedHtml.style.fill = fill;
|
|
525
|
+
|
|
526
|
+
const stroke = overrideStroke || (isCurrentColorStroke ? overrideColor : undefined) || computed.stroke;
|
|
527
|
+
if (stroke && stroke !== "none") clonedHtml.style.stroke = stroke;
|
|
528
|
+
|
|
529
|
+
if (computed.strokeWidth && computed.strokeWidth !== "0px")
|
|
530
|
+
clonedHtml.style.strokeWidth = computed.strokeWidth;
|
|
531
|
+
|
|
532
|
+
const color = overrideColor || computed.color;
|
|
533
|
+
if (color) clonedHtml.style.color = color;
|
|
534
|
+
|
|
535
|
+
const opacity = overrideOpacity || computed.opacity;
|
|
536
|
+
if (opacity && opacity !== "1") clonedHtml.style.opacity = opacity;
|
|
537
|
+
|
|
538
|
+
for (let i = 0; i < orig.children.length; i++) {
|
|
539
|
+
inlineSVGStyles(orig.children[i], cloned.children[i]);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
inlineSVGStyles(element, clone);
|
|
544
|
+
|
|
545
|
+
const svgRect = element.getBoundingClientRect();
|
|
546
|
+
const scale = window.devicePixelRatio * qualityFactor; // High-DPI 대응을 위한 해상도 스케일업
|
|
547
|
+
|
|
548
|
+
if (!clone.hasAttribute("viewBox")) {
|
|
549
|
+
clone.setAttribute("viewBox", `0 0 ${svgRect.width} ${svgRect.height}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
clone.setAttribute("width", (svgRect.width * scale).toString());
|
|
553
|
+
clone.setAttribute("height", (svgRect.height * scale).toString());
|
|
554
|
+
|
|
555
|
+
let svgString = new XMLSerializer().serializeToString(clone);
|
|
556
|
+
if (!svgString.includes("xmlns=")) {
|
|
557
|
+
svgString = svgString.replace(
|
|
558
|
+
"<svg",
|
|
559
|
+
'<svg xmlns="http://www.w3.org/2000/svg"',
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
imageSrc = `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;
|
|
564
|
+
} else if (computed.backgroundImage && computed.backgroundImage !== "none") {
|
|
565
|
+
const match = computed.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/);
|
|
566
|
+
if (match) {
|
|
567
|
+
imageSrc = match[1];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Base node uses pure DOM styles, unpolluted by travelerStyles
|
|
572
|
+
const baseStyles: BoxStyles = {
|
|
201
573
|
backgroundColor: computed.backgroundColor,
|
|
202
|
-
|
|
203
|
-
|
|
574
|
+
backgroundImage: computed.backgroundImage,
|
|
575
|
+
opacity:
|
|
576
|
+
element.dataset[ATTR_DOM.KEY] === ATTR_DOM.VALUES.HIDE
|
|
577
|
+
? 1
|
|
578
|
+
: parseFloat(computed.opacity),
|
|
579
|
+
zIndex: effectiveZIndex,
|
|
204
580
|
borderRadius: computed.borderRadius,
|
|
205
581
|
borderColor: computed.borderColor,
|
|
206
582
|
borderWidth: computed.borderWidth,
|
|
583
|
+
imageSrc,
|
|
584
|
+
isTraveler: isTraveler,
|
|
207
585
|
};
|
|
208
586
|
|
|
587
|
+
const styles: BoxStyles = baseStyles;
|
|
588
|
+
|
|
209
589
|
let textContent: string | undefined;
|
|
210
590
|
let textStyles: TextStyles | undefined;
|
|
211
591
|
const children: SceneNode[] = [];
|
|
212
592
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
593
|
+
if (element.tagName.toLowerCase() !== "svg") {
|
|
594
|
+
Array.from(element.childNodes).forEach((child) => {
|
|
595
|
+
const visibleFlowToPass =
|
|
596
|
+
child.nodeType === Node.TEXT_NODE ? visibleFlag : visibleFlow;
|
|
597
|
+
const childNode = extractSceneGraph(
|
|
598
|
+
child,
|
|
599
|
+
initialMask,
|
|
600
|
+
visibleFlowToPass,
|
|
601
|
+
captureLayer,
|
|
602
|
+
effectiveZIndex,
|
|
603
|
+
qualityFactor,
|
|
604
|
+
nativeLayer,
|
|
605
|
+
child.nodeType === Node.TEXT_NODE && Object.keys(nativeParsedStyles).length > 0
|
|
606
|
+
? nativeParsedStyles
|
|
607
|
+
: undefined,
|
|
608
|
+
);
|
|
609
|
+
if (childNode) {
|
|
610
|
+
children.push(childNode);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
226
614
|
|
|
227
615
|
return {
|
|
228
616
|
id,
|
|
@@ -240,6 +628,52 @@ export function extractSceneGraph(
|
|
|
240
628
|
dirtyMask: initialMask,
|
|
241
629
|
visibility: visibleFlag,
|
|
242
630
|
isTraveler: isTraveler,
|
|
631
|
+
captureLayer,
|
|
632
|
+
nativeLayer,
|
|
633
|
+
nativeStyles:
|
|
634
|
+
nativeLayer !== undefined
|
|
635
|
+
? {
|
|
636
|
+
...baseStyles,
|
|
637
|
+
backgroundColor:
|
|
638
|
+
nativeParsedStyles.backgroundColor ?? baseStyles.backgroundColor,
|
|
639
|
+
backgroundImage:
|
|
640
|
+
nativeParsedStyles.backgroundImage ?? baseStyles.backgroundImage,
|
|
641
|
+
opacity: nativeParsedStyles.opacity ?? baseStyles.opacity,
|
|
642
|
+
zIndex:
|
|
643
|
+
nativeParsedStyles.zIndex !== undefined
|
|
644
|
+
? nativeParsedStyles.zIndex + effectiveZIndex
|
|
645
|
+
: baseStyles.zIndex,
|
|
646
|
+
borderRadius:
|
|
647
|
+
nativeParsedStyles.borderRadius ?? baseStyles.borderRadius,
|
|
648
|
+
borderColor:
|
|
649
|
+
nativeParsedStyles.borderColor ?? baseStyles.borderColor,
|
|
650
|
+
borderWidth:
|
|
651
|
+
nativeParsedStyles.borderWidth ?? baseStyles.borderWidth,
|
|
652
|
+
isTraveler: baseStyles.isTraveler,
|
|
653
|
+
}
|
|
654
|
+
: undefined,
|
|
655
|
+
nativeRect:
|
|
656
|
+
nativeLayer !== undefined
|
|
657
|
+
? {
|
|
658
|
+
x:
|
|
659
|
+
nativeParsedStyles.x !== undefined
|
|
660
|
+
? parseFloat(nativeParsedStyles.x)
|
|
661
|
+
: rect.left + window.scrollX,
|
|
662
|
+
y:
|
|
663
|
+
nativeParsedStyles.y !== undefined
|
|
664
|
+
? parseFloat(nativeParsedStyles.y)
|
|
665
|
+
: rect.top + window.scrollY,
|
|
666
|
+
width:
|
|
667
|
+
nativeParsedStyles.width !== undefined
|
|
668
|
+
? parseFloat(nativeParsedStyles.width)
|
|
669
|
+
: rect.width,
|
|
670
|
+
height:
|
|
671
|
+
nativeParsedStyles.height !== undefined
|
|
672
|
+
? parseFloat(nativeParsedStyles.height)
|
|
673
|
+
: rect.height,
|
|
674
|
+
}
|
|
675
|
+
: undefined,
|
|
676
|
+
isFixed: computed.position === "fixed",
|
|
243
677
|
children,
|
|
244
678
|
shaderHooks,
|
|
245
679
|
};
|
package/src/env.d.ts
ADDED