mirage-engine 0.2.3 → 0.2.5

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
@@ -2,6 +2,10 @@
2
2
 
3
3
  > **An engine that mirrors HTML DOM elements to a WebGL scene in real-time.**
4
4
 
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
+
5
9
  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.
6
10
 
7
11
  ## Installation
@@ -17,4 +21,6 @@ import { Mirage } from 'mirage-engine';
17
21
 
18
22
  const engine = new Mirage("#target");
19
23
  engine.start();
20
- ```
24
+ ```
25
+
26
+ **License | MIT © dltldn333**
@@ -1,29 +1,57 @@
1
- var D = Object.defineProperty;
2
- var E = (n, e, t) => e in n ? D(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t;
3
- var s = (n, e, t) => (E(n, typeof e != "symbol" ? e + "" : e, t), t);
4
- import * as a from "three";
5
- const m = 0, u = 1, p = 2, y = 8;
6
- class R {
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);
15
+ }
16
+ r.push(l);
17
+ }), r;
18
+ }
19
+ function N(n, e, t, i) {
20
+ const r = document.createElement("canvas"), s = r.getContext("2d");
21
+ if (!s)
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);
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;
33
+ }
34
+ class L {
7
35
  constructor() {
8
- s(this, "canvas");
9
- s(this, "scene");
10
- s(this, "camera");
11
- s(this, "renderer");
12
- s(this, "renderOrder", 0);
13
- s(this, "meshMap", /* @__PURE__ */ new Map());
14
- this.canvas = document.createElement("canvas"), this.scene = new a.Scene();
36
+ o(this, "canvas");
37
+ o(this, "scene");
38
+ o(this, "camera");
39
+ o(this, "renderer");
40
+ o(this, "renderOrder", 0);
41
+ o(this, "meshMap", /* @__PURE__ */ new Map());
42
+ this.canvas = document.createElement("canvas"), this.scene = new m.Scene();
15
43
  const e = window.innerWidth, t = window.innerHeight;
16
- this.camera = new a.OrthographicCamera(
44
+ this.camera = new m.OrthographicCamera(
17
45
  e / -2,
18
46
  e / 2,
19
47
  t / 2,
20
48
  t / -2,
21
49
  1,
22
50
  1e3
23
- ), this.camera.position.z = 100, this.renderer = new a.WebGLRenderer({
51
+ ), this.camera.position.z = 100, this.renderer = new m.WebGLRenderer({
24
52
  canvas: this.canvas,
25
53
  alpha: !0
26
- }), this.renderer.setSize(e, t);
54
+ }), this.renderer.setPixelRatio(window.devicePixelRatio), this.renderer.setSize(e, t);
27
55
  }
28
56
  mount(e) {
29
57
  e.appendChild(this.canvas);
@@ -42,108 +70,199 @@ class R {
42
70
  this.renderOrder = 0;
43
71
  const t = /* @__PURE__ */ new Set();
44
72
  this.reconcileNode(e, t);
45
- for (const [r, i] of this.meshMap.entries())
46
- t.has(r) || (this.scene.remove(i), i.geometry.dispose(), i.material instanceof a.Material && i.material.dispose(), this.meshMap.delete(r));
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));
47
75
  }
48
76
  reconcileNode(e, t) {
49
- if (e.type === "BOX") {
50
- t.add(e.element);
51
- let r = this.meshMap.get(e.element);
52
- if (r)
53
- console.log("[V2] 기존 매쉬 재사용:", e.element);
54
- else {
55
- console.log("[V2] 매쉬 신규 생성:", e.element);
56
- const i = new a.PlaneGeometry(1, 1), o = new a.MeshBasicMaterial({ transparent: !0 });
57
- r = new a.Mesh(i, o), this.scene.add(r), this.meshMap.set(e.element, r);
58
- }
59
- this.updateMeshProperties(r, e);
60
- for (const i of e.children)
61
- this.reconcileNode(i, t);
77
+ t.add(e.element);
78
+ let i = this.meshMap.get(e.element);
79
+ 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);
62
82
  }
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);
86
+ else
87
+ e.type === "TEXT" && this.reconcileTextChild(i, e);
63
88
  }
64
- updateMeshProperties(e, t) {
65
- console.log(
66
- `[V2] 업데이트 중인 매쉬 ID: ${e.uuid}, 마스크: ${t.dirtyMask}`
89
+ reconcileTextChild(e, t) {
90
+ var l, p;
91
+ let i = e.children.find(
92
+ (a) => a.name === "TEXT_CHILD"
67
93
  );
68
- const { rect: r, styles: i } = t, o = this.renderer.domElement.width, l = this.renderer.domElement.height;
69
- e.scale.set(r.width, r.height, 1);
70
- const g = 1e-3;
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);
109
+ }
110
+ 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);
113
+ }
114
+ }
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;
71
119
  this.renderOrder++, e.position.set(
72
- r.x - o / 2 + r.width / 2,
73
- -r.y + l / 2 - r.height / 2,
74
- i.zIndex + this.renderOrder * g
120
+ i.x - g / 2 + i.width / 2,
121
+ -i.y + l / 2 - i.height / 2,
122
+ r.zIndex + this.renderOrder * p
75
123
  );
76
- const c = e.material, d = i.backgroundColor;
77
- let f = d, w = 1;
78
- if (d === "transparent" || d === "rgba(0, 0, 0, 0)")
79
- f = "#ffffff", w = 0;
80
- else if (d.startsWith("rgba")) {
81
- const h = d.match(/[\d.]+/g);
82
- if (h && h.length >= 4) {
83
- const M = h[0], b = h[1], S = h[2];
84
- w = parseFloat(h[3]), f = `rgb(${M}, ${b}, ${S})`;
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})`;
85
133
  }
86
134
  }
87
- const T = i.opacity * w;
88
- c.color.set(f), c.opacity = T, c.transparent = T < 1, t.dirtyMask & u && console.log(" -> 위치/크기만 업데이트됨"), t.dirtyMask & p && console.log(" -> 스타일만 업데이트됨");
135
+ const f = r.opacity * u;
136
+ a.color.set(h), a.opacity = f, a.transparent = f < 1;
89
137
  }
90
138
  render() {
91
139
  this.renderer.render(this.scene, this.camera);
92
140
  }
93
141
  }
94
- function v(n, e = u | p) {
95
- if (n.tagName === "SCRIPT" || n.tagName === "STYLE")
96
- return null;
97
- const t = n.getBoundingClientRect(), r = {
98
- x: t.x + window.scrollX,
99
- y: t.y + window.scrollY,
142
+ function z(n) {
143
+ const e = document.createRange();
144
+ e.selectNodeContents(n);
145
+ const t = e.getBoundingClientRect();
146
+ return {
147
+ left: t.left,
148
+ top: t.top,
100
149
  width: t.width,
101
150
  height: t.height
102
- }, i = window.getComputedStyle(n), o = {
103
- backgroundColor: i.backgroundColor,
104
- opacity: parseFloat(i.opacity),
105
- zIndex: parseInt(i.zIndex, 10) || 0
106
- }, l = [];
107
- for (const g of n.children) {
108
- const c = v(g);
109
- c && l.push(c);
151
+ };
152
+ }
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);
158
+ return isNaN(i) && (i = 0), {
159
+ font: `${n.fontStyle} ${n.fontWeight} ${n.fontSize} ${n.fontFamily}`,
160
+ color: n.color,
161
+ textAlign: n.textAlign || "start",
162
+ textBaseline: "alphabetic",
163
+ direction: n.direction || "inherit",
164
+ lineHeight: t,
165
+ letterSpacing: i
166
+ };
167
+ }
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())
172
+ return null;
173
+ const u = h.textContent.replace(/\s+/g, " ").trim();
174
+ if (u.length === 0)
175
+ return null;
176
+ const f = z(h);
177
+ if (f.width === 0 || f.height === 0)
178
+ return null;
179
+ const d = h.parentElement, w = d ? window.getComputedStyle(d) : null;
180
+ return w ? {
181
+ id: Math.random().toString(36).substring(2, 9),
182
+ type: "TEXT",
183
+ element: h,
184
+ rect: {
185
+ x: f.left + window.scrollX,
186
+ y: f.top + window.scrollY,
187
+ width: f.width,
188
+ height: f.height
189
+ },
190
+ styles: {
191
+ backgroundColor: "transparent",
192
+ opacity: parseFloat(w.opacity),
193
+ zIndex: 0,
194
+ borderRadius: "0px",
195
+ borderColor: "transparent",
196
+ borderWidth: "0px"
197
+ },
198
+ textContent: u,
199
+ textStyles: I(w),
200
+ dirtyMask: e,
201
+ children: []
202
+ } : null;
110
203
  }
111
- return {
204
+ const t = n, i = t.getBoundingClientRect(), r = window.getComputedStyle(t);
205
+ if (i.width === 0 || i.height === 0 || r.display === "none")
206
+ 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
216
+ };
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);
222
+ }), {
223
+ id: s,
112
224
  type: "BOX",
113
- element: n,
114
- rect: r,
115
- styles: o,
225
+ element: t,
226
+ rect: {
227
+ x: i.left + window.scrollX,
228
+ y: i.top + window.scrollY,
229
+ width: i.width,
230
+ height: i.height
231
+ },
232
+ styles: l,
233
+ textContent: p,
234
+ textStyles: a,
116
235
  dirtyMask: e,
117
- children: l
236
+ children: c
118
237
  };
119
238
  }
120
- class C {
239
+ class H {
121
240
  constructor(e, t) {
122
- s(this, "target");
123
- s(this, "renderer");
124
- s(this, "observer");
125
- s(this, "isDomDirty", !1);
126
- s(this, "isRunning", !1);
127
- s(this, "pendingMask", m);
128
- s(this, "mutationTimer", null);
129
- s(this, "cssTimer", null);
130
- s(this, "onTransitionFinished", (e) => {
131
- this.target.contains(e.target) && this.mutationTimer === null && (this.cssTimer && clearTimeout(this.cssTimer), this.pendingMask |= u | p, this.cssTimer = window.setTimeout(() => {
241
+ o(this, "target");
242
+ o(this, "renderer");
243
+ o(this, "observer");
244
+ o(this, "isDomDirty", !1);
245
+ o(this, "isRunning", !1);
246
+ o(this, "pendingMask", T);
247
+ o(this, "mutationTimer", null);
248
+ 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(() => {
132
251
  this.isDomDirty = !0, this.cssTimer = null;
133
252
  }, 50));
134
253
  });
135
- s(this, "onWindowResize", () => {
254
+ o(this, "onWindowResize", () => {
136
255
  this.renderer.setSize(window.innerWidth, window.innerHeight), this.isDomDirty = !0;
137
256
  });
138
- s(this, "renderLoop", () => {
257
+ o(this, "renderLoop", () => {
139
258
  this.isRunning && (this.isDomDirty && this.forceUpdateScene(), this.renderer.render(), requestAnimationFrame(this.renderLoop));
140
259
  });
141
- this.target = e, this.renderer = t, this.observer = new MutationObserver((r) => {
142
- let i = m;
143
- for (const o of r)
144
- o.type === "childList" ? i |= y : o.type === "attributes" && (o.attributeName === "style" || o.attributeName === "class") && (i |= u | p);
145
- if (i !== m) {
146
- if (this.pendingMask |= i, i & y) {
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) {
147
266
  this.clearTimers(), console.log("Structural Change detected"), this.isDomDirty = !0;
148
267
  return;
149
268
  }
@@ -169,19 +288,19 @@ class C {
169
288
  }
170
289
  forceUpdateScene() {
171
290
  this.isDomDirty = !1;
172
- const e = v(this.target, this.pendingMask);
173
- e && this.renderer.syncScene(e), this.pendingMask = m;
291
+ const e = b(this.target, this.pendingMask);
292
+ e && this.renderer.syncScene(e), this.pendingMask = T;
174
293
  }
175
294
  }
176
- class k {
295
+ class X {
177
296
  constructor(e) {
178
- s(this, "renderer");
179
- s(this, "syncer");
180
- s(this, "target");
297
+ o(this, "renderer");
298
+ o(this, "syncer");
299
+ o(this, "target");
181
300
  const t = document.querySelector(e);
182
301
  if (!t)
183
302
  throw new Error(`[Mirage] Element not found: ${e}`);
184
- this.target = t, this.renderer = new R(), this.renderer.mount(document.body), this.syncer = new C(this.target, this.renderer);
303
+ this.target = t, this.renderer = new L(), this.renderer.mount(document.body), this.syncer = new H(this.target, this.renderer);
185
304
  }
186
305
  start() {
187
306
  this.syncer.start();
@@ -191,5 +310,5 @@ class k {
191
310
  }
192
311
  }
193
312
  export {
194
- k as Mirage
313
+ X as Mirage
195
314
  };
@@ -1 +1,2 @@
1
- (function(o,a){typeof exports=="object"&&typeof module<"u"?a(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],a):(o=typeof globalThis<"u"?globalThis:o||self,a(o.MirageEngine={},o.THREE))})(this,function(o,a){"use strict";var z=Object.defineProperty;var L=(o,a,h)=>a in o?z(o,a,{enumerable:!0,configurable:!0,writable:!0,value:h}):o[a]=h;var s=(o,a,h)=>(L(o,typeof a!="symbol"?a+"":a,h),h);function h(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=h(a),f=0,p=1,g=2,b=8;class E{constructor(){s(this,"canvas");s(this,"scene");s(this,"camera");s(this,"renderer");s(this,"renderOrder",0);s(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.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){if(e.type==="BOX"){t.add(e.element);let i=this.meshMap.get(e.element);if(i)console.log("[V2] 기존 매쉬 재사용:",e.element);else{console.log("[V2] 매쉬 신규 생성:",e.element);const r=new d.PlaneGeometry(1,1),c=new d.MeshBasicMaterial({transparent:!0});i=new d.Mesh(r,c),this.scene.add(i),this.meshMap.set(e.element,i)}this.updateMeshProperties(i,e);for(const r of e.children)this.reconcileNode(r,t)}}updateMeshProperties(e,t){console.log(`[V2] 업데이트 중인 매쉬 ID: ${e.uuid}, 마스크: ${t.dirtyMask}`);const{rect:i,styles:r}=t,c=this.renderer.domElement.width,y=this.renderer.domElement.height;e.scale.set(i.width,i.height,1);const T=.001;this.renderOrder++,e.position.set(i.x-c/2+i.width/2,-i.y+y/2-i.height/2,r.zIndex+this.renderOrder*T);const l=e.material,u=r.backgroundColor;let w=u,M=1;if(u==="transparent"||u==="rgba(0, 0, 0, 0)")w="#ffffff",M=0;else if(u.startsWith("rgba")){const m=u.match(/[\d.]+/g);if(m&&m.length>=4){const O=m[0],C=m[1],k=m[2];M=parseFloat(m[3]),w=`rgb(${O}, ${C}, ${k})`}}const S=r.opacity*M;l.color.set(w),l.opacity=S,l.transparent=S<1,t.dirtyMask&p&&console.log(" -> 위치/크기만 업데이트됨"),t.dirtyMask&g&&console.log(" -> 스타일만 업데이트됨")}render(){this.renderer.render(this.scene,this.camera)}}function v(n,e=p|g){if(n.tagName==="SCRIPT"||n.tagName==="STYLE")return null;const t=n.getBoundingClientRect(),i={x:t.x+window.scrollX,y:t.y+window.scrollY,width:t.width,height:t.height},r=window.getComputedStyle(n),c={backgroundColor:r.backgroundColor,opacity:parseFloat(r.opacity),zIndex:parseInt(r.zIndex,10)||0},y=[];for(const T of n.children){const l=v(T);l&&y.push(l)}return{type:"BOX",element:n,rect:i,styles:c,dirtyMask:e,children:y}}class D{constructor(e,t){s(this,"target");s(this,"renderer");s(this,"observer");s(this,"isDomDirty",!1);s(this,"isRunning",!1);s(this,"pendingMask",f);s(this,"mutationTimer",null);s(this,"cssTimer",null);s(this,"onTransitionFinished",e=>{this.target.contains(e.target)&&this.mutationTimer===null&&(this.cssTimer&&clearTimeout(this.cssTimer),this.pendingMask|=p|g,this.cssTimer=window.setTimeout(()=>{this.isDomDirty=!0,this.cssTimer=null},50))});s(this,"onWindowResize",()=>{this.renderer.setSize(window.innerWidth,window.innerHeight),this.isDomDirty=!0});s(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=f;for(const c of i)c.type==="childList"?r|=b:c.type==="attributes"&&(c.attributeName==="style"||c.attributeName==="class")&&(r|=p|g);if(r!==f){if(this.pendingMask|=r,r&b){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=f}}class R{constructor(e){s(this,"renderer");s(this,"syncer");s(this,"target");const t=document.querySelector(e);if(!t)throw new Error(`[Mirage] Element not found: ${e}`);this.target=t,this.renderer=new E,this.renderer.mount(document.body),this.syncer=new D(this.target,this.renderer)}start(){this.syncer.start()}stop(){this.syncer.stop(),this.renderer.dispose()}}o.Mirage=R,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})});
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,2 +1,2 @@
1
1
  import { SceneNode } from '../types';
2
- export declare function extractSceneGraph(element: HTMLElement, initialMask?: number): SceneNode | null;
2
+ export declare function extractSceneGraph(sourceNode: HTMLElement | Node, initialMask?: number): SceneNode | null;
@@ -12,6 +12,7 @@ export declare class Renderer {
12
12
  setSize(width: number, height: number): void;
13
13
  syncScene(graphNode: SceneNode): void;
14
14
  private reconcileNode;
15
+ private reconcileTextChild;
15
16
  private updateMeshProperties;
16
17
  render(): void;
17
18
  }
@@ -0,0 +1,3 @@
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;
@@ -1,3 +1,4 @@
1
+ export type NodeType = "BOX" | "TEXT";
1
2
  export interface NodeRect {
2
3
  x: number;
3
4
  y: number;
@@ -8,12 +9,27 @@ export interface BoxStyles {
8
9
  backgroundColor: string;
9
10
  opacity: number;
10
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;
11
24
  }
12
25
  export interface SceneNode {
13
- type: "BOX";
26
+ id: string;
27
+ type: NodeType;
14
28
  element: HTMLElement;
15
29
  rect: NodeRect;
16
30
  styles: BoxStyles;
31
+ textContent?: string;
32
+ textStyles?: TextStyles;
17
33
  dirtyMask: number;
18
34
  children: SceneNode[];
19
35
  }
@@ -22,3 +38,4 @@ export declare const DIRTY_RECT: number;
22
38
  export declare const DIRTY_STYLE: number;
23
39
  export declare const DIRTY_ZINDEX: number;
24
40
  export declare const DIRTY_STRUCTURE: number;
41
+ export declare const DIRTY_CONTENT: number;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "mirage-engine",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
+ "description": "An engine that mirrors HTML DOM elements to a WebGL scene in real-time.",
4
5
  "type": "module",
5
6
  "types": "./dist/index.d.ts",
6
7
  "main": "./dist/mirage-engine.umd.js",
@@ -21,7 +22,7 @@
21
22
  ],
22
23
  "scripts": {
23
24
  "dev": "vite",
24
- "build": "tsc && vite build",
25
+ "build": "tsc && vite build",
25
26
  "prepublishOnly": "npm run build"
26
27
  },
27
28
  "peerDependencies": {
@@ -34,7 +35,13 @@
34
35
  "vite": "^4.0.0",
35
36
  "vite-plugin-dts": "^4.5.4"
36
37
  },
37
- "keywords": ["webgl", "dom", "mirroring", "threejs", "typescript"],
38
+ "keywords": [
39
+ "webgl",
40
+ "dom",
41
+ "mirroring",
42
+ "threejs",
43
+ "typescript"
44
+ ],
38
45
  "author": "dltldn333@gmail.com",
39
46
  "license": "MIT"
40
47
  }