mirage-engine 0.2.5 → 0.2.7

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/README.md CHANGED
@@ -1,26 +1,57 @@
1
+
2
+
3
+ <p align="center">
4
+ <img src="../../.github/assets/mirage-engine.png" width="300px">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/mirage-engine"><img src="https://img.shields.io/npm/v/mirage-engine.svg?color=black"></a>
9
+ <a href="https://www.npmjs.org/package/mirage-engine"><img src="https://img.shields.io/npm/dm/mirage-engine.svg?color=black"></a>
10
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?color=black"></a>
11
+ <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-Ready-blue.svg?color=black"></a>
12
+ </p>
13
+
1
14
  # MirageEngine
2
15
 
3
16
  > **An engine that mirrors HTML DOM elements to a WebGL scene in real-time.**
4
17
 
5
- [![npm version](https://img.shields.io/npm/v/mirage-engine.svg?style=flat-square)](https://www.npmjs.com/package/mirage-engine)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
8
-
9
18
  MirageEngine directly mirrors HTML DOM elements to WebGL objects. It observes DOM mutations and synchronizes position, style, and content in real-time, allowing standard HTML elements to exist within a WebGL context.
10
19
 
11
20
  ## Installation
12
21
 
13
22
  ```bash
14
- npm install mirage-engine
23
+ npm install mirage-engine three
15
24
  ```
16
25
 
17
26
  ## Usage
18
27
 
28
+ ### Use simple
29
+
19
30
  ```ts
20
- import { Mirage } from 'mirage-engine';
31
+ import { Mirage } from "mirage-engine";
32
+
33
+ const target = document.querySelector("#target") as HTMLElement;
34
+
35
+ const mirage = new Mirage(target);
36
+
37
+ mirage.start();
38
+ ```
39
+
40
+ ### Use option
41
+
42
+ ```ts
43
+ import { Mirage } from "mirage-engine";
44
+
45
+ const target = document.querySelector("#target") as HTMLElement;
46
+ const container = document.querySelector("#container") as HTMLElement;
47
+
48
+ const mirage = new Mirage(target, {
49
+ textQuality: "low", //default is "medium" (== 2) ("low" | "medium" | "high" | number;)
50
+ mode: "duplicate", //default is "overaly" ("overlay" | "duplicate")
51
+ container: container, //The container option is only available in "duplicate" mode.
52
+ });
21
53
 
22
- const engine = new Mirage("#target");
23
- engine.start();
54
+ mirage.start();
24
55
  ```
25
56
 
26
- **License | MIT © dltldn333**
57
+ **License | MIT © dltldn333**
@@ -1,253 +1,413 @@
1
- var v = Object.defineProperty;
2
- var D = (n, e, t) => e in n ? v(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t;
3
- var o = (n, e, t) => (D(n, typeof e != "symbol" ? e + "" : e, t), t);
4
- import * as m from "three";
5
- const T = 0, y = 1, x = 2, M = 4, S = 8, C = 16;
6
- function F(n, e, t) {
7
- const i = e.split(`
8
- `), r = [];
9
- return i.forEach((s) => {
10
- const g = s.split(" ");
11
- let l = g[0];
12
- for (let p = 1; p < g.length; p++) {
13
- const a = g[p];
14
- n.measureText(l + " " + a).width < t ? l += " " + a : (r.push(l), l = a);
1
+ var B = Object.defineProperty;
2
+ var z = (r, t, e) => t in r ? B(r, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[t] = e;
3
+ var o = (r, t, e) => (z(r, typeof t != "symbol" ? t + "" : t, e), e);
4
+ import * as h from "three";
5
+ const v = 0, R = 1, b = 2, D = 4, C = 8, E = 16;
6
+ function F(r, t, e) {
7
+ const i = t.split(`
8
+ `), s = [];
9
+ return i.forEach((n) => {
10
+ const a = n.split(" ");
11
+ let c = a[0];
12
+ for (let u = 1; u < a.length; u++) {
13
+ const l = a[u];
14
+ r.measureText(c + " " + l).width < e ? c += " " + l : (s.push(c), c = l);
15
15
  }
16
- r.push(l);
17
- }), r;
16
+ s.push(c);
17
+ }), s;
18
18
  }
19
- function N(n, e, t, i) {
20
- const r = document.createElement("canvas"), s = r.getContext("2d");
21
- if (!s)
19
+ function S(r, t, e, i, s = 2) {
20
+ const n = document.createElement("canvas"), a = n.getContext("2d");
21
+ if (!a)
22
22
  throw new Error("[Mirage] Failed to create canvas context");
23
- const g = 2, l = (window.devicePixelRatio || 2) * g;
24
- r.width = t * l, r.height = i * l, s.scale(l, l), s.font = e.font, s.fillStyle = e.color, s.textBaseline = "top", s.globalAlpha = 1;
25
- const p = F(s, n, t), a = e.lineHeight;
26
- p.forEach((h, u) => {
27
- const f = u * a + 2;
28
- let d = 0;
29
- e.textAlign === "center" ? d = t / 2 : e.textAlign === "right" && (d = t), s.textAlign = e.textAlign, s.fillText(h, d, f);
23
+ const u = (window.devicePixelRatio || 1) * s;
24
+ n.width = e * u, n.height = i * u, a.scale(u, u), a.font = t.font, a.fillStyle = t.color, a.textBaseline = "top", a.globalAlpha = 1;
25
+ const l = F(a, r, e), p = t.lineHeight;
26
+ l.forEach((m, g) => {
27
+ const y = g * p + 2;
28
+ let f = 0;
29
+ t.textAlign === "center" ? f = e / 2 : t.textAlign === "right" && (f = e), a.textAlign = t.textAlign, a.fillText(m, f, y);
30
30
  });
31
- const c = new m.CanvasTexture(r);
32
- return c.colorSpace = m.SRGBColorSpace, c.minFilter = m.LinearFilter, c.magFilter = m.LinearFilter, c.needsUpdate = !0, c;
31
+ const d = new h.CanvasTexture(n);
32
+ return d.colorSpace = h.SRGBColorSpace, d.minFilter = h.LinearFilter, d.magFilter = h.LinearFilter, d.needsUpdate = !0, d;
33
33
  }
34
- class L {
35
- constructor() {
34
+ function w(r) {
35
+ return typeof r == "number" ? r : parseFloat(r) || 0;
36
+ }
37
+ const O = `
38
+ varying vec2 vUv;
39
+ void main() {
40
+ vUv = uv;
41
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
42
+ }
43
+ `, A = `
44
+ varying vec2 vUv;
45
+
46
+ uniform vec2 uSize;
47
+ uniform float uRadius;
48
+ uniform float uBorderWidth;
49
+ uniform vec3 uColor;
50
+ uniform vec3 uBorderColor;
51
+ uniform float uOpacity;
52
+ uniform float uBgOpacity;
53
+
54
+ // SDF 박스 함수
55
+ float sdRoundedBox(vec2 p, vec2 b, float r) {
56
+ vec2 q = abs(p) - b + r;
57
+ return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
58
+ }
59
+
60
+ void main() {
61
+ vec2 p = (vUv - 0.5) * uSize;
62
+ vec2 halfSize = uSize * 0.5;
63
+
64
+ float d = sdRoundedBox(p, halfSize, uRadius);
65
+
66
+ float smoothEdge = 1.0;
67
+
68
+ float fillAlpha = 1.0 - smoothstep(-uBorderWidth - smoothEdge, -uBorderWidth, d);
69
+
70
+ float borderAlpha = 0.0;
71
+
72
+ if (uBorderWidth > 0.01) {
73
+ borderAlpha = (1.0 - smoothstep(0.0, smoothEdge, d)) - fillAlpha;
74
+ }
75
+
76
+ vec3 color = uColor;
77
+ float totalAlpha = borderAlpha + fillAlpha;
78
+
79
+ if (totalAlpha > 0.001) {
80
+ color = mix(uColor, uBorderColor, borderAlpha / totalAlpha);
81
+ }
82
+
83
+ float shapeAlpha = borderAlpha + (fillAlpha * uBgOpacity);
84
+ float finalOpacity = shapeAlpha * uOpacity;
85
+
86
+ if (finalOpacity < 0.001) discard;
87
+
88
+ gl_FragColor = vec4(color, finalOpacity);
89
+ }
90
+ `;
91
+ function x(r) {
92
+ if (!r || r === "transparent")
93
+ return { color: new h.Color(16777215), alpha: 0 };
94
+ const t = r.match(
95
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
96
+ );
97
+ if (t) {
98
+ const e = parseInt(t[1], 10), i = parseInt(t[2], 10), s = parseInt(t[3], 10), n = t[4] !== void 0 ? parseFloat(t[4]) : 1;
99
+ return { color: new h.Color(`rgb(${e}, ${i}, ${s})`), alpha: n };
100
+ }
101
+ return { color: new h.Color(r), alpha: 1 };
102
+ }
103
+ function I(r, t, e) {
104
+ const i = x(r.backgroundColor), s = x(r.borderColor), n = {
105
+ uSize: { value: new h.Vector2(t, e) },
106
+ uRadius: { value: w(r.borderRadius) },
107
+ uBorderWidth: { value: w(r.borderWidth) },
108
+ uColor: { value: i.color },
109
+ uBorderColor: { value: s.color },
110
+ uOpacity: { value: r.opacity ?? 1 },
111
+ uBgOpacity: { value: i.alpha }
112
+ };
113
+ return new h.ShaderMaterial({
114
+ uniforms: n,
115
+ vertexShader: O,
116
+ fragmentShader: A,
117
+ transparent: !0,
118
+ side: h.FrontSide
119
+ // for better performance
120
+ });
121
+ }
122
+ function L(r, t, e, i) {
123
+ const s = x(t.backgroundColor), n = x(t.borderColor);
124
+ r.uniforms.uSize.value.set(e, i), r.uniforms.uRadius.value = w(t.borderRadius), r.uniforms.uBorderWidth.value = w(t.borderWidth), r.uniforms.uColor.value.copy(s.color), r.uniforms.uBorderColor.value.copy(n.color), r.uniforms.uOpacity.value = t.opacity ?? 1, r.uniforms.uBgOpacity.value = s.alpha;
125
+ }
126
+ const T = {
127
+ create(r, t, e, i, s, n = 2) {
128
+ if (r === "BOX")
129
+ return I(t, i, s);
130
+ if (r === "TEXT") {
131
+ const a = S(
132
+ e || "",
133
+ t,
134
+ i,
135
+ s,
136
+ n
137
+ );
138
+ return new h.MeshBasicMaterial({
139
+ map: a,
140
+ transparent: !0,
141
+ side: h.FrontSide,
142
+ color: 16777215
143
+ });
144
+ }
145
+ return new h.MeshBasicMaterial({ visible: !1 });
146
+ },
147
+ update(r, t, e, i, s, n, a = 2) {
148
+ if (t === "BOX")
149
+ L(
150
+ r,
151
+ e,
152
+ s,
153
+ n
154
+ );
155
+ else if (t === "TEXT") {
156
+ const c = r;
157
+ c.map && c.map.dispose();
158
+ const u = S(
159
+ i || "",
160
+ e,
161
+ s,
162
+ n,
163
+ a
164
+ );
165
+ c.map = u, c.needsUpdate = !0;
166
+ }
167
+ }
168
+ };
169
+ class X {
170
+ constructor(t, e, i) {
36
171
  o(this, "canvas");
37
172
  o(this, "scene");
38
173
  o(this, "camera");
39
174
  o(this, "renderer");
40
175
  o(this, "renderOrder", 0);
176
+ o(this, "textQualityFactor", 2);
177
+ o(this, "mode", "overlay");
178
+ o(this, "customZIndex", "9999");
179
+ o(this, "target");
180
+ o(this, "mountContainer");
181
+ o(this, "targetRect");
41
182
  o(this, "meshMap", /* @__PURE__ */ new Map());
42
- this.canvas = document.createElement("canvas"), this.scene = new m.Scene();
43
- const e = window.innerWidth, t = window.innerHeight;
44
- this.camera = new m.OrthographicCamera(
45
- e / -2,
46
- e / 2,
47
- t / 2,
48
- t / -2,
183
+ var a;
184
+ this.target = t, this.mountContainer = i, this.mode = e.mode ?? "overlay", (a = e.style) != null && a.zIndex && (this.customZIndex = e.style.zIndex), this.canvas = document.createElement("canvas"), this.scene = new h.Scene(), this.targetRect = this.target.getBoundingClientRect();
185
+ const s = this.targetRect.width, n = this.targetRect.height;
186
+ this.camera = new h.OrthographicCamera(
187
+ s / -2,
188
+ s / 2,
189
+ n / 2,
190
+ n / -2,
49
191
  1,
50
192
  1e3
51
- ), this.camera.position.z = 100, this.renderer = new m.WebGLRenderer({
193
+ ), this.camera.position.z = 100, this.renderer = new h.WebGLRenderer({
52
194
  canvas: this.canvas,
53
- alpha: !0
54
- }), this.renderer.setPixelRatio(window.devicePixelRatio), this.renderer.setSize(e, t);
195
+ alpha: !0,
196
+ antialias: !0
197
+ }), this.renderer.setPixelRatio(window.devicePixelRatio), this.renderer.setSize(s, n), this.applyTextQuality(e.textQuality ?? "medium");
198
+ }
199
+ applyTextQuality(t) {
200
+ if (typeof t == "number") {
201
+ this.textQualityFactor = Math.max(0.1, t);
202
+ return;
203
+ }
204
+ switch (t) {
205
+ case "low":
206
+ this.textQualityFactor = 1;
207
+ break;
208
+ case "high":
209
+ this.textQualityFactor = 4;
210
+ break;
211
+ case "medium":
212
+ default:
213
+ this.textQualityFactor = 2;
214
+ break;
215
+ }
55
216
  }
56
- mount(e) {
57
- e.appendChild(this.canvas);
217
+ mount() {
218
+ this.mountContainer.appendChild(this.canvas), this.canvas.style.zIndex = this.customZIndex, this.canvas.style.pointerEvents = this.mode === "overlay" ? "none" : "auto", this.updateCanvasLayout();
219
+ }
220
+ updateCanvasLayout() {
221
+ this.canvas.style.width = `${this.targetRect.width}px`, this.canvas.style.height = `${this.targetRect.height}px`, this.mode === "duplicate" ? (this.canvas.style.position = "", this.canvas.style.top = "", this.canvas.style.left = "", this.canvas.style.display = "block") : (this.canvas.style.position = "absolute", this.canvas.style.top = `${this.target.offsetTop}px`, this.canvas.style.left = `${this.target.offsetLeft}px`, this.canvas.style.display = "block");
58
222
  }
59
223
  dispose() {
60
- try {
61
- this.renderer.dispose();
62
- } catch {
63
- }
64
- this.canvas.parentElement && this.canvas.parentElement.removeChild(this.canvas);
224
+ this.renderer.dispose(), this.canvas.remove();
65
225
  }
66
- setSize(e, t) {
67
- this.renderer.setSize(e, t), this.camera.left = e / -2, this.camera.right = e / 2, this.camera.top = t / 2, this.camera.bottom = t / -2, this.camera.updateProjectionMatrix();
226
+ setSize(t, e) {
227
+ this.renderer.setSize(t, e), this.camera.left = t / -2, this.camera.right = t / 2, this.camera.top = e / 2, this.camera.bottom = e / -2, this.camera.updateProjectionMatrix();
68
228
  }
69
- syncScene(e) {
70
- this.renderOrder = 0;
71
- const t = /* @__PURE__ */ new Set();
72
- this.reconcileNode(e, t);
73
- for (const [i, r] of this.meshMap.entries())
74
- t.has(i) || (this.scene.remove(r), r.geometry.dispose(), r.material instanceof m.Material && r.material.dispose(), this.meshMap.delete(i));
229
+ syncScene(t) {
230
+ const e = this.target.getBoundingClientRect(), i = Math.abs(e.width - this.targetRect.width) > 0.1 || Math.abs(e.height - this.targetRect.height) > 0.1, s = this.mode === "overlay" && (Math.abs(e.top - this.targetRect.top) > 0.1 || Math.abs(e.left - this.targetRect.left) > 0.1);
231
+ i ? (this.targetRect = e, this.renderer.setSize(this.targetRect.width, this.targetRect.height), this.camera.left = this.targetRect.width / -2, this.camera.right = this.targetRect.width / 2, this.camera.top = this.targetRect.height / 2, this.camera.bottom = this.targetRect.height / -2, this.camera.updateProjectionMatrix(), this.updateCanvasLayout()) : s ? (this.targetRect = e, this.updateCanvasLayout()) : this.targetRect = e, this.renderOrder = 0;
232
+ const n = /* @__PURE__ */ new Set();
233
+ this.reconcileNode(t, n);
234
+ for (const [a, c] of this.meshMap.entries())
235
+ n.has(a) || (this.scene.remove(c), c.geometry.dispose(), c.material instanceof h.Material && c.material.dispose(), this.meshMap.delete(a));
75
236
  }
76
- reconcileNode(e, t) {
77
- t.add(e.element);
78
- let i = this.meshMap.get(e.element);
237
+ reconcileNode(t, e) {
238
+ e.add(t.element);
239
+ let i = this.meshMap.get(t.element);
79
240
  if (!i) {
80
- const r = new m.PlaneGeometry(1, 1), s = new m.MeshBasicMaterial({ transparent: !0 });
81
- i = new m.Mesh(r, s), e.type === "TEXT" && (i.name = "BG_MESH"), this.scene.add(i), this.meshMap.set(e.element, i);
241
+ const s = new h.PlaneGeometry(1, 1), n = T.create(
242
+ "BOX",
243
+ t.styles,
244
+ "",
245
+ t.rect.width,
246
+ t.rect.height
247
+ );
248
+ i = new h.Mesh(s, n), t.type === "TEXT" && (i.name = "BG_MESH"), this.scene.add(i), this.meshMap.set(t.element, i);
82
249
  }
83
- if (i.userData.domRect = e.rect, this.updateMeshProperties(i, e), e.type === "BOX")
84
- for (const r of e.children)
85
- this.reconcileNode(r, t);
250
+ if (i.userData.domRect = t.rect, this.updateMeshProperties(i, t), t.type === "BOX")
251
+ for (const s of t.children)
252
+ this.reconcileNode(s, e);
86
253
  else
87
- e.type === "TEXT" && this.reconcileTextChild(i, e);
254
+ t.type === "TEXT" && this.reconcileTextChild(i, t);
88
255
  }
89
- reconcileTextChild(e, t) {
90
- var l, p;
91
- let i = e.children.find(
92
- (a) => a.name === "TEXT_CHILD"
256
+ reconcileTextChild(t, e) {
257
+ var c, u;
258
+ let i = t.children.find(
259
+ (l) => l.name === "TEXT_CHILD"
93
260
  );
94
- const r = JSON.stringify(t.textStyles), s = (l = i == null ? void 0 : i.userData) == null ? void 0 : l.styleHash;
95
- if (!i || t.dirtyMask & C || r !== s) {
96
- i && ((p = i.material.map) == null || p.dispose(), i.geometry.dispose(), e.remove(i));
97
- const a = N(
98
- t.textContent || "",
99
- t.textStyles,
100
- t.rect.width,
101
- t.rect.height
102
- ), c = new m.PlaneGeometry(1, 1), h = new m.MeshBasicMaterial({
103
- map: a,
104
- transparent: !0,
105
- side: m.FrontSide,
106
- color: 16777215
107
- });
108
- i = new m.Mesh(c, h), i.name = "TEXT_CHILD", i.userData = { styleHash: r }, e.add(i);
261
+ const s = JSON.stringify(e.textStyles), n = (c = i == null ? void 0 : i.userData) == null ? void 0 : c.styleHash;
262
+ if (!i || e.dirtyMask & E || s !== n) {
263
+ i && ((u = i.material.map) == null || u.dispose(), i.geometry.dispose(), t.remove(i));
264
+ const l = T.create(
265
+ "TEXT",
266
+ e.textStyles,
267
+ e.textContent || "",
268
+ e.rect.width,
269
+ e.rect.height,
270
+ this.textQualityFactor
271
+ ), p = new h.PlaneGeometry(1, 1);
272
+ i = new h.Mesh(p, l), i.name = "TEXT_CHILD", i.userData = { styleHash: s }, i.position.z = 5e-3, t.add(i);
109
273
  }
110
274
  if (i) {
111
- const a = e.userData.domRect, c = a.x + a.width / 2, h = a.y + a.height / 2, u = t.rect.x + t.rect.width / 2, f = t.rect.y + t.rect.height / 2, d = u - c, w = -(f - h);
112
- i.position.set(d, w, 5e-3);
275
+ const l = t.userData.domRect, p = l.x + l.width / 2, d = l.y + l.height / 2, m = e.rect.x + e.rect.width / 2, g = e.rect.y + e.rect.height / 2, y = m - p, f = -(g - d);
276
+ i.position.set(y, f, 5e-3);
113
277
  }
114
278
  }
115
- updateMeshProperties(e, t) {
116
- const { rect: i, styles: r } = t, s = this.renderer.getPixelRatio(), g = this.renderer.domElement.width / s, l = this.renderer.domElement.height / s;
117
- e.scale.set(i.width, i.height, 1);
118
- const p = 1e-3;
119
- this.renderOrder++, e.position.set(
120
- i.x - g / 2 + i.width / 2,
121
- -i.y + l / 2 - i.height / 2,
122
- r.zIndex + this.renderOrder * p
279
+ updateMeshProperties(t, e) {
280
+ const { rect: i, styles: s } = e, n = this.renderer.getPixelRatio(), a = this.renderer.domElement.width / n, c = this.renderer.domElement.height / n;
281
+ t.scale.set(i.width, i.height, 1);
282
+ const u = 1e-3;
283
+ this.renderOrder++;
284
+ const l = this.targetRect.left + window.scrollX, p = this.targetRect.top + window.scrollY, d = i.x - l, m = i.y - p;
285
+ t.position.set(
286
+ d - a / 2 + i.width / 2,
287
+ -m + c / 2 - i.height / 2,
288
+ s.zIndex + this.renderOrder * u
289
+ ), T.update(
290
+ t.material,
291
+ "BOX",
292
+ e.styles,
293
+ "",
294
+ e.rect.width,
295
+ e.rect.height
123
296
  );
124
- const a = e.material, c = r.backgroundColor;
125
- let h = c, u = 1;
126
- if (c === "transparent" || c === "rgba(0, 0, 0, 0)")
127
- h = "#ffffff", u = 0;
128
- else if (c.startsWith("rgba")) {
129
- const d = c.match(/[\d.]+/g);
130
- if (d && d.length >= 4) {
131
- const w = d[0], R = d[1], E = d[2];
132
- u = parseFloat(d[3]), h = `rgb(${w}, ${R}, ${E})`;
133
- }
134
- }
135
- const f = r.opacity * u;
136
- a.color.set(h), a.opacity = f, a.transparent = f < 1;
137
297
  }
138
298
  render() {
139
299
  this.renderer.render(this.scene, this.camera);
140
300
  }
141
301
  }
142
- function z(n) {
143
- const e = document.createRange();
144
- e.selectNodeContents(n);
145
- const t = e.getBoundingClientRect();
302
+ function N(r) {
303
+ const t = document.createRange();
304
+ t.selectNodeContents(r);
305
+ const e = t.getBoundingClientRect();
146
306
  return {
147
- left: t.left,
148
- top: t.top,
149
- width: t.width,
150
- height: t.height
307
+ left: e.left,
308
+ top: e.top,
309
+ width: e.width,
310
+ height: e.height
151
311
  };
152
312
  }
153
- function I(n) {
154
- const e = parseFloat(n.fontSize);
155
- let t = parseFloat(n.lineHeight);
156
- isNaN(t) && (t = e * 1.2);
157
- let i = parseFloat(n.letterSpacing);
313
+ function _(r) {
314
+ const t = parseFloat(r.fontSize);
315
+ let e = parseFloat(r.lineHeight);
316
+ isNaN(e) && (e = t * 1.2);
317
+ let i = parseFloat(r.letterSpacing);
158
318
  return isNaN(i) && (i = 0), {
159
- font: `${n.fontStyle} ${n.fontWeight} ${n.fontSize} ${n.fontFamily}`,
160
- color: n.color,
161
- textAlign: n.textAlign || "start",
319
+ font: `${r.fontStyle} ${r.fontWeight} ${r.fontSize} ${r.fontFamily}`,
320
+ color: r.color,
321
+ textAlign: r.textAlign || "start",
162
322
  textBaseline: "alphabetic",
163
- direction: n.direction || "inherit",
164
- lineHeight: t,
323
+ direction: r.direction || "inherit",
324
+ lineHeight: e,
165
325
  letterSpacing: i
166
326
  };
167
327
  }
168
- function b(n, e = y | x | M | C | S) {
169
- if (n.nodeType === Node.TEXT_NODE) {
170
- const h = n;
171
- if (!h.textContent || !h.textContent.trim())
328
+ function M(r, t = R | b | D | E | C) {
329
+ if (r.nodeType === Node.TEXT_NODE) {
330
+ const d = r;
331
+ if (!d.textContent || !d.textContent.trim())
172
332
  return null;
173
- const u = h.textContent.replace(/\s+/g, " ").trim();
174
- if (u.length === 0)
333
+ const m = d.textContent.replace(/\s+/g, " ").trim();
334
+ if (m.length === 0)
175
335
  return null;
176
- const f = z(h);
177
- if (f.width === 0 || f.height === 0)
336
+ const g = N(d);
337
+ if (g.width === 0 || g.height === 0)
178
338
  return null;
179
- const d = h.parentElement, w = d ? window.getComputedStyle(d) : null;
180
- return w ? {
339
+ const y = d.parentElement, f = y ? window.getComputedStyle(y) : null;
340
+ return f ? {
181
341
  id: Math.random().toString(36).substring(2, 9),
182
342
  type: "TEXT",
183
- element: h,
343
+ element: d,
184
344
  rect: {
185
- x: f.left + window.scrollX,
186
- y: f.top + window.scrollY,
187
- width: f.width,
188
- height: f.height
345
+ x: g.left + window.scrollX,
346
+ y: g.top + window.scrollY,
347
+ width: g.width,
348
+ height: g.height
189
349
  },
190
350
  styles: {
191
351
  backgroundColor: "transparent",
192
- opacity: parseFloat(w.opacity),
352
+ opacity: parseFloat(f.opacity),
193
353
  zIndex: 0,
194
354
  borderRadius: "0px",
195
355
  borderColor: "transparent",
196
356
  borderWidth: "0px"
197
357
  },
198
- textContent: u,
199
- textStyles: I(w),
200
- dirtyMask: e,
358
+ textContent: m,
359
+ textStyles: _(f),
360
+ dirtyMask: t,
201
361
  children: []
202
362
  } : null;
203
363
  }
204
- const t = n, i = t.getBoundingClientRect(), r = window.getComputedStyle(t);
205
- if (i.width === 0 || i.height === 0 || r.display === "none")
364
+ const e = r, i = e.getBoundingClientRect(), s = window.getComputedStyle(e);
365
+ if (i.width === 0 || i.height === 0 || s.display === "none")
206
366
  return null;
207
- let s = t.getAttribute("data-mid");
208
- s || (s = Math.random().toString(36).substring(2, 11), t.setAttribute("data-mid", s));
209
- const g = parseInt(r.zIndex), l = {
210
- backgroundColor: r.backgroundColor,
211
- opacity: parseFloat(r.opacity),
212
- zIndex: isNaN(g) ? 0 : g,
213
- borderRadius: r.borderRadius,
214
- borderColor: r.borderColor,
215
- borderWidth: r.borderWidth
367
+ let n = e.getAttribute("data-mid");
368
+ n || (n = Math.random().toString(36).substring(2, 11), e.setAttribute("data-mid", n));
369
+ const a = parseInt(s.zIndex), c = {
370
+ backgroundColor: s.backgroundColor,
371
+ opacity: parseFloat(s.opacity),
372
+ zIndex: isNaN(a) ? 0 : a,
373
+ borderRadius: s.borderRadius,
374
+ borderColor: s.borderColor,
375
+ borderWidth: s.borderWidth
216
376
  };
217
- let p, a;
218
- const c = [];
219
- return Array.from(t.childNodes).forEach((h) => {
220
- const u = b(h, e);
221
- u && c.push(u);
377
+ let u, l;
378
+ const p = [];
379
+ return Array.from(e.childNodes).forEach((d) => {
380
+ const m = M(d, t);
381
+ m && p.push(m);
222
382
  }), {
223
- id: s,
383
+ id: n,
224
384
  type: "BOX",
225
- element: t,
385
+ element: e,
226
386
  rect: {
227
387
  x: i.left + window.scrollX,
228
388
  y: i.top + window.scrollY,
229
389
  width: i.width,
230
390
  height: i.height
231
391
  },
232
- styles: l,
233
- textContent: p,
234
- textStyles: a,
235
- dirtyMask: e,
236
- children: c
392
+ styles: c,
393
+ textContent: u,
394
+ textStyles: l,
395
+ dirtyMask: t,
396
+ children: p
237
397
  };
238
398
  }
239
- class H {
240
- constructor(e, t) {
399
+ class k {
400
+ constructor(t, e) {
241
401
  o(this, "target");
242
402
  o(this, "renderer");
243
403
  o(this, "observer");
244
404
  o(this, "isDomDirty", !1);
245
405
  o(this, "isRunning", !1);
246
- o(this, "pendingMask", T);
406
+ o(this, "pendingMask", v);
247
407
  o(this, "mutationTimer", null);
248
408
  o(this, "cssTimer", null);
249
- o(this, "onTransitionFinished", (e) => {
250
- this.target.contains(e.target) && this.mutationTimer === null && (this.cssTimer && clearTimeout(this.cssTimer), this.pendingMask |= y | x, this.cssTimer = window.setTimeout(() => {
409
+ o(this, "onTransitionFinished", (t) => {
410
+ this.target.contains(t.target) && this.mutationTimer === null && (this.cssTimer && clearTimeout(this.cssTimer), this.pendingMask |= R | b, this.cssTimer = window.setTimeout(() => {
251
411
  this.isDomDirty = !0, this.cssTimer = null;
252
412
  }, 50));
253
413
  });
@@ -257,12 +417,12 @@ class H {
257
417
  o(this, "renderLoop", () => {
258
418
  this.isRunning && (this.isDomDirty && this.forceUpdateScene(), this.renderer.render(), requestAnimationFrame(this.renderLoop));
259
419
  });
260
- this.target = e, this.renderer = t, this.observer = new MutationObserver((i) => {
261
- let r = T;
262
- for (const s of i)
263
- s.type === "childList" ? r |= S : s.type === "attributes" && (s.attributeName === "style" || s.attributeName === "class") && (r |= y | x);
264
- if (r !== T) {
265
- if (this.pendingMask |= r, r & S) {
420
+ this.target = t, this.renderer = e, this.observer = new MutationObserver((i) => {
421
+ let s = v;
422
+ for (const n of i)
423
+ n.type === "childList" ? s |= C : n.type === "attributes" && (n.attributeName === "style" || n.attributeName === "class") && (s |= R | b);
424
+ if (s !== v) {
425
+ if (this.pendingMask |= s, s & C) {
266
426
  this.clearTimers(), console.log("Structural Change detected"), this.isDomDirty = !0;
267
427
  return;
268
428
  }
@@ -288,27 +448,51 @@ class H {
288
448
  }
289
449
  forceUpdateScene() {
290
450
  this.isDomDirty = !1;
291
- const e = b(this.target, this.pendingMask);
292
- e && this.renderer.syncScene(e), this.pendingMask = T;
451
+ const t = M(this.target, this.pendingMask);
452
+ t && this.renderer.syncScene(t), this.pendingMask = v;
293
453
  }
294
454
  }
295
- class X {
296
- constructor(e) {
455
+ class P {
456
+ constructor(t, e) {
297
457
  o(this, "renderer");
298
458
  o(this, "syncer");
299
459
  o(this, "target");
300
- const t = document.querySelector(e);
301
- if (!t)
302
- throw new Error(`[Mirage] Element not found: ${e}`);
303
- this.target = t, this.renderer = new L(), this.renderer.mount(document.body), this.syncer = new H(this.target, this.renderer);
460
+ this.target = t;
461
+ let i;
462
+ if (e.mode === "duplicate" ? i = e.container ?? this.target.parentElement ?? void 0 : i = this.target.parentElement ?? void 0, !i)
463
+ throw new Error("[Mirage] Cannot find a container (parent or option).");
464
+ this.renderer = new X(this.target, e, i), this.renderer.mount(), this.syncer = new k(this.target, this.renderer);
304
465
  }
305
466
  start() {
306
467
  this.syncer.start();
307
468
  }
308
469
  stop() {
470
+ this.syncer.stop();
471
+ }
472
+ dispose() {
309
473
  this.syncer.stop(), this.renderer.dispose();
310
474
  }
311
475
  }
476
+ class Y {
477
+ constructor(t, e = {}) {
478
+ o(this, "_engine");
479
+ if (!t)
480
+ throw new Error("[Mirage] Target element is required.");
481
+ this._engine = new P(t, e);
482
+ }
483
+ start() {
484
+ this._engine.start();
485
+ }
486
+ stop() {
487
+ this._engine.stop();
488
+ }
489
+ /**
490
+ * 엔진 종료 및 메모리 해제
491
+ */
492
+ destroy() {
493
+ this._engine.dispose();
494
+ }
495
+ }
312
496
  export {
313
- X as Mirage
497
+ Y as Mirage
314
498
  };
@@ -1,2 +1,55 @@
1
- (function(m,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],f):(m=typeof globalThis<"u"?globalThis:m||self,f(m.MirageEngine={},m.THREE))})(this,function(m,f){"use strict";var X=Object.defineProperty;var k=(m,f,y)=>f in m?X(m,f,{enumerable:!0,configurable:!0,writable:!0,value:y}):m[f]=y;var o=(m,f,y)=>(k(m,typeof f!="symbol"?f+"":f,y),y);function y(n){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const t in n)if(t!=="default"){const i=Object.getOwnPropertyDescriptor(n,t);Object.defineProperty(e,t,i.get?i:{enumerable:!0,get:()=>n[t]})}}return e.default=n,Object.freeze(e)}const d=y(f),S=0,b=1,C=2,D=4,E=8,R=16;function M(n,e,t){const i=e.split(`
2
- `),r=[];return i.forEach(s=>{const T=s.split(" ");let l=T[0];for(let w=1;w<T.length;w++){const a=T[w];n.measureText(l+" "+a).width<t?l+=" "+a:(r.push(l),l=a)}r.push(l)}),r}function F(n,e,t,i){const r=document.createElement("canvas"),s=r.getContext("2d");if(!s)throw new Error("[Mirage] Failed to create canvas context");const T=2,l=(window.devicePixelRatio||2)*T;r.width=t*l,r.height=i*l,s.scale(l,l),s.font=e.font,s.fillStyle=e.color,s.textBaseline="top",s.globalAlpha=1;const w=M(s,n,t),a=e.lineHeight;w.forEach((h,p)=>{const g=p*a+2;let u=0;e.textAlign==="center"?u=t/2:e.textAlign==="right"&&(u=t),s.textAlign=e.textAlign,s.fillText(h,u,g)});const c=new d.CanvasTexture(r);return c.colorSpace=d.SRGBColorSpace,c.minFilter=d.LinearFilter,c.magFilter=d.LinearFilter,c.needsUpdate=!0,c}class N{constructor(){o(this,"canvas");o(this,"scene");o(this,"camera");o(this,"renderer");o(this,"renderOrder",0);o(this,"meshMap",new Map);this.canvas=document.createElement("canvas"),this.scene=new d.Scene;const e=window.innerWidth,t=window.innerHeight;this.camera=new d.OrthographicCamera(e/-2,e/2,t/2,t/-2,1,1e3),this.camera.position.z=100,this.renderer=new d.WebGLRenderer({canvas:this.canvas,alpha:!0}),this.renderer.setPixelRatio(window.devicePixelRatio),this.renderer.setSize(e,t)}mount(e){e.appendChild(this.canvas)}dispose(){try{this.renderer.dispose()}catch{}this.canvas.parentElement&&this.canvas.parentElement.removeChild(this.canvas)}setSize(e,t){this.renderer.setSize(e,t),this.camera.left=e/-2,this.camera.right=e/2,this.camera.top=t/2,this.camera.bottom=t/-2,this.camera.updateProjectionMatrix()}syncScene(e){this.renderOrder=0;const t=new Set;this.reconcileNode(e,t);for(const[i,r]of this.meshMap.entries())t.has(i)||(this.scene.remove(r),r.geometry.dispose(),r.material instanceof d.Material&&r.material.dispose(),this.meshMap.delete(i))}reconcileNode(e,t){t.add(e.element);let i=this.meshMap.get(e.element);if(!i){const r=new d.PlaneGeometry(1,1),s=new d.MeshBasicMaterial({transparent:!0});i=new d.Mesh(r,s),e.type==="TEXT"&&(i.name="BG_MESH"),this.scene.add(i),this.meshMap.set(e.element,i)}if(i.userData.domRect=e.rect,this.updateMeshProperties(i,e),e.type==="BOX")for(const r of e.children)this.reconcileNode(r,t);else e.type==="TEXT"&&this.reconcileTextChild(i,e)}reconcileTextChild(e,t){var l,w;let i=e.children.find(a=>a.name==="TEXT_CHILD");const r=JSON.stringify(t.textStyles),s=(l=i==null?void 0:i.userData)==null?void 0:l.styleHash;if(!i||t.dirtyMask&R||r!==s){i&&((w=i.material.map)==null||w.dispose(),i.geometry.dispose(),e.remove(i));const a=F(t.textContent||"",t.textStyles,t.rect.width,t.rect.height),c=new d.PlaneGeometry(1,1),h=new d.MeshBasicMaterial({map:a,transparent:!0,side:d.FrontSide,color:16777215});i=new d.Mesh(c,h),i.name="TEXT_CHILD",i.userData={styleHash:r},e.add(i)}if(i){const a=e.userData.domRect,c=a.x+a.width/2,h=a.y+a.height/2,p=t.rect.x+t.rect.width/2,g=t.rect.y+t.rect.height/2,u=p-c,x=-(g-h);i.position.set(u,x,.005)}}updateMeshProperties(e,t){const{rect:i,styles:r}=t,s=this.renderer.getPixelRatio(),T=this.renderer.domElement.width/s,l=this.renderer.domElement.height/s;e.scale.set(i.width,i.height,1);const w=.001;this.renderOrder++,e.position.set(i.x-T/2+i.width/2,-i.y+l/2-i.height/2,r.zIndex+this.renderOrder*w);const a=e.material,c=r.backgroundColor;let h=c,p=1;if(c==="transparent"||c==="rgba(0, 0, 0, 0)")h="#ffffff",p=0;else if(c.startsWith("rgba")){const u=c.match(/[\d.]+/g);if(u&&u.length>=4){const x=u[0],I=u[1],_=u[2];p=parseFloat(u[3]),h=`rgb(${x}, ${I}, ${_})`}}const g=r.opacity*p;a.color.set(h),a.opacity=g,a.transparent=g<1}render(){this.renderer.render(this.scene,this.camera)}}function O(n){const e=document.createRange();e.selectNodeContents(n);const t=e.getBoundingClientRect();return{left:t.left,top:t.top,width:t.width,height:t.height}}function z(n){const e=parseFloat(n.fontSize);let t=parseFloat(n.lineHeight);isNaN(t)&&(t=e*1.2);let i=parseFloat(n.letterSpacing);return isNaN(i)&&(i=0),{font:`${n.fontStyle} ${n.fontWeight} ${n.fontSize} ${n.fontFamily}`,color:n.color,textAlign:n.textAlign||"start",textBaseline:"alphabetic",direction:n.direction||"inherit",lineHeight:t,letterSpacing:i}}function v(n,e=b|C|D|R|E){if(n.nodeType===Node.TEXT_NODE){const h=n;if(!h.textContent||!h.textContent.trim())return null;const p=h.textContent.replace(/\s+/g," ").trim();if(p.length===0)return null;const g=O(h);if(g.width===0||g.height===0)return null;const u=h.parentElement,x=u?window.getComputedStyle(u):null;return x?{id:Math.random().toString(36).substring(2,9),type:"TEXT",element:h,rect:{x:g.left+window.scrollX,y:g.top+window.scrollY,width:g.width,height:g.height},styles:{backgroundColor:"transparent",opacity:parseFloat(x.opacity),zIndex:0,borderRadius:"0px",borderColor:"transparent",borderWidth:"0px"},textContent:p,textStyles:z(x),dirtyMask:e,children:[]}:null}const t=n,i=t.getBoundingClientRect(),r=window.getComputedStyle(t);if(i.width===0||i.height===0||r.display==="none")return null;let s=t.getAttribute("data-mid");s||(s=Math.random().toString(36).substring(2,11),t.setAttribute("data-mid",s));const T=parseInt(r.zIndex),l={backgroundColor:r.backgroundColor,opacity:parseFloat(r.opacity),zIndex:isNaN(T)?0:T,borderRadius:r.borderRadius,borderColor:r.borderColor,borderWidth:r.borderWidth};let w,a;const c=[];return Array.from(t.childNodes).forEach(h=>{const p=v(h,e);p&&c.push(p)}),{id:s,type:"BOX",element:t,rect:{x:i.left+window.scrollX,y:i.top+window.scrollY,width:i.width,height:i.height},styles:l,textContent:w,textStyles:a,dirtyMask:e,children:c}}class L{constructor(e,t){o(this,"target");o(this,"renderer");o(this,"observer");o(this,"isDomDirty",!1);o(this,"isRunning",!1);o(this,"pendingMask",S);o(this,"mutationTimer",null);o(this,"cssTimer",null);o(this,"onTransitionFinished",e=>{this.target.contains(e.target)&&this.mutationTimer===null&&(this.cssTimer&&clearTimeout(this.cssTimer),this.pendingMask|=b|C,this.cssTimer=window.setTimeout(()=>{this.isDomDirty=!0,this.cssTimer=null},50))});o(this,"onWindowResize",()=>{this.renderer.setSize(window.innerWidth,window.innerHeight),this.isDomDirty=!0});o(this,"renderLoop",()=>{this.isRunning&&(this.isDomDirty&&this.forceUpdateScene(),this.renderer.render(),requestAnimationFrame(this.renderLoop))});this.target=e,this.renderer=t,this.observer=new MutationObserver(i=>{let r=S;for(const s of i)s.type==="childList"?r|=E:s.type==="attributes"&&(s.attributeName==="style"||s.attributeName==="class")&&(r|=b|C);if(r!==S){if(this.pendingMask|=r,r&E){this.clearTimers(),console.log("Structural Change detected"),this.isDomDirty=!0;return}this.mutationTimer&&clearTimeout(this.mutationTimer),this.mutationTimer=window.setTimeout(()=>{this.mutationTimer=null,this.isDomDirty=!0},200)}})}start(){this.isRunning||(this.isRunning=!0,this.observer.observe(this.target,{childList:!0,subtree:!0,attributes:!0,characterData:!0}),this.target.addEventListener("transitionend",this.onTransitionFinished),this.target.addEventListener("animationend",this.onTransitionFinished),window.addEventListener("resize",this.onWindowResize),this.forceUpdateScene(),this.renderLoop())}stop(){this.isRunning=!1,this.observer.disconnect(),this.clearTimers(),this.target.removeEventListener("transitionend",this.onTransitionFinished),this.target.removeEventListener("animationend",this.onTransitionFinished),window.removeEventListener("resize",this.onWindowResize)}clearTimers(){this.mutationTimer&&(clearTimeout(this.mutationTimer),this.mutationTimer=null),this.cssTimer&&(clearTimeout(this.cssTimer),this.cssTimer=null)}forceUpdateScene(){this.isDomDirty=!1;const e=v(this.target,this.pendingMask);e&&this.renderer.syncScene(e),this.pendingMask=S}}class H{constructor(e){o(this,"renderer");o(this,"syncer");o(this,"target");const t=document.querySelector(e);if(!t)throw new Error(`[Mirage] Element not found: ${e}`);this.target=t,this.renderer=new N,this.renderer.mount(document.body),this.syncer=new L(this.target,this.renderer)}start(){this.syncer.start()}stop(){this.syncer.stop(),this.renderer.dispose()}}m.Mirage=H,Object.defineProperty(m,Symbol.toStringTag,{value:"Module"})});
1
+ (function(p,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],f):(p=typeof globalThis<"u"?globalThis:p||self,f(p.MirageEngine={},p.THREE))})(this,function(p,f){"use strict";var Y=Object.defineProperty;var U=(p,f,w)=>f in p?Y(p,f,{enumerable:!0,configurable:!0,writable:!0,value:w}):p[f]=w;var o=(p,f,w)=>(U(p,typeof f!="symbol"?f+"":f,w),w);function w(r){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(r){for(const e in r)if(e!=="default"){const i=Object.getOwnPropertyDescriptor(r,e);Object.defineProperty(t,e,i.get?i:{enumerable:!0,get:()=>r[e]})}}return t.default=r,Object.freeze(t)}const h=w(f),T=0,C=1,S=2,z=4,E=8,B=16;function F(r,t,e){const i=t.split(`
2
+ `),s=[];return i.forEach(n=>{const a=n.split(" ");let c=a[0];for(let u=1;u<a.length;u++){const l=a[u];r.measureText(c+" "+l).width<e?c+=" "+l:(s.push(c),c=l)}s.push(c)}),s}function O(r,t,e,i,s=2){const n=document.createElement("canvas"),a=n.getContext("2d");if(!a)throw new Error("[Mirage] Failed to create canvas context");const u=(window.devicePixelRatio||1)*s;n.width=e*u,n.height=i*u,a.scale(u,u),a.font=t.font,a.fillStyle=t.color,a.textBaseline="top",a.globalAlpha=1;const l=F(a,r,e),m=t.lineHeight;l.forEach((g,y)=>{const x=y*m+2;let v=0;t.textAlign==="center"?v=e/2:t.textAlign==="right"&&(v=e),a.textAlign=t.textAlign,a.fillText(g,v,x)});const d=new h.CanvasTexture(n);return d.colorSpace=h.SRGBColorSpace,d.minFilter=h.LinearFilter,d.magFilter=h.LinearFilter,d.needsUpdate=!0,d}function b(r){return typeof r=="number"?r:parseFloat(r)||0}const A=`
3
+ varying vec2 vUv;
4
+ void main() {
5
+ vUv = uv;
6
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
7
+ }
8
+ `,I=`
9
+ varying vec2 vUv;
10
+
11
+ uniform vec2 uSize;
12
+ uniform float uRadius;
13
+ uniform float uBorderWidth;
14
+ uniform vec3 uColor;
15
+ uniform vec3 uBorderColor;
16
+ uniform float uOpacity;
17
+ uniform float uBgOpacity;
18
+
19
+ // SDF 박스 함수
20
+ float sdRoundedBox(vec2 p, vec2 b, float r) {
21
+ vec2 q = abs(p) - b + r;
22
+ return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
23
+ }
24
+
25
+ void main() {
26
+ vec2 p = (vUv - 0.5) * uSize;
27
+ vec2 halfSize = uSize * 0.5;
28
+
29
+ float d = sdRoundedBox(p, halfSize, uRadius);
30
+
31
+ float smoothEdge = 1.0;
32
+
33
+ float fillAlpha = 1.0 - smoothstep(-uBorderWidth - smoothEdge, -uBorderWidth, d);
34
+
35
+ float borderAlpha = 0.0;
36
+
37
+ if (uBorderWidth > 0.01) {
38
+ borderAlpha = (1.0 - smoothstep(0.0, smoothEdge, d)) - fillAlpha;
39
+ }
40
+
41
+ vec3 color = uColor;
42
+ float totalAlpha = borderAlpha + fillAlpha;
43
+
44
+ if (totalAlpha > 0.001) {
45
+ color = mix(uColor, uBorderColor, borderAlpha / totalAlpha);
46
+ }
47
+
48
+ float shapeAlpha = borderAlpha + (fillAlpha * uBgOpacity);
49
+ float finalOpacity = shapeAlpha * uOpacity;
50
+
51
+ if (finalOpacity < 0.001) discard;
52
+
53
+ gl_FragColor = vec4(color, finalOpacity);
54
+ }
55
+ `;function R(r){if(!r||r==="transparent")return{color:new h.Color(16777215),alpha:0};const t=r.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);if(t){const e=parseInt(t[1],10),i=parseInt(t[2],10),s=parseInt(t[3],10),n=t[4]!==void 0?parseFloat(t[4]):1;return{color:new h.Color(`rgb(${e}, ${i}, ${s})`),alpha:n}}return{color:new h.Color(r),alpha:1}}function L(r,t,e){const i=R(r.backgroundColor),s=R(r.borderColor),n={uSize:{value:new h.Vector2(t,e)},uRadius:{value:b(r.borderRadius)},uBorderWidth:{value:b(r.borderWidth)},uColor:{value:i.color},uBorderColor:{value:s.color},uOpacity:{value:r.opacity??1},uBgOpacity:{value:i.alpha}};return new h.ShaderMaterial({uniforms:n,vertexShader:A,fragmentShader:I,transparent:!0,side:h.FrontSide})}function X(r,t,e,i){const s=R(t.backgroundColor),n=R(t.borderColor);r.uniforms.uSize.value.set(e,i),r.uniforms.uRadius.value=b(t.borderRadius),r.uniforms.uBorderWidth.value=b(t.borderWidth),r.uniforms.uColor.value.copy(s.color),r.uniforms.uBorderColor.value.copy(n.color),r.uniforms.uOpacity.value=t.opacity??1,r.uniforms.uBgOpacity.value=s.alpha}const M={create(r,t,e,i,s,n=2){if(r==="BOX")return L(t,i,s);if(r==="TEXT"){const a=O(e||"",t,i,s,n);return new h.MeshBasicMaterial({map:a,transparent:!0,side:h.FrontSide,color:16777215})}return new h.MeshBasicMaterial({visible:!1})},update(r,t,e,i,s,n,a=2){if(t==="BOX")X(r,e,s,n);else if(t==="TEXT"){const c=r;c.map&&c.map.dispose();const u=O(i||"",e,s,n,a);c.map=u,c.needsUpdate=!0}}};class N{constructor(t,e,i){o(this,"canvas");o(this,"scene");o(this,"camera");o(this,"renderer");o(this,"renderOrder",0);o(this,"textQualityFactor",2);o(this,"mode","overlay");o(this,"customZIndex","9999");o(this,"target");o(this,"mountContainer");o(this,"targetRect");o(this,"meshMap",new Map);var a;this.target=t,this.mountContainer=i,this.mode=e.mode??"overlay",(a=e.style)!=null&&a.zIndex&&(this.customZIndex=e.style.zIndex),this.canvas=document.createElement("canvas"),this.scene=new h.Scene,this.targetRect=this.target.getBoundingClientRect();const s=this.targetRect.width,n=this.targetRect.height;this.camera=new h.OrthographicCamera(s/-2,s/2,n/2,n/-2,1,1e3),this.camera.position.z=100,this.renderer=new h.WebGLRenderer({canvas:this.canvas,alpha:!0,antialias:!0}),this.renderer.setPixelRatio(window.devicePixelRatio),this.renderer.setSize(s,n),this.applyTextQuality(e.textQuality??"medium")}applyTextQuality(t){if(typeof t=="number"){this.textQualityFactor=Math.max(.1,t);return}switch(t){case"low":this.textQualityFactor=1;break;case"high":this.textQualityFactor=4;break;case"medium":default:this.textQualityFactor=2;break}}mount(){this.mountContainer.appendChild(this.canvas),this.canvas.style.zIndex=this.customZIndex,this.canvas.style.pointerEvents=this.mode==="overlay"?"none":"auto",this.updateCanvasLayout()}updateCanvasLayout(){this.canvas.style.width=`${this.targetRect.width}px`,this.canvas.style.height=`${this.targetRect.height}px`,this.mode==="duplicate"?(this.canvas.style.position="",this.canvas.style.top="",this.canvas.style.left="",this.canvas.style.display="block"):(this.canvas.style.position="absolute",this.canvas.style.top=`${this.target.offsetTop}px`,this.canvas.style.left=`${this.target.offsetLeft}px`,this.canvas.style.display="block")}dispose(){this.renderer.dispose(),this.canvas.remove()}setSize(t,e){this.renderer.setSize(t,e),this.camera.left=t/-2,this.camera.right=t/2,this.camera.top=e/2,this.camera.bottom=e/-2,this.camera.updateProjectionMatrix()}syncScene(t){const e=this.target.getBoundingClientRect(),i=Math.abs(e.width-this.targetRect.width)>.1||Math.abs(e.height-this.targetRect.height)>.1,s=this.mode==="overlay"&&(Math.abs(e.top-this.targetRect.top)>.1||Math.abs(e.left-this.targetRect.left)>.1);i?(this.targetRect=e,this.renderer.setSize(this.targetRect.width,this.targetRect.height),this.camera.left=this.targetRect.width/-2,this.camera.right=this.targetRect.width/2,this.camera.top=this.targetRect.height/2,this.camera.bottom=this.targetRect.height/-2,this.camera.updateProjectionMatrix(),this.updateCanvasLayout()):s?(this.targetRect=e,this.updateCanvasLayout()):this.targetRect=e,this.renderOrder=0;const n=new Set;this.reconcileNode(t,n);for(const[a,c]of this.meshMap.entries())n.has(a)||(this.scene.remove(c),c.geometry.dispose(),c.material instanceof h.Material&&c.material.dispose(),this.meshMap.delete(a))}reconcileNode(t,e){e.add(t.element);let i=this.meshMap.get(t.element);if(!i){const s=new h.PlaneGeometry(1,1),n=M.create("BOX",t.styles,"",t.rect.width,t.rect.height);i=new h.Mesh(s,n),t.type==="TEXT"&&(i.name="BG_MESH"),this.scene.add(i),this.meshMap.set(t.element,i)}if(i.userData.domRect=t.rect,this.updateMeshProperties(i,t),t.type==="BOX")for(const s of t.children)this.reconcileNode(s,e);else t.type==="TEXT"&&this.reconcileTextChild(i,t)}reconcileTextChild(t,e){var c,u;let i=t.children.find(l=>l.name==="TEXT_CHILD");const s=JSON.stringify(e.textStyles),n=(c=i==null?void 0:i.userData)==null?void 0:c.styleHash;if(!i||e.dirtyMask&B||s!==n){i&&((u=i.material.map)==null||u.dispose(),i.geometry.dispose(),t.remove(i));const l=M.create("TEXT",e.textStyles,e.textContent||"",e.rect.width,e.rect.height,this.textQualityFactor),m=new h.PlaneGeometry(1,1);i=new h.Mesh(m,l),i.name="TEXT_CHILD",i.userData={styleHash:s},i.position.z=.005,t.add(i)}if(i){const l=t.userData.domRect,m=l.x+l.width/2,d=l.y+l.height/2,g=e.rect.x+e.rect.width/2,y=e.rect.y+e.rect.height/2,x=g-m,v=-(y-d);i.position.set(x,v,.005)}}updateMeshProperties(t,e){const{rect:i,styles:s}=e,n=this.renderer.getPixelRatio(),a=this.renderer.domElement.width/n,c=this.renderer.domElement.height/n;t.scale.set(i.width,i.height,1);const u=.001;this.renderOrder++;const l=this.targetRect.left+window.scrollX,m=this.targetRect.top+window.scrollY,d=i.x-l,g=i.y-m;t.position.set(d-a/2+i.width/2,-g+c/2-i.height/2,s.zIndex+this.renderOrder*u),M.update(t.material,"BOX",e.styles,"",e.rect.width,e.rect.height)}render(){this.renderer.render(this.scene,this.camera)}}function _(r){const t=document.createRange();t.selectNodeContents(r);const e=t.getBoundingClientRect();return{left:e.left,top:e.top,width:e.width,height:e.height}}function k(r){const t=parseFloat(r.fontSize);let e=parseFloat(r.lineHeight);isNaN(e)&&(e=t*1.2);let i=parseFloat(r.letterSpacing);return isNaN(i)&&(i=0),{font:`${r.fontStyle} ${r.fontWeight} ${r.fontSize} ${r.fontFamily}`,color:r.color,textAlign:r.textAlign||"start",textBaseline:"alphabetic",direction:r.direction||"inherit",lineHeight:e,letterSpacing:i}}function D(r,t=C|S|z|B|E){if(r.nodeType===Node.TEXT_NODE){const d=r;if(!d.textContent||!d.textContent.trim())return null;const g=d.textContent.replace(/\s+/g," ").trim();if(g.length===0)return null;const y=_(d);if(y.width===0||y.height===0)return null;const x=d.parentElement,v=x?window.getComputedStyle(x):null;return v?{id:Math.random().toString(36).substring(2,9),type:"TEXT",element:d,rect:{x:y.left+window.scrollX,y:y.top+window.scrollY,width:y.width,height:y.height},styles:{backgroundColor:"transparent",opacity:parseFloat(v.opacity),zIndex:0,borderRadius:"0px",borderColor:"transparent",borderWidth:"0px"},textContent:g,textStyles:k(v),dirtyMask:t,children:[]}:null}const e=r,i=e.getBoundingClientRect(),s=window.getComputedStyle(e);if(i.width===0||i.height===0||s.display==="none")return null;let n=e.getAttribute("data-mid");n||(n=Math.random().toString(36).substring(2,11),e.setAttribute("data-mid",n));const a=parseInt(s.zIndex),c={backgroundColor:s.backgroundColor,opacity:parseFloat(s.opacity),zIndex:isNaN(a)?0:a,borderRadius:s.borderRadius,borderColor:s.borderColor,borderWidth:s.borderWidth};let u,l;const m=[];return Array.from(e.childNodes).forEach(d=>{const g=D(d,t);g&&m.push(g)}),{id:n,type:"BOX",element:e,rect:{x:i.left+window.scrollX,y:i.top+window.scrollY,width:i.width,height:i.height},styles:c,textContent:u,textStyles:l,dirtyMask:t,children:m}}class P{constructor(t,e){o(this,"target");o(this,"renderer");o(this,"observer");o(this,"isDomDirty",!1);o(this,"isRunning",!1);o(this,"pendingMask",T);o(this,"mutationTimer",null);o(this,"cssTimer",null);o(this,"onTransitionFinished",t=>{this.target.contains(t.target)&&this.mutationTimer===null&&(this.cssTimer&&clearTimeout(this.cssTimer),this.pendingMask|=C|S,this.cssTimer=window.setTimeout(()=>{this.isDomDirty=!0,this.cssTimer=null},50))});o(this,"onWindowResize",()=>{this.renderer.setSize(window.innerWidth,window.innerHeight),this.isDomDirty=!0});o(this,"renderLoop",()=>{this.isRunning&&(this.isDomDirty&&this.forceUpdateScene(),this.renderer.render(),requestAnimationFrame(this.renderLoop))});this.target=t,this.renderer=e,this.observer=new MutationObserver(i=>{let s=T;for(const n of i)n.type==="childList"?s|=E:n.type==="attributes"&&(n.attributeName==="style"||n.attributeName==="class")&&(s|=C|S);if(s!==T){if(this.pendingMask|=s,s&E){this.clearTimers(),console.log("Structural Change detected"),this.isDomDirty=!0;return}this.mutationTimer&&clearTimeout(this.mutationTimer),this.mutationTimer=window.setTimeout(()=>{this.mutationTimer=null,this.isDomDirty=!0},200)}})}start(){this.isRunning||(this.isRunning=!0,this.observer.observe(this.target,{childList:!0,subtree:!0,attributes:!0,characterData:!0}),this.target.addEventListener("transitionend",this.onTransitionFinished),this.target.addEventListener("animationend",this.onTransitionFinished),window.addEventListener("resize",this.onWindowResize),this.forceUpdateScene(),this.renderLoop())}stop(){this.isRunning=!1,this.observer.disconnect(),this.clearTimers(),this.target.removeEventListener("transitionend",this.onTransitionFinished),this.target.removeEventListener("animationend",this.onTransitionFinished),window.removeEventListener("resize",this.onWindowResize)}clearTimers(){this.mutationTimer&&(clearTimeout(this.mutationTimer),this.mutationTimer=null),this.cssTimer&&(clearTimeout(this.cssTimer),this.cssTimer=null)}forceUpdateScene(){this.isDomDirty=!1;const t=D(this.target,this.pendingMask);t&&this.renderer.syncScene(t),this.pendingMask=T}}class H{constructor(t,e){o(this,"renderer");o(this,"syncer");o(this,"target");this.target=t;let i;if(e.mode==="duplicate"?i=e.container??this.target.parentElement??void 0:i=this.target.parentElement??void 0,!i)throw new Error("[Mirage] Cannot find a container (parent or option).");this.renderer=new N(this.target,e,i),this.renderer.mount(),this.syncer=new P(this.target,this.renderer)}start(){this.syncer.start()}stop(){this.syncer.stop()}dispose(){this.syncer.stop(),this.renderer.dispose()}}class W{constructor(t,e={}){o(this,"_engine");if(!t)throw new Error("[Mirage] Target element is required.");this._engine=new H(t,e)}start(){this._engine.start()}stop(){this._engine.stop()}destroy(){this._engine.dispose()}}p.Mirage=W,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,10 @@
1
+ import { MirageConfig } from '../types';
2
+ export declare class Engine {
3
+ private renderer;
4
+ private syncer;
5
+ private target;
6
+ constructor(target: HTMLElement, config: MirageConfig);
7
+ start(): void;
8
+ stop(): void;
9
+ dispose(): void;
10
+ }
@@ -1,8 +1,11 @@
1
+ import { MirageConfig } from '../types';
1
2
  export declare class Mirage {
2
- private renderer;
3
- private syncer;
4
- private target;
5
- constructor(selector: string);
3
+ private _engine;
4
+ constructor(element: HTMLElement, config?: MirageConfig);
6
5
  start(): void;
7
6
  stop(): void;
7
+ /**
8
+ * 엔진 종료 및 메모리 해제
9
+ */
10
+ destroy(): void;
8
11
  }
@@ -1,13 +1,21 @@
1
- import { SceneNode } from '../types';
1
+ import { SceneNode, MirageConfig } from '../types';
2
2
  export declare class Renderer {
3
3
  readonly canvas: HTMLCanvasElement;
4
4
  private readonly scene;
5
5
  private readonly camera;
6
6
  private readonly renderer;
7
7
  private renderOrder;
8
+ private textQualityFactor;
9
+ private mode;
10
+ private customZIndex;
11
+ private target;
12
+ private mountContainer;
13
+ private targetRect;
8
14
  private meshMap;
9
- constructor();
10
- mount(parent: HTMLElement): void;
15
+ constructor(target: HTMLElement, config: MirageConfig, mountContainer: HTMLElement);
16
+ private applyTextQuality;
17
+ mount(): void;
18
+ private updateCanvasLayout;
11
19
  dispose(): void;
12
20
  setSize(width: number, height: number): void;
13
21
  syncScene(graphNode: SceneNode): void;
@@ -0,0 +1,19 @@
1
+ import { TextStyles, BoxStyles } from '../../packages/painter/src/index.ts';
2
+ export type NodeType = "BOX" | "TEXT";
3
+ export interface NodeRect {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export interface SceneNode {
10
+ id: string;
11
+ type: NodeType;
12
+ element: HTMLElement;
13
+ rect: NodeRect;
14
+ styles: BoxStyles;
15
+ textContent?: string;
16
+ textStyles?: TextStyles;
17
+ dirtyMask: number;
18
+ children: SceneNode[];
19
+ }
@@ -0,0 +1,18 @@
1
+ export type TextQuality = "low" | "medium" | "high" | number;
2
+ export type MirageMode = "overlay" | "duplicate";
3
+ interface BaseConfig {
4
+ debug?: boolean;
5
+ textQuality?: TextQuality;
6
+ style?: {
7
+ zIndex?: string;
8
+ };
9
+ }
10
+ export interface OverlayConfig extends BaseConfig {
11
+ mode?: "overlay";
12
+ }
13
+ export interface DuplicateConfig extends BaseConfig {
14
+ mode: "duplicate";
15
+ container?: HTMLElement;
16
+ }
17
+ export type MirageConfig = OverlayConfig | DuplicateConfig;
18
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare const DIRTY_NONE = 0;
2
+ export declare const DIRTY_RECT: number;
3
+ export declare const DIRTY_STYLE: number;
4
+ export declare const DIRTY_ZINDEX: number;
5
+ export declare const DIRTY_STRUCTURE: number;
6
+ export declare const DIRTY_CONTENT: number;
@@ -1,41 +1,3 @@
1
- export type NodeType = "BOX" | "TEXT";
2
- export interface NodeRect {
3
- x: number;
4
- y: number;
5
- width: number;
6
- height: number;
7
- }
8
- export interface BoxStyles {
9
- backgroundColor: string;
10
- opacity: number;
11
- zIndex: number;
12
- borderRadius: string;
13
- borderColor: string;
14
- borderWidth: string;
15
- }
16
- export interface TextStyles {
17
- font: string;
18
- color: string;
19
- textAlign: CanvasTextAlign;
20
- textBaseline: CanvasTextBaseline;
21
- direction: CanvasDirection;
22
- lineHeight: number;
23
- letterSpacing: number;
24
- }
25
- export interface SceneNode {
26
- id: string;
27
- type: NodeType;
28
- element: HTMLElement;
29
- rect: NodeRect;
30
- styles: BoxStyles;
31
- textContent?: string;
32
- textStyles?: TextStyles;
33
- dirtyMask: number;
34
- children: SceneNode[];
35
- }
36
- export declare const DIRTY_NONE = 0;
37
- export declare const DIRTY_RECT: number;
38
- export declare const DIRTY_STYLE: number;
39
- export declare const DIRTY_ZINDEX: number;
40
- export declare const DIRTY_STRUCTURE: number;
41
- export declare const DIRTY_CONTENT: number;
1
+ export * from './config';
2
+ export * from './common';
3
+ export * from './flags';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Converts DOM coordinates (Top-Left 0,0) to WebGL coordinates (Center 0,0).
3
+ * @param x - The x coordinate of the DOM element (Left)
4
+ * @param y - The y coordinate of the DOM element (Top)
5
+ * @param width - The width of the DOM element
6
+ * @param height - The height of the DOM element
7
+ * @param canvasWidth - The total width of the canvas
8
+ * @param canvasHeight - The total height of the canvas
9
+ */
10
+ export declare function domToWebGL(x: number, y: number, width: number, height: number, canvasWidth: number, canvasHeight: number): {
11
+ x: number;
12
+ y: number;
13
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: import('vite').UserConfig;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirage-engine",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "An engine that mirrors HTML DOM elements to a WebGL scene in real-time.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -20,10 +20,8 @@
20
20
  "files": [
21
21
  "dist"
22
22
  ],
23
- "scripts": {
24
- "dev": "vite",
25
- "build": "tsc && vite build",
26
- "prepublishOnly": "npm run build"
23
+ "dependencies": {
24
+ "@mirage-engine/painter": "0.1.0"
27
25
  },
28
26
  "peerDependencies": {
29
27
  "three": "^0.150.0"
@@ -42,6 +40,15 @@
42
40
  "threejs",
43
41
  "typescript"
44
42
  ],
43
+ "private": false,
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "registry": "https://registry.npmjs.org/"
47
+ },
45
48
  "author": "dltldn333@gmail.com",
46
- "license": "MIT"
47
- }
49
+ "license": "MIT",
50
+ "scripts": {
51
+ "dev": "vite",
52
+ "build": "tsc && vite build"
53
+ }
54
+ }
@@ -1,4 +0,0 @@
1
- export declare function domToWebGL(x: number, y: number, canvasWidth: number, canvasHeight: number): {
2
- x: number;
3
- y: number;
4
- };
@@ -1,3 +0,0 @@
1
- import { TextStyles } from '../types';
2
- import * as THREE from "three";
3
- export declare function createTextTexture(text: string, styles: TextStyles, rectWidth: number, rectHeight: number): THREE.CanvasTexture;