@mirage-engine/painter 0.4.0 → 1.0.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,85 +1,145 @@
1
- (function(s,v){typeof exports=="object"&&typeof module<"u"?v(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],v):(s=typeof globalThis<"u"?globalThis:s||self,v(s.MiragePainter={},s.THREE))})(this,function(s,v){"use strict";function S(e){const r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const o in e)if(o!=="default"){const i=Object.getOwnPropertyDescriptor(e,o);Object.defineProperty(r,o,i.get?i:{enumerable:!0,get:()=>e[o]})}}return r.default=e,Object.freeze(r)}const c=S(v);function E(e,r,o){const i=r.split(`
2
- `),t=[];return i.forEach(n=>{const a=n.split(" ");let l=a[0];for(let u=1;u<a.length;u++){const d=a[u];e.measureText(l+" "+d).width<o?l+=" "+d:(t.push(l),l=d)}t.push(l)}),t}function m(e,r,o,i,t=2){const n=document.createElement("canvas"),a=n.getContext("2d");if(!a)throw new Error("[Mirage] Failed to create canvas context");const u=(window.devicePixelRatio||1)*t;n.width=o*u,n.height=i*u,a.scale(u,u),a.font=r.font,a.fillStyle=r.color,a.textBaseline="top",a.globalAlpha=1;const d=E(a,e,o),x=r.lineHeight;d.forEach((b,B)=>{const R=B*x+2;let C=0;r.textAlign==="center"?C=o/2:r.textAlign==="right"&&(C=o),a.textAlign=r.textAlign,a.fillText(b,C,R)});const f=new c.CanvasTexture(n);return f.colorSpace=c.SRGBColorSpace,f.minFilter=c.LinearFilter,f.magFilter=c.LinearFilter,f.needsUpdate=!0,f}function p(e){return typeof e=="number"?e:parseFloat(e)||0}function h(e,r){if(!r){e.set(0,0,0,0);return}const o=r.split("/")[0].trim().split(/\s+/),i=p(o[0]),t=p(o[1]??o[0]),n=p(o[2]??o[0]),a=p(o[3]??o[1]??o[0]);e.set(i,t,n,a)}const M=`
3
- varying vec2 vUv;
4
- varying vec4 vScreenPos;
5
- void main() {
6
- vUv = uv;
7
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
8
- vScreenPos = gl_Position;
1
+ (function(d,h){typeof exports=="object"&&typeof module<"u"?h(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],h):(d=typeof globalThis<"u"?globalThis:d||self,h(d.MiragePainter={},d.THREE))})(this,function(d,h){"use strict";var V=Object.defineProperty;var D=(d,h,m)=>h in d?V(d,h,{enumerable:!0,configurable:!0,writable:!0,value:m}):d[h]=m;var S=(d,h,m)=>(D(d,typeof h!="symbol"?h+"":h,m),m);function m(o){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(o){for(const t in o)if(t!=="default"){const n=Object.getOwnPropertyDescriptor(o,t);Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>o[t]})}}return e.default=o,Object.freeze(e)}const f=m(h);class y{static getBaseline(e){if(this.cache.has(e))return this.cache.get(e);const t=this.calculateBaselineFromDOM(e);return this.cache.set(e,t),t}static calculateBaselineFromDOM(e){if(typeof document>"u")return 0;const t=document.createElement("div"),n=document.createElement("span"),r=document.createElement("img");t.style.visibility="hidden",t.style.position="absolute",t.style.top="0px",t.style.left="0px",t.style.font=e,t.style.margin="0",t.style.padding="0",t.style.border="none",n.style.margin="0",n.style.padding="0",n.style.border="none",n.style.lineHeight="normal",r.width=1,r.height=1,r.style.verticalAlign="baseline",n.appendChild(document.createTextNode("Hidden Text")),t.appendChild(n),t.appendChild(r),document.body.appendChild(t);const i=r.offsetTop-n.offsetTop;return document.body.removeChild(t),i}}S(y,"cache",new Map);class I extends f.MeshBasicMaterial{constructor(t,n,r,i,a=2){super({transparent:!0,side:f.FrontSide,color:16777215});S(this,"canvas");S(this,"ctx");S(this,"qualityFactor");if(this.canvas=document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),!this.ctx)throw new Error("[Mirage] Failed to create canvas context");this.qualityFactor=a,this.map=new f.CanvasTexture(this.canvas),this.map.colorSpace=f.LinearSRGBColorSpace,this.map.minFilter=f.LinearFilter,this.map.magFilter=f.LinearFilter,this.updateText(t,n,r,i)}wrapText(t,n){const r=t.split(`
2
+ `),i=[];return r.forEach(a=>{const c=a.match(/[^\s\-]+\-?|\-|\s+/g)||[];if(c.length===0){i.push("");return}let l=c[0];for(let s=1;s<c.length;s++){const u=c[s];this.ctx.measureText(l+u).width<=n+2?l+=u:(l&&i.push(l),l=u.trimStart())}l&&i.push(l)}),i}updateText(t,n,r,i,a){a!==void 0&&(this.qualityFactor=a);const l=(window.devicePixelRatio||1)*this.qualityFactor,s=r*l,u=i*l;this.canvas.width!==s||this.canvas.height!==u?(this.canvas.width=s,this.canvas.height=u):this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.setTransform(l,0,0,l,0,0),this.ctx.font=n.font,this.ctx.fillStyle=n.color,this.ctx.textBaseline="alphabetic",this.ctx.globalAlpha=1;const b=this.wrapText(t,r),v=n.lineHeight,g=y.getBaseline(n.font);b.forEach((p,B)=>{const z=B*v+g;let O=0;n.textAlign==="center"?O=r/2:n.textAlign==="right"&&(O=r),this.ctx.textAlign=n.textAlign,this.ctx.fillText(p,O,z)}),this.map&&(this.map.needsUpdate=!0)}dispose(){this.map&&this.map.dispose(),super.dispose()}}function C(o){return typeof o=="number"?o:parseFloat(o)||0}function x(o){if(!o||o==="transparent")return{color:new f.Color(16777215),alpha:0};const e=o.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);if(e){const t=parseInt(e[1],10),n=parseInt(e[2],10),r=parseInt(e[3],10),i=e[4]!==void 0?parseFloat(e[4]):1;return{color:new f.Color(`rgb(${t}, ${n}, ${r})`),alpha:i}}return{color:new f.Color(o),alpha:1}}function M(o){const e=[];let t="",n=0;for(let r=0;r<o.length;r++){const i=o[r];if(i==="(")n++;else if(i===")")n--;else if(i===","&&n===0){e.push(t.trim()),t="";continue}t+=i}return t&&e.push(t.trim()),e}function R(o){if(!o||typeof o!="string"||!o.includes("linear-gradient"))return null;const e=o.match(/linear-gradient\((.*)\)/);if(!e)return null;const t=e[1],n=M(t);let r=Math.PI,i=0;const a=n[0].trim();if(a.startsWith("to "))a==="to top"?r=0:a==="to right"?r=Math.PI/2:a==="to bottom"?r=Math.PI:a==="to left"?r=Math.PI*1.5:a==="to top right"||a==="to right top"?r=Math.PI/4:a==="to bottom right"||a==="to right bottom"?r=Math.PI*.75:a==="to bottom left"||a==="to left bottom"?r=Math.PI*1.25:(a==="to top left"||a==="to left top")&&(r=Math.PI*1.75),i=1;else if(a.endsWith("deg")||a.endsWith("rad")||a.endsWith("turn")){const l=parseFloat(a);a.endsWith("deg")?r=l*(Math.PI/180):a.endsWith("rad")?r=l:a.endsWith("turn")&&(r=l*Math.PI*2),i=1}const c=[];for(let l=i;l<n.length&&!(c.length>=8);l++){const s=n[l].trim(),u=s.lastIndexOf(" ");let b=s,v=null;if(u!==-1&&!s.endsWith(")")){const p=s.substring(u+1);(p.endsWith("%")||p.endsWith("px")||!isNaN(parseFloat(p)))&&(b=s.substring(0,u).trim(),v=p)}const g=x(b);c.push({color:g.color,alpha:g.alpha,rawStop:v,stop:0})}if(c.length>0){for(let s=0;s<c.length;s++)c[s].rawStop!==null&&(c[s].stop=parseFloat(c[s].rawStop)/100);c[0].rawStop===null&&(c[0].stop=0),c.length>1&&c[c.length-1].rawStop===null&&(c[c.length-1].stop=1);let l=0;for(let s=1;s<c.length;s++)if(c[s].rawStop!==null||s===c.length-1){const u=s-l;if(u>1){const b=c[l].stop,g=(c[s].stop-b)/u;for(let p=1;p<u;p++)c[l+p].stop=b+g*p}l=s}}return{angle:r,stops:c}}const E=`varying vec2 vUv;
3
+ varying vec4 vScreenPos;
4
+ void main() {
5
+ vUv = uv;
6
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
7
+ vScreenPos = gl_Position;
8
+ }`,L=`varying vec2 vUv;
9
+ uniform vec2 uSize;
10
+ uniform vec4 uBorderRadius;
11
+ uniform float uBorderWidth;
12
+ uniform vec4 uBgColor;
13
+ uniform vec4 uBorderColor;
14
+ uniform float uOpacity;
15
+
16
+ uniform int uGradientCount;
17
+ uniform float uGradientAngle;
18
+ uniform vec4 uGradientColors[8];
19
+ uniform float uGradientStops[8];
20
+
21
+ #INJECT_DECLARATIONS
22
+
23
+ vec3 linearToSrgb(vec3 linearColor) {
24
+ vec3 linearPart = 12.92 * linearColor;
25
+ vec3 curvePart = 1.055 * pow(linearColor, vec3(1.0 / 2.4)) - 0.055;
26
+ vec3 switchCondition = step(vec3(0.0031308), linearColor);
27
+ return mix(linearPart, curvePart, switchCondition);
28
+ }
29
+
30
+ vec3 srgbToLinear(vec3 srgbColor) {
31
+ vec3 linearPart = srgbColor / 12.92;
32
+ vec3 curvePart = pow((srgbColor + vec3(0.055)) / 1.055, vec3(2.4));
33
+ vec3 switchCondition = step(vec3(0.04045), srgbColor);
34
+ return mix(linearPart, curvePart, switchCondition);
35
+ }
36
+
37
+ vec4 blendSrcOverInLinear(vec4 front, vec4 back) {
38
+ front.rgb = srgbToLinear(front.rgb);
39
+ back.rgb = srgbToLinear(back.rgb);
40
+ float aOut = front.a + back.a * (1.0 - front.a);
41
+ float safeAlpha = max(aOut, 0.0001);
42
+ vec3 cOut = (front.rgb * front.a + back.rgb * back.a * (1.0 - front.a)) / safeAlpha;
43
+ return vec4(linearToSrgb(cOut), aOut);
44
+ }
45
+
46
+ vec4 blendSrcOver(vec4 front, vec4 back) {
47
+ float aOut = front.a + back.a * (1.0 - front.a);
48
+ float safeAlpha = max(aOut, 0.0001);
49
+ vec3 cOut = (front.rgb * front.a + back.rgb * back.a * (1.0 - front.a)) / safeAlpha;
50
+ return vec4(cOut, aOut);
51
+ }
52
+
53
+ float sdRoundedBox(vec2 p, vec2 b, float r) {
54
+ vec2 q = abs(p) - b + r;
55
+ return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
56
+ }
57
+
58
+ float sdVisualBox(vec2 p, vec2 b, float r) {
59
+ vec2 q = abs(p) - b + r;
60
+ float n = 2.01;
61
+ float d = pow(pow(max(q.x, 0.0), n) + pow(max(q.y, 0.0), n), 1.0/n) - r;
62
+ return min(max(q.x, q.y), 0.0) + d;
63
+ }
64
+
65
+ vec4 calculateGradientLayer(vec2 uv) {
66
+ if (uGradientCount < 2) return uGradientColors[0];
67
+
68
+ vec2 dir = vec2(sin(uGradientAngle), cos(uGradientAngle));
69
+ vec2 p = (uv - 0.5) * uSize;
70
+ float proj = dot(p, dir);
71
+ float L = abs(uSize.x * dir.x) + abs(uSize.y * dir.y);
72
+ float t = clamp((proj / L) + 0.5, 0.0, 1.0);
73
+
74
+ vec4 color = uGradientColors[0];
75
+ for (int i = 1; i < 8; i++) {
76
+ if (i >= uGradientCount) break;
77
+ float prevStop = uGradientStops[i-1];
78
+ float currStop = uGradientStops[i];
79
+
80
+ float factor = clamp((t - prevStop) / (currStop - prevStop + 0.00001), 0.0, 1.0);
81
+ color = mix(color, uGradientColors[i], factor);
9
82
  }
10
- `,w=`
83
+ // return vec4(linearToSrgb(color.rgb), color.a);
84
+ return vec4(color);
85
+ }
11
86
 
12
- varying vec2 vUv;
13
-
14
- uniform vec2 uSize;
15
- uniform vec4 uBorderRadius;
16
- uniform float uBorderWidth;
17
- uniform vec3 uBgColor;
18
- uniform vec3 uBorderColor;
19
- uniform float uOpacity;
20
- uniform float uBgOpacity;
21
- uniform float uBorderOpacity;
87
+ void main() {
88
+ vec2 p = (vUv - 0.5) * uSize;
89
+ vec2 halfSize = uSize * 0.5;
22
90
 
23
- #INJECT_DECLARATIONS
91
+ #INJECT_UV_MODIFIER
24
92
 
25
- float sdRoundedBox(vec2 p, vec2 b, float r) {
26
- vec2 q = abs(p) - b + r;
93
+ // color decision pipeline
94
+ vec4 baseColor = vec4(uBgColor.rgb, uBgColor.a);
27
95
 
28
- return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
96
+ if (uGradientCount > 0) {
97
+ vec4 gradColor = calculateGradientLayer(vUv);
98
+ baseColor = blendSrcOver(gradColor, baseColor);
29
99
  }
30
100
 
31
- void main() {
32
- vec2 p = (vUv - 0.5) * uSize;
33
- vec2 halfSize = uSize * 0.5;
101
+ #INJECT_BASE_COLOR
34
102
 
35
- #INJECT_UV_MODIFIER
36
-
37
- // color decision pipeline
38
- vec4 baseColor = vec4(uBgColor, uBgOpacity);
103
+ // Hybrid SDF
104
+ vec2 xRadii = mix(uBorderRadius.xw, uBorderRadius.yz, step(0.0, p.x));
105
+ float r = mix(xRadii.y, xRadii.x, step(0.0, p.y));
106
+ r = min(r, min(halfSize.x, halfSize.y));
39
107
 
40
- #INJECT_BASE_COLOR
108
+ float d = sdRoundedBox(p, halfSize, r);
41
109
 
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);
110
+ float d_smooth = sdVisualBox(p, halfSize, r);
111
+ float d_visual = d_smooth / fwidth(d_smooth);
46
112
 
47
- float aa = 1.0;
48
- float bgMask = 1.0 - smoothstep(0.0, aa, d);
113
+ // rendering pipeline
114
+ float bgMask = 1.0 - smoothstep(-0.5, 0.5, d_visual);
49
115
 
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;
55
- }
116
+ float halfBorder = uBorderWidth * 0.5;
117
+ float borderD = abs(d + halfBorder) - halfBorder;
118
+ float borderAlpha = (1.0 - smoothstep(0.0, 1.0, borderD)) * uBorderColor.a;
119
+ if (uBorderWidth <= 0.01) {
120
+ borderAlpha = 0.0;
121
+ }
56
122
 
57
- // final blending (border + background)
58
- float aFront = borderAlpha;
59
- float aBack = baseColor.a;
60
- float aOut = aFront + aBack * (1.0 - aFront);
123
+ // final blending (border + background) using blendSrcOver
124
+ vec4 borderLayer = vec4(uBorderColor.rgb, borderAlpha);
125
+ vec4 finalColor = blendSrcOver(borderLayer, baseColor);
126
+ // final color control (Tint, Noise)
127
+ #INJECT_COLOR_MODIFIER
61
128
 
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);
129
+ float finalOpacity = finalColor.a * bgMask * uOpacity;
130
+ if (finalOpacity < 0.001) discard;
65
131
 
66
- // final color control (Tint, Noise)
67
- #INJECT_COLOR_MODIFIER
132
+ gl_FragColor = vec4(finalColor.rgb, finalOpacity);
68
133
 
69
- float finalOpacity = finalColor.a * bgMask * uOpacity;
70
- if (finalOpacity < 0.001) discard;
71
134
 
72
- gl_FragColor = vec4(finalColor.rgb, finalOpacity);
135
+ // #include <colorspace_fragment>
136
+ }
137
+ `,_=`uniform sampler2D uTexture;
138
+ varying vec4 vScreenPos;
139
+ uniform vec2 uTextureRepeat;
140
+ uniform vec2 uTextureOffset;`,F=`vec2 screenUv = (vScreenPos.xy / vScreenPos.w) * 0.5 + 0.5;
141
+ vec2 resultUv = screenUv;
73
142
 
74
- #include <colorspace_fragment>
75
- }
76
- `;function g(e){if(!e||e==="transparent")return{color:new c.Color(16777215),alpha:0};const r=e.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);if(r){const o=parseInt(r[1],10),i=parseInt(r[2],10),t=parseInt(r[3],10),n=r[4]!==void 0?parseFloat(r[4]):1;return{color:new c.Color(`rgb(${o}, ${i}, ${t})`),alpha:n}}return{color:new c.Color(e),alpha:1}}function O(e,r,o,i=null,t){const n=i!==null,a=n?`
77
- uniform sampler2D uTexture;
78
- varying vec4 vScreenPos;
79
- `:"",l=n?`
80
- vec2 screenUv = (vScreenPos.xy / vScreenPos.w) * 0.5 + 0.5;
81
- vec2 resultUv = screenUv;
82
- ${(t==null?void 0:t.uvModifier)||""}
83
- `:"",u=n?`
84
- baseColor = texture2D(uTexture, resultUv);
85
- `:"",d=(t==null?void 0:t.colorModifier)||"",x=w.replace("#INJECT_DECLARATIONS",a).replace("#INJECT_UV_MODIFIER",l).replace("#INJECT_BASE_COLOR",u).replace("#INJECT_COLOR_MODIFIER",d),f=g(e.backgroundColor),b=g(e.borderColor),B={uSize:{value:new c.Vector2(r,o)},uBorderRadius:{value:new c.Vector4(0,0,0,0)},uBorderWidth:{value:p(e.borderWidth)},uBgColor:{value:f.color},uBorderColor:{value:b.color},uOpacity:{value:e.opacity??1},uBgOpacity:{value:f.alpha},uBorderOpacity:{value:b.alpha},uTexture:{value:null}};return h(B.uBorderRadius.value,e.borderRadius),n&&(B.uTexture.value=i),new c.ShaderMaterial({uniforms:B,vertexShader:M,fragmentShader:x,transparent:!0,side:c.FrontSide})}function T(e,r,o,i,t){const n=g(r.backgroundColor),a=g(r.borderColor);e.uniforms.uSize.value.set(o,i),h(e.uniforms.uBorderRadius.value,r.borderRadius),e.uniforms.uBorderWidth.value=p(r.borderWidth),e.uniforms.uBgColor.value.copy(n.color),e.uniforms.uBorderColor.value.copy(a.color),e.uniforms.uOpacity.value=r.opacity??1,e.uniforms.uBgOpacity.value=n.alpha,e.uniforms.uBorderOpacity.value=a.alpha,e.uniforms.uTexture&&t!==void 0&&(e.uniforms.uTexture.value=t)}const y={create(e,r,o,i,t,n=2,a=null,l){if(e==="BOX")return O(r,i,t,a,l);if(e==="TEXT"){const u=m(o||"",r,i,t,n);return new c.MeshBasicMaterial({map:u,transparent:!0,side:c.FrontSide,color:16777215})}return new c.MeshBasicMaterial({visible:!1})},update(e,r,o,i,t,n,a=2,l){if(r==="BOX")T(e,o,t,n,l);else if(r==="TEXT"){const u=e;u.map&&u.map.dispose();const d=m(i||"",o,t,n,a);u.map=d,u.needsUpdate=!0}}};s.Painter=y,s.createBoxMaterial=O,s.createTextTexture=m,s.updateBoxMaterial=T,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
143
+ `,W=`vec4 texColor = texture2D(uTexture, resultUv);
144
+ baseColor = blendSrcOver(baseColor, texColor);`,k={vertexShader:E,fragmentShader:L},w={declChunk:_,uvChunk:F,baseColorChunk:W};function A(o,e,t,n=null,r){const i=n!==null||!!o.imageSrc,a=i?w.declChunk:"",c=o.isTraveler?w.uvChunk:`vec2 resultUv = vUv * uTextureRepeat + uTextureOffset;
145
+ `,l=i?c+((r==null?void 0:r.uvModifier)||""):"",s=i?w.baseColorChunk:"",u=(r==null?void 0:r.colorModifier)||"",b=k.fragmentShader.replace("#INJECT_DECLARATIONS",a).replace("#INJECT_UV_MODIFIER",l).replace("#INJECT_BASE_COLOR",s).replace("#INJECT_COLOR_MODIFIER",u),v=x(o.backgroundColor),g=x(o.borderColor),p={uSize:{value:new f.Vector2(e,t)},uBgColor:{value:new f.Vector4(v.color.r,v.color.g,v.color.b,v.alpha)},uBorderColor:{value:new f.Vector4(g.color.r,g.color.g,g.color.b,g.alpha)},uBorderRadius:{value:new f.Vector4(0,0,0,0)},uBorderWidth:{value:C(o.borderWidth)},uOpacity:{value:o.opacity??1},uTexture:{value:null},uTextureRepeat:{value:new f.Vector2(1,1)},uTextureOffset:{value:new f.Vector2(0,0)},uGradientCount:{value:0},uGradientAngle:{value:0},uGradientColors:{value:Array.from({length:8},()=>new f.Vector4(0,0,0,0))},uGradientStops:{value:new Float32Array(8)}};G(p.uBorderRadius.value,o.borderRadius),i&&(p.uTexture.value=n);const B=new f.ShaderMaterial({uniforms:p,vertexShader:k.vertexShader,fragmentShader:b,transparent:!0,side:f.FrontSide});return o.backgroundImage&&T(B,{backgroundImage:o.backgroundImage}),B}function P(o,e,t,n,r){const i=C(e.borderWidth);T(o,{width:t,height:n,borderRadius:e.borderRadius,borderWidth:i,backgroundColor:e.backgroundColor,borderColor:e.borderColor,opacity:e.opacity,texture:r,backgroundImage:e.backgroundImage})}function T(o,e){var n;if(e.width!==void 0&&e.height!==void 0&&o.uniforms.uSize.value.set(e.width,e.height),e.borderRadius!==void 0&&G(o.uniforms.uBorderRadius.value,e.borderRadius),e.borderWidth!==void 0&&(o.uniforms.uBorderWidth.value=e.borderWidth),e.backgroundColor!==void 0)if(Array.isArray(e.backgroundColor)){const r=o.uniforms.uBgColor.value.w;o.uniforms.uBgColor.value.set(e.backgroundColor[0],e.backgroundColor[1],e.backgroundColor[2],r)}else if(typeof e.backgroundColor=="string"){const r=x(e.backgroundColor);o.uniforms.uBgColor.value.set(r.color.r,r.color.g,r.color.b,r.alpha)}else{const r=o.uniforms.uBgColor.value.w;o.uniforms.uBgColor.value.set(e.backgroundColor.r,e.backgroundColor.g,e.backgroundColor.b,r)}if(e.borderColor!==void 0)if(Array.isArray(e.borderColor)){const r=o.uniforms.uBorderColor.value.w;o.uniforms.uBorderColor.value.set(e.borderColor[0],e.borderColor[1],e.borderColor[2],r)}else if(typeof e.borderColor=="string"){const r=x(e.borderColor);o.uniforms.uBorderColor.value.set(r.color.r,r.color.g,r.color.b,r.alpha)}else{const r=o.uniforms.uBorderColor.value.w;o.uniforms.uBorderColor.value.set(e.borderColor.r,e.borderColor.g,e.borderColor.b,r)}e.opacity!==void 0&&(o.uniforms.uOpacity.value=e.opacity),e.bgOpacity!==void 0&&(o.uniforms.uBgColor.value.w=e.bgOpacity),e.borderOpacity!==void 0&&(o.uniforms.uBorderColor.value.w=e.borderOpacity),o.uniforms.uTexture&&e.texture!==void 0&&(o.uniforms.uTexture.value=e.texture);const t=e.texture!==void 0?e.texture:(n=o.uniforms.uTexture)==null?void 0:n.value;if(t&&(t.image instanceof ImageBitmap||t.image instanceof HTMLImageElement||t.image instanceof HTMLCanvasElement)){const r=t.image.width,i=t.image.height,a=e.width??o.uniforms.uSize.value.x,c=e.height??o.uniforms.uSize.value.y;if(r&&i&&a&&c){const l=r/i,s=a/c;if(l>s){const u=s/l;o.uniforms.uTextureRepeat.value.set(u,1),o.uniforms.uTextureOffset.value.set((1-u)/2,0)}else{const u=l/s;o.uniforms.uTextureRepeat.value.set(1,u),o.uniforms.uTextureOffset.value.set(0,(1-u)/2)}}}else o.uniforms.uTextureRepeat&&(o.uniforms.uTextureRepeat.value.set(1,1),o.uniforms.uTextureOffset.value.set(0,0));if(e.backgroundImage!==void 0){const r=R(e.backgroundImage);if(r){o.uniforms.uGradientCount.value=r.stops.length,o.uniforms.uGradientAngle.value=r.angle;for(let i=0;i<8;i++)if(i<r.stops.length){const a=r.stops[i];o.uniforms.uGradientColors.value[i].set(a.color.r,a.color.g,a.color.b,a.alpha),console.log(a.color.r,a.color.g,a.color.b),o.uniforms.uGradientStops.value[i]=a.stop}else o.uniforms.uGradientColors.value[i].set(0,0,0,0),o.uniforms.uGradientStops.value[i]=1}else o.uniforms.uGradientCount.value=0}}function G(o,e){if(e==null){o.set(0,0,0,0);return}if(typeof e=="number"){o.set(e,e,e,e);return}if(Array.isArray(e)){o.set(e[0],e[1],e[2],e[3]);return}const t=e.split("/")[0].trim().split(/\s+/),n=C(t[0]),r=C(t[1]??t[0]),i=C(t[2]??t[0]),a=C(t[3]??t[1]??t[0]);o.set(n,r,i,a)}const U={create(o,e,t,n,r,i=2,a=null,c){return o==="BOX"?A(e,n,r,a,c):o==="TEXT"?new I(t||"",e,n,r,i):new f.MeshBasicMaterial({visible:!1})},update(o,e,t,n,r,i,a=2,c){e==="BOX"?P(o,t,r,i,c):e==="TEXT"&&o.updateText(n||"",t,r,i,a)},forceUpdateUniforms(o,e){T(o,e)}};d.Painter=U,d.TextGenerator=I,d.createBoxMaterial=A,d.parseColor=x,d.parseLinearGradient=R,d.parsePixelValue=C,d.setBoxUniforms=T,d.splitByComma=M,d.updateBoxMaterial=P,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,18 @@
1
+ import { BoxStyles, ShaderHooks } from '../types';
2
+ import * as THREE from "three";
3
+ export declare function createBoxMaterial(styles: BoxStyles, width: number, height: number, texture?: THREE.Texture | null, hooks?: ShaderHooks): THREE.ShaderMaterial;
4
+ export declare function updateBoxMaterial(material: THREE.ShaderMaterial, styles: BoxStyles, width: number, height: number, texture?: THREE.Texture | null): void;
5
+ export interface BoxUniformValues {
6
+ width?: number;
7
+ height?: number;
8
+ borderRadius?: string | number | [number, number, number, number];
9
+ borderWidth?: number;
10
+ backgroundColor?: THREE.Color | [number, number, number] | string;
11
+ borderColor?: THREE.Color | [number, number, number] | string;
12
+ opacity?: number;
13
+ bgOpacity?: number;
14
+ borderOpacity?: number;
15
+ texture?: THREE.Texture | null;
16
+ backgroundImage?: string;
17
+ }
18
+ export declare function setBoxUniforms(material: THREE.ShaderMaterial, values: BoxUniformValues): void;
@@ -1,6 +1,8 @@
1
- import { ShaderHooks } from './types';
1
+ import { BoxStyles, TextStyles, ShaderHooks } from './types';
2
+ import { BoxUniformValues } from './Box/BoxGenerator';
2
3
  import * as THREE from "three";
3
4
  export declare const Painter: {
4
- create(type: "BOX" | "TEXT", styles: any, content: string, width: number, height: number, quality?: number, texture?: THREE.Texture | null, shaderHooks?: ShaderHooks): THREE.Material;
5
- update(material: THREE.Material, type: "BOX" | "TEXT", styles: any, content: string, width: number, height: number, quality?: number, texture?: THREE.Texture | null): void;
5
+ create(type: "BOX" | "TEXT", styles: BoxStyles | TextStyles, content: string, width: number, height: number, quality?: number, texture?: THREE.Texture | null, shaderHooks?: ShaderHooks): THREE.Material;
6
+ update(material: THREE.Material, type: "BOX" | "TEXT", styles: BoxStyles | TextStyles, content: string, width: number, height: number, quality?: number, texture?: THREE.Texture | null): void;
7
+ forceUpdateUniforms(material: THREE.ShaderMaterial, values: BoxUniformValues): void;
6
8
  };
@@ -0,0 +1,10 @@
1
+ export declare class FontMetricsManager {
2
+ private static cache;
3
+ /**
4
+ * Calculates or retrieves the exact alphabetic baseline (in pixels) for the given font.
5
+ * @param font CSS font shorthand string (e.g., "400 16px Inter")
6
+ * @returns Baseline Y offset in pixels
7
+ */
8
+ static getBaseline(font: string): number;
9
+ private static calculateBaselineFromDOM;
10
+ }
@@ -0,0 +1,11 @@
1
+ import { TextStyles } from '../types';
2
+ import * as THREE from "three";
3
+ export declare class TextGenerator extends THREE.MeshBasicMaterial {
4
+ private canvas;
5
+ private ctx;
6
+ private qualityFactor;
7
+ constructor(text: string, styles: TextStyles, rectWidth: number, rectHeight: number, qualityFactor?: number);
8
+ private wrapText;
9
+ updateText(text: string, styles: TextStyles, rectWidth: number, rectHeight: number, qualityFactor?: number): void;
10
+ dispose(): void;
11
+ }
@@ -1,4 +1,5 @@
1
- export * from './TextGenerator';
1
+ export * from './Text/TextGenerator';
2
2
  export * from './types';
3
- export * from './BoxGenerator';
3
+ export * from './Box/BoxGenerator';
4
4
  export * from './Painter';
5
+ export * from './tools/parser';
@@ -0,0 +1,9 @@
1
+ export declare const BoxShader: {
2
+ vertexShader: string;
3
+ fragmentShader: string;
4
+ };
5
+ export declare const BoxChunk: {
6
+ declChunk: string;
7
+ uvChunk: string;
8
+ baseColorChunk: string;
9
+ };
@@ -0,0 +1,16 @@
1
+ import * as THREE from "three";
2
+ export declare function parsePixelValue(value: string | number): number;
3
+ export declare function parseColor(colorStr: string): {
4
+ color: THREE.Color;
5
+ alpha: number;
6
+ };
7
+ export declare function splitByComma(str: string): string[];
8
+ export declare function parseLinearGradient(value: string | null | undefined): {
9
+ angle: number;
10
+ stops: {
11
+ color: THREE.Color;
12
+ alpha: number;
13
+ rawStop: string | null;
14
+ stop: number;
15
+ }[];
16
+ } | null;
@@ -1,5 +1,6 @@
1
1
  export interface TextStyles {
2
2
  font: string;
3
+ fontSize: string;
3
4
  color: string;
4
5
  textAlign: CanvasTextAlign;
5
6
  textBaseline: CanvasTextBaseline;
@@ -9,6 +10,9 @@ export interface TextStyles {
9
10
  }
10
11
  export interface BoxStyles {
11
12
  backgroundColor: string;
13
+ backgroundImage: string;
14
+ imageSrc?: string;
15
+ isTraveler?: boolean;
12
16
  opacity: number;
13
17
  zIndex: number;
14
18
  borderRadius: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirage-engine/painter",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "peerDependencies": {
@@ -0,0 +1,286 @@
1
+ import * as THREE from "three";
2
+ import { BoxStyles, ShaderHooks } from "../types";
3
+ import {
4
+ parsePixelValue,
5
+ parseColor,
6
+ parseLinearGradient,
7
+ } from "../tools/parser";
8
+ import { BoxShader, BoxChunk } from "../shaders/index";
9
+
10
+ export function createBoxMaterial(
11
+ styles: BoxStyles,
12
+ width: number,
13
+ height: number,
14
+ texture: THREE.Texture | null = null,
15
+ hooks?: ShaderHooks,
16
+ ): THREE.ShaderMaterial {
17
+ const hasTexture = texture !== null || !!styles.imageSrc;
18
+
19
+ const declChunk = hasTexture ? BoxChunk.declChunk : "";
20
+ const baseUvCode = styles.isTraveler ? BoxChunk.uvChunk : "vec2 resultUv = vUv * uTextureRepeat + uTextureOffset;\n";
21
+ const uvChunk = hasTexture ? baseUvCode + (hooks?.uvModifier || "") : "";
22
+ const baseColorChunk = hasTexture ? BoxChunk.baseColorChunk : "";
23
+ const colorModChunk = hooks?.colorModifier || "";
24
+
25
+ const fragmentShader = BoxShader.fragmentShader
26
+ .replace("#INJECT_DECLARATIONS", declChunk)
27
+ .replace("#INJECT_UV_MODIFIER", uvChunk)
28
+ .replace("#INJECT_BASE_COLOR", baseColorChunk)
29
+ .replace("#INJECT_COLOR_MODIFIER", colorModChunk);
30
+
31
+ // uniform setting
32
+ const parsedBg = parseColor(styles.backgroundColor);
33
+ const parsedBorder = parseColor(styles.borderColor);
34
+ const uniforms = {
35
+ uSize: { value: new THREE.Vector2(width, height) },
36
+ uBgColor: {
37
+ value: new THREE.Vector4(
38
+ parsedBg.color.r,
39
+ parsedBg.color.g,
40
+ parsedBg.color.b,
41
+ parsedBg.alpha,
42
+ ),
43
+ },
44
+ uBorderColor: {
45
+ value: new THREE.Vector4(
46
+ parsedBorder.color.r,
47
+ parsedBorder.color.g,
48
+ parsedBorder.color.b,
49
+ parsedBorder.alpha,
50
+ ),
51
+ },
52
+ uBorderRadius: { value: new THREE.Vector4(0, 0, 0, 0) },
53
+ uBorderWidth: { value: parsePixelValue(styles.borderWidth) },
54
+ uOpacity: { value: styles.opacity ?? 1.0 },
55
+ uTexture: { value: null as THREE.Texture | null },
56
+ uTextureRepeat: { value: new THREE.Vector2(1.0, 1.0) },
57
+ uTextureOffset: { value: new THREE.Vector2(0.0, 0.0) },
58
+
59
+ uGradientCount: { value: 0 },
60
+ uGradientAngle: { value: 0 },
61
+ uGradientColors: {
62
+ value: Array.from({ length: 8 }, () => new THREE.Vector4(0, 0, 0, 0)),
63
+ },
64
+ uGradientStops: { value: new Float32Array(8) },
65
+ };
66
+ // border radius value initialize
67
+ setBorderRadius(uniforms.uBorderRadius.value, styles.borderRadius);
68
+
69
+ if (hasTexture) {
70
+ uniforms.uTexture.value = texture;
71
+ }
72
+
73
+ const material = new THREE.ShaderMaterial({
74
+ uniforms: uniforms,
75
+ vertexShader: BoxShader.vertexShader,
76
+ fragmentShader: fragmentShader,
77
+ transparent: true,
78
+ side: THREE.FrontSide, // for better performance
79
+ });
80
+
81
+ // Initial gradient setup
82
+ if (styles.backgroundImage) {
83
+ setBoxUniforms(material, { backgroundImage: styles.backgroundImage });
84
+ }
85
+
86
+ return material;
87
+ }
88
+
89
+ export function updateBoxMaterial(
90
+ material: THREE.ShaderMaterial,
91
+ styles: BoxStyles,
92
+ width: number,
93
+ height: number,
94
+ texture?: THREE.Texture | null,
95
+ ) {
96
+ const parsedBorderWidth = parsePixelValue(styles.borderWidth);
97
+ setBoxUniforms(material, {
98
+ width,
99
+ height,
100
+ borderRadius: styles.borderRadius,
101
+ borderWidth: parsedBorderWidth,
102
+ backgroundColor: styles.backgroundColor,
103
+ borderColor: styles.borderColor,
104
+ opacity: styles.opacity,
105
+ texture,
106
+ backgroundImage: styles.backgroundImage,
107
+ });
108
+ }
109
+
110
+ export interface BoxUniformValues {
111
+ width?: number;
112
+ height?: number;
113
+ borderRadius?: string | number | [number, number, number, number];
114
+ borderWidth?: number;
115
+ backgroundColor?: THREE.Color | [number, number, number] | string;
116
+ borderColor?: THREE.Color | [number, number, number] | string;
117
+ opacity?: number;
118
+ bgOpacity?: number;
119
+ borderOpacity?: number;
120
+ texture?: THREE.Texture | null;
121
+ backgroundImage?: string;
122
+ }
123
+
124
+ export function setBoxUniforms(
125
+ material: THREE.ShaderMaterial,
126
+ values: BoxUniformValues,
127
+ ) {
128
+ if (values.width !== undefined && values.height !== undefined) {
129
+ material.uniforms.uSize.value.set(values.width, values.height);
130
+ }
131
+ if (values.borderRadius !== undefined) {
132
+ setBorderRadius(material.uniforms.uBorderRadius.value, values.borderRadius);
133
+ }
134
+ if (values.borderWidth !== undefined) {
135
+ material.uniforms.uBorderWidth.value = values.borderWidth;
136
+ }
137
+
138
+ if (values.backgroundColor !== undefined) {
139
+ if (Array.isArray(values.backgroundColor)) {
140
+ const currentAlpha = material.uniforms.uBgColor.value.w;
141
+ material.uniforms.uBgColor.value.set(
142
+ values.backgroundColor[0],
143
+ values.backgroundColor[1],
144
+ values.backgroundColor[2],
145
+ currentAlpha,
146
+ );
147
+ } else if (typeof values.backgroundColor === "string") {
148
+ const parsed = parseColor(values.backgroundColor);
149
+ material.uniforms.uBgColor.value.set(
150
+ parsed.color.r,
151
+ parsed.color.g,
152
+ parsed.color.b,
153
+ parsed.alpha,
154
+ );
155
+ } else {
156
+ const currentAlpha = material.uniforms.uBgColor.value.w;
157
+ material.uniforms.uBgColor.value.set(
158
+ values.backgroundColor.r,
159
+ values.backgroundColor.g,
160
+ values.backgroundColor.b,
161
+ currentAlpha,
162
+ );
163
+ }
164
+ }
165
+
166
+ if (values.borderColor !== undefined) {
167
+ if (Array.isArray(values.borderColor)) {
168
+ const currentAlpha = material.uniforms.uBorderColor.value.w;
169
+ material.uniforms.uBorderColor.value.set(
170
+ values.borderColor[0],
171
+ values.borderColor[1],
172
+ values.borderColor[2],
173
+ currentAlpha,
174
+ );
175
+ } else if (typeof values.borderColor === "string") {
176
+ const parsed = parseColor(values.borderColor);
177
+ material.uniforms.uBorderColor.value.set(
178
+ parsed.color.r,
179
+ parsed.color.g,
180
+ parsed.color.b,
181
+ parsed.alpha,
182
+ );
183
+ } else {
184
+ const currentAlpha = material.uniforms.uBorderColor.value.w;
185
+ material.uniforms.uBorderColor.value.set(
186
+ values.borderColor.r,
187
+ values.borderColor.g,
188
+ values.borderColor.b,
189
+ currentAlpha,
190
+ );
191
+ }
192
+ }
193
+
194
+ if (values.opacity !== undefined) {
195
+ material.uniforms.uOpacity.value = values.opacity;
196
+ }
197
+ if (values.bgOpacity !== undefined) {
198
+ material.uniforms.uBgColor.value.w = values.bgOpacity;
199
+ }
200
+ if (values.borderOpacity !== undefined) {
201
+ material.uniforms.uBorderColor.value.w = values.borderOpacity;
202
+ }
203
+ if (material.uniforms.uTexture && values.texture !== undefined) {
204
+ material.uniforms.uTexture.value = values.texture;
205
+ }
206
+
207
+ const currentTex = values.texture !== undefined ? values.texture : material.uniforms.uTexture?.value;
208
+ if (currentTex && (currentTex.image instanceof ImageBitmap || currentTex.image instanceof HTMLImageElement || currentTex.image instanceof HTMLCanvasElement)) {
209
+ const imgWidth = currentTex.image.width;
210
+ const imgHeight = currentTex.image.height;
211
+ const domWidth = values.width ?? material.uniforms.uSize.value.x;
212
+ const domHeight = values.height ?? material.uniforms.uSize.value.y;
213
+
214
+ if (imgWidth && imgHeight && domWidth && domHeight) {
215
+ const imageAspect = imgWidth / imgHeight;
216
+ const domAspect = domWidth / domHeight;
217
+
218
+ if (imageAspect > domAspect) {
219
+ const repeatX = domAspect / imageAspect;
220
+ material.uniforms.uTextureRepeat.value.set(repeatX, 1);
221
+ material.uniforms.uTextureOffset.value.set((1 - repeatX) / 2, 0);
222
+ } else {
223
+ const repeatY = imageAspect / domAspect;
224
+ material.uniforms.uTextureRepeat.value.set(1, repeatY);
225
+ material.uniforms.uTextureOffset.value.set(0, (1 - repeatY) / 2);
226
+ }
227
+ }
228
+ } else if (material.uniforms.uTextureRepeat) {
229
+ material.uniforms.uTextureRepeat.value.set(1, 1);
230
+ material.uniforms.uTextureOffset.value.set(0, 0);
231
+ }
232
+
233
+ if (values.backgroundImage !== undefined) {
234
+ const gradient = parseLinearGradient(values.backgroundImage);
235
+ if (gradient) {
236
+ material.uniforms.uGradientCount.value = gradient.stops.length;
237
+ material.uniforms.uGradientAngle.value = gradient.angle;
238
+ for (let i = 0; i < 8; i++) {
239
+ if (i < gradient.stops.length) {
240
+ const s = gradient.stops[i];
241
+ material.uniforms.uGradientColors.value[i].set(
242
+ s.color.r,
243
+ s.color.g,
244
+ s.color.b,
245
+ s.alpha,
246
+ );
247
+ console.log(s.color.r, s.color.g, s.color.b);
248
+ material.uniforms.uGradientStops.value[i] = s.stop;
249
+ } else {
250
+ material.uniforms.uGradientColors.value[i].set(0, 0, 0, 0);
251
+ material.uniforms.uGradientStops.value[i] = 1.0;
252
+ }
253
+ }
254
+ } else {
255
+ material.uniforms.uGradientCount.value = 0;
256
+ }
257
+ }
258
+ }
259
+
260
+ function setBorderRadius(
261
+ target: THREE.Vector4,
262
+ radius?: string | number | [number, number, number, number],
263
+ ) {
264
+ if (radius === undefined || radius === null) {
265
+ target.set(0, 0, 0, 0);
266
+ return;
267
+ }
268
+
269
+ if (typeof radius === "number") {
270
+ target.set(radius, radius, radius, radius);
271
+ return;
272
+ }
273
+
274
+ if (Array.isArray(radius)) {
275
+ target.set(radius[0], radius[1], radius[2], radius[3]);
276
+ return;
277
+ }
278
+
279
+ const arr = radius.split("/")[0].trim().split(/\s+/);
280
+ const tl = parsePixelValue(arr[0]);
281
+ const tr = parsePixelValue(arr[1] ?? arr[0]);
282
+ const br = parsePixelValue(arr[2] ?? arr[0]);
283
+ const bl = parsePixelValue(arr[3] ?? arr[1] ?? arr[0]);
284
+
285
+ target.set(tl, tr, br, bl);
286
+ }