@mirage-engine/core 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,55 +1,85 @@
1
- (function(d,f){typeof exports=="object"&&typeof module!="undefined"?f(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],f):(d=typeof globalThis!="undefined"?globalThis:d||self,f(d.MirageEngine={},d.THREE))})(this,function(d,f){"use strict";var H=Object.defineProperty;var W=(d,f,T)=>f in d?H(d,f,{enumerable:!0,configurable:!0,writable:!0,value:T}):d[f]=T;var c=(d,f,T)=>(W(d,typeof f!="symbol"?f+"":f,T),T);function T(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=T(f),x=0,R=1,b=2,B=4,C=8,M=16;function I(r,t,e){const i=t.split(`
2
- `),n=[];return i.forEach(s=>{const a=s.split(" ");let o=a[0];for(let l=1;l<a.length;l++){const u=a[l];r.measureText(o+" "+u).width<e?o+=" "+u:(n.push(o),o=u)}n.push(o)}),n}function O(r,t,e,i,n=2){const s=document.createElement("canvas"),a=s.getContext("2d");if(!a)throw new Error("[Mirage] Failed to create canvas context");const l=(window.devicePixelRatio||1)*n;s.width=e*l,s.height=i*l,a.scale(l,l),a.font=t.font,a.fillStyle=t.color,a.textBaseline="top",a.globalAlpha=1;const u=I(a,r,e),m=t.lineHeight;u.forEach((g,y)=>{const w=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,w)});const p=new h.CanvasTexture(s);return p.colorSpace=h.SRGBColorSpace,p.minFilter=h.LinearFilter,p.magFilter=h.LinearFilter,p.needsUpdate=!0,p}function S(r){return typeof r=="number"?r:parseFloat(r)||0}const F=`
1
+ (function(u,T){typeof exports=="object"&&typeof module!="undefined"?T(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],T):(u=typeof globalThis!="undefined"?globalThis:u||self,T(u.MirageEngine={},u.THREE))})(this,function(u,T){"use strict";var ee=Object.defineProperty;var te=(u,T,w)=>T in u?ee(u,T,{enumerable:!0,configurable:!0,writable:!0,value:w}):u[T]=w;var d=(u,T,w)=>(te(u,typeof T!="symbol"?T+"":T,w),w);function w(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 h=w(T),M=0,B=1,O=2,A=4,D=8,L=16,R=1,E=2,q=0,X=["include-tree","exclude-tree","include-self","exclude-self","end"];function W(i,e,t){const r=e.split(`
2
+ `),s=[];return r.forEach(n=>{const a=n.split(" ");let o=a[0];for(let l=1;l<a.length;l++){const c=a[l];i.measureText(o+" "+c).width<t?o+=" "+c:(s.push(o),o=c)}s.push(o)}),s}function Y(i,e,t,r,s=2){const n=document.createElement("canvas"),a=n.getContext("2d");if(!a)throw new Error("[Mirage] Failed to create canvas context");const l=(window.devicePixelRatio||1)*s;n.width=t*l,n.height=r*l,a.scale(l,l),a.font=e.font,a.fillStyle=e.color,a.textBaseline="top",a.globalAlpha=1;const c=W(a,i,t),p=e.lineHeight;c.forEach((m,v)=>{const b=v*p+2;let y=0;e.textAlign==="center"?y=t/2:e.textAlign==="right"&&(y=t),a.textAlign=e.textAlign,a.fillText(m,y,b)});const f=new h.CanvasTexture(n);return f.colorSpace=h.SRGBColorSpace,f.minFilter=h.LinearFilter,f.magFilter=h.LinearFilter,f.needsUpdate=!0,f}function S(i){return typeof i=="number"?i:parseFloat(i)||0}function P(i,e){var o,l,c,p;if(!e){i.set(0,0,0,0);return}const t=e.split("/")[0].trim().split(/\s+/),r=S(t[0]),s=S((o=t[1])!=null?o:t[0]),n=S((l=t[2])!=null?l:t[0]),a=S((p=(c=t[3])!=null?c:t[1])!=null?p:t[0]);i.set(r,s,n,a)}const H=`
3
3
  varying vec2 vUv;
4
+ varying vec4 vScreenPos;
4
5
  void main() {
5
6
  vUv = uv;
6
7
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
8
+ vScreenPos = gl_Position;
7
9
  }
8
- `,N=`
10
+ `,$=`
11
+
9
12
  varying vec2 vUv;
10
13
 
11
14
  uniform vec2 uSize;
12
- uniform float uRadius;
15
+ uniform vec4 uBorderRadius;
13
16
  uniform float uBorderWidth;
14
- uniform vec3 uColor;
17
+ uniform vec3 uBgColor;
15
18
  uniform vec3 uBorderColor;
16
19
  uniform float uOpacity;
17
20
  uniform float uBgOpacity;
21
+ uniform float uBorderOpacity;
22
+
23
+ #INJECT_DECLARATIONS
18
24
 
19
- // SDF 박스 함수
20
25
  float sdRoundedBox(vec2 p, vec2 b, float r) {
21
26
  vec2 q = abs(p) - b + r;
27
+
22
28
  return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
23
29
  }
24
30
 
25
31
  void main() {
26
32
  vec2 p = (vUv - 0.5) * uSize;
27
33
  vec2 halfSize = uSize * 0.5;
28
-
29
- float d = sdRoundedBox(p, halfSize, uRadius);
30
-
31
- float smoothEdge = 1.0;
32
34
 
33
- float fillAlpha = 1.0 - smoothstep(-uBorderWidth - smoothEdge, -uBorderWidth, d);
35
+ #INJECT_UV_MODIFIER
34
36
 
35
- float borderAlpha = 0.0;
36
-
37
- if (uBorderWidth > 0.01) {
38
- borderAlpha = (1.0 - smoothstep(0.0, smoothEdge, d)) - fillAlpha;
39
- }
37
+ // color decision pipeline
38
+ vec4 baseColor = vec4(uBgColor, uBgOpacity);
40
39
 
41
- vec3 color = uColor;
42
- float totalAlpha = borderAlpha + fillAlpha;
43
-
44
- if (totalAlpha > 0.001) {
45
- color = mix(uColor, uBorderColor, borderAlpha / totalAlpha);
40
+ #INJECT_BASE_COLOR
41
+
42
+ // sdf shape pipeline
43
+ vec2 xRadii = mix(uBorderRadius.xw, uBorderRadius.yz, step(0.0, p.x));
44
+ float r = mix(xRadii.y, xRadii.x, step(0.0, p.y));
45
+ float d = sdRoundedBox(p, halfSize, r);
46
+
47
+ float aa = 1.0;
48
+ float bgMask = 1.0 - smoothstep(0.0, aa, d);
49
+
50
+ float halfBorder = uBorderWidth * 0.5;
51
+ float borderD = abs(d + halfBorder) - halfBorder;
52
+ float borderAlpha = (1.0 - smoothstep(0.0, aa, borderD)) * uBorderOpacity;
53
+ if (uBorderWidth <= 0.01) {
54
+ borderAlpha = 0.0;
46
55
  }
47
-
48
- float shapeAlpha = borderAlpha + (fillAlpha * uBgOpacity);
49
- float finalOpacity = shapeAlpha * uOpacity;
50
-
56
+
57
+ // final blending (border + background)
58
+ float aFront = borderAlpha;
59
+ float aBack = baseColor.a;
60
+ float aOut = aFront + aBack * (1.0 - aFront);
61
+
62
+ float safeAlpha = max(aOut, 0.0001);
63
+ vec3 cOut = (uBorderColor * aFront + baseColor.rgb * aBack * (1.0 - aFront)) / safeAlpha;
64
+ vec4 finalColor = vec4(cOut, aOut);
65
+
66
+ // final color control (Tint, Noise)
67
+ #INJECT_COLOR_MODIFIER
68
+
69
+ float finalOpacity = finalColor.a * bgMask * uOpacity;
51
70
  if (finalOpacity < 0.001) discard;
52
71
 
53
- gl_FragColor = vec4(color, finalOpacity);
72
+ gl_FragColor = vec4(finalColor.rgb, finalOpacity);
73
+
74
+ #include <colorspace_fragment>
54
75
  }
55
- `;function E(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),n=parseInt(t[3],10),s=t[4]!==void 0?parseFloat(t[4]):1;return{color:new h.Color(`rgb(${e}, ${i}, ${n})`),alpha:s}}return{color:new h.Color(r),alpha:1}}function L(r,t,e){var o;const i=E(r.backgroundColor),n=E(r.borderColor),s={uSize:{value:new h.Vector2(t,e)},uRadius:{value:S(r.borderRadius)},uBorderWidth:{value:S(r.borderWidth)},uColor:{value:i.color},uBorderColor:{value:n.color},uOpacity:{value:(o=r.opacity)!=null?o:1},uBgOpacity:{value:i.alpha}};return new h.ShaderMaterial({uniforms:s,vertexShader:F,fragmentShader:N,transparent:!0,side:h.FrontSide})}function A(r,t,e,i){var a;const n=E(t.backgroundColor),s=E(t.borderColor);r.uniforms.uSize.value.set(e,i),r.uniforms.uRadius.value=S(t.borderRadius),r.uniforms.uBorderWidth.value=S(t.borderWidth),r.uniforms.uColor.value.copy(n.color),r.uniforms.uBorderColor.value.copy(s.color),r.uniforms.uOpacity.value=(a=t.opacity)!=null?a:1,r.uniforms.uBgOpacity.value=n.alpha}const D={create(r,t,e,i,n,s=2){if(r==="BOX")return L(t,i,n);if(r==="TEXT"){const a=O(e||"",t,i,n,s);return new h.MeshBasicMaterial({map:a,transparent:!0,side:h.FrontSide,color:16777215})}return new h.MeshBasicMaterial({visible:!1})},update(r,t,e,i,n,s,a=2){if(t==="BOX")A(r,e,n,s);else if(t==="TEXT"){const o=r;o.map&&o.map.dispose();const l=O(i||"",e,n,s,a);o.map=l,o.needsUpdate=!0}}};class X{constructor(t,e,i){c(this,"canvas");c(this,"scene");c(this,"camera");c(this,"renderer");c(this,"renderOrder",0);c(this,"textQualityFactor",2);c(this,"mode","overlay");c(this,"customZIndex","9999");c(this,"target");c(this,"mountContainer");c(this,"targetRect");c(this,"meshMap",new Map);var a,o,l;this.target=t,this.mountContainer=i,this.mode=(a=e.mode)!=null?a:"overlay",(o=e.style)!=null&&o.zIndex&&(this.customZIndex=e.style.zIndex),this.canvas=document.createElement("canvas"),this.scene=new h.Scene,this.targetRect=this.target.getBoundingClientRect();const n=this.targetRect.width,s=this.targetRect.height;this.camera=new h.OrthographicCamera(n/-2,n/2,s/2,s/-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(n,s),this.applyTextQuality((l=e.textQuality)!=null?l:"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,n=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()):n?(this.targetRect=e,this.updateCanvasLayout()):this.targetRect=e,this.renderOrder=0;const s=new Set;this.reconcileNode(t,s);for(const[a,o]of this.meshMap.entries())s.has(a)||(this.scene.remove(o),o.geometry.dispose(),o.material instanceof h.Material&&o.material.dispose(),this.meshMap.delete(a))}reconcileNode(t,e){e.add(t.element);let i=this.meshMap.get(t.element);if(!i){const n=new h.PlaneGeometry(1,1),s=D.create("BOX",t.styles,"",t.rect.width,t.rect.height);i=new h.Mesh(n,s),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 n of t.children)this.reconcileNode(n,e);else t.type==="TEXT"&&this.reconcileTextChild(i,t)}reconcileTextChild(t,e){var o,l;let i=t.children.find(u=>u.name==="TEXT_CHILD");const n=JSON.stringify(e.textStyles),s=(o=i==null?void 0:i.userData)==null?void 0:o.styleHash;if(!i||e.dirtyMask&M||n!==s){i&&((l=i.material.map)==null||l.dispose(),i.geometry.dispose(),t.remove(i));const u=D.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,u),i.name="TEXT_CHILD",i.userData={styleHash:n},i.position.z=.005,t.add(i)}if(i){const u=t.userData.domRect,m=u.x+u.width/2,p=u.y+u.height/2,g=e.rect.x+e.rect.width/2,y=e.rect.y+e.rect.height/2,w=g-m,v=-(y-p);i.position.set(w,v,.005)}}updateMeshProperties(t,e){const{rect:i,styles:n}=e,s=this.renderer.getPixelRatio(),a=this.renderer.domElement.width/s,o=this.renderer.domElement.height/s;t.scale.set(i.width,i.height,1);const l=.001;this.renderOrder++;const u=this.targetRect.left+window.scrollX,m=this.targetRect.top+window.scrollY,p=i.x-u,g=i.y-m;t.position.set(p-a/2+i.width/2,-g+o/2-i.height/2,n.zIndex+this.renderOrder*l),D.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 Y(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 z(r,t=R|b|B|M|C){if(r.nodeType===Node.TEXT_NODE){const p=r;if(!p.textContent||!p.textContent.trim())return null;const g=p.textContent.replace(/\s+/g," ").trim();if(g.length===0)return null;const y=_(p);if(y.width===0||y.height===0)return null;const w=p.parentElement,v=w?window.getComputedStyle(w):null;return v?{id:Math.random().toString(36).substring(2,9),type:"TEXT",element:p,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:Y(v),dirtyMask:t,children:[]}:null}const e=r,i=e.getBoundingClientRect(),n=window.getComputedStyle(e);if(i.width===0||i.height===0||n.display==="none")return null;let s=e.getAttribute("data-mid");s||(s=Math.random().toString(36).substring(2,11),e.setAttribute("data-mid",s));const a=parseInt(n.zIndex),o={backgroundColor:n.backgroundColor,opacity:parseFloat(n.opacity),zIndex:isNaN(a)?0:a,borderRadius:n.borderRadius,borderColor:n.borderColor,borderWidth:n.borderWidth};let l,u;const m=[];return Array.from(e.childNodes).forEach(p=>{const g=z(p,t);g&&m.push(g)}),{id:s,type:"BOX",element:e,rect:{x:i.left+window.scrollX,y:i.top+window.scrollY,width:i.width,height:i.height},styles:o,textContent:l,textStyles:u,dirtyMask:t,children:m}}class k{constructor(t,e){c(this,"target");c(this,"renderer");c(this,"observer");c(this,"isDomDirty",!1);c(this,"isRunning",!1);c(this,"pendingMask",x);c(this,"mutationTimer",null);c(this,"cssTimer",null);c(this,"onTransitionFinished",t=>{this.target.contains(t.target)&&this.mutationTimer===null&&(this.cssTimer&&clearTimeout(this.cssTimer),this.pendingMask|=R|b,this.cssTimer=window.setTimeout(()=>{this.isDomDirty=!0,this.cssTimer=null},50))});c(this,"onWindowResize",()=>{this.renderer.setSize(window.innerWidth,window.innerHeight),this.isDomDirty=!0});c(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 n=x;for(const s of i)s.type==="childList"?n|=C:s.type==="attributes"&&(s.attributeName==="style"||s.attributeName==="class")&&(n|=R|b);if(n!==x){if(this.pendingMask|=n,n&C){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=z(this.target,this.pendingMask);t&&this.renderer.syncScene(t),this.pendingMask=x}}class P{constructor(t,e){c(this,"renderer");c(this,"syncer");c(this,"target");var n,s,a;this.target=t;let i;if(e.mode==="duplicate"?i=(s=(n=e.container)!=null?n:this.target.parentElement)!=null?s:void 0:i=(a=this.target.parentElement)!=null?a:void 0,!i)throw new Error("[Mirage] Cannot find a container (parent or option).");this.renderer=new X(this.target,e,i),this.renderer.mount(),this.syncer=new k(this.target,this.renderer)}start(){this.syncer.start()}stop(){this.syncer.stop()}dispose(){this.syncer.stop(),this.renderer.dispose()}}d.DIRTY_CONTENT=M,d.DIRTY_NONE=x,d.DIRTY_RECT=R,d.DIRTY_STRUCTURE=C,d.DIRTY_STYLE=b,d.DIRTY_ZINDEX=B,d.Engine=P,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})});
76
+ `;function F(i){if(!i||i==="transparent")return{color:new h.Color(16777215),alpha:0};const e=i.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);if(e){const t=parseInt(e[1],10),r=parseInt(e[2],10),s=parseInt(e[3],10),n=e[4]!==void 0?parseFloat(e[4]):1;return{color:new h.Color(`rgb(${t}, ${r}, ${s})`),alpha:n}}return{color:new h.Color(i),alpha:1}}function G(i,e,t,r=null,s){var y;const n=r!==null,a=n?`
77
+ uniform sampler2D uTexture;
78
+ varying vec4 vScreenPos;
79
+ `:"",o=n?`
80
+ vec2 screenUv = (vScreenPos.xy / vScreenPos.w) * 0.5 + 0.5;
81
+ vec2 resultUv = screenUv;
82
+ ${(s==null?void 0:s.uvModifier)||""}
83
+ `:"",l=n?`
84
+ baseColor = texture2D(uTexture, resultUv);
85
+ `:"",c=(s==null?void 0:s.colorModifier)||"",p=$.replace("#INJECT_DECLARATIONS",a).replace("#INJECT_UV_MODIFIER",o).replace("#INJECT_BASE_COLOR",l).replace("#INJECT_COLOR_MODIFIER",c),f=F(i.backgroundColor),m=F(i.borderColor),v={uSize:{value:new h.Vector2(e,t)},uBorderRadius:{value:new h.Vector4(0,0,0,0)},uBorderWidth:{value:S(i.borderWidth)},uBgColor:{value:f.color},uBorderColor:{value:m.color},uOpacity:{value:(y=i.opacity)!=null?y:1},uBgOpacity:{value:f.alpha},uBorderOpacity:{value:m.alpha},uTexture:{value:null}};return P(v.uBorderRadius.value,i.borderRadius),n&&(v.uTexture.value=r),new h.ShaderMaterial({uniforms:v,vertexShader:H,fragmentShader:p,transparent:!0,side:h.FrontSide})}function j(i,e,t,r,s){var o;const n=F(e.backgroundColor),a=F(e.borderColor);i.uniforms.uSize.value.set(t,r),P(i.uniforms.uBorderRadius.value,e.borderRadius),i.uniforms.uBorderWidth.value=S(e.borderWidth),i.uniforms.uBgColor.value.copy(n.color),i.uniforms.uBorderColor.value.copy(a.color),i.uniforms.uOpacity.value=(o=e.opacity)!=null?o:1,i.uniforms.uBgOpacity.value=n.alpha,i.uniforms.uBorderOpacity.value=a.alpha,i.uniforms.uTexture&&s!==void 0&&(i.uniforms.uTexture.value=s)}const I={create(i,e,t,r,s,n=2,a=null){if(i==="BOX")return G(e,r,s,a);if(i==="TEXT"){const o=Y(t||"",e,r,s,n);return new h.MeshBasicMaterial({map:o,transparent:!0,side:h.FrontSide,color:16777215})}return new h.MeshBasicMaterial({visible:!1})},update(i,e,t,r,s,n,a=2,o){if(e==="BOX")j(i,t,s,n,o);else if(e==="TEXT"){const l=i;l.map&&l.map.dispose();const c=Y(r||"",t,s,n,a);l.map=c,l.needsUpdate=!0}}};class J{constructor(e,t,r){d(this,"canvas");d(this,"scene");d(this,"camera");d(this,"renderer");d(this,"renderTarget",null);d(this,"renderOrder",0);d(this,"qualityFactor",2);d(this,"mode","overlay");d(this,"target");d(this,"mountContainer");d(this,"targetRect");d(this,"meshMap",new Map);var a,o;this.target=e,this.mountContainer=r,this.mode=(a=t.mode)!=null?a:"overlay",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.camera.layers.set(0),this.renderer=new h.WebGLRenderer({canvas:this.canvas,alpha:!0,antialias:!0}),this.renderer.setPixelRatio(window.devicePixelRatio),this.renderer.setSize(s,n),this.applyTextQuality((o=t.quality)!=null?o:"medium")}createRenderTarget(){this.renderTarget=new h.WebGLRenderTarget(this.targetRect.width*this.qualityFactor,this.targetRect.height*this.qualityFactor,{minFilter:h.LinearFilter,magFilter:h.LinearFilter,format:h.RGBAFormat,stencilBuffer:!1,depthBuffer:!0,samples:4})}applyTextQuality(e){if(typeof e=="number"){this.qualityFactor=Math.max(.1,e);return}switch(e){case"low":this.qualityFactor=1;break;case"high":this.qualityFactor=4;break;case"medium":default:this.qualityFactor=2;break}}mount(){this.mountContainer.prepend(this.canvas),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(e,t){this.renderer.setSize(e,t),this.renderTarget&&this.renderTarget.setSize(e*this.qualityFactor,t*this.qualityFactor),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){const t=this.target.getBoundingClientRect(),r=Math.abs(t.width-this.targetRect.width)>.1||Math.abs(t.height-this.targetRect.height)>.1,s=this.mode==="overlay"&&(Math.abs(t.top-this.targetRect.top)>.1||Math.abs(t.left-this.targetRect.left)>.1);r?(this.targetRect=t,this.setSize(this.targetRect.width,this.targetRect.height),this.updateCanvasLayout()):s?(this.targetRect=t,this.updateCanvasLayout()):this.targetRect=t,this.renderOrder=0;const n=new Set;this.reconcileNode(e,n);for(const[a,o]of this.meshMap.entries())n.has(a)||(this.scene.remove(o),o.geometry.dispose(),o.material instanceof h.Material&&o.material.dispose(),this.meshMap.delete(a))}reconcileNode(e,t){var s;t.add(e.element);let r=this.meshMap.get(e.element);if(!r){const n=new h.PlaneGeometry(1,1);let a;a=I.create("BOX",e.styles,"",e.rect.width,e.rect.height,this.qualityFactor,e.isTraveler?(s=this.renderTarget)==null?void 0:s.texture:void 0),r=new h.Mesh(n,a),e.type==="TEXT"&&(r.name="BG_MESH"),this.scene.add(r),this.meshMap.set(e.element,r)}if(r.userData.domRect=e.rect,this.updateMeshProperties(r,e),this.updateMeshLayers(r,e),e.isTraveler&&r.layers.enable(28),e.type==="BOX")for(const n of e.children)this.reconcileNode(n,t);else e.type==="TEXT"&&this.reconcileTextChild(r,e)}reconcileTextChild(e,t){var o,l;let r=e.children.find(c=>c.name==="TEXT_CHILD");const s=JSON.stringify(t.textStyles),n=(o=r==null?void 0:r.userData)==null?void 0:o.styleHash;if(!r||t.dirtyMask&L||s!==n){r&&((l=r.material.map)==null||l.dispose(),r.geometry.dispose(),e.remove(r));const c=I.create("TEXT",t.textStyles,t.textContent||"",t.rect.width,t.rect.height,this.qualityFactor),p=new h.PlaneGeometry(1,1);r=new h.Mesh(p,c),r.name="TEXT_CHILD",r.userData={styleHash:s},this.updateMeshLayers(r,t),r.position.z=.005,e.add(r)}if(r){const c=e.userData.domRect,p=c.x+c.width/2,f=c.y+c.height/2,m=t.rect.x+t.rect.width/2,v=t.rect.y+t.rect.height/2,b=m-p,y=-(v-f);r.position.set(b,y,.005)}}updateMeshProperties(e,t){var v;const{rect:r,styles:s}=t,n=this.renderer.getPixelRatio(),a=this.renderer.domElement.width/n,o=this.renderer.domElement.height/n;e.scale.set(r.width,r.height,1);const l=.001;this.renderOrder++;const c=this.targetRect.left+window.scrollX,p=this.targetRect.top+window.scrollY,f=r.x-c,m=r.y-p;e.position.set(f-a/2+r.width/2,-m+o/2-r.height/2,s.zIndex+this.renderOrder*l),I.update(e.material,"BOX",t.styles,"",t.rect.width,t.rect.height,this.qualityFactor,t.isTraveler?(v=this.renderTarget)==null?void 0:v.texture:void 0)}updateMeshLayers(e,t){const r=(1-(t.visibility&R))*30;e.layers.set(r),t.visibility===(R|E)&&e.layers.enable(29)}captureRenderTarget(){const e=[];for(const l of this.meshMap.values())l.layers.mask&1<<28&&e.push(l);if(e.length===0)return;const t=new h.Color,r=this.renderer.getClearAlpha();this.renderer.getClearColor(t),this.renderer.setClearColor(0,0),this.renderer.setRenderTarget(this.renderTarget),this.renderer.clear(),this.renderer.autoClear=!1,this.renderer.setScissorTest(!0),this.camera.layers.set(29);const s=new h.Vector3,n=this.targetRect.width,a=this.targetRect.height,o=this.renderer.getPixelRatio();for(const l of e){s.setFromMatrixPosition(l.matrixWorld),s.project(this.camera);const c=(s.x+1)/2*n,p=(s.y+1)/2*a,f=l.scale.x,m=l.scale.y,v=c-f/2,b=p-m/2,y=v*this.qualityFactor/o,_=b*this.qualityFactor/o,z=f*this.qualityFactor/o,g=m*this.qualityFactor/o;this.renderer.setScissor(y,_,z,g),this.renderer.render(this.scene,this.camera)}this.renderer.setScissorTest(!1),this.renderer.autoClear=!0,this.renderer.setRenderTarget(null),this.camera.layers.set(28),this.renderer.setClearColor(t,r)}render(){this.renderTarget&&this.captureRenderTarget(),this.renderer.render(this.scene,this.camera)}showScissoredRenderTarget(){if(!this.renderTarget)return;const e=this.targetRect.width,t=this.targetRect.height,r=new h.PlaneGeometry(e,t),s=new h.MeshBasicMaterial({map:this.renderTarget.texture,side:h.DoubleSide,transparent:!0,opacity:.8}),n=new h.Mesh(r,s);n.position.set(0,0,90),n.layers.set(28),this.scene.add(n)}}function V(i){const e=document.createRange();e.selectNodeContents(i);const t=e.getBoundingClientRect();return{left:t.left,top:t.top,width:t.width,height:t.height}}function Z(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 U(i,e=B|O|A|L|D,t,r){if(i.nodeType===Node.TEXT_NODE){const g=i;if(!g.textContent||!g.textContent.trim())return null;const x=g.textContent.replace(/\s+/g," ").trim();if(x.length===0)return null;const C=V(g);if(C.width===0||C.height===0)return null;const k=g.parentElement,N=k?window.getComputedStyle(k):null;return N?{id:Math.random().toString(36).substring(2,9),type:"TEXT",element:g,rect:{x:C.left+window.scrollX,y:C.top+window.scrollY,width:C.width,height:C.height},styles:{backgroundColor:"transparent",opacity:parseFloat(N.opacity),zIndex:0,borderRadius:"0px",borderColor:"transparent",borderWidth:"0px"},textContent:x,textStyles:Z(N),dirtyMask:e,visibility:t,isTraveler:!1,children:[]}:null}const s=i,n=s.dataset.mirageFilter;let a=t,o=t;if(n){const g=new Set(n.split(/\s+/));for(const x of g)if(!X.includes(x))throw new Error(`[MirageEngine] Invalid filter token: '${x}'. Expected one of: 'include-tree', 'exclude-tree', 'include-self', 'exclude-self', 'end'.`);if(g.has("end"))return null;if(g.has("include-tree")&&g.has("exclude-tree"))throw new Error("[MirageEngine] Conflicting filters: 'include-tree' and 'exclude-tree' cannot be used together on the same element.");if(g.has("include-self")&&g.has("exclude-self"))throw new Error("[MirageEngine] Conflicting filters: 'include-self' and 'exclude-self' cannot be used together on the same element.");g.has("include-tree")?a=a|R:g.has("exclude-tree")&&(a=a&~R),o=a,g.has("include-self")?o=o|R:g.has("exclude-self")&&(o=o&~R)}o=o|t&E;const l=s.dataset.mirageTravel;let c=!1;l&&new Set(l.split(/\s+/)).has("traveler")&&(o=o&~E,a=a&~E,c=!0);const p=s.getBoundingClientRect(),f=window.getComputedStyle(s);if(p.width===0||p.height===0||f.display==="none")return null;let m=s.getAttribute("data-mid");m||(m=Math.random().toString(36).substring(2,11),s.setAttribute("data-mid",m));const v=parseInt(f.zIndex),b={backgroundColor:f.backgroundColor,opacity:parseFloat(f.opacity),zIndex:isNaN(v)?0:v,borderRadius:f.borderRadius,borderColor:f.borderColor,borderWidth:f.borderWidth};let y,_;const z=[];return Array.from(s.childNodes).forEach(g=>{const x=g.nodeType===Node.TEXT_NODE?o:a,C=U(g,e,x);C&&z.push(C)}),{id:m,type:"BOX",element:s,rect:{x:p.left+window.scrollX,y:p.top+window.scrollY,width:p.width,height:p.height},styles:b,textContent:y,textStyles:_,dirtyMask:e,visibility:o,isTraveler:c,children:z}}class Q{constructor(e,t,r){d(this,"target");d(this,"renderer");d(this,"filter");d(this,"observer");d(this,"isDomDirty",!1);d(this,"isRunning",!1);d(this,"isTravelEnabled",!1);d(this,"pendingMask",M);d(this,"mutationTimer",null);d(this,"cssTimer",null);d(this,"resizeConfig");d(this,"resizeTimer",null);d(this,"isResizing",!1);d(this,"onTransitionFinished",e=>{this.target.contains(e.target)&&this.mutationTimer===null&&(this.cssTimer&&clearTimeout(this.cssTimer),this.pendingMask|=B|O,this.cssTimer=window.setTimeout(()=>{this.isDomDirty=!0,this.cssTimer=null},50))});d(this,"onWindowResize",()=>{if(!this.resizeConfig.enabled){this.isDomDirty=!0;return}this.isResizing||(this.isResizing=!0,this.resizeConfig.onStart&&this.resizeConfig.onStart()),this.resizeTimer&&clearTimeout(this.resizeTimer),this.resizeTimer=window.setTimeout(()=>{this.isDomDirty=!0,this.resizeConfig.onEnd&&this.resizeConfig.onEnd(),this.isResizing=!1,this.resizeTimer=null},this.resizeConfig.delay)});d(this,"renderLoop",()=>{this.isRunning&&(this.isDomDirty&&this.forceUpdateScene(),this.renderer.render(),requestAnimationFrame(this.renderLoop))});var n,a;this.target=e,this.renderer=t,this.filter=r.filter;const s=(n=r.resizeDebounce)!=null?n:!0;s===!1?this.resizeConfig={enabled:!1,delay:0}:s===!0?this.resizeConfig={enabled:!0,delay:150}:this.resizeConfig={enabled:!0,delay:(a=s.delay)!=null?a:150,onStart:s.onStart,onEnd:s.onEnd},this.observer=new MutationObserver(o=>{let l=M;for(const c of o)c.type==="childList"?l|=D:c.type==="attributes"&&(c.attributeName==="style"||c.attributeName==="class")&&(l|=B|O);if(l!==M){if(this.pendingMask|=l,l&D){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=document.querySelector("[data-mirage-travel='traveler']")!==null;e&&!this.isTravelEnabled&&(this.isTravelEnabled=!0,this.renderer.createRenderTarget());const t=U(this.target,this.pendingMask,e?R|E:R,this.filter);t&&this.renderer.syncScene(t),this.pendingMask=M}}class K{constructor(e,t){d(this,"renderer");d(this,"syncer");d(this,"target");var s,n,a;this.target=e;let r;if(t.mode==="duplicate"?r=(n=(s=t.container)!=null?s:this.target.parentElement)!=null?n:void 0:r=(a=this.target.parentElement)!=null?a:void 0,!r)throw new Error("[Mirage] Cannot find a container (parent or option).");this.renderer=new J(this.target,t,r),this.renderer.mount(),this.syncer=new Q(this.target,this.renderer,t)}start(){this.syncer.start()}stop(){this.syncer.stop()}dispose(){this.syncer.stop(),this.renderer.dispose()}}u.ALLOWED_FILTERS=X,u.DIRTY_CONTENT=L,u.DIRTY_NONE=M,u.DIRTY_RECT=B,u.DIRTY_STRUCTURE=D,u.DIRTY_STYLE=O,u.DIRTY_ZINDEX=A,u.EXCLUDED=q,u.Engine=K,u.SYSTEM_LAYER=E,u.USER_LAYER=R,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})});
@@ -1,15 +1,21 @@
1
+ import { CoreConfig } from '../types/config';
1
2
  import { Renderer } from '../renderer/Renderer';
2
3
 
3
4
  export declare class Syncer {
4
5
  private target;
5
6
  private renderer;
7
+ private filter;
6
8
  private observer;
7
9
  private isDomDirty;
8
10
  private isRunning;
11
+ private isTravelEnabled;
9
12
  private pendingMask;
10
13
  private mutationTimer;
11
14
  private cssTimer;
12
- constructor(target: HTMLElement, renderer: Renderer);
15
+ private resizeConfig;
16
+ private resizeTimer;
17
+ private isResizing;
18
+ constructor(target: HTMLElement, renderer: Renderer, config: CoreConfig);
13
19
  start(): void;
14
20
  stop(): void;
15
21
  private clearTimers;
@@ -1,3 +1,4 @@
1
- import { SceneNode } from '../types';
1
+ import { SceneNode, Visibility } from '../types';
2
+ import { FilterConfig } from '../types/config';
2
3
 
3
- export declare function extractSceneGraph(sourceNode: HTMLElement | Node, initialMask?: number): SceneNode | null;
4
+ export declare function extractSceneGraph(sourceNode: HTMLElement | Node, initialMask: number | undefined, inheritedFlow: Visibility, filterConfig?: FilterConfig): SceneNode | null;
@@ -5,15 +5,16 @@ export declare class Renderer {
5
5
  private readonly scene;
6
6
  private readonly camera;
7
7
  private readonly renderer;
8
+ private renderTarget;
8
9
  private renderOrder;
9
- private textQualityFactor;
10
+ private qualityFactor;
10
11
  private mode;
11
- private customZIndex;
12
12
  private target;
13
13
  private mountContainer;
14
14
  private targetRect;
15
15
  private meshMap;
16
16
  constructor(target: HTMLElement, config: CoreConfig, mountContainer: HTMLElement);
17
+ createRenderTarget(): void;
17
18
  private applyTextQuality;
18
19
  mount(): void;
19
20
  private updateCanvasLayout;
@@ -23,5 +24,8 @@ export declare class Renderer {
23
24
  private reconcileNode;
24
25
  private reconcileTextChild;
25
26
  private updateMeshProperties;
27
+ private updateMeshLayers;
28
+ private captureRenderTarget;
26
29
  render(): void;
30
+ showScissoredRenderTarget(): void;
27
31
  }
@@ -1,4 +1,5 @@
1
1
  import { TextStyles, BoxStyles } from '../../packages/painter/src/index.ts';
2
+ import { Visibility } from './flags';
2
3
 
3
4
  export type NodeType = "BOX" | "TEXT";
4
5
  export interface NodeRect {
@@ -16,5 +17,7 @@ export interface SceneNode {
16
17
  textContent?: string;
17
18
  textStyles?: TextStyles;
18
19
  dirtyMask: number;
20
+ visibility: Visibility;
21
+ isTraveler: boolean;
19
22
  children: SceneNode[];
20
23
  }
@@ -1,11 +1,25 @@
1
- export type TextQuality = "low" | "medium" | "high" | number;
1
+ export type Quality = "low" | "medium" | "high" | number;
2
2
  export type MirageMode = "overlay" | "duplicate";
3
+ export interface FilterConfig {
4
+ includeTree?: string[];
5
+ excludeTree?: string[];
6
+ includeSelf?: string[];
7
+ excludeSelf?: string[];
8
+ end?: string[];
9
+ }
10
+ export interface ResizeConfig {
11
+ delay?: number;
12
+ onStart?: () => void;
13
+ onEnd?: () => void;
14
+ }
3
15
  interface BaseConfig {
4
16
  debug?: boolean;
5
- textQuality?: TextQuality;
17
+ quality?: Quality;
6
18
  style?: {
7
19
  zIndex?: string;
8
20
  };
21
+ filter?: FilterConfig;
22
+ resizeDebounce?: boolean | ResizeConfig;
9
23
  }
10
24
  export interface OverlayConfig extends BaseConfig {
11
25
  mode?: "overlay";
@@ -4,3 +4,8 @@ export declare const DIRTY_STYLE: number;
4
4
  export declare const DIRTY_ZINDEX: number;
5
5
  export declare const DIRTY_STRUCTURE: number;
6
6
  export declare const DIRTY_CONTENT: number;
7
+ export declare const USER_LAYER: number;
8
+ export declare const SYSTEM_LAYER: number;
9
+ export declare const EXCLUDED = 0;
10
+ export type Visibility = 0 | 1 | 2 | 3;
11
+ export declare const ALLOWED_FILTERS: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirage-engine/core",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "peerDependencies": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@types/three": "^0.181.0",
16
- "@mirage-engine/painter": "0.2.3"
16
+ "@mirage-engine/painter": "0.3.0"
17
17
  },
18
18
  "private": false,
19
19
  "publishConfig": {
@@ -27,7 +27,7 @@ export class Engine {
27
27
 
28
28
  this.renderer.mount();
29
29
 
30
- this.syncer = new Syncer(this.target, this.renderer);
30
+ this.syncer = new Syncer(this.target, this.renderer, config);
31
31
  }
32
32
 
33
33
  public start() {
@@ -1,3 +1,4 @@
1
+ import { CoreConfig } from "../types/config";
1
2
  import { Renderer } from "../renderer/Renderer";
2
3
  import { extractSceneGraph } from "../dom/Extractor";
3
4
  import {
@@ -5,24 +6,57 @@ import {
5
6
  DIRTY_RECT,
6
7
  DIRTY_STRUCTURE,
7
8
  DIRTY_STYLE,
9
+ USER_LAYER,
10
+ SYSTEM_LAYER,
11
+ Visibility,
8
12
  } from "../types";
9
13
 
14
+ interface InternalResizeConfig {
15
+ enabled: boolean;
16
+ delay: number;
17
+ onStart?: () => void;
18
+ onEnd?: () => void;
19
+ }
20
+
10
21
  export class Syncer {
11
22
  private target: HTMLElement;
12
23
  private renderer: Renderer;
24
+ private filter: CoreConfig["filter"];
25
+
13
26
  private observer: MutationObserver;
14
27
 
15
28
  private isDomDirty: boolean = false;
16
29
  private isRunning: boolean = false;
30
+ private isTravelEnabled: boolean = false;
17
31
 
18
32
  private pendingMask: number = DIRTY_NONE;
19
33
 
20
34
  private mutationTimer: number | null = null;
21
35
  private cssTimer: number | null = null;
22
36
 
23
- constructor(target: HTMLElement, renderer: Renderer) {
37
+ private resizeConfig: InternalResizeConfig;
38
+ private resizeTimer: number | null = null;
39
+ private isResizing: boolean = false;
40
+
41
+ constructor(target: HTMLElement, renderer: Renderer, config: CoreConfig) {
24
42
  this.target = target;
25
43
  this.renderer = renderer;
44
+ this.filter = config.filter;
45
+
46
+ // Resize Debounce Configuration
47
+ const debounceOpt = config.resizeDebounce ?? true;
48
+ if (debounceOpt === false) {
49
+ this.resizeConfig = { enabled: false, delay: 0 };
50
+ } else if (debounceOpt === true) {
51
+ this.resizeConfig = { enabled: true, delay: 150 };
52
+ } else {
53
+ this.resizeConfig = {
54
+ enabled: true,
55
+ delay: debounceOpt.delay ?? 150,
56
+ onStart: debounceOpt.onStart,
57
+ onEnd: debounceOpt.onEnd,
58
+ };
59
+ }
26
60
 
27
61
  this.observer = new MutationObserver((mutations) => {
28
62
  let currentMask = DIRTY_NONE;
@@ -80,6 +114,8 @@ export class Syncer {
80
114
 
81
115
  this.forceUpdateScene();
82
116
  this.renderLoop();
117
+ // for debugging
118
+ // this.renderer.showScissoredRenderTarget();
83
119
  }
84
120
 
85
121
  public stop() {
@@ -120,13 +156,45 @@ export class Syncer {
120
156
  };
121
157
 
122
158
  private onWindowResize = () => {
123
- this.renderer.setSize(window.innerWidth, window.innerHeight);
124
- this.isDomDirty = true;
159
+ if (!this.resizeConfig.enabled) {
160
+ this.isDomDirty = true;
161
+ return;
162
+ }
163
+
164
+ if (!this.isResizing) {
165
+ this.isResizing = true;
166
+ if (this.resizeConfig.onStart) this.resizeConfig.onStart();
167
+ }
168
+
169
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
170
+
171
+ this.resizeTimer = window.setTimeout(() => {
172
+ this.isDomDirty = true;
173
+
174
+ if (this.resizeConfig.onEnd) this.resizeConfig.onEnd();
175
+ this.isResizing = false;
176
+ this.resizeTimer = null;
177
+ }, this.resizeConfig.delay);
125
178
  };
126
179
 
127
180
  private forceUpdateScene() {
128
181
  this.isDomDirty = false;
129
- const sceneGraph = extractSceneGraph(this.target, this.pendingMask);
182
+
183
+ const discoveredTraveler =
184
+ document.querySelector("[data-mirage-travel='traveler']") !== null;
185
+ if (discoveredTraveler && !this.isTravelEnabled) {
186
+ this.isTravelEnabled = true;
187
+ this.renderer.createRenderTarget();
188
+ }
189
+
190
+ const sceneGraph = extractSceneGraph(
191
+ this.target,
192
+ this.pendingMask,
193
+ (discoveredTraveler
194
+ ? USER_LAYER | SYSTEM_LAYER
195
+ : USER_LAYER) as Visibility,
196
+ this.filter,
197
+ );
130
198
 
131
199
  if (sceneGraph) {
132
200
  this.renderer.syncScene(sceneGraph);
@@ -5,9 +5,14 @@ import {
5
5
  DIRTY_ZINDEX,
6
6
  DIRTY_STRUCTURE,
7
7
  SceneNode,
8
+ Visibility,
9
+ USER_LAYER,
10
+ SYSTEM_LAYER,
11
+ ALLOWED_FILTERS,
8
12
  } from "../types";
9
13
 
10
14
  import { BoxStyles, TextStyles } from "@mirage-engine/painter";
15
+ import { FilterConfig } from "../types/config";
11
16
 
12
17
  // Helper function: getTextNodeRect, isValidTextNode, isLeafTextElement, extractTextStyles
13
18
 
@@ -50,12 +55,13 @@ export function extractSceneGraph(
50
55
  DIRTY_STYLE |
51
56
  DIRTY_ZINDEX |
52
57
  DIRTY_CONTENT |
53
- DIRTY_STRUCTURE
58
+ DIRTY_STRUCTURE,
59
+ inheritedFlow: Visibility,
60
+ filterConfig?: FilterConfig,
54
61
  ): SceneNode | null {
55
62
  // Check text node
56
63
  if (sourceNode.nodeType === Node.TEXT_NODE) {
57
64
  const textNode = sourceNode as Text;
58
-
59
65
  // empthy text check
60
66
  if (!textNode.textContent || !textNode.textContent.trim()) return null;
61
67
  const normalizedText = textNode.textContent.replace(/\s+/g, " ").trim();
@@ -93,11 +99,80 @@ export function extractSceneGraph(
93
99
  textContent: normalizedText,
94
100
  textStyles: extractTextStyles(computed),
95
101
  dirtyMask: initialMask,
102
+ visibility: inheritedFlow,
103
+ isTraveler: false,
96
104
  children: [],
97
105
  };
98
106
  }
99
107
 
100
108
  const element = sourceNode as HTMLElement;
109
+ // [[Filter]] data attribute based filtering
110
+ const filterData = element.dataset.mirageFilter;
111
+ let visibleFlow = inheritedFlow;
112
+ let visibleFlag = inheritedFlow;
113
+ if (filterData) {
114
+ const filterSet = new Set(filterData.split(/\s+/));
115
+ // error check
116
+ for (const token of filterSet) {
117
+ if (!ALLOWED_FILTERS.includes(token)) {
118
+ throw new Error(
119
+ `[MirageEngine] Invalid filter token: '${token}'. ` +
120
+ `Expected one of: 'include-tree', 'exclude-tree', 'include-self', 'exclude-self', 'end'.`,
121
+ );
122
+ }
123
+ }
124
+
125
+ if (filterSet.has("end")) return null;
126
+
127
+ // error check
128
+ if (filterSet.has("include-tree") && filterSet.has("exclude-tree")) {
129
+ throw new Error(
130
+ `[MirageEngine] Conflicting filters: 'include-tree' and 'exclude-tree' cannot be used together on the same element.`,
131
+ );
132
+ }
133
+ if (filterSet.has("include-self") && filterSet.has("exclude-self")) {
134
+ throw new Error(
135
+ `[MirageEngine] Conflicting filters: 'include-self' and 'exclude-self' cannot be used together on the same element.`,
136
+ );
137
+ }
138
+
139
+ if (filterSet.has("include-tree")) {
140
+ visibleFlow = (visibleFlow | USER_LAYER) as Visibility;
141
+ } else if (filterSet.has("exclude-tree")) {
142
+ visibleFlow = (visibleFlow & ~USER_LAYER) as Visibility;
143
+ }
144
+
145
+ visibleFlag = visibleFlow;
146
+
147
+ if (filterSet.has("include-self")) {
148
+ visibleFlag = (visibleFlag | USER_LAYER) as Visibility;
149
+ } else if (filterSet.has("exclude-self")) {
150
+ visibleFlag = (visibleFlag & ~USER_LAYER) as Visibility;
151
+ }
152
+ }
153
+
154
+ // [[filter]] class based filtering
155
+ // [Filter] end
156
+ // if (filterConfig && filterConfig.end && filterConfig.end.length > 0) {
157
+ // const isEnd = filterConfig.end.some((cls) =>
158
+ // element.classList.contains(cls),
159
+ // );
160
+ // if (isEnd) return null;
161
+ // }
162
+
163
+ visibleFlag = (visibleFlag | (inheritedFlow & SYSTEM_LAYER)) as Visibility;
164
+
165
+ const travelData = element.dataset.mirageTravel;
166
+ let isTraveler = false;
167
+ if (travelData) {
168
+ const travelSet = new Set(travelData.split(/\s+/));
169
+ if (travelSet.has("traveler")) {
170
+ visibleFlag = (visibleFlag & ~SYSTEM_LAYER) as Visibility;
171
+ visibleFlow = (visibleFlow & ~SYSTEM_LAYER) as Visibility;
172
+ isTraveler = true;
173
+ }
174
+ }
175
+
101
176
  const rect = element.getBoundingClientRect();
102
177
  const computed = window.getComputedStyle(element);
103
178
 
@@ -127,8 +202,14 @@ export function extractSceneGraph(
127
202
  const children: SceneNode[] = [];
128
203
 
129
204
  Array.from(element.childNodes).forEach((child) => {
130
- // Recurring
131
- const childNode = extractSceneGraph(child, initialMask);
205
+ const visibleFlowToPass =
206
+ child.nodeType === Node.TEXT_NODE ? visibleFlag : visibleFlow;
207
+ const childNode = extractSceneGraph(
208
+ child,
209
+ initialMask,
210
+ visibleFlowToPass,
211
+ filterConfig,
212
+ );
132
213
  if (childNode) {
133
214
  children.push(childNode);
134
215
  }
@@ -148,6 +229,8 @@ export function extractSceneGraph(
148
229
  textContent,
149
230
  textStyles,
150
231
  dirtyMask: initialMask,
232
+ visibility: visibleFlag,
233
+ isTraveler: isTraveler,
151
234
  children,
152
235
  };
153
236
  }