@mirage-engine/core 0.2.2 → 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 +12 -0
- package/dist/mirage-engine.js +990 -639
- package/dist/mirage-engine.umd.js +16 -10
- package/dist/src/animation/Animator.d.ts +1 -6
- package/dist/src/core/Engine.d.ts +3 -0
- package/dist/src/core/Syncer.d.ts +2 -20
- package/dist/src/dom/Extractor.d.ts +1 -2
- package/dist/src/renderer/Renderer.d.ts +7 -5
- package/dist/src/types/attributes.d.ts +63 -0
- package/dist/src/types/common.d.ts +4 -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 -1
- package/package.json +3 -2
- package/src/animation/Animator.ts +2 -57
- package/src/core/Engine.ts +14 -2
- package/src/core/Syncer.ts +39 -235
- package/src/dom/Extractor.ts +357 -68
- package/src/renderer/Renderer.ts +283 -98
- package/src/store/TextureLifecycleManager.ts +24 -9
- package/src/types/attributes.ts +57 -0
- package/src/types/common.ts +4 -0
- package/src/types/config.ts +5 -10
- package/src/types/flags.ts +20 -15
- package/src/types/index.ts +2 -1
package/src/dom/Extractor.ts
CHANGED
|
@@ -7,21 +7,36 @@ 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 extractTextLines(textNode: Text): {
|
|
23
|
+
function extractTextLines(textNode: Text): {
|
|
24
|
+
text: string;
|
|
25
|
+
rect: { left: number; top: number; width: number; height: number };
|
|
26
|
+
}[] {
|
|
20
27
|
const text = textNode.textContent || "";
|
|
21
|
-
const lines: {
|
|
22
|
-
|
|
28
|
+
const lines: {
|
|
29
|
+
text: string;
|
|
30
|
+
rect: { left: number; top: number; width: number; height: number };
|
|
31
|
+
}[] = [];
|
|
32
|
+
|
|
23
33
|
let currentLineText = "";
|
|
24
|
-
let currentLineRect: {
|
|
34
|
+
let currentLineRect: {
|
|
35
|
+
left: number;
|
|
36
|
+
top: number;
|
|
37
|
+
right: number;
|
|
38
|
+
bottom: number;
|
|
39
|
+
} | null = null;
|
|
25
40
|
let currentTop = -1;
|
|
26
41
|
|
|
27
42
|
const processChunk = (chunkText: string, offset: number) => {
|
|
@@ -31,13 +46,16 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
31
46
|
range.setStart(textNode, offset + i);
|
|
32
47
|
range.setEnd(textNode, offset + i + 1);
|
|
33
48
|
const rect = range.getBoundingClientRect();
|
|
34
|
-
|
|
49
|
+
|
|
35
50
|
if (rect.width === 0 && rect.height === 0) {
|
|
36
51
|
currentLineText += char;
|
|
37
52
|
continue;
|
|
38
53
|
}
|
|
39
54
|
|
|
40
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
currentTop === -1 ||
|
|
57
|
+
Math.abs(rect.top - currentTop) > rect.height / 2
|
|
58
|
+
) {
|
|
41
59
|
if (currentLineText && currentLineRect) {
|
|
42
60
|
lines.push({
|
|
43
61
|
text: currentLineText,
|
|
@@ -50,7 +68,12 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
50
68
|
});
|
|
51
69
|
}
|
|
52
70
|
currentLineText = char;
|
|
53
|
-
currentLineRect = {
|
|
71
|
+
currentLineRect = {
|
|
72
|
+
left: rect.left,
|
|
73
|
+
top: rect.top,
|
|
74
|
+
right: rect.right,
|
|
75
|
+
bottom: rect.bottom,
|
|
76
|
+
};
|
|
54
77
|
currentTop = rect.top;
|
|
55
78
|
} else {
|
|
56
79
|
currentLineText += char;
|
|
@@ -58,7 +81,10 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
58
81
|
currentLineRect.left = Math.min(currentLineRect.left, rect.left);
|
|
59
82
|
currentLineRect.top = Math.min(currentLineRect.top, rect.top);
|
|
60
83
|
currentLineRect.right = Math.max(currentLineRect.right, rect.right);
|
|
61
|
-
currentLineRect.bottom = Math.max(
|
|
84
|
+
currentLineRect.bottom = Math.max(
|
|
85
|
+
currentLineRect.bottom,
|
|
86
|
+
rect.bottom,
|
|
87
|
+
);
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
90
|
}
|
|
@@ -79,15 +105,19 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
79
105
|
processChunk(token, currentOffset);
|
|
80
106
|
} else {
|
|
81
107
|
// Token is on a single line. Process it as a single block!
|
|
82
|
-
const rect =
|
|
83
|
-
|
|
108
|
+
const rect =
|
|
109
|
+
rects.length === 1 ? rects[0] : range.getBoundingClientRect();
|
|
110
|
+
|
|
84
111
|
if (rect.width === 0 && rect.height === 0) {
|
|
85
112
|
currentLineText += token;
|
|
86
113
|
currentOffset += token.length;
|
|
87
114
|
continue;
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
currentTop === -1 ||
|
|
119
|
+
Math.abs(rect.top - currentTop) > rect.height / 2
|
|
120
|
+
) {
|
|
91
121
|
if (currentLineText && currentLineRect) {
|
|
92
122
|
lines.push({
|
|
93
123
|
text: currentLineText,
|
|
@@ -100,7 +130,12 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
100
130
|
});
|
|
101
131
|
}
|
|
102
132
|
currentLineText = token;
|
|
103
|
-
currentLineRect = {
|
|
133
|
+
currentLineRect = {
|
|
134
|
+
left: rect.left,
|
|
135
|
+
top: rect.top,
|
|
136
|
+
right: rect.right,
|
|
137
|
+
bottom: rect.bottom,
|
|
138
|
+
};
|
|
104
139
|
currentTop = rect.top;
|
|
105
140
|
} else {
|
|
106
141
|
currentLineText += token;
|
|
@@ -108,11 +143,14 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
108
143
|
currentLineRect.left = Math.min(currentLineRect.left, rect.left);
|
|
109
144
|
currentLineRect.top = Math.min(currentLineRect.top, rect.top);
|
|
110
145
|
currentLineRect.right = Math.max(currentLineRect.right, rect.right);
|
|
111
|
-
currentLineRect.bottom = Math.max(
|
|
146
|
+
currentLineRect.bottom = Math.max(
|
|
147
|
+
currentLineRect.bottom,
|
|
148
|
+
rect.bottom,
|
|
149
|
+
);
|
|
112
150
|
}
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
|
-
|
|
153
|
+
|
|
116
154
|
currentOffset += token.length;
|
|
117
155
|
}
|
|
118
156
|
|
|
@@ -128,7 +166,12 @@ function extractTextLines(textNode: Text): { text: string; rect: { left: number;
|
|
|
128
166
|
});
|
|
129
167
|
}
|
|
130
168
|
|
|
131
|
-
return lines.filter(
|
|
169
|
+
return lines.filter(
|
|
170
|
+
(line) =>
|
|
171
|
+
line.text.trim().length > 0 &&
|
|
172
|
+
line.rect.width > 0 &&
|
|
173
|
+
line.rect.height > 0,
|
|
174
|
+
);
|
|
132
175
|
}
|
|
133
176
|
|
|
134
177
|
function extractTextStyles(computed: CSSStyleDeclaration): TextStyles {
|
|
@@ -161,7 +204,11 @@ export function extractSceneGraph(
|
|
|
161
204
|
DIRTY_CONTENT |
|
|
162
205
|
DIRTY_STRUCTURE,
|
|
163
206
|
inheritedFlow: Visibility,
|
|
164
|
-
|
|
207
|
+
captureLayer: number = 1,
|
|
208
|
+
inheritedZIndex: number = 0,
|
|
209
|
+
qualityFactor: number = 2,
|
|
210
|
+
inheritedNativeLayer?: number,
|
|
211
|
+
inheritedNativeStyles?: any,
|
|
165
212
|
): SceneNode | null {
|
|
166
213
|
// Check text node
|
|
167
214
|
if (sourceNode.nodeType === Node.TEXT_NODE) {
|
|
@@ -181,10 +228,10 @@ export function extractSceneGraph(
|
|
|
181
228
|
if (!computed) return null;
|
|
182
229
|
|
|
183
230
|
// 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));
|
|
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));
|
|
188
235
|
|
|
189
236
|
// Create SceneNode for the text node
|
|
190
237
|
return {
|
|
@@ -201,30 +248,62 @@ export function extractSceneGraph(
|
|
|
201
248
|
backgroundColor: "transparent",
|
|
202
249
|
backgroundImage: "",
|
|
203
250
|
opacity:
|
|
204
|
-
parent && parent.dataset.
|
|
251
|
+
parent && parent.dataset[ATTR_DOM.KEY] === ATTR_DOM.VALUES.HIDE
|
|
205
252
|
? 1
|
|
206
253
|
: parseFloat(computed.opacity),
|
|
207
|
-
zIndex:
|
|
254
|
+
zIndex:
|
|
255
|
+
(isNaN(parseInt(computed.zIndex)) ? 0 : parseInt(computed.zIndex)) +
|
|
256
|
+
inheritedZIndex,
|
|
208
257
|
borderRadius: "0px",
|
|
209
258
|
borderColor: "transparent",
|
|
210
259
|
borderWidth: "0px",
|
|
211
260
|
isTraveler: false,
|
|
212
261
|
},
|
|
213
262
|
textContent: normalizedText,
|
|
214
|
-
textLines: textLines.map(l => ({
|
|
263
|
+
textLines: textLines.map((l) => ({
|
|
215
264
|
text: l.text.trim(),
|
|
216
265
|
rect: {
|
|
217
266
|
x: l.rect.left + window.scrollX,
|
|
218
267
|
y: l.rect.top + window.scrollY,
|
|
219
268
|
width: l.rect.width,
|
|
220
269
|
height: l.rect.height,
|
|
221
|
-
}
|
|
270
|
+
},
|
|
222
271
|
})),
|
|
223
272
|
textStyles: extractTextStyles(computed),
|
|
224
273
|
dirtyMask: initialMask,
|
|
225
274
|
visibility: inheritedFlow,
|
|
226
275
|
isTraveler: false,
|
|
276
|
+
captureLayer,
|
|
227
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,
|
|
228
307
|
children: [],
|
|
229
308
|
};
|
|
230
309
|
}
|
|
@@ -235,14 +314,14 @@ export function extractSceneGraph(
|
|
|
235
314
|
|
|
236
315
|
const element = sourceNode as HTMLElement;
|
|
237
316
|
// [[Filter]] data attribute based filtering
|
|
238
|
-
const filterData = element.dataset.
|
|
317
|
+
const filterData = element.dataset[ATTR_FILTER.KEY];
|
|
239
318
|
let visibleFlow = inheritedFlow;
|
|
240
319
|
let visibleFlag = inheritedFlow;
|
|
241
320
|
if (filterData) {
|
|
242
321
|
const filterSet = new Set(filterData.split(/\s+/));
|
|
243
322
|
// error check
|
|
244
323
|
for (const token of filterSet) {
|
|
245
|
-
if (!ALLOWED_FILTERS.includes(token)) {
|
|
324
|
+
if (!ALLOWED_FILTERS.includes(token as any)) {
|
|
246
325
|
throw new Error(
|
|
247
326
|
`[MirageEngine] Invalid filter token: '${token}'. ` +
|
|
248
327
|
`Expected one of: 'include-tree', 'exclude-tree', 'include-self', 'exclude-self', 'end'.`,
|
|
@@ -250,58 +329,152 @@ export function extractSceneGraph(
|
|
|
250
329
|
}
|
|
251
330
|
}
|
|
252
331
|
|
|
253
|
-
if (filterSet.has(
|
|
332
|
+
if (filterSet.has(ATTR_FILTER.VALUES.END)) return null;
|
|
254
333
|
|
|
255
334
|
// error check
|
|
256
|
-
if (
|
|
335
|
+
if (
|
|
336
|
+
filterSet.has(ATTR_FILTER.VALUES.INCLUDE_TREE) &&
|
|
337
|
+
filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_TREE)
|
|
338
|
+
) {
|
|
257
339
|
throw new Error(
|
|
258
340
|
`[MirageEngine] Conflicting filters: 'include-tree' and 'exclude-tree' cannot be used together on the same element.`,
|
|
259
341
|
);
|
|
260
342
|
}
|
|
261
|
-
if (
|
|
343
|
+
if (
|
|
344
|
+
filterSet.has(ATTR_FILTER.VALUES.INCLUDE_SELF) &&
|
|
345
|
+
filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_SELF)
|
|
346
|
+
) {
|
|
262
347
|
throw new Error(
|
|
263
348
|
`[MirageEngine] Conflicting filters: 'include-self' and 'exclude-self' cannot be used together on the same element.`,
|
|
264
349
|
);
|
|
265
350
|
}
|
|
266
351
|
|
|
267
|
-
if (filterSet.has(
|
|
352
|
+
if (filterSet.has(ATTR_FILTER.VALUES.INCLUDE_TREE)) {
|
|
268
353
|
visibleFlow = (visibleFlow | USER_LAYER) as Visibility;
|
|
269
|
-
} else if (filterSet.has(
|
|
354
|
+
} else if (filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_TREE)) {
|
|
270
355
|
visibleFlow = (visibleFlow & ~USER_LAYER) as Visibility;
|
|
271
356
|
}
|
|
272
357
|
|
|
273
358
|
visibleFlag = visibleFlow;
|
|
274
359
|
|
|
275
|
-
if (filterSet.has(
|
|
360
|
+
if (filterSet.has(ATTR_FILTER.VALUES.INCLUDE_SELF)) {
|
|
276
361
|
visibleFlag = (visibleFlag | USER_LAYER) as Visibility;
|
|
277
|
-
} else if (filterSet.has(
|
|
362
|
+
} else if (filterSet.has(ATTR_FILTER.VALUES.EXCLUDE_SELF)) {
|
|
278
363
|
visibleFlag = (visibleFlag & ~USER_LAYER) as Visibility;
|
|
279
364
|
}
|
|
280
365
|
}
|
|
281
366
|
|
|
282
|
-
// [[
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
|
290
380
|
|
|
291
|
-
|
|
381
|
+
if (selectSet.has(ATTR_SELECT.VALUES.END)) return null;
|
|
292
382
|
|
|
293
|
-
|
|
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
|
+
}
|
|
414
|
+
|
|
415
|
+
const travelData = element.dataset[ATTR_TRAVEL.KEY];
|
|
294
416
|
let isTraveler = false;
|
|
417
|
+
let nativeParsedStyles: any = inheritedNativeStyles
|
|
418
|
+
? { ...inheritedNativeStyles }
|
|
419
|
+
: {};
|
|
420
|
+
let nativeLayer: number | undefined = inheritedNativeLayer;
|
|
295
421
|
if (travelData) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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)) {
|
|
300
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);
|
|
301
474
|
}
|
|
302
475
|
}
|
|
303
476
|
|
|
304
|
-
const shaderData = element.dataset.
|
|
477
|
+
const shaderData = element.dataset[ATTR_SHADER.KEY];
|
|
305
478
|
let shaderHooks: ShaderHooks | undefined;
|
|
306
479
|
if (shaderData) {
|
|
307
480
|
shaderHooks = JSON.parse(shaderData) as ShaderHooks;
|
|
@@ -324,12 +497,70 @@ export function extractSceneGraph(
|
|
|
324
497
|
element.setAttribute("data-mid", id);
|
|
325
498
|
}
|
|
326
499
|
|
|
327
|
-
const
|
|
500
|
+
const localZIndex = parseInt(computed.zIndex);
|
|
501
|
+
const effectiveZIndex =
|
|
502
|
+
(isNaN(localZIndex) ? 0 : localZIndex) + inheritedZIndex;
|
|
328
503
|
// console.log(`${element.id}: ${computed.background}`);
|
|
329
504
|
// console.log(computed.backgroundImage);
|
|
330
505
|
let imageSrc: string | undefined;
|
|
331
506
|
if (element.tagName === "IMG") {
|
|
332
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)}`;
|
|
333
564
|
} else if (computed.backgroundImage && computed.backgroundImage !== "none") {
|
|
334
565
|
const match = computed.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/);
|
|
335
566
|
if (match) {
|
|
@@ -337,36 +568,49 @@ export function extractSceneGraph(
|
|
|
337
568
|
}
|
|
338
569
|
}
|
|
339
570
|
|
|
340
|
-
|
|
571
|
+
// Base node uses pure DOM styles, unpolluted by travelerStyles
|
|
572
|
+
const baseStyles: BoxStyles = {
|
|
341
573
|
backgroundColor: computed.backgroundColor,
|
|
342
574
|
backgroundImage: computed.backgroundImage,
|
|
343
575
|
opacity:
|
|
344
|
-
element.dataset.
|
|
345
|
-
|
|
576
|
+
element.dataset[ATTR_DOM.KEY] === ATTR_DOM.VALUES.HIDE
|
|
577
|
+
? 1
|
|
578
|
+
: parseFloat(computed.opacity),
|
|
579
|
+
zIndex: effectiveZIndex,
|
|
346
580
|
borderRadius: computed.borderRadius,
|
|
347
581
|
borderColor: computed.borderColor,
|
|
348
582
|
borderWidth: computed.borderWidth,
|
|
349
583
|
imageSrc,
|
|
350
|
-
isTraveler,
|
|
584
|
+
isTraveler: isTraveler,
|
|
351
585
|
};
|
|
352
586
|
|
|
587
|
+
const styles: BoxStyles = baseStyles;
|
|
588
|
+
|
|
353
589
|
let textContent: string | undefined;
|
|
354
590
|
let textStyles: TextStyles | undefined;
|
|
355
591
|
const children: SceneNode[] = [];
|
|
356
592
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
614
|
|
|
371
615
|
return {
|
|
372
616
|
id,
|
|
@@ -384,6 +628,51 @@ export function extractSceneGraph(
|
|
|
384
628
|
dirtyMask: initialMask,
|
|
385
629
|
visibility: visibleFlag,
|
|
386
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,
|
|
387
676
|
isFixed: computed.position === "fixed",
|
|
388
677
|
children,
|
|
389
678
|
shaderHooks,
|