@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.
@@ -7,25 +7,171 @@ import {
7
7
  SceneNode,
8
8
  Visibility,
9
9
  USER_LAYER,
10
- SYSTEM_LAYER,
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 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,
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
- filterConfig?: FilterConfig,
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, " ").trim();
218
+ const normalizedText = textNode.textContent.replace(/\s+/g, " ");
68
219
  if (normalizedText.length === 0) return null;
69
220
 
70
- const rect = getTextNodeRect(textNode);
221
+ const textLines = extractTextLines(textNode);
71
222
 
72
- // size check
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
- // Create SceneNode for text node
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: rect.left + window.scrollX,
87
- y: rect.top + window.scrollY,
88
- width: rect.width,
89
- height: rect.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
- opacity: parseFloat(computed.opacity),
94
- zIndex: 0,
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.mirageFilter;
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("end")) return null;
332
+ if (filterSet.has(ATTR_FILTER.VALUES.END)) return null;
126
333
 
127
334
  // error check
128
- if (filterSet.has("include-tree") && filterSet.has("exclude-tree")) {
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 (filterSet.has("include-self") && filterSet.has("exclude-self")) {
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("include-tree")) {
352
+ if (filterSet.has(ATTR_FILTER.VALUES.INCLUDE_TREE)) {
140
353
  visibleFlow = (visibleFlow | USER_LAYER) as Visibility;
141
- } else if (filterSet.has("exclude-tree")) {
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("include-self")) {
360
+ if (filterSet.has(ATTR_FILTER.VALUES.INCLUDE_SELF)) {
148
361
  visibleFlag = (visibleFlag | USER_LAYER) as Visibility;
149
- } else if (filterSet.has("exclude-self")) {
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
- // [[filter]] class based filtering
155
- // [Filter] end
156
- // if (filterConfig && filterConfig.end && filterConfig.end.length > 0) {
157
- // const isEnd = filterConfig.end.some((cls) =>
158
- // element.classList.contains(cls),
159
- // );
160
- // if (isEnd) return null;
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
- visibleFlag = (visibleFlag | (inheritedFlow & SYSTEM_LAYER)) as Visibility;
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.mirageTravel;
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
- const travelSet = new Set(travelData.split(/\s+/));
169
- if (travelSet.has("traveler")) {
170
- visibleFlag = (visibleFlag & ~SYSTEM_LAYER) as Visibility;
171
- visibleFlow = (visibleFlow & ~SYSTEM_LAYER) as Visibility;
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.mirageShader;
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 zIndex = parseInt(computed.zIndex);
200
- const styles: BoxStyles = {
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
- opacity: parseFloat(computed.opacity),
203
- zIndex: isNaN(zIndex) ? 0 : zIndex,
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
- Array.from(element.childNodes).forEach((child) => {
214
- const visibleFlowToPass =
215
- child.nodeType === Node.TEXT_NODE ? visibleFlag : visibleFlow;
216
- const childNode = extractSceneGraph(
217
- child,
218
- initialMask,
219
- visibleFlowToPass,
220
- filterConfig,
221
- );
222
- if (childNode) {
223
- children.push(childNode);
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
@@ -0,0 +1,4 @@
1
+ declare module "*?raw" {
2
+ const content: string;
3
+ export default content;
4
+ }