@mirage-engine/painter 0.2.3 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @mirage-engine/painter
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4b9e1b0: shader hooks pipeline
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - e4efe5c: travel api
14
+
3
15
  ## 0.2.3
4
16
 
5
17
  ### Patch Changes
@@ -1,138 +1,193 @@
1
1
  import * as l from "three";
2
- function b(o, e, t) {
3
- const n = e.split(`
4
- `), r = [];
5
- return n.forEach((i) => {
6
- const a = i.split(" ");
7
- let u = a[0];
8
- for (let c = 1; c < a.length; c++) {
9
- const d = a[c];
10
- o.measureText(u + " " + d).width < t ? u += " " + d : (r.push(u), u = d);
2
+ function h(e, r, o) {
3
+ const i = r.split(`
4
+ `), a = [];
5
+ return i.forEach((n) => {
6
+ const t = n.split(" ");
7
+ let c = t[0];
8
+ for (let u = 1; u < t.length; u++) {
9
+ const s = t[u];
10
+ e.measureText(c + " " + s).width < o ? c += " " + s : (a.push(c), c = s);
11
11
  }
12
- r.push(u);
13
- }), r;
12
+ a.push(c);
13
+ }), a;
14
14
  }
15
- function m(o, e, t, n, r = 2) {
16
- const i = document.createElement("canvas"), a = i.getContext("2d");
17
- if (!a)
15
+ function m(e, r, o, i, a = 2) {
16
+ const n = document.createElement("canvas"), t = n.getContext("2d");
17
+ if (!t)
18
18
  throw new Error("[Mirage] Failed to create canvas context");
19
- const c = (window.devicePixelRatio || 1) * r;
20
- i.width = t * c, i.height = n * c, a.scale(c, c), a.font = e.font, a.fillStyle = e.color, a.textBaseline = "top", a.globalAlpha = 1;
21
- const d = b(a, o, t), v = e.lineHeight;
22
- d.forEach((x, g) => {
23
- const B = g * v + 2;
24
- let h = 0;
25
- e.textAlign === "center" ? h = t / 2 : e.textAlign === "right" && (h = t), a.textAlign = e.textAlign, a.fillText(x, h, B);
19
+ const u = (window.devicePixelRatio || 1) * a;
20
+ n.width = o * u, n.height = i * u, t.scale(u, u), t.font = r.font, t.fillStyle = r.color, t.textBaseline = "top", t.globalAlpha = 1;
21
+ const s = h(t, e, o), v = r.lineHeight;
22
+ s.forEach((B, p) => {
23
+ const g = p * v + 2;
24
+ let C = 0;
25
+ r.textAlign === "center" ? C = o / 2 : r.textAlign === "right" && (C = o), t.textAlign = r.textAlign, t.fillText(B, C, g);
26
26
  });
27
- const s = new l.CanvasTexture(i);
28
- return s.colorSpace = l.SRGBColorSpace, s.minFilter = l.LinearFilter, s.magFilter = l.LinearFilter, s.needsUpdate = !0, s;
27
+ const d = new l.CanvasTexture(n);
28
+ return d.colorSpace = l.SRGBColorSpace, d.minFilter = l.LinearFilter, d.magFilter = l.LinearFilter, d.needsUpdate = !0, d;
29
29
  }
30
- function p(o) {
31
- return typeof o == "number" ? o : parseFloat(o) || 0;
30
+ function f(e) {
31
+ return typeof e == "number" ? e : parseFloat(e) || 0;
32
32
  }
33
- const C = `
33
+ function b(e, r) {
34
+ if (!r) {
35
+ e.set(0, 0, 0, 0);
36
+ return;
37
+ }
38
+ const o = r.split("/")[0].trim().split(/\s+/), i = f(o[0]), a = f(o[1] ?? o[0]), n = f(o[2] ?? o[0]), t = f(o[3] ?? o[1] ?? o[0]);
39
+ e.set(i, a, n, t);
40
+ }
41
+ const O = (
42
+ /* glsl */
43
+ `
34
44
  varying vec2 vUv;
45
+ varying vec4 vScreenPos;
35
46
  void main() {
36
47
  vUv = uv;
37
48
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
49
+ vScreenPos = gl_Position;
38
50
  }
39
- `, w = `
51
+ `
52
+ ), T = (
53
+ /* glsl */
54
+ `
55
+
40
56
  varying vec2 vUv;
41
57
 
42
58
  uniform vec2 uSize;
43
- uniform float uRadius;
59
+ uniform vec4 uBorderRadius;
44
60
  uniform float uBorderWidth;
45
- uniform vec3 uColor;
61
+ uniform vec3 uBgColor;
46
62
  uniform vec3 uBorderColor;
47
63
  uniform float uOpacity;
48
64
  uniform float uBgOpacity;
65
+ uniform float uBorderOpacity;
66
+
67
+ #INJECT_DECLARATIONS
49
68
 
50
- // SDF 박스 함수
51
69
  float sdRoundedBox(vec2 p, vec2 b, float r) {
52
70
  vec2 q = abs(p) - b + r;
71
+
53
72
  return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
54
73
  }
55
74
 
56
75
  void main() {
57
76
  vec2 p = (vUv - 0.5) * uSize;
58
77
  vec2 halfSize = uSize * 0.5;
59
-
60
- float d = sdRoundedBox(p, halfSize, uRadius);
61
-
62
- float smoothEdge = 1.0;
63
78
 
64
- float fillAlpha = 1.0 - smoothstep(-uBorderWidth - smoothEdge, -uBorderWidth, d);
65
-
66
- float borderAlpha = 0.0;
79
+ #INJECT_UV_MODIFIER
67
80
 
68
- if (uBorderWidth > 0.01) {
69
- borderAlpha = (1.0 - smoothstep(0.0, smoothEdge, d)) - fillAlpha;
70
- }
81
+ // color decision pipeline
82
+ vec4 baseColor = vec4(uBgColor, uBgOpacity);
71
83
 
72
- vec3 color = uColor;
73
- float totalAlpha = borderAlpha + fillAlpha;
74
-
75
- if (totalAlpha > 0.001) {
76
- color = mix(uColor, uBorderColor, borderAlpha / totalAlpha);
84
+ #INJECT_BASE_COLOR
85
+
86
+ // sdf shape pipeline
87
+ vec2 xRadii = mix(uBorderRadius.xw, uBorderRadius.yz, step(0.0, p.x));
88
+ float r = mix(xRadii.y, xRadii.x, step(0.0, p.y));
89
+ float d = sdRoundedBox(p, halfSize, r);
90
+
91
+ float aa = 1.0;
92
+ float bgMask = 1.0 - smoothstep(0.0, aa, d);
93
+
94
+ float halfBorder = uBorderWidth * 0.5;
95
+ float borderD = abs(d + halfBorder) - halfBorder;
96
+ float borderAlpha = (1.0 - smoothstep(0.0, aa, borderD)) * uBorderOpacity;
97
+ if (uBorderWidth <= 0.01) {
98
+ borderAlpha = 0.0;
77
99
  }
78
-
79
- float shapeAlpha = borderAlpha + (fillAlpha * uBgOpacity);
80
- float finalOpacity = shapeAlpha * uOpacity;
81
-
100
+
101
+ // final blending (border + background)
102
+ float aFront = borderAlpha;
103
+ float aBack = baseColor.a;
104
+ float aOut = aFront + aBack * (1.0 - aFront);
105
+
106
+ float safeAlpha = max(aOut, 0.0001);
107
+ vec3 cOut = (uBorderColor * aFront + baseColor.rgb * aBack * (1.0 - aFront)) / safeAlpha;
108
+ vec4 finalColor = vec4(cOut, aOut);
109
+
110
+ // final color control (Tint, Noise)
111
+ #INJECT_COLOR_MODIFIER
112
+
113
+ float finalOpacity = finalColor.a * bgMask * uOpacity;
82
114
  if (finalOpacity < 0.001) discard;
83
115
 
84
- gl_FragColor = vec4(color, finalOpacity);
116
+ gl_FragColor = vec4(finalColor.rgb, finalOpacity);
117
+
118
+ #include <colorspace_fragment>
85
119
  }
86
- `;
87
- function f(o) {
88
- if (!o || o === "transparent")
120
+ `
121
+ );
122
+ function x(e) {
123
+ if (!e || e === "transparent")
89
124
  return { color: new l.Color(16777215), alpha: 0 };
90
- const e = o.match(
125
+ const r = e.match(
91
126
  /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
92
127
  );
93
- if (e) {
94
- 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;
95
- return { color: new l.Color(`rgb(${t}, ${n}, ${r})`), alpha: i };
128
+ if (r) {
129
+ const o = parseInt(r[1], 10), i = parseInt(r[2], 10), a = parseInt(r[3], 10), n = r[4] !== void 0 ? parseFloat(r[4]) : 1;
130
+ return { color: new l.Color(`rgb(${o}, ${i}, ${a})`), alpha: n };
96
131
  }
97
- return { color: new l.Color(o), alpha: 1 };
132
+ return { color: new l.Color(e), alpha: 1 };
98
133
  }
99
- function A(o, e, t) {
100
- const n = f(o.backgroundColor), r = f(o.borderColor), i = {
101
- uSize: { value: new l.Vector2(e, t) },
102
- uRadius: { value: p(o.borderRadius) },
103
- uBorderWidth: { value: p(o.borderWidth) },
104
- uColor: { value: n.color },
105
- uBorderColor: { value: r.color },
106
- uOpacity: { value: o.opacity ?? 1 },
107
- uBgOpacity: { value: n.alpha }
134
+ function R(e, r, o, i = null, a) {
135
+ const n = i !== null, t = n ? (
136
+ /* glsl */
137
+ `
138
+ uniform sampler2D uTexture;
139
+ varying vec4 vScreenPos;
140
+ `
141
+ ) : "", c = n ? (
142
+ /* glsl */
143
+ `
144
+ vec2 screenUv = (vScreenPos.xy / vScreenPos.w) * 0.5 + 0.5;
145
+ vec2 resultUv = screenUv;
146
+ ${(a == null ? void 0 : a.uvModifier) || ""}
147
+ `
148
+ ) : "", u = n ? (
149
+ /* glsl */
150
+ `
151
+ baseColor = texture2D(uTexture, resultUv);
152
+ `
153
+ ) : "", s = (a == null ? void 0 : a.colorModifier) || "", v = T.replace("#INJECT_DECLARATIONS", t).replace("#INJECT_UV_MODIFIER", c).replace("#INJECT_BASE_COLOR", u).replace("#INJECT_COLOR_MODIFIER", s), d = x(e.backgroundColor), B = x(e.borderColor), p = {
154
+ uSize: { value: new l.Vector2(r, o) },
155
+ uBorderRadius: { value: new l.Vector4(0, 0, 0, 0) },
156
+ uBorderWidth: { value: f(e.borderWidth) },
157
+ uBgColor: { value: d.color },
158
+ uBorderColor: { value: B.color },
159
+ uOpacity: { value: e.opacity ?? 1 },
160
+ uBgOpacity: { value: d.alpha },
161
+ uBorderOpacity: { value: B.alpha },
162
+ uTexture: { value: null }
108
163
  };
109
- return new l.ShaderMaterial({
110
- uniforms: i,
111
- vertexShader: C,
112
- fragmentShader: w,
164
+ return b(p.uBorderRadius.value, e.borderRadius), n && (p.uTexture.value = i), new l.ShaderMaterial({
165
+ uniforms: p,
166
+ vertexShader: O,
167
+ fragmentShader: v,
113
168
  transparent: !0,
114
169
  side: l.FrontSide
115
170
  // for better performance
116
171
  });
117
172
  }
118
- function S(o, e, t, n) {
119
- const r = f(e.backgroundColor), i = f(e.borderColor);
120
- o.uniforms.uSize.value.set(t, n), o.uniforms.uRadius.value = p(e.borderRadius), o.uniforms.uBorderWidth.value = p(e.borderWidth), o.uniforms.uColor.value.copy(r.color), o.uniforms.uBorderColor.value.copy(i.color), o.uniforms.uOpacity.value = e.opacity ?? 1, o.uniforms.uBgOpacity.value = r.alpha;
173
+ function E(e, r, o, i, a) {
174
+ const n = x(r.backgroundColor), t = x(r.borderColor);
175
+ e.uniforms.uSize.value.set(o, i), b(e.uniforms.uBorderRadius.value, r.borderRadius), e.uniforms.uBorderWidth.value = f(r.borderWidth), e.uniforms.uBgColor.value.copy(n.color), e.uniforms.uBorderColor.value.copy(t.color), e.uniforms.uOpacity.value = r.opacity ?? 1, e.uniforms.uBgOpacity.value = n.alpha, e.uniforms.uBorderOpacity.value = t.alpha, e.uniforms.uTexture && a !== void 0 && (e.uniforms.uTexture.value = a);
121
176
  }
122
- const O = {
123
- create(o, e, t, n, r, i = 2) {
124
- if (o === "BOX")
125
- return A(e, n, r);
126
- if (o === "TEXT") {
127
- const a = m(
128
- t || "",
129
- e,
130
- n,
177
+ const S = {
178
+ create(e, r, o, i, a, n = 2, t = null, c) {
179
+ if (e === "BOX")
180
+ return R(r, i, a, t, c);
181
+ if (e === "TEXT") {
182
+ const u = m(
183
+ o || "",
131
184
  r,
132
- i
185
+ i,
186
+ a,
187
+ n
133
188
  );
134
189
  return new l.MeshBasicMaterial({
135
- map: a,
190
+ map: u,
136
191
  transparent: !0,
137
192
  side: l.FrontSide,
138
193
  color: 16777215
@@ -140,31 +195,32 @@ const O = {
140
195
  }
141
196
  return new l.MeshBasicMaterial({ visible: !1 });
142
197
  },
143
- update(o, e, t, n, r, i, a = 2) {
144
- if (e === "BOX")
145
- S(
198
+ update(e, r, o, i, a, n, t = 2, c) {
199
+ if (r === "BOX")
200
+ E(
201
+ e,
146
202
  o,
147
- t,
148
- r,
149
- i
203
+ a,
204
+ n,
205
+ c
150
206
  );
151
- else if (e === "TEXT") {
152
- const u = o;
207
+ else if (r === "TEXT") {
208
+ const u = e;
153
209
  u.map && u.map.dispose();
154
- const c = m(
155
- n || "",
156
- t,
157
- r,
158
- i,
159
- a
210
+ const s = m(
211
+ i || "",
212
+ o,
213
+ a,
214
+ n,
215
+ t
160
216
  );
161
- u.map = c, u.needsUpdate = !0;
217
+ u.map = s, u.needsUpdate = !0;
162
218
  }
163
219
  }
164
220
  };
165
221
  export {
166
- O as Painter,
167
- A as createBoxMaterial,
222
+ S as Painter,
223
+ R as createBoxMaterial,
168
224
  m as createTextTexture,
169
- S as updateBoxMaterial
225
+ E as updateBoxMaterial
170
226
  };
@@ -1,55 +1,85 @@
1
- (function(c,s){typeof exports=="object"&&typeof module<"u"?s(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],s):(c=typeof globalThis<"u"?globalThis:c||self,s(c.MiragePainter={},c.THREE))})(this,function(c,s){"use strict";function C(e){const o=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const r in e)if(r!=="default"){const t=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(o,r,t.get?t:{enumerable:!0,get:()=>e[r]})}}return o.default=e,Object.freeze(o)}const l=C(s);function w(e,o,r){const t=o.split(`
2
- `),a=[];return t.forEach(i=>{const n=i.split(" ");let u=n[0];for(let d=1;d<n.length;d++){const f=n[d];e.measureText(u+" "+f).width<r?u+=" "+f:(a.push(u),u=f)}a.push(u)}),a}function m(e,o,r,t,a=2){const i=document.createElement("canvas"),n=i.getContext("2d");if(!n)throw new Error("[Mirage] Failed to create canvas context");const d=(window.devicePixelRatio||1)*a;i.width=r*d,i.height=t*d,n.scale(d,d),n.font=o.font,n.fillStyle=o.color,n.textBaseline="top",n.globalAlpha=1;const f=w(n,e,r),B=o.lineHeight;f.forEach((S,M)=>{const R=M*B+2;let g=0;o.textAlign==="center"?g=r/2:o.textAlign==="right"&&(g=r),n.textAlign=o.textAlign,n.fillText(S,g,R)});const p=new l.CanvasTexture(i);return p.colorSpace=l.SRGBColorSpace,p.minFilter=l.LinearFilter,p.magFilter=l.LinearFilter,p.needsUpdate=!0,p}function h(e){return typeof e=="number"?e:parseFloat(e)||0}const A=`
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
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
- `,O=`
10
+ `,w=`
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 v(e){if(!e||e==="transparent")return{color:new l.Color(16777215),alpha:0};const o=e.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);if(o){const r=parseInt(o[1],10),t=parseInt(o[2],10),a=parseInt(o[3],10),i=o[4]!==void 0?parseFloat(o[4]):1;return{color:new l.Color(`rgb(${r}, ${t}, ${a})`),alpha:i}}return{color:new l.Color(e),alpha:1}}function x(e,o,r){const t=v(e.backgroundColor),a=v(e.borderColor),i={uSize:{value:new l.Vector2(o,r)},uRadius:{value:h(e.borderRadius)},uBorderWidth:{value:h(e.borderWidth)},uColor:{value:t.color},uBorderColor:{value:a.color},uOpacity:{value:e.opacity??1},uBgOpacity:{value:t.alpha}};return new l.ShaderMaterial({uniforms:i,vertexShader:A,fragmentShader:O,transparent:!0,side:l.FrontSide})}function b(e,o,r,t){const a=v(o.backgroundColor),i=v(o.borderColor);e.uniforms.uSize.value.set(r,t),e.uniforms.uRadius.value=h(o.borderRadius),e.uniforms.uBorderWidth.value=h(o.borderWidth),e.uniforms.uColor.value.copy(a.color),e.uniforms.uBorderColor.value.copy(i.color),e.uniforms.uOpacity.value=o.opacity??1,e.uniforms.uBgOpacity.value=a.alpha}const T={create(e,o,r,t,a,i=2){if(e==="BOX")return x(o,t,a);if(e==="TEXT"){const n=m(r||"",o,t,a,i);return new l.MeshBasicMaterial({map:n,transparent:!0,side:l.FrontSide,color:16777215})}return new l.MeshBasicMaterial({visible:!1})},update(e,o,r,t,a,i,n=2){if(o==="BOX")b(e,r,a,i);else if(o==="TEXT"){const u=e;u.map&&u.map.dispose();const d=m(t||"",r,a,i,n);u.map=d,u.needsUpdate=!0}}};c.Painter=T,c.createBoxMaterial=x,c.createTextTexture=m,c.updateBoxMaterial=b,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})});
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"})});
@@ -1,4 +1,4 @@
1
- import { BoxStyles } from './types';
1
+ import { BoxStyles, ShaderHooks } from './types';
2
2
  import * as THREE from "three";
3
- export declare function createBoxMaterial(styles: BoxStyles, width: number, height: number): THREE.ShaderMaterial;
4
- export declare function updateBoxMaterial(material: THREE.ShaderMaterial, styles: BoxStyles, width: number, height: number): void;
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;
@@ -1,5 +1,6 @@
1
+ import { ShaderHooks } from './types';
1
2
  import * as THREE from "three";
2
3
  export declare const Painter: {
3
- create(type: "BOX" | "TEXT", styles: any, content: string, width: number, height: number, quality?: number): THREE.Material;
4
- update(material: THREE.Material, type: "BOX" | "TEXT", styles: any, content: string, width: number, height: number, quality?: number): void;
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
6
  };
@@ -0,0 +1,5 @@
1
+ export declare const isTraveler = true;
2
+ export declare const glassHooks: {
3
+ uvModifier: string;
4
+ colorModifier: string;
5
+ };
@@ -15,3 +15,7 @@ export interface BoxStyles {
15
15
  borderColor: string;
16
16
  borderWidth: string;
17
17
  }
18
+ export interface ShaderHooks {
19
+ uvModifier?: string;
20
+ colorModifier?: string;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirage-engine/painter",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "peerDependencies": {
@@ -1,65 +1,102 @@
1
1
  import * as THREE from "three";
2
- import { BoxStyles } from "./types";
2
+ import { BoxStyles, ShaderHooks } from "./types";
3
+ // [for dev]
4
+ // import { glassHooks } from "./dev/devShader";
3
5
 
4
6
  function parsePixelValue(value: string | number): number {
5
7
  if (typeof value === "number") return value;
6
8
  return parseFloat(value) || 0;
7
9
  }
8
10
 
9
- const vertexShader = `
11
+ function setBorderRadius(target: THREE.Vector4, radius: string) {
12
+ if (!radius) {
13
+ target.set(0, 0, 0, 0);
14
+ return;
15
+ }
16
+ const arr = radius.split("/")[0].trim().split(/\s+/);
17
+ const tl = parsePixelValue(arr[0]);
18
+ const tr = parsePixelValue(arr[1] ?? arr[0]);
19
+ const br = parsePixelValue(arr[2] ?? arr[0]);
20
+ const bl = parsePixelValue(arr[3] ?? arr[1] ?? arr[0]);
21
+
22
+ target.set(tl, tr, br, bl);
23
+ }
24
+
25
+ const vertexShader = /* glsl */ `
10
26
  varying vec2 vUv;
27
+ varying vec4 vScreenPos;
11
28
  void main() {
12
29
  vUv = uv;
13
30
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
31
+ vScreenPos = gl_Position;
14
32
  }
15
33
  `;
16
34
 
17
- const fragmentShader = `
35
+ const fragmentShaderTemplate = /* glsl */ `
36
+
18
37
  varying vec2 vUv;
19
38
 
20
39
  uniform vec2 uSize;
21
- uniform float uRadius;
40
+ uniform vec4 uBorderRadius;
22
41
  uniform float uBorderWidth;
23
- uniform vec3 uColor;
42
+ uniform vec3 uBgColor;
24
43
  uniform vec3 uBorderColor;
25
44
  uniform float uOpacity;
26
45
  uniform float uBgOpacity;
46
+ uniform float uBorderOpacity;
47
+
48
+ #INJECT_DECLARATIONS
27
49
 
28
- // SDF 박스 함수
29
50
  float sdRoundedBox(vec2 p, vec2 b, float r) {
30
51
  vec2 q = abs(p) - b + r;
52
+
31
53
  return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
32
54
  }
33
55
 
34
56
  void main() {
35
57
  vec2 p = (vUv - 0.5) * uSize;
36
58
  vec2 halfSize = uSize * 0.5;
37
-
38
- float d = sdRoundedBox(p, halfSize, uRadius);
39
-
40
- float smoothEdge = 1.0;
41
59
 
42
- float fillAlpha = 1.0 - smoothstep(-uBorderWidth - smoothEdge, -uBorderWidth, d);
60
+ #INJECT_UV_MODIFIER
43
61
 
44
- float borderAlpha = 0.0;
45
-
46
- if (uBorderWidth > 0.01) {
47
- borderAlpha = (1.0 - smoothstep(0.0, smoothEdge, d)) - fillAlpha;
48
- }
62
+ // color decision pipeline
63
+ vec4 baseColor = vec4(uBgColor, uBgOpacity);
49
64
 
50
- vec3 color = uColor;
51
- float totalAlpha = borderAlpha + fillAlpha;
52
-
53
- if (totalAlpha > 0.001) {
54
- color = mix(uColor, uBorderColor, borderAlpha / totalAlpha);
65
+ #INJECT_BASE_COLOR
66
+
67
+ // sdf shape pipeline
68
+ vec2 xRadii = mix(uBorderRadius.xw, uBorderRadius.yz, step(0.0, p.x));
69
+ float r = mix(xRadii.y, xRadii.x, step(0.0, p.y));
70
+ float d = sdRoundedBox(p, halfSize, r);
71
+
72
+ float aa = 1.0;
73
+ float bgMask = 1.0 - smoothstep(0.0, aa, d);
74
+
75
+ float halfBorder = uBorderWidth * 0.5;
76
+ float borderD = abs(d + halfBorder) - halfBorder;
77
+ float borderAlpha = (1.0 - smoothstep(0.0, aa, borderD)) * uBorderOpacity;
78
+ if (uBorderWidth <= 0.01) {
79
+ borderAlpha = 0.0;
55
80
  }
56
-
57
- float shapeAlpha = borderAlpha + (fillAlpha * uBgOpacity);
58
- float finalOpacity = shapeAlpha * uOpacity;
59
-
81
+
82
+ // final blending (border + background)
83
+ float aFront = borderAlpha;
84
+ float aBack = baseColor.a;
85
+ float aOut = aFront + aBack * (1.0 - aFront);
86
+
87
+ float safeAlpha = max(aOut, 0.0001);
88
+ vec3 cOut = (uBorderColor * aFront + baseColor.rgb * aBack * (1.0 - aFront)) / safeAlpha;
89
+ vec4 finalColor = vec4(cOut, aOut);
90
+
91
+ // final color control (Tint, Noise)
92
+ #INJECT_COLOR_MODIFIER
93
+
94
+ float finalOpacity = finalColor.a * bgMask * uOpacity;
60
95
  if (finalOpacity < 0.001) discard;
61
96
 
62
- gl_FragColor = vec4(color, finalOpacity);
97
+ gl_FragColor = vec4(finalColor.rgb, finalOpacity);
98
+
99
+ #include <colorspace_fragment>
63
100
  }
64
101
  `;
65
102
 
@@ -77,10 +114,8 @@ function parseColor(colorStr: string) {
77
114
  const g = parseInt(rgbaMatch[2], 10);
78
115
  const b = parseInt(rgbaMatch[3], 10);
79
116
  const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1.0;
80
-
81
117
  return { color: new THREE.Color(`rgb(${r}, ${g}, ${b})`), alpha: a };
82
118
  }
83
-
84
119
  return { color: new THREE.Color(colorStr), alpha: 1.0 };
85
120
  }
86
121
 
@@ -88,19 +123,79 @@ export function createBoxMaterial(
88
123
  styles: BoxStyles,
89
124
  width: number,
90
125
  height: number,
126
+ texture: THREE.Texture | null = null,
127
+ hooks?: ShaderHooks,
91
128
  ): THREE.ShaderMaterial {
129
+ const hasTexture = texture !== null;
130
+ // [for dev]
131
+ // const activeHooks = hasTexture ? glassHooks : hooks;
132
+
133
+ const declChunk = hasTexture
134
+ ? /* glsl */ `
135
+ uniform sampler2D uTexture;
136
+ varying vec4 vScreenPos;
137
+ `
138
+ : "";
139
+
140
+ // [un dev]
141
+ const uvChunk = hasTexture
142
+ ? /* glsl */ `
143
+ vec2 screenUv = (vScreenPos.xy / vScreenPos.w) * 0.5 + 0.5;
144
+ vec2 resultUv = screenUv;
145
+ ${hooks?.uvModifier || ""}
146
+ `
147
+ : "";
148
+
149
+
150
+ // [for dev]
151
+ // const uvChunk = hasTexture
152
+ // ? /* glsl */ `
153
+ // vec2 screenUv = (vScreenPos.xy / vScreenPos.w) * 0.5 + 0.5;
154
+ // vec2 resultUv = screenUv;
155
+ // ${activeHooks?.uvModifier || ""}
156
+ // `
157
+ // : "";
158
+
159
+ const baseColorChunk = hasTexture
160
+ ? /* glsl */ `
161
+ baseColor = texture2D(uTexture, resultUv);
162
+ `
163
+ : "";
164
+
165
+ // [un dev]
166
+ const colorModChunk = hooks?.colorModifier || "";
167
+
168
+ // [for dev]
169
+ // const colorModChunk = hasTexture
170
+ // ? activeHooks?.colorModifier || ""
171
+ // : hooks?.colorModifier || "";
172
+
173
+ const fragmentShader = fragmentShaderTemplate
174
+ .replace("#INJECT_DECLARATIONS", declChunk)
175
+ .replace("#INJECT_UV_MODIFIER", uvChunk)
176
+ .replace("#INJECT_BASE_COLOR", baseColorChunk)
177
+ .replace("#INJECT_COLOR_MODIFIER", colorModChunk);
178
+
179
+ // uniform setting
92
180
  const parsedBg = parseColor(styles.backgroundColor);
93
181
  const parsedBorder = parseColor(styles.borderColor);
94
-
95
182
  const uniforms = {
96
183
  uSize: { value: new THREE.Vector2(width, height) },
97
- uRadius: { value: parsePixelValue(styles.borderRadius) },
184
+ uBorderRadius: { value: new THREE.Vector4(0, 0, 0, 0) },
98
185
  uBorderWidth: { value: parsePixelValue(styles.borderWidth) },
99
- uColor: { value: parsedBg.color },
186
+ uBgColor: { value: parsedBg.color },
100
187
  uBorderColor: { value: parsedBorder.color },
101
188
  uOpacity: { value: styles.opacity ?? 1.0 },
102
189
  uBgOpacity: { value: parsedBg.alpha },
190
+ uBorderOpacity: { value: parsedBorder.alpha },
191
+ uTexture: { value: null as THREE.Texture | null },
103
192
  };
193
+ // border radius value initialize
194
+ setBorderRadius(uniforms.uBorderRadius.value, styles.borderRadius);
195
+
196
+ if (hasTexture) {
197
+ uniforms.uTexture.value = texture;
198
+ }
104
199
 
105
200
  const material = new THREE.ShaderMaterial({
106
201
  uniforms: uniforms,
@@ -118,18 +213,24 @@ export function updateBoxMaterial(
118
213
  styles: BoxStyles,
119
214
  width: number,
120
215
  height: number,
216
+ texture?: THREE.Texture | null,
121
217
  ) {
122
218
  const parsedBg = parseColor(styles.backgroundColor);
123
219
  const parsedBorder = parseColor(styles.borderColor);
124
220
 
125
221
  material.uniforms.uSize.value.set(width, height);
126
222
 
127
- material.uniforms.uRadius.value = parsePixelValue(styles.borderRadius);
223
+ // material.uniforms.uBorderRadius.value = parsePixelValue(styles.borderRadius);
224
+ setBorderRadius(material.uniforms.uBorderRadius.value, styles.borderRadius);
128
225
  material.uniforms.uBorderWidth.value = parsePixelValue(styles.borderWidth);
129
226
 
130
- material.uniforms.uColor.value.copy(parsedBg.color);
227
+ material.uniforms.uBgColor.value.copy(parsedBg.color);
131
228
  material.uniforms.uBorderColor.value.copy(parsedBorder.color);
132
229
 
133
230
  material.uniforms.uOpacity.value = styles.opacity ?? 1.0;
134
231
  material.uniforms.uBgOpacity.value = parsedBg.alpha;
232
+ material.uniforms.uBorderOpacity.value = parsedBorder.alpha;
233
+ if (material.uniforms.uTexture && texture !== undefined) {
234
+ material.uniforms.uTexture.value = texture;
235
+ }
135
236
  }
package/src/Painter.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as THREE from "three";
2
- import { BoxStyles, TextStyles } from "./types";
2
+ import { BoxStyles, TextStyles, ShaderHooks} from "./types";
3
3
  import { createBoxMaterial, updateBoxMaterial } from "./BoxGenerator";
4
4
  import { createTextTexture } from "./TextGenerator";
5
5
 
@@ -11,9 +11,11 @@ export const Painter = {
11
11
  width: number,
12
12
  height: number,
13
13
  quality: number = 2,
14
+ texture: THREE.Texture | null = null,
15
+ shaderHooks?: ShaderHooks
14
16
  ): THREE.Material {
15
17
  if (type === "BOX") {
16
- return createBoxMaterial(styles as BoxStyles, width, height);
18
+ return createBoxMaterial(styles as BoxStyles, width, height, texture, shaderHooks);
17
19
  } else if (type === "TEXT") {
18
20
  const texture = createTextTexture(
19
21
  content || "",
@@ -42,6 +44,7 @@ export const Painter = {
42
44
  width: number,
43
45
  height: number,
44
46
  quality: number = 2,
47
+ texture?: THREE.Texture | null
45
48
  ) {
46
49
  if (type === "BOX") {
47
50
  updateBoxMaterial(
@@ -49,6 +52,7 @@ export const Painter = {
49
52
  styles as BoxStyles,
50
53
  width,
51
54
  height,
55
+ texture
52
56
  );
53
57
  } else if (type === "TEXT") {
54
58
  const basicMat = material as THREE.MeshBasicMaterial;
@@ -0,0 +1,114 @@
1
+ export const isTraveler = true;
2
+
3
+ export const glassHooks = {
4
+ uvModifier: /* glsl */ `
5
+ // 1. 박스 크기에 비례한 두께감
6
+ // float thickness_uv = min(uSize.x, uSize.y) * 0.5;
7
+ float thickness_uv = 25.0;
8
+ float index = 1.3; // 굴절률 (공기: 1.0, 유리: 1.5~1.9)
9
+ float base_height = thickness_uv * 1.5;
10
+
11
+ // 2. 로컬 SDF 계산 (p는 픽셀 단위)
12
+ vec2 xRadii_uv = mix(uBorderRadius.xw, uBorderRadius.yz, step(0.0, p.x));
13
+ float r_uv = mix(xRadii_uv.y, xRadii_uv.x, step(0.0, p.y));
14
+ float d_uv = sdRoundedBox(p, halfSize, r_uv);
15
+
16
+ // 3. 3D 노말(Normal) 계산
17
+ float dx_uv = dFdx(d_uv);
18
+ float dy_uv = dFdy(d_uv);
19
+ if (abs(dx_uv) < 0.001 && abs(dy_uv) < 0.001) {
20
+ vec2 fallbackDir = normalize(p);
21
+ dx_uv = fallbackDir.x; dy_uv = fallbackDir.y;
22
+ }
23
+ float n_cos_uv = max(thickness_uv + d_uv, 0.0) / max(thickness_uv, 0.001);
24
+ float n_sin_uv = sqrt(max(1.0 - n_cos_uv * n_cos_uv, 0.0));
25
+ vec3 normal_uv = normalize(vec3(dx_uv * n_cos_uv, dy_uv * n_cos_uv, n_sin_uv));
26
+
27
+ // 4. Z 높이 계산
28
+ float h = 0.0;
29
+ if (d_uv < -thickness_uv) {
30
+ h = thickness_uv;
31
+ } else if (d_uv < 0.0) {
32
+ float x = thickness_uv + d_uv;
33
+ h = sqrt(max(thickness_uv * thickness_uv - x * x, 0.0));
34
+ }
35
+
36
+ // 5. 굴절(Refraction) 적용
37
+ vec3 incident = vec3(0.0, 0.0, -1.0);
38
+ vec3 refract_vec = refract(incident, normal_uv, 1.0 / index);
39
+
40
+ float denom = dot(vec3(0.0, 0.0, -1.0), refract_vec);
41
+ float refract_length = (h + base_height) / max(denom, 0.001);
42
+
43
+ // offsetPx는 '박스 내부의 픽셀 단위' 왜곡 거리입니다.
44
+ vec2 offsetPx = refract_vec.xy * refract_length;
45
+
46
+ // [다시 복구!] 큰 배경 텍스쳐를 자르기 위해 screenUv를 사용
47
+ // 화면 1픽셀이 전체 screenUv(0~1)에서 차지하는 비율을 편미분으로 동적 계산
48
+ vec2 pixelToUv = vec2(dFdx(screenUv.x), dFdy(screenUv.y));
49
+ if (abs(pixelToUv.x) < 0.000001) pixelToUv.x = 1.0 / 1920.0; // 안전 장치
50
+ if (abs(pixelToUv.y) < 0.000001) pixelToUv.y = 1.0 / 1080.0; // 안전 장치
51
+
52
+ // 픽셀 오프셋을 화면 UV 오프셋으로 변환
53
+ vec2 distortOffset = offsetPx * pixelToUv;
54
+
55
+ // vUv 대신 screenUv에서 왜곡값을 빼서 최종 배경 픽셀을 당겨옵니다.
56
+ resultUv = screenUv - distortOffset;
57
+ `,
58
+
59
+ // colorModifier: /* glsl */ `
60
+ // // float thickness_c = min(uSize.x, uSize.y) * 0.5;
61
+ // float thickness_c = 25.0;
62
+ // float dx_c = dFdx(d);
63
+ // float dy_c = dFdy(d);
64
+ // if (abs(dx_c) < 0.001 && abs(dy_c) < 0.001) {
65
+ // vec2 fallbackDir = normalize(p);
66
+ // dx_c = fallbackDir.x; dy_c = fallbackDir.y;
67
+ // }
68
+ // float n_cos_c = max(thickness_c + d, 0.0) / max(thickness_c, 0.001);
69
+ // float n_sin_c = sqrt(max(1.0 - n_cos_c * n_cos_c, 0.0));
70
+ // vec3 normal_c = normalize(vec3(dx_c * n_cos_c, dy_c * n_cos_c, n_sin_c));
71
+
72
+ // float edgeReflection = smoothstep(-5.0, 0.0, d) * smoothstep(0.0, -5.0, d);
73
+ // float fresnel = pow(1.0 - max(dot(normal_c, vec3(0.0, 0.0, 1.0)), 0.0), 3.0);
74
+
75
+ // finalColor.rgb += vec3(1.0) * (edgeReflection * 0.4 + fresnel * 0.5);
76
+ // `,
77
+ colorModifier: /* glsl */ `
78
+ // 1. 노말 계산 (기존과 동일)
79
+ float thickness_c = 25.0;
80
+ float dx_c = dFdx(d);
81
+ float dy_c = dFdy(d);
82
+ if (abs(dx_c) < 0.001 && abs(dy_c) < 0.001) {
83
+ vec2 fallbackDir = normalize(p);
84
+ dx_c = fallbackDir.x; dy_c = fallbackDir.y;
85
+ }
86
+ float n_cos_c = max(thickness_c + d, 0.0) / max(thickness_c, 0.001);
87
+ float n_sin_c = sqrt(max(1.0 - n_cos_c * n_cos_c, 0.0));
88
+ vec3 normal_c = normalize(vec3(dx_c * n_cos_c, dy_c * n_cos_c, n_sin_c));
89
+
90
+ // ==========================================
91
+ // [애플 스타일 리퀴드 글래스 틴트 & 질감 추가]
92
+ // ==========================================
93
+
94
+ // A. 틴트(Tint) 조작
95
+ // 밝은 테마라면 vec3(1.0), 어두운 테마라면 vec3(0.1) 등을 사용
96
+ vec3 tintColor = vec3(0.03, 0.03, 0.8);
97
+ float tintStrength = 0.3; // 틴트 강도 (0.1 ~ 0.3 사이 추천)
98
+ finalColor.rgb = mix(finalColor.rgb, tintColor, tintStrength);
99
+
100
+ // B. 프로스트 글래스 노이즈 (시각적 밀도감)
101
+ // vUv를 이용한 간단한 난수 생성으로 화면 픽셀 단위의 미세한 질감을 줌
102
+ float noise = fract(sin(dot(vUv, vec2(12.9898, 78.233))) * 43758.5453) * 2.0 - 1.0;
103
+ finalColor.rgb += noise * 0.01; // 노이즈 강도 (너무 강하면 지저분해짐)
104
+
105
+ // C. 프레넬(Fresnel) 및 날카로운 단면 반사광
106
+ float fresnel = pow(1.0 - max(dot(normal_c, vec3(0.0, 0.0, 1.0)), 0.0), 4.0);
107
+
108
+ // 기존의 두꺼운 15px 빛 맺힘 대신 1.5px~2px 수준의 예리한 테두리 하이라이트
109
+ float sharpEdge = smoothstep(-5.0, 0.0, d) * smoothstep(0.0, -5.0, d);
110
+
111
+ // 빛 합성 (테두리가 살짝 빛나며 유리 질감을 완성)
112
+ finalColor.rgb += vec3(1.0) * (sharpEdge * 0.6 + fresnel * 0.3);
113
+ `,
114
+ };
@@ -18,3 +18,9 @@ export interface BoxStyles {
18
18
  borderColor: string;
19
19
  borderWidth: string;
20
20
  }
21
+
22
+ export interface ShaderHooks {
23
+ uvModifier?: string;
24
+ colorModifier?: string;
25
+ }
26
+