mirage-engine 0.2.3 → 0.2.4

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,26 +1,39 @@
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;
1
+ var b = Object.defineProperty;
2
+ var D = (i, e, t) => e in i ? b(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t;
3
+ var o = (i, e, t) => (D(i, typeof e != "symbol" ? e + "" : e, t), t);
4
+ import * as c from "three";
5
+ const g = 0, T = 1, y = 2, C = 4, w = 8, x = 16;
6
+ function N(i, e, t, s) {
7
+ const n = document.createElement("canvas"), r = n.getContext("2d");
8
+ if (!r)
9
+ throw new Error("[Mirage] Failed to create canvas context");
10
+ const a = window.devicePixelRatio || 2;
11
+ n.width = t * a, n.height = s * a, r.scale(a, a), r.font = e.font, r.fillStyle = e.color, r.textAlign = e.textAlign, r.direction = e.direction, r.textBaseline = "middle";
12
+ let l = 0;
13
+ e.textAlign === "center" && (l = t / 2), e.textAlign === "right" && (l = t);
14
+ const m = s / 2;
15
+ r.fillText(i, l, m);
16
+ const h = new c.CanvasTexture(n);
17
+ return h.colorSpace = c.SRGBColorSpace, h.minFilter = c.LinearFilter, h.needsUpdate = !0, h;
18
+ }
6
19
  class R {
7
20
  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();
21
+ o(this, "canvas");
22
+ o(this, "scene");
23
+ o(this, "camera");
24
+ o(this, "renderer");
25
+ o(this, "renderOrder", 0);
26
+ o(this, "meshMap", /* @__PURE__ */ new Map());
27
+ this.canvas = document.createElement("canvas"), this.scene = new c.Scene();
15
28
  const e = window.innerWidth, t = window.innerHeight;
16
- this.camera = new a.OrthographicCamera(
29
+ this.camera = new c.OrthographicCamera(
17
30
  e / -2,
18
31
  e / 2,
19
32
  t / 2,
20
33
  t / -2,
21
34
  1,
22
35
  1e3
23
- ), this.camera.position.z = 100, this.renderer = new a.WebGLRenderer({
36
+ ), this.camera.position.z = 100, this.renderer = new c.WebGLRenderer({
24
37
  canvas: this.canvas,
25
38
  alpha: !0
26
39
  }), this.renderer.setSize(e, t);
@@ -42,108 +55,167 @@ class R {
42
55
  this.renderOrder = 0;
43
56
  const t = /* @__PURE__ */ new Set();
44
57
  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));
58
+ for (const [s, n] of this.meshMap.entries())
59
+ t.has(s) || (this.scene.remove(n), n.geometry.dispose(), n.material instanceof c.Material && n.material.dispose(), this.meshMap.delete(s));
47
60
  }
48
61
  reconcileNode(e, t) {
62
+ var s, n;
49
63
  if (e.type === "BOX") {
50
64
  t.add(e.element);
51
65
  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);
66
+ if (!r) {
67
+ const a = new c.PlaneGeometry(1, 1), l = new c.MeshBasicMaterial({ transparent: !0 });
68
+ r = new c.Mesh(a, l), this.scene.add(r), this.meshMap.set(e.element, r);
58
69
  }
59
70
  this.updateMeshProperties(r, e);
60
- for (const i of e.children)
61
- this.reconcileNode(i, t);
71
+ for (const a of e.children)
72
+ this.reconcileNode(a, t);
73
+ }
74
+ if (e.type === "TEXT") {
75
+ t.add(e.element);
76
+ let r = this.meshMap.get(e.element);
77
+ if (!r) {
78
+ const d = new c.PlaneGeometry(1, 1), u = new c.MeshBasicMaterial({ transparent: !0 });
79
+ r = new c.Mesh(d, u), r.name = "BG_MESH", this.scene.add(r), this.meshMap.set(e.element, r);
80
+ }
81
+ this.updateMeshProperties(r, e);
82
+ let a = r.children.find(
83
+ (d) => d.name === "TEXT_CHILD"
84
+ );
85
+ const l = JSON.stringify(e.textStyles), m = (s = a == null ? void 0 : a.userData) == null ? void 0 : s.styleHash, h = e.dirtyMask & x;
86
+ if (!a || h || l !== m) {
87
+ a && ((n = a.material.map) == null || n.dispose(), a.geometry.dispose(), r.remove(a));
88
+ const d = N(
89
+ e.textContent || "",
90
+ e.textStyles,
91
+ e.rect.width,
92
+ e.rect.height
93
+ ), u = new c.PlaneGeometry(1, 1), f = new c.MeshBasicMaterial({
94
+ map: d,
95
+ transparent: !0,
96
+ side: c.FrontSide,
97
+ color: 16777215,
98
+ opacity: 1
99
+ });
100
+ a = new c.Mesh(u, f), a.name = "TEXT_CHILD", a.userData = { styleHash: l }, a.position.z = 5e-3, r.add(a);
101
+ }
62
102
  }
63
103
  }
64
104
  updateMeshProperties(e, t) {
65
- console.log(
66
- `[V2] 업데이트 중인 매쉬 ID: ${e.uuid}, 마스크: ${t.dirtyMask}`
67
- );
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;
105
+ const { rect: s, styles: n } = t, r = this.renderer.domElement.width, a = this.renderer.domElement.height;
106
+ e.scale.set(s.width, s.height, 1);
107
+ const l = 1e-3;
71
108
  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
109
+ s.x - r / 2 + s.width / 2,
110
+ -s.y + a / 2 - s.height / 2,
111
+ n.zIndex + this.renderOrder * l
75
112
  );
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})`;
113
+ const m = e.material, h = n.backgroundColor;
114
+ let p = h, d = 1;
115
+ if (h === "transparent" || h === "rgba(0, 0, 0, 0)")
116
+ p = "#ffffff", d = 0;
117
+ else if (h.startsWith("rgba")) {
118
+ const f = h.match(/[\d.]+/g);
119
+ if (f && f.length >= 4) {
120
+ const E = f[0], M = f[1], v = f[2];
121
+ d = parseFloat(f[3]), p = `rgb(${E}, ${M}, ${v})`;
85
122
  }
86
123
  }
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(" -> 스타일만 업데이트됨");
124
+ const u = n.opacity * d;
125
+ m.color.set(p), m.opacity = u, m.transparent = u < 1;
89
126
  }
90
127
  render() {
91
128
  this.renderer.render(this.scene, this.camera);
92
129
  }
93
130
  }
94
- function v(n, e = u | p) {
95
- if (n.tagName === "SCRIPT" || n.tagName === "STYLE")
131
+ function F(i) {
132
+ var e;
133
+ return i.nodeType === Node.TEXT_NODE && (((e = i.textContent) == null ? void 0 : e.trim().length) || 0) > 0;
134
+ }
135
+ function z(i) {
136
+ const e = Array.from(i.childNodes);
137
+ return e.length === 0 || e.some(
138
+ (n) => n.nodeType === Node.ELEMENT_NODE
139
+ ) ? !1 : e.some(F);
140
+ }
141
+ function L(i) {
142
+ const e = parseFloat(i.fontSize);
143
+ let t = parseFloat(i.lineHeight);
144
+ isNaN(t) && (t = e * 1.2);
145
+ let s = parseFloat(i.letterSpacing);
146
+ return isNaN(s) && (s = 0), {
147
+ font: `${i.fontStyle} ${i.fontWeight} ${i.fontSize} ${i.fontFamily}`,
148
+ color: i.color,
149
+ textAlign: i.textAlign || "start",
150
+ textBaseline: "alphabetic",
151
+ direction: i.direction || "inherit",
152
+ lineHeight: t,
153
+ letterSpacing: s
154
+ };
155
+ }
156
+ function S(i, e = T | y | C | x | w) {
157
+ const t = i.getBoundingClientRect(), s = window.getComputedStyle(i);
158
+ if (t.width === 0 || t.height === 0 || s.display === "none")
96
159
  return null;
97
- const t = n.getBoundingClientRect(), r = {
98
- x: t.x + window.scrollX,
99
- y: t.y + window.scrollY,
100
- width: t.width,
101
- 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);
110
- }
111
- return {
112
- type: "BOX",
113
- element: n,
114
- rect: r,
115
- styles: o,
160
+ let n = i.getAttribute("data-mid");
161
+ n || (n = Math.random().toString(36).substring(2, 11), i.setAttribute("data-mid", n));
162
+ const r = parseInt(s.zIndex), a = {
163
+ backgroundColor: s.backgroundColor,
164
+ opacity: parseFloat(s.opacity),
165
+ zIndex: isNaN(r) ? 0 : r,
166
+ borderRadius: s.borderRadius,
167
+ borderColor: s.borderColor,
168
+ borderWidth: s.borderWidth
169
+ };
170
+ let l = "BOX", m, h;
171
+ const p = [];
172
+ return z(i) ? (l = "TEXT", m = i.textContent || "", h = L(s)) : Array.from(i.children).forEach((d) => {
173
+ const u = S(d, e);
174
+ u && p.push(u);
175
+ }), {
176
+ id: n,
177
+ type: l,
178
+ element: i,
179
+ rect: {
180
+ x: t.left + window.scrollX,
181
+ y: t.top + window.scrollY,
182
+ width: t.width,
183
+ height: t.height
184
+ },
185
+ styles: a,
186
+ textContent: m,
187
+ textStyles: h,
116
188
  dirtyMask: e,
117
- children: l
189
+ children: p
118
190
  };
119
191
  }
120
- class C {
192
+ class O {
121
193
  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(() => {
194
+ o(this, "target");
195
+ o(this, "renderer");
196
+ o(this, "observer");
197
+ o(this, "isDomDirty", !1);
198
+ o(this, "isRunning", !1);
199
+ o(this, "pendingMask", g);
200
+ o(this, "mutationTimer", null);
201
+ o(this, "cssTimer", null);
202
+ o(this, "onTransitionFinished", (e) => {
203
+ this.target.contains(e.target) && this.mutationTimer === null && (this.cssTimer && clearTimeout(this.cssTimer), this.pendingMask |= T | y, this.cssTimer = window.setTimeout(() => {
132
204
  this.isDomDirty = !0, this.cssTimer = null;
133
205
  }, 50));
134
206
  });
135
- s(this, "onWindowResize", () => {
207
+ o(this, "onWindowResize", () => {
136
208
  this.renderer.setSize(window.innerWidth, window.innerHeight), this.isDomDirty = !0;
137
209
  });
138
- s(this, "renderLoop", () => {
210
+ o(this, "renderLoop", () => {
139
211
  this.isRunning && (this.isDomDirty && this.forceUpdateScene(), this.renderer.render(), requestAnimationFrame(this.renderLoop));
140
212
  });
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) {
213
+ this.target = e, this.renderer = t, this.observer = new MutationObserver((s) => {
214
+ let n = g;
215
+ for (const r of s)
216
+ r.type === "childList" ? n |= w : r.type === "attributes" && (r.attributeName === "style" || r.attributeName === "class") && (n |= T | y);
217
+ if (n !== g) {
218
+ if (this.pendingMask |= n, n & w) {
147
219
  this.clearTimers(), console.log("Structural Change detected"), this.isDomDirty = !0;
148
220
  return;
149
221
  }
@@ -169,19 +241,19 @@ class C {
169
241
  }
170
242
  forceUpdateScene() {
171
243
  this.isDomDirty = !1;
172
- const e = v(this.target, this.pendingMask);
173
- e && this.renderer.syncScene(e), this.pendingMask = m;
244
+ const e = S(this.target, this.pendingMask);
245
+ e && this.renderer.syncScene(e), this.pendingMask = g;
174
246
  }
175
247
  }
176
- class k {
248
+ class _ {
177
249
  constructor(e) {
178
- s(this, "renderer");
179
- s(this, "syncer");
180
- s(this, "target");
250
+ o(this, "renderer");
251
+ o(this, "syncer");
252
+ o(this, "target");
181
253
  const t = document.querySelector(e);
182
254
  if (!t)
183
255
  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);
256
+ this.target = t, this.renderer = new R(), this.renderer.mount(document.body), this.syncer = new O(this.target, this.renderer);
185
257
  }
186
258
  start() {
187
259
  this.syncer.start();
@@ -191,5 +263,5 @@ class k {
191
263
  }
192
264
  }
193
265
  export {
194
- k as Mirage
266
+ _ as Mirage
195
267
  };
@@ -1 +1 @@
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(l,d){typeof exports=="object"&&typeof module<"u"?d(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],d):(l=typeof globalThis<"u"?globalThis:l||self,d(l.MirageEngine={},l.THREE))})(this,function(l,d){"use strict";var H=Object.defineProperty;var k=(l,d,T)=>d in l?H(l,d,{enumerable:!0,configurable:!0,writable:!0,value:T}):l[d]=T;var o=(l,d,T)=>(k(l,typeof d!="symbol"?d+"":d,T),T);function T(i){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(i){for(const t in i)if(t!=="default"){const r=Object.getOwnPropertyDescriptor(i,t);Object.defineProperty(e,t,r.get?r:{enumerable:!0,get:()=>i[t]})}}return e.default=i,Object.freeze(e)}const c=T(d),w=0,S=1,x=2,v=4,E=8,M=16;function D(i,e,t,r){const s=document.createElement("canvas"),n=s.getContext("2d");if(!n)throw new Error("[Mirage] Failed to create canvas context");const a=window.devicePixelRatio||2;s.width=t*a,s.height=r*a,n.scale(a,a),n.font=e.font,n.fillStyle=e.color,n.textAlign=e.textAlign,n.direction=e.direction,n.textBaseline="middle";let m=0;e.textAlign==="center"&&(m=t/2),e.textAlign==="right"&&(m=t);const f=r/2;n.fillText(i,m,f);const h=new c.CanvasTexture(s);return h.colorSpace=c.SRGBColorSpace,h.minFilter=c.LinearFilter,h.needsUpdate=!0,h}class C{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 c.Scene;const e=window.innerWidth,t=window.innerHeight;this.camera=new c.OrthographicCamera(e/-2,e/2,t/2,t/-2,1,1e3),this.camera.position.z=100,this.renderer=new c.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[r,s]of this.meshMap.entries())t.has(r)||(this.scene.remove(s),s.geometry.dispose(),s.material instanceof c.Material&&s.material.dispose(),this.meshMap.delete(r))}reconcileNode(e,t){var r,s;if(e.type==="BOX"){t.add(e.element);let n=this.meshMap.get(e.element);if(!n){const a=new c.PlaneGeometry(1,1),m=new c.MeshBasicMaterial({transparent:!0});n=new c.Mesh(a,m),this.scene.add(n),this.meshMap.set(e.element,n)}this.updateMeshProperties(n,e);for(const a of e.children)this.reconcileNode(a,t)}if(e.type==="TEXT"){t.add(e.element);let n=this.meshMap.get(e.element);if(!n){const u=new c.PlaneGeometry(1,1),p=new c.MeshBasicMaterial({transparent:!0});n=new c.Mesh(u,p),n.name="BG_MESH",this.scene.add(n),this.meshMap.set(e.element,n)}this.updateMeshProperties(n,e);let a=n.children.find(u=>u.name==="TEXT_CHILD");const m=JSON.stringify(e.textStyles),f=(r=a==null?void 0:a.userData)==null?void 0:r.styleHash,h=e.dirtyMask&M;if(!a||h||m!==f){a&&((s=a.material.map)==null||s.dispose(),a.geometry.dispose(),n.remove(a));const u=D(e.textContent||"",e.textStyles,e.rect.width,e.rect.height),p=new c.PlaneGeometry(1,1),g=new c.MeshBasicMaterial({map:u,transparent:!0,side:c.FrontSide,color:16777215,opacity:1});a=new c.Mesh(p,g),a.name="TEXT_CHILD",a.userData={styleHash:m},a.position.z=.005,n.add(a)}}}updateMeshProperties(e,t){const{rect:r,styles:s}=t,n=this.renderer.domElement.width,a=this.renderer.domElement.height;e.scale.set(r.width,r.height,1);const m=.001;this.renderOrder++,e.position.set(r.x-n/2+r.width/2,-r.y+a/2-r.height/2,s.zIndex+this.renderOrder*m);const f=e.material,h=s.backgroundColor;let y=h,u=1;if(h==="transparent"||h==="rgba(0, 0, 0, 0)")y="#ffffff",u=0;else if(h.startsWith("rgba")){const g=h.match(/[\d.]+/g);if(g&&g.length>=4){const L=g[0],_=g[1],I=g[2];u=parseFloat(g[3]),y=`rgb(${L}, ${_}, ${I})`}}const p=s.opacity*u;f.color.set(y),f.opacity=p,f.transparent=p<1}render(){this.renderer.render(this.scene,this.camera)}}function N(i){var e;return i.nodeType===Node.TEXT_NODE&&(((e=i.textContent)==null?void 0:e.trim().length)||0)>0}function R(i){const e=Array.from(i.childNodes);return e.length===0||e.some(s=>s.nodeType===Node.ELEMENT_NODE)?!1:e.some(N)}function O(i){const e=parseFloat(i.fontSize);let t=parseFloat(i.lineHeight);isNaN(t)&&(t=e*1.2);let r=parseFloat(i.letterSpacing);return isNaN(r)&&(r=0),{font:`${i.fontStyle} ${i.fontWeight} ${i.fontSize} ${i.fontFamily}`,color:i.color,textAlign:i.textAlign||"start",textBaseline:"alphabetic",direction:i.direction||"inherit",lineHeight:t,letterSpacing:r}}function b(i,e=S|x|v|M|E){const t=i.getBoundingClientRect(),r=window.getComputedStyle(i);if(t.width===0||t.height===0||r.display==="none")return null;let s=i.getAttribute("data-mid");s||(s=Math.random().toString(36).substring(2,11),i.setAttribute("data-mid",s));const n=parseInt(r.zIndex),a={backgroundColor:r.backgroundColor,opacity:parseFloat(r.opacity),zIndex:isNaN(n)?0:n,borderRadius:r.borderRadius,borderColor:r.borderColor,borderWidth:r.borderWidth};let m="BOX",f,h;const y=[];return R(i)?(m="TEXT",f=i.textContent||"",h=O(r)):Array.from(i.children).forEach(u=>{const p=b(u,e);p&&y.push(p)}),{id:s,type:m,element:i,rect:{x:t.left+window.scrollX,y:t.top+window.scrollY,width:t.width,height:t.height},styles:a,textContent:f,textStyles:h,dirtyMask:e,children:y}}class z{constructor(e,t){o(this,"target");o(this,"renderer");o(this,"observer");o(this,"isDomDirty",!1);o(this,"isRunning",!1);o(this,"pendingMask",w);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|=S|x,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(r=>{let s=w;for(const n of r)n.type==="childList"?s|=E:n.type==="attributes"&&(n.attributeName==="style"||n.attributeName==="class")&&(s|=S|x);if(s!==w){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 e=b(this.target,this.pendingMask);e&&this.renderer.syncScene(e),this.pendingMask=w}}class F{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 C,this.renderer.mount(document.body),this.syncer=new z(this.target,this.renderer)}start(){this.syncer.start()}stop(){this.syncer.stop(),this.renderer.dispose()}}l.Mirage=F,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"})});
@@ -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.4",
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
  }