Setting the file. One moment.
Glass Main · Glass Shard Title · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page function simplifyPoly
— line 412
This file
Number 11.3
Position 3 of 7
Type JavaScript
Size 37 KB
Lines 1,018 assets/ glass-main.mjs
JavaScript · 1,018 lines · 37 KB
11 /* ---- One instance per document -------------------------------------
12 This composition is mounted in short-lived documents all over the
13 place: Studio's preview stage, the sidebar's hover previews, a
14 composition-stack push, the gallery's per-block tiles. One live
15 instance costs roughly 350 MB (a 1920x1080 WebGL context with
16 antialias + preserveDrawingBuffer, a 4096x2304 headline raster with
17 mipmaps, a PMREM environment, a transmission render target and 34
18 extruded shard geometries), so a stale one is not a rounding error.
19 Never build two on the same document, and always hand the host an
20 explicit way to release one. */
21 if (window.__gstInstance && window.__gstInstance.dispose) {
22 try {
23 window.__gstInstance. dispose ();
24 } catch (e) {
25 /* best effort */
26 }
27 }
28
29 const canvas = document. getElementById ( "gst-canvas" );
30 const renderer = new THREE . WebGLRenderer ({
31 canvas,
32 alpha: true ,
33 antialias: true ,
34 preserveDrawingBuffer: true ,
35 });
36 renderer. setSize ( 1920 , 1080 , false );
37 renderer. setPixelRatio ( 1 );
38 renderer. setClearColor ( 0x0a0a0f , 1 ); /* flat dark — no gradient wash */
39 renderer.toneMapping = THREE .ACESFilmicToneMapping;
40
41 const FOV = 42 ;
42 const CAM_Z = 5.2 ;
43 const PANE_Z = 0.6 ;
44 const scene = new THREE . Scene ();
45 /* Include the full configured flight, 15% seeded depth jitter, lateral
46 offsets rotated by the pane group, and shard extents. The old fixed
47 far=60 clipped distant starts even with fog disabled. */
48 const FAR = Math. max (
49 60 ,
50 CAM_Z + Math. max ( - G .vars.zDist, - G .vars.zOut, 0 ) * 1.15 + G .vars.sideDist * 1.5 + 12 ,
51 );
52 const camera = new THREE . PerspectiveCamera ( FOV , 1920 / 1080 , 0.1 , FAR );
53 camera.position. set ( 0 , 0 , CAM_Z );
54
55 /* Frustum size at the shard plane: world <-> texture mapping basis. */
56 const distPane = CAM_Z - PANE_Z ;
57 const VH = 2 * distPane * Math. tan (( FOV * Math. PI ) / 360 );
58 const VW = VH * ( 1920 / 1080 );
59
60 /* ---- Lights ---- */
61 scene. add ( new THREE . AmbientLight ( 0xffffff , 0.6 ));
62 const spot = new THREE . SpotLight ( 0xffffff , 7 , 0 , Math. PI / 4 , 0.5 , 1 );
63 spot.position. set ( 0 , 7.1 , 2 );
64 scene. add (spot);
65 const point = new THREE . PointLight ( 0xccddff , 4 , 0 , 1 );
66 point.position. set ( 0 , - 3 , 1 );
67 scene. add (point);
68
69 /* ---- Environment + matcap (glass look) ---- */
70 const hdrTex = await new HDRLoader (). loadAsync ( "assets/ferndale_studio_01_1k.hdr" );
71 const pmrem = new THREE . PMREMGenerator (renderer);
72 const envRT = pmrem. fromEquirectangular (hdrTex);
73 scene.environment = envRT.texture;
74 scene.environmentRotation = new THREE . Euler ( 0 , 0.6 , 0 );
75 hdrTex. dispose ();
76 pmrem. dispose ();
77
78 const matcapTex = await new THREE . TextureLoader (). loadAsync ( "assets/matcap-1.png" );
79 matcapTex.colorSpace = THREE .SRGBColorSpace;
80 matcapTex.wrapS = matcapTex.wrapT = THREE .RepeatWrapping;
81 const mk = 1 / 0.2 ;
82 matcapTex.repeat. set (mk, mk);
83 matcapTex.offset. set (( 1 - mk) * 0.5 , ( 1 - mk) * 0.5 );
84
85 /* Fonts must be resident BEFORE rasterising: shard selection samples
86 the text pixels. */
87 try {
88 await Promise . all ([
89 document.fonts. load ( '600 100px "Geist"' ),
90 document.fonts. load ( '400 100px "Geist"' ),
91 ]);
92 await document.fonts.ready;
93 } catch (e) {
94 /* system fallback */
95 }
96
97 /* ---- Headline raster (the ONLY content). Canvas spans the camera
98 frustum at the shard plane: world (x,y) -> px via VW/VH. ---- */
99 const TEXW = 4096 ;
100 const TEXH = Math. round ( TEXW * ( VH / VW ));
101 const headCanvas = document. createElement ( "canvas" );
102 headCanvas.width = TEXW ;
103 headCanvas.height = TEXH ;
104 const hctx = headCanvas. getContext ( "2d" );
105 {
106 hctx. clearRect ( 0 , 0 , TEXW , TEXH );
107 hctx.textAlign = "center" ;
108 hctx.textBaseline = "middle" ;
109 /* Fit headline to 70% of frame width so the blob silhouette + padding stays inside frame. */
110 let fpx = 300 ;
111 hctx.font = `600 ${ fpx }px "Geist", ui-sans-serif, sans-serif` ;
112 hctx.letterSpacing = `${ - 0.035 * fpx }px` ;
113 const w0 = hctx. measureText ( G .vars.headline).width;
114 const maxW = TEXW * 0.7 ;
115 if (w0 > maxW) fpx = Math. floor ((fpx * maxW) / w0);
116 hctx.font = `600 ${ fpx }px "Geist", ui-sans-serif, sans-serif` ;
117 hctx.letterSpacing = `${ - 0.035 * fpx }px` ;
118 hctx.fillStyle = "#f4f4f8" ;
119 hctx. fillText ( G .vars.headline, TEXW / 2 , TEXH / 2 );
120 G .fontPx = fpx;
121 }
122 const headTex = new THREE . CanvasTexture (headCanvas);
123 headTex.colorSpace = THREE .SRGBColorSpace;
124 headTex.anisotropy = 16 ;
125
126 /* ---- Fattened mask for cell selection (thick stroke = keep shards
127 that hug the letterforms, not just ones dead-centre on strokes) ---- */
128 const maskCanvas = document. createElement ( "canvas" );
129 maskCanvas.width = 1280 ;
130 maskCanvas.height = Math. round ( 1280 * ( VH / VW ));
131 const mctx = maskCanvas. getContext ( "2d" , { willReadFrequently: true });
132 {
133 const ms = maskCanvas.width / TEXW ;
134 mctx. clearRect ( 0 , 0 , maskCanvas.width, maskCanvas.height);
135 mctx.textAlign = "center" ;
136 mctx.textBaseline = "middle" ;
137 mctx.font = `600 ${ G . fontPx * ms }px "Geist", ui-sans-serif, sans-serif` ;
138 mctx.letterSpacing = `${ - 0.035 * G . fontPx * ms }px` ;
139 mctx.fillStyle = "#fff" ;
140 mctx.strokeStyle = "#fff" ;
141 mctx.lineWidth = G .fontPx * ms * 0.22 ;
142 mctx.lineJoin = "round" ;
143 mctx. strokeText ( G .vars.headline, maskCanvas.width / 2 , maskCanvas.height / 2 );
144 mctx. fillText ( G .vars.headline, maskCanvas.width / 2 , maskCanvas.height / 2 );
145 }
146 /* Setup-only: released the moment the shard set is final (see below). */
147 let maskData = mctx. getImageData ( 0 , 0 , maskCanvas.width, maskCanvas.height).data;
148 let maskW = maskCanvas.width,
149 maskH = maskCanvas.height;
150 function maskHit ( wx , wy ) {
151 if ( ! maskData) return false ;
152 const px = Math. round ((wx / VW + 0.5 ) * (maskW - 1 ));
153 const py = Math. round (( 0.5 - wy / VH ) * (maskH - 1 ));
154 if (px < 0 || py < 0 || px >= maskW || py >= maskH) return false ;
155 return maskData[(py * maskW + px) * 4 + 3 ] > 24 ;
156 }
157
158 /* ---- Headline band in world units (from the raster metrics) ---- */
159 {
160 hctx.font = `600 ${ G . fontPx }px "Geist", ui-sans-serif, sans-serif` ;
161 hctx.letterSpacing = `${ - 0.035 * G . fontPx }px` ;
162 const m = hctx. measureText ( G .vars.headline);
163 G .bandW = ((m.width + G .fontPx * 0.2 ) / TEXW ) * VW ;
164 G .bandH =
165 ((m.actualBoundingBoxAscent + m.actualBoundingBoxDescent + G .fontPx * 0.34 ) / TEXH ) * VH ;
166 }
167
168 /* ---- Organic silhouette: the assembled cluster reads as one shard of
169 a larger broken pane, not a rounded rectangle. A seeded harmonic blob
170 around the headline defines the region; chaos scales the noise,
171 padding sets how far the glass extends past the glyphs. The jagged
172 outer edge comes from natural Voronoi cell borders against a ring of
173 sacrificial outside sites (never rectangle clipping). ---- */
174 const SEED = 11 ;
175 const rnd = gstMulberry32 ( SEED );
176 const HW = G .bandW / 2 ,
177 HH = G .bandH / 2 ;
178
179 const PADW = G .bandH * ( 0.18 + G .vars.padding * 0.85 );
180 const A0 = HW + PADW ,
181 B0 = HH + PADW ;
182 const randBlob = gstMulberry32 ( SEED * 511 + 3 );
183 const HARM = [];
184 for ( let k = 2 ; k <= 9 ; k ++ )
185 HARM . push ({ k, amp: ( randBlob () * 2 - 1 ) / Math. sqrt (k), ph: randBlob () * Math. PI * 2 });
186 function ellipseR ( a , b , th ) {
187 return (a * b) / Math. hypot (b * Math. cos (th), a * Math. sin (th));
188 }
189 function blobR ( th ) {
190 let n = 0 ;
191 for ( const h of HARM ) n += h.amp * Math. sin (h.k * th + h.ph);
192 /* chaos is deliberately violent at the top end — 1.0 is ~10x the old
193 deformation, so the outline can go properly feral. */
194 let r = ellipseR ( A0 , B0 , th) * ( 1 + G .vars.chaos * 2.4 * n);
195 /* Floor on the text band RECT (not an ellipse): the whole headline plus
196 a padding margin is always inside the blob, so tiling the blob always
197 covers the text — at any tile count. */
198 const rc = Math. abs (Math. cos (th)),
199 rs = Math. abs (Math. sin (th));
200 const rectR = Math. min (
201 rc > 1e-6 ? ( HW + PADW * 0.3 ) / rc : 1e9 ,
202 rs > 1e-6 ? ( HH + PADW * 0.3 ) / rs : 1e9 ,
203 );
204 r = Math. max (r, rectR);
205 /* Hard frustum cap: whatever the sliders say, the silhouette never
206 spills past the frame and gets viewport-cut. */
207 const acx = Math. abs (Math. cos (th)),
208 asy = Math. abs (Math. sin (th));
209 const lim = Math. min (acx > 1e-6 ? ( VW * 0.44 ) / acx : 1e9 , asy > 1e-6 ? ( VH * 0.43 ) / asy : 1e9 );
210 return Math. min (r, lim);
211 }
212 function inBlob ( x , y ) {
213 return Math. hypot (x, y) <= blobR (Math. atan2 (y, x));
214 }
215
216 /* Domain + density: keep tileCount as shard density over the blob. */
217 let blobArea = 0 ,
218 maxRX = 0 ,
219 maxRY = 0 ;
220 for ( let i = 0 ; i < 96 ; i ++ ) {
221 const th = (i / 96 ) * Math. PI * 2 ;
222 const r = blobR (th);
223 blobArea += 0.5 * r * r * ((Math. PI * 2 ) / 96 );
224 maxRX = Math. max (maxRX, Math. abs (r * Math. cos (th)));
225 maxRY = Math. max (maxRY, Math. abs (r * Math. sin (th)));
226 }
227 /* tileCount is the exact number of pieces — no density inflation, no
228 minimum. tileCount 1 = the whole silhouette as one pane of glass. */
229 const N_INT = Math. max ( 1 , Math. min ( 400 , Math. round ( G .vars.tileCount)));
230 void blobArea;
231 const bounds = [ - maxRX * 1.3 , - maxRY * 1.3 , maxRX * 1.3 , maxRY * 1.3 ];
232
233 /* Size variance: 0 = uniform cells (full Lloyd relaxation, even spread);
234 1 = chaotic — sites cluster around seeded attractors (dense regions =
235 small shards, sparse = huge ones) and relaxation is skipped. */
236 const VAR = G .vars.sizeVariance;
237 const randVar = gstMulberry32 ( SEED * 977 + 5 );
238 const centres = [];
239 let cGuard = 0 ;
240 while (centres. length < 4 && cGuard ++ < 400 ) {
241 const x = ( randVar () * 2 - 1 ) * maxRX;
242 const y = ( randVar () * 2 - 1 ) * maxRY;
243 if ( inBlob (x, y)) centres. push ([x, y]);
244 }
245 function gauss ( r ) {
246 /* Box-Muller from the seeded stream */
247 const u = Math. max ( 1e-9 , r ()),
248 v = r ();
249 return Math. sqrt ( - 2 * Math. log (u)) * Math. cos ( 2 * Math. PI * v);
250 }
251 let points = [];
252 let guard = 0 ;
253 const sigma = Math. max (maxRX, maxRY) * 0.14 ;
254 while (points. length < N_INT && guard ++ < N_INT * 200 ) {
255 let x, y;
256 if (centres. length && rnd () < 0.85 * VAR ) {
257 const c = centres[Math. floor ( rnd () * centres. length ) % centres. length ];
258 x = c[ 0 ] + gauss (rnd) * sigma;
259 y = c[ 1 ] + gauss (rnd) * sigma;
260 } else {
261 x = ( rnd () * 2 - 1 ) * maxRX;
262 y = ( rnd () * 2 - 1 ) * maxRY;
263 }
264 if ( inBlob (x, y)) points. push ([x, y]);
265 }
266 const interiorCount = points. length ;
267 /* The blob polygon itself is the outer boundary — every cell is clipped
268 against it, so the union of pieces IS the silhouette at any count. */
269 /* The silhouette polygon is COARSE and jittered on purpose: broken
270 glass has straight facet edges meeting at sharp vertices. A smooth
271 densely-sampled outline gave the boundary shards melted, scooped
272 edges — nothing like the experiment's fragments. */
273 const randEdge = gstMulberry32 ( SEED * 733 + 9 );
274 const BLOB_VERTS = 22 ;
275 const blobPoly = [];
276 for ( let j = 0 ; j < BLOB_VERTS ; j ++ ) {
277 const th = ((j + ( randEdge () - 0.5 ) * 0.72 ) / BLOB_VERTS ) * Math. PI * 2 ;
278 const r = blobR (th) * ( 1 + ( randEdge () - 0.5 ) * 0.09 );
279 blobPoly. push ([r * Math. cos (th), r * Math. sin (th)]);
280 }
281 const RELAX = Math. round ( 3 * ( 1 - VAR ));
282 for ( let iter = 0 ; iter < RELAX ; iter ++ ) {
283 const vorR = d3.Delaunay. from (points). voronoi (bounds);
284 points = points. map (( p , idx ) => {
285 const cell = vorR. cellPolygon (idx);
286 if ( ! cell || cell. length < 3 ) return p;
287 let cx = 0 ,
288 cy = 0 ;
289 for ( const pt of cell) {
290 cx += pt[ 0 ];
291 cy += pt[ 1 ];
292 }
293 return [cx / cell. length , cy / cell. length ];
294 });
295 }
296
297 /* Polygon inset (seams between shards). */
298 function lineIntersect ( p1 , p2 , p3 , p4 ) {
299 const d = (p1[ 0 ] - p2[ 0 ]) * (p3[ 1 ] - p4[ 1 ]) - (p1[ 1 ] - p2[ 1 ]) * (p3[ 0 ] - p4[ 0 ]);
300 if (Math. abs (d) < 1e-9 ) return null ;
301 const t = ((p1[ 0 ] - p3[ 0 ]) * (p3[ 1 ] - p4[ 1 ]) - (p1[ 1 ] - p3[ 1 ]) * (p3[ 0 ] - p4[ 0 ])) / d;
302 return [p1[ 0 ] + t * (p2[ 0 ] - p1[ 0 ]), p1[ 1 ] + t * (p2[ 1 ] - p1[ 1 ])];
303 }
304 function insetPolygon ( poly , inset ) {
305 if (inset <= 0 ) return poly;
306 const n = poly. length ;
307 if (n < 3 ) return null ;
308 const edges = [];
309 for ( let i = 0 ; i < n; i ++ ) {
310 const a = poly[i],
311 b = poly[(i + 1 ) % n];
312 const dx = b[ 0 ] - a[ 0 ],
313 dy = b[ 1 ] - a[ 1 ];
314 const len = Math. hypot (dx, dy);
315 if (len < 1e-9 ) continue ;
316 const nx = - dy / len,
317 ny = dx / len;
318 edges. push ({
319 p1: [a[ 0 ] + nx * inset, a[ 1 ] + ny * inset],
320 p2: [b[ 0 ] + nx * inset, b[ 1 ] + ny * inset],
321 });
322 }
323 const m = edges. length ;
324 if (m < 3 ) return null ;
325 const out = [];
326 for ( let k = 0 ; k < m; k ++ ) {
327 const e1 = edges[(k + m - 1 ) % m],
328 e2 = edges[k];
329 out. push ( lineIntersect (e1.p1, e1.p2, e2.p1, e2.p2) || e2.p1);
330 }
331 let area = 0 ;
332 for ( let q = 0 ; q < out. length ; q ++ ) {
333 const [ x1 , y1 ] = out[q];
334 const [ x2 , y2 ] = out[(q + 1 ) % out. length ];
335 area += x1 * y2 - x2 * y1;
336 }
337 return Math. abs (area) < 1e-6 ? null : out;
338 }
339
340 function pointInPoly ( x , y , poly ) {
341 let inside = false ;
342 for ( let i = 0 , j = poly. length - 1 ; i < poly. length ; j = i ++ ) {
343 const [ xi , yi ] = poly[i],
344 [ xj , yj ] = poly[j];
345 if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = ! inside;
346 }
347 return inside;
348 }
349 function cellTouchesText ( poly ) {
350 let minX = 1e9 ,
351 minY = 1e9 ,
352 maxX = - 1e9 ,
353 maxY = - 1e9 ;
354 for ( const [ x , y ] of poly) {
355 if (x < minX) minX = x;
356 if (x > maxX) maxX = x;
357 if (y < minY) minY = y;
358 if (y > maxY) maxY = y;
359 }
360 const N = 7 ;
361 for ( let i = 0 ; i < N ; i ++ ) {
362 for ( let j = 0 ; j < N ; j ++ ) {
363 const x = minX + ((i + 0.5 ) / N ) * (maxX - minX);
364 const y = minY + ((j + 0.5 ) / N ) * (maxY - minY);
365 if ( pointInPoly (x, y, poly) && maskHit (x, y)) return true ;
366 }
367 }
368 return false ;
369 }
370
371 /* ---- Cells -> shard descriptors (seeded choreography per shard) ---- */
372 const GAP = Math. max ( 0 , G .vars.gap);
373 const CORNER_R = 0.01 + G .vars.roundness * 0.16 ;
374 const PANE_DEPTH = 0.09 ;
375
376 function polyArea2 ( poly ) {
377 let a = 0 ;
378 for ( let i = 0 ; i < poly. length ; i ++ ) {
379 const [ x1 , y1 ] = poly[i],
380 [ x2 , y2 ] = poly[(i + 1 ) % poly. length ];
381 a += x1 * y2 - x2 * y1;
382 }
383 return a / 2 ;
384 }
385 /* Sutherland-Hodgman: clip any polygon against a CONVEX Voronoi cell. */
386 function clipToConvex ( subject , clip ) {
387 const ccw = polyArea2 (clip) > 0 ? 1 : - 1 ;
388 let out = subject;
389 for ( let i = 0 ; i < clip. length && out. length ; i ++ ) {
390 const a = clip[i],
391 b = clip[(i + 1 ) % clip. length ];
392 const input = out;
393 out = [];
394 const inside = ( pt ) =>
395 ccw * ((b[ 0 ] - a[ 0 ]) * (pt[ 1 ] - a[ 1 ]) - (b[ 1 ] - a[ 1 ]) * (pt[ 0 ] - a[ 0 ])) >= - 1e-9 ;
396 for ( let j = 0 ; j < input. length ; j ++ ) {
397 const P = input[j],
398 Q = input[(j + 1 ) % input. length ];
399 const Pin = inside ( P ),
400 Qin = inside ( Q );
401 if (Pin) {
402 out. push ( P );
403 if ( ! Qin) out. push ( lineIntersect ( P , Q , a, b) || Q );
404 } else if (Qin) {
405 out. push ( lineIntersect ( P , Q , a, b) || P );
406 }
407 }
408 }
409 return out;
410 }
411 /* Merge near-collinear boundary samples so corner rounding keeps scale. */
412 function simplifyPoly ( poly , angTol ) {
413 let out = poly;
414 for ( let pass = 0 ; pass < 3 && out. length > 6 ; pass ++ ) {
415 const next = [];
416 let dropped = false ;
417 for ( let i = 0 ; i < out. length ; i ++ ) {
418 const A = out[(i + out. length - 1 ) % out. length ];
419 const B = out[i];
420 const C = out[(i + 1 ) % out. length ];
421 const v1x = B [ 0 ] - A [ 0 ],
422 v1y = B [ 1 ] - A [ 1 ];
423 const v2x = C [ 0 ] - B [ 0 ],
424 v2y = C [ 1 ] - B [ 1 ];
425 const ang = Math. abs (Math. atan2 (v1x * v2y - v1y * v2x, v1x * v2x + v1y * v2y));
426 if (ang < angTol && ! dropped && out. length - 1 > 5 ) {
427 dropped = true ;
428 continue ;
429 }
430 dropped = false ;
431 next. push ( B );
432 }
433 if (next. length === out. length ) break ;
434 out = next;
435 }
436 return out;
437 }
438
439 const vor = d3.Delaunay. from (points). voronoi (bounds);
440 /* ---- polygon hygiene: the blob clip can emit degenerate results
441 (duplicate points, collinear runs, self-intersecting "bowties").
442 Those extrude into folded shards with broken reflections, so every
443 piece is sanitised and must be a simple polygon before it becomes
444 geometry. ---- */
445 function sanitizePoly ( poly ) {
446 if ( ! poly || poly. length < 3 ) return null ;
447 let out = [];
448 for ( const pt of poly) {
449 const last = out[out. length - 1 ];
450 if (last && Math. hypot (pt[ 0 ] - last[ 0 ], pt[ 1 ] - last[ 1 ]) < 1e-4 ) continue ;
451 out. push (pt);
452 }
453 while (
454 out. length > 1 &&
455 Math. hypot (out[ 0 ][ 0 ] - out[out. length - 1 ][ 0 ], out[ 0 ][ 1 ] - out[out. length - 1 ][ 1 ]) < 1e-4
456 )
457 out. pop ();
458 if (out. length < 3 ) return null ;
459 /* drop collinear vertices */
460 const clean = [];
461 for ( let i = 0 ; i < out. length ; i ++ ) {
462 const a = out[(i + out. length - 1 ) % out. length ];
463 const b = out[i];
464 const c = out[(i + 1 ) % out. length ];
465 const cr = (b[ 0 ] - a[ 0 ]) * (c[ 1 ] - b[ 1 ]) - (b[ 1 ] - a[ 1 ]) * (c[ 0 ] - b[ 0 ]);
466 const l1 = Math. hypot (b[ 0 ] - a[ 0 ], b[ 1 ] - a[ 1 ]);
467 const l2 = Math. hypot (c[ 0 ] - b[ 0 ], c[ 1 ] - b[ 1 ]);
468 if (Math. abs (cr) < 1e-6 * Math. max ( 1e-6 , l1 * l2)) continue ;
469 clean. push (b);
470 }
471 if (clean. length < 3 ) return null ;
472 /* enforce CCW (the source inset assumes it) */
473 if ( polyArea2 (clean) < 0 ) clean. reverse ();
474 return clean;
475 }
476 function segsIntersect ( a , b , c , d ) {
477 const o = ( p , q , r ) => Math. sign ((q[ 0 ] - p[ 0 ]) * (r[ 1 ] - p[ 1 ]) - (q[ 1 ] - p[ 1 ]) * (r[ 0 ] - p[ 0 ]));
478 const o1 = o (a, b, c),
479 o2 = o (a, b, d),
480 o3 = o (c, d, a),
481 o4 = o (c, d, b);
482 return o1 !== o2 && o3 !== o4 && o1 !== 0 && o2 !== 0 && o3 !== 0 && o4 !== 0 ;
483 }
484 function isSimplePolygon ( poly ) {
485 const n = poly. length ;
486 for ( let i = 0 ; i < n; i ++ ) {
487 const a = poly[i],
488 b = poly[(i + 1 ) % n];
489 for ( let j = i + 1 ; j < n; j ++ ) {
490 if (j === i || (j + 1 ) % n === i || j === (i + 1 ) % n) continue ;
491 const c = poly[j],
492 d = poly[(j + 1 ) % n];
493 if ( segsIntersect (a, b, c, d)) return false ;
494 }
495 }
496 return true ;
497 }
498 function goodPoly ( poly , minArea ) {
499 const sp = sanitizePoly (poly);
500 if ( ! sp) return null ;
501 if ( polyArea2 (sp) < minArea) return null ;
502 if ( ! isSimplePolygon (sp)) return null ;
503 return sp;
504 }
505
506 const randFly = gstMulberry32 ( SEED * 31337 + 7 );
507 const randTilt = gstMulberry32 ( SEED * 9973 + 1 );
508 let cells = [];
509 let globalMinEdge = Infinity ;
510 for ( let ci = 0 ; ci < interiorCount; ci ++ ) {
511 const cellPoly = vor. cellPolygon (ci);
512 if ( ! cellPoly) continue ;
513 const cell = cellPoly. slice ( 0 , - 1 );
514 /* The piece is the blob ∩ this cell: pieces always tile the whole
515 silhouette, so the text is always fully covered — tileCount only
516 decides how many pieces share the job. */
517 let raw = goodPoly ( simplifyPoly ( clipToConvex (blobPoly, cell), 0.14 ), 3e-4 );
518 if ( ! raw) {
519 /* Degenerate clip. A text-bearing cell must not vanish (that is a
520 hole in a letter) — fall back to the unclipped Voronoi cell;
521 a non-text sliver is simply dropped. */
522 const fallback = goodPoly (cell, 3e-4 );
523 if ( ! fallback || ! cellTouchesText (fallback)) continue ;
524 raw = fallback;
525 }
526 let off = insetPolygon (raw, GAP / 2 );
527 off = (off && goodPoly (off, 2e-4 )) || raw;
528 let cellMinEdge = Infinity ;
529 for ( let ei = 0 ; ei < off. length ; ei ++ ) {
530 const pa = off[ei],
531 pb = off[(ei + 1 ) % off. length ];
532 const el = Math. hypot (pb[ 0 ] - pa[ 0 ], pb[ 1 ] - pa[ 1 ]);
533 if (el > 1e-6 && el < cellMinEdge) cellMinEdge = el;
534 }
535 let ccx = 0 ,
536 ccy = 0 ;
537 for ( const pt of off) {
538 ccx += pt[ 0 ];
539 ccy += pt[ 1 ];
540 }
541 ccx /= off. length ;
542 ccy /= off. length ;
543 /* Per-corner clamping happens inside buildPieceGeometry (each corner
544 is limited by its own adjacent edges), so the radius is passed
545 through — a single short edge no longer disables rounding for the
546 whole shard. */
547 cells. push ({
548 cx: ccx,
549 cy: ccy,
550 radius: CORNER_R ,
551 local: off. map (( pt ) => [pt[ 0 ] - ccx, pt[ 1 ] - ccy]),
552 });
553 void cellMinEdge;
554 }
555 void globalMinEdge;
556
557 /* The fattened text mask has done its only job (cell selection). Drop
558 the 1280x720 RGBA readback and its backing canvas now instead of
559 keeping ~7 MB alive for the whole life of the instance. */
560 maskData = null ;
561 maskCanvas.width = maskCanvas.height = 1 ;
562
563 const shards = cells. map (( c ) => {
564 const sign = c.cx >= 0 ? 1 : - 1 ;
565 /* Direct fly coordinates: sideDist is how far to the side (world
566 units, 0 = no sideways motion at all), zDist is the z coordinate
567 the shards come from (negative = behind the pane). Both jittered
568 slightly per shard so the swarm doesn't move as one block. */
569 const inOff = [
570 sign * G .vars.sideDist * ( 0.7 + randFly () * 0.6 ),
571 G .vars.sideDist === 0 && G .vars.zDist === 0 ? 0 : ( randFly () - 0.5 ) * 1.7 ,
572 G .vars.zDist * ( 0.85 + randFly () * 0.3 ),
573 ];
574 const inSpin = [
575 ( randFly () - 0.5 ) * Math. PI * 1.0 * G .vars.flyInRotation,
576 ( randFly () - 0.5 ) * Math. PI * 1.3 * G .vars.flyInRotation,
577 ( randFly () - 0.5 ) * Math. PI * 0.5 * G .vars.flyInRotation,
578 ];
579 /* Fly-out: mirror of the fly-in — back out the same side and back
580 into the same depth, faster, spinning harder. */
581 const outOff = [
582 sign * G .vars.sideDist * ( 0.8 + randFly () * 0.7 ),
583 G .vars.sideDist === 0 && G .vars.zOut === 0 ? 0 : ( randFly () - 0.5 ) * 2.2 ,
584 G .vars.zOut * ( 0.85 + randFly () * 0.3 ),
585 ];
586 const outSpin = [
587 ( randFly () - 0.5 ) * Math. PI * 1.6 * G .vars.flyOutRotation,
588 ( randFly () - 0.5 ) * Math. PI * 2.0 * G .vars.flyOutRotation,
589 ( randFly () - 0.5 ) * Math. PI * 0.8 * G .vars.flyOutRotation,
590 ];
591 return {
592 cx: c.cx,
593 cy: c.cy,
594 local: c.local,
595 radius: c.radius,
596 baseRot: [( randTilt () - 0.5 ) * 0.08 , ( randTilt () - 0.5 ) * 0.08 , ( randTilt () - 0.5 ) * 0.03 ],
597 wobPhase: randTilt () * Math. PI * 2 ,
598 wobFreq: 0.45 + randTilt () * 0.5 ,
599 inOff,
600 inSpin,
601 outOff,
602 outSpin,
603 rIn: randFly (),
604 rOut: randFly (),
605 };
606 });
607 const n1 = Math. max ( 1 , shards. length - 1 );
608 /* IN: centre shards knit first. OUT: edges peel first. */
609 const inRank = shards. map (( s , i ) => [Math. abs (s.cx), i]). sort (( a , b ) => a[ 0 ] - b[ 0 ]);
610 const STG = G .vars.stagger;
611 inRank. forEach (( pair , rank ) => {
612 const s = shards[pair[ 1 ]];
613 s.inDelay = STG * 0.85 * (rank / n1) + s.rIn * 0.05 * STG ;
614 /* The delay is bounded below 0.9, so at least 10% remains. A 0.12
615 duration floor pushed the last shards past tin=1 at high stagger,
616 leaving residual flight rotation throughout the hold and exit. */
617 s.inDur = 1 - s.inDelay;
618 });
619 const outRank = shards. map (( s , i ) => [Math. abs (s.cx), i]). sort (( a , b ) => b[ 0 ] - a[ 0 ]);
620 outRank. forEach (( pair , rank ) => {
621 const s = shards[pair[ 1 ]];
622 s.outDelay = STG * 0.85 * (rank / n1) + s.rOut * 0.05 * STG ;
623 s.outDur = Math. max ( 0.12 , 1 - s.outDelay);
624 });
625
626 /* ---- Materials: iridescent glass carrying its slice of the headline
627 (planar UVs + emissive map — no Chrome HTML-in-canvas API) ---- */
628 function makeIridescenceThicknessMap ( size ) {
629 const c = document. createElement ( "canvas" );
630 c.width = size;
631 c.height = size;
632 const ctx = c. getContext ( "2d" );
633 const grad = ctx. createLinearGradient ( 0 , 0 , size, size);
634 grad. addColorStop ( 0.0 , "#202020" );
635 grad. addColorStop ( 0.5 , "#a0a0a0" );
636 grad. addColorStop ( 1.0 , "#f0f0f0" );
637 ctx.fillStyle = grad;
638 ctx. fillRect ( 0 , 0 , size, size);
639 const radial = ctx. createRadialGradient (
640 size * 0.3 ,
641 size * 0.4 ,
642 0 ,
643 size * 0.3 ,
644 size * 0.4 ,
645 size * 0.9 ,
646 );
647 radial. addColorStop ( 0 , "rgba(255,255,255,0.4)" );
648 radial. addColorStop ( 1 , "rgba(0,0,0,0)" );
649 ctx.fillStyle = radial;
650 ctx. fillRect ( 0 , 0 , size, size);
651 const tex = new THREE . CanvasTexture (c);
652 tex.wrapS = tex.wrapT = THREE .RepeatWrapping;
653 return tex;
654 }
655
656 /* Flat dark transmission target, painted before all shards WITHOUT
657 occupying depth. Writing depth here hid every shard behind z=-3.5:
658 neither increasing fog distance nor disabling fog could reveal it.
659 Keep the original shading/color, including in the transmission pass. */
660 {
661 const d = CAM_Z + 3.5 ;
662 const bh2 = 2 * d * Math. tan (( FOV * Math. PI ) / 360 );
663 const back = new THREE . Mesh (
664 new THREE . PlaneGeometry (bh2 * ( 1920 / 1080 ) * 1.05 , bh2 * 1.05 ),
665 new THREE . MeshBasicMaterial ({ color: 0x0a0a0f , depthWrite: false , depthTest: false }),
666 );
667 back.position.z = - 3.5 ;
668 back.renderOrder = - 1 ;
669 scene. add (back);
670 }
671
672 /* Manual per-FRAGMENT fog (three's scene.fog is bypassed entirely):
673 each fragment's own view depth drives an exp2 mix toward the
674 background, starting just behind the seated plane — so a tumbling
675 shard's near edge resolves before its far edge, like flying through
676 real fog. Applied to the glass shading and, as a pure fade, to the
677 additive matcap sheen (otherwise the sheen ghosts through the fog). */
678 const FOG_START = CAM_Z - PANE_Z + 0.28 ;
679 const FOG_RHO = G .vars.fog > 0 ? 4.6 / Math. max ( 2 , G .vars.fog) : 0.0 ;
680
681 /* The headline belongs on the FRONT cap only — not mirrored on the back
682 cap, not smeared down the side walls. Masked by the object-space
683 normal. The same patch pushes the fog start just past the seated
684 plane so resting shards are fog-free. */
685 function addManualFog ( shader , additive ) {
686 shader.uniforms.uGstFogStart = { value: FOG_START };
687 shader.uniforms.uGstFogRho = { value: FOG_RHO };
688 shader.uniforms.uGstFogColor = { value: new THREE . Color ( 0x0a0a0f ) };
689 shader.vertexShader = shader.vertexShader
690 . replace ( "#include <common>" , "#include <common> \n varying float vGstDep;" )
691 . replace ( "#include <project_vertex>" , "#include <project_vertex> \n vGstDep = -mvPosition.z;" );
692 shader.fragmentShader = shader.fragmentShader
693 . replace (
694 "#include <common>" ,
695 "#include <common> \n varying float vGstDep; \n uniform float uGstFogStart; \n uniform float uGstFogRho; \n uniform vec3 uGstFogColor;" ,
696 )
697 . replace (
698 "#include <dithering_fragment>" ,
699 (additive
700 ? "gl_FragColor.rgb *= exp(-pow(max(0.0, vGstDep - uGstFogStart) * uGstFogRho, 2.0)); \n "
701 : "gl_FragColor.rgb = mix(gl_FragColor.rgb, uGstFogColor, 1.0 - exp(-pow(max(0.0, vGstDep - uGstFogStart) * uGstFogRho, 2.0))); \n " ) +
702 "#include <dithering_fragment>" ,
703 );
704 }
705 function patchGlass ( shader ) {
706 shader.vertexShader = shader.vertexShader
707 . replace ( "#include <common>" , "#include <common> \n varying float vNzObj;" )
708 . replace ( "#include <beginnormal_vertex>" , "#include <beginnormal_vertex> \n vNzObj = normal.z;" );
709 shader.fragmentShader = shader.fragmentShader
710 . replace ( "#include <common>" , "#include <common> \n varying float vNzObj;" )
711 . replace (
712 "#include <emissivemap_fragment>" ,
713 "#include <emissivemap_fragment> \n totalEmissiveRadiance *= smoothstep(0.5, 0.78, vNzObj);" ,
714 );
715 addManualFog (shader, false );
716 }
717
718 const glassMat = new THREE . MeshPhysicalMaterial ({
719 color: 0xffffff ,
720 transmission: 1 ,
721 thickness: st.thickness,
722 ior: st.ior,
723 dispersion: 0.24 ,
724 roughness: st.roughness,
725 metalness: 0 ,
726 clearcoat: 1 ,
727 clearcoatRoughness: 0.59 ,
728 specularIntensity: 1 ,
729 specularColor: 0xffffff ,
730 attenuationDistance: 4.7 ,
731 attenuationColor: 0xffffff ,
732 envMapIntensity: 1.1 ,
733 iridescence: 1 ,
734 iridescenceIOR: 2.34 ,
735 iridescenceThicknessRange: [ 80 , 500 ],
736 iridescenceThicknessMap: makeIridescenceThicknessMap ( 256 ),
737 side: THREE .DoubleSide,
738 emissive: 0xffffff ,
739 emissiveMap: headTex,
740 emissiveIntensity: 0.95 ,
741 });
742 glassMat.onBeforeCompile = patchGlass;
743 const matcapMat = new THREE . MeshMatcapMaterial ({
744 fog: false ,
745 matcap: matcapTex,
746 transparent: true ,
747 opacity: 0.11 ,
748 blending: THREE .AdditiveBlending,
749 depthWrite: false ,
750 side: THREE .DoubleSide,
751 });
752 matcapMat. onBeforeCompile = ( sh ) => addManualFog (sh, true );
753
754 /* ---- Shard geometry (rounded Voronoi cells, extruded + beveled) ---- */
755 function softMin3 ( a , b , c , k ) {
756 if (k <= 0 ) return Math. min (a, b, c);
757 const m = Math. min (a, b, c);
758 return m - k * Math. log (Math. exp ( - (a - m) / k) + Math. exp ( - (b - m) / k) + Math. exp ( - (c - m) / k));
759 }
760 function buildPieceGeometry (
761 poly ,
762 cornerRadius ,
763 depth ,
764 clampSmoothness ,
765 smoothChamferDist ,
766 smoothChamferAngle ,
767 ) {
768 const shape = new THREE . Shape ();
769 const n = poly. length ;
770 for ( let i = 0 ; i < n; i ++ ) {
771 const prev = poly[(i + n - 1 ) % n];
772 const curr = poly[i];
773 const next = poly[(i + 1 ) % n];
774 const v1x = curr[ 0 ] - prev[ 0 ],
775 v1y = curr[ 1 ] - prev[ 1 ];
776 const v2x = next[ 0 ] - curr[ 0 ],
777 v2y = next[ 1 ] - curr[ 1 ];
778 const len1 = Math. hypot (v1x, v1y),
779 len2 = Math. hypot (v2x, v2y);
780 if (len1 < 1e-6 || len2 < 1e-6 ) continue ;
781 const u1x = v1x / len1,
782 u1y = v1y / len1;
783 const u2x = v2x / len2,
784 u2y = v2y / len2;
785 const cosI = Math. max ( - 1 , Math. min ( 1 , - u1x * u2x - u1y * u2y));
786 const sinI = Math. sqrt (Math. max ( 0 , 1 - cosI * cosI));
787 const oneCosI = 1 + cosI;
788 const tanHalf = oneCosI > 1e-6 ? sinI / oneCosI : 0 ;
789 let d = 0 ;
790 if (tanHalf > 1e-4 ) {
791 const dRaw = cornerRadius / tanHalf;
792 const lim1 = len1 * 0.49 ,
793 lim2 = len2 * 0.49 ;
794 let dTarget = dRaw;
795 if (smoothChamferDist > 0 && smoothChamferAngle > 0 ) {
796 const interior = Math. acos (cosI);
797 if (interior < smoothChamferAngle) {
798 const tLin = Math. min ( 1 , (smoothChamferAngle - interior) / smoothChamferAngle);
799 const tEased = tLin * tLin * ( 3 - 2 * tLin);
800 dTarget = Math. max (dRaw, smoothChamferDist * tEased);
801 }
802 }
803 d = Math. max ( 0 , softMin3 (dTarget, lim1, lim2, clampSmoothness));
804 }
805 const startX = curr[ 0 ] - u1x * d,
806 startY = curr[ 1 ] - u1y * d;
807 const endX = curr[ 0 ] + u2x * d,
808 endY = curr[ 1 ] + u2y * d;
809 if (i === 0 ) shape. moveTo (startX, startY);
810 else shape. lineTo (startX, startY);
811 if (d > 1e-4 ) {
812 const kappa = 0.5523 ;
813 shape. bezierCurveTo (
814 startX + u1x * d * kappa,
815 startY + u1y * d * kappa,
816 endX - u2x * d * kappa,
817 endY - u2y * d * kappa,
818 endX,
819 endY,
820 );
821 } else {
822 shape. lineTo (curr[ 0 ], curr[ 1 ]);
823 }
824 }
825 shape. closePath ();
826 /* Source: bevelThickness = bevelSize = depth*0.25,
827 bevelSegments 6, curveSegments 24. The vars scale those constants:
828 bevel 0.35 and meshSmooth 0.6 reproduce the source exactly, and a
829 near-zero bevel disables bevelling outright — clean flat-sided
830 tiles instead of a micro-sliver bevel. */
831 const bev = depth * G .vars.bevel * 0.714 ;
832 const bevelOn = G .vars.bevel > 0.02 ;
833 return new THREE . ExtrudeGeometry (shape, {
834 depth,
835 bevelEnabled: bevelOn,
836 bevelThickness: bevelOn ? bev : 0 ,
837 bevelSize: bevelOn ? bev : 0 ,
838 bevelSegments: Math. max ( 1 , Math. round (( G .vars.meshSmooth / 0.6 ) * 6 )),
839 curveSegments: Math. max ( 4 , Math. round (( G .vars.meshSmooth / 0.6 ) * 24 )),
840 });
841 }
842
843 const paneGroup = new THREE . Group ();
844 paneGroup.position.z = PANE_Z ;
845 scene. add (paneGroup);
846
847 const chamferAngle = ( 28 * Math. PI ) / 180 ;
848 const shardGroups = [];
849 for ( const sh of shards) {
850 const chamferDist = Math. max ( GAP / 2 , sh.radius * 1.5 );
851 const geom = buildPieceGeometry (sh.local, sh.radius, PANE_DEPTH , 0 , chamferDist, chamferAngle);
852 geom. translate ( 0 , 0 , - PANE_DEPTH / 2 );
853 /* Planar frustum-space UVs: each shard samples its slice of the
854 headline raster, so the assembled shards read as the text. */
855 const pos = geom.attributes.position;
856 const uvs = new Float32Array (pos.count * 2 );
857 for ( let v = 0 ; v < pos.count; v ++ ) {
858 uvs[v * 2 ] = (pos. getX (v) + sh.cx) / VW + 0.5 ;
859 uvs[v * 2 + 1 ] = (pos. getY (v) + sh.cy) / VH + 0.5 ;
860 }
861 geom. setAttribute ( "uv" , new THREE . BufferAttribute (uvs, 2 ));
862 const grp = new THREE . Group ();
863 grp. add ( new THREE . Mesh (geom, glassMat));
864 grp. add ( new THREE . Mesh (geom, matcapMat));
865 grp.position. set (sh.cx, sh.cy, 0 );
866 paneGroup. add (grp);
867 shardGroups. push (grp);
868 }
869
870 /* ---- Closed-form easings for per-shard local progress ---- */
871 /* Position-only power ease-out — never overshoots. Rotation has its
872 own gentle curve below, but shares position's local flight clock. */
873 function powOut ( x ) {
874 return 1 - Math. pow ( 1 - x, G .vars.easePow);
875 }
876 function clamp01 ( x ) {
877 return x < 0 ? 0 : x > 1 ? 1 : x;
878 }
879
880 function renderScene () {
881 const t = st.time;
882 glassMat.thickness = st.thickness;
883 glassMat.roughness = st.roughness;
884 glassMat.ior = st.ior;
885
886 /* Whole-cluster idle drift (the "all rotate slightly" hold motion). */
887 paneGroup.rotation.x = 0.02 * Math. sin (t * 0.5 + 0.7 );
888 paneGroup.rotation.y = 0.045 * Math. sin (t * 0.33 + 2.1 );
889 paneGroup.position.z = PANE_Z + 0.02 * Math. sin (t * 0.5 );
890 scene.environmentRotation.y = 0.6 + t * 0.04 ;
891
892 for ( let i = 0 ; i < shards. length ; i ++ ) {
893 const sh = shards[i];
894 const grp = shardGroups[i];
895
896 /* Local fly-in progress from the master, staggered + eased. */
897 const ui = clamp01 ((st.tin - sh.inDelay) / sh.inDur);
898 const p = 1 - powOut (ui); /* 1 = out at edge, 0 = seated */
899 /* Depth travels near-linearly: with the full ease-out applied to z
900 the shard crossed the whole fog zone in the first frames and
901 arrived already clear — the fog had nothing to act on. */
902 const pz = 1 - ( 1 - Math. pow ( 1 - ui, Math. max ( 1.15 , G .vars.easePow * 0.38 )));
903 /* Keep tumbling through the FULL staggered flight window. Using p
904 here consumed 99.9% of the spin halfway through an easePow=10
905 entrance, long before depth arrived. A gentle, seeded ease-out
906 retains visible rotation late in transit and reaches exactly
907 zero only at this shard's position-arrival (ui=1), with zero
908 terminal angular velocity. Reuse rIn so the exit's seeded random
909 stream, all positions, and the arrival schedule stay unchanged.
910 flyInRotation remains a linear amplitude, including true zero. */
911 const pr = Math. pow ( 1 - ui, 1.15 + sh.rIn * 0.2 );
912 const uo = clamp01 ((st.tout - sh.outDelay) / sh.outDur);
913 const q = uo * uo; /* power2.in departure */
914 const qz = Math. pow (uo, 1.35 ); /* gentler depth exit too */
915
916 let x = sh.cx,
917 y = sh.cy,
918 z = 0 ;
919 let rx = sh.baseRot[ 0 ],
920 ry = sh.baseRot[ 1 ],
921 rz = sh.baseRot[ 2 ];
922
923 if (p !== 0 || pz !== 0 || pr !== 0 ) {
924 x += sh.inOff[ 0 ] * p;
925 y += sh.inOff[ 1 ] * p;
926 z += sh.inOff[ 2 ] * pz;
927 rx += sh.inSpin[ 0 ] * pr;
928 ry += sh.inSpin[ 1 ] * pr;
929 rz += sh.inSpin[ 2 ] * pr;
930 }
931 /* Idle wobble: only when seated, fades in as p settles, out as q rises. */
932 const seated = Math. max ( 0 , 1 - Math. abs (p) * 3 ) * Math. max ( 0 , 1 - q * 3 );
933 if (seated > 0.001 ) {
934 const w = sh.wobFreq;
935 rx += 0.03 * Math. sin (t * w + sh.wobPhase) * seated;
936 ry += 0.045 * Math. sin (t * w * 0.83 + sh.wobPhase * 2.1 ) * seated;
937 rz += 0.012 * Math. sin (t * w * 1.31 + sh.wobPhase * 0.7 ) * seated;
938 z += 0.018 * Math. sin (t * 0.9 + sh.wobPhase) * seated;
939 }
940 if (uo > 0 ) {
941 x += sh.outOff[ 0 ] * q;
942 y += sh.outOff[ 1 ] * q;
943 z += sh.outOff[ 2 ] * qz;
944 rx += sh.outSpin[ 0 ] * q;
945 ry += sh.outSpin[ 1 ] * q;
946 rz += sh.outSpin[ 2 ] * q;
947 }
948 grp.position. set (x, y, z);
949 grp.rotation. set (rx, ry, rz);
950 }
951
952 renderer. render (scene, camera);
953 }
954
955 window.__gstRender = renderScene;
956 renderScene ();
957
958 /* ---- Teardown ------------------------------------------------------
959 Nothing above this line was ever released. Every host that mounts
960 this block does so in a document it later throws away (Studio detaches
961 its preview iframes; the gallery keeps one per tile), and until the
962 collector happens to run, a discarded instance still owns its WebGL
963 context, its render targets, its 4096x2304 headline raster and 34
964 extruded shard geometries. Release all of it the moment the document
965 goes away, and expose the same call so a host can do it on demand.
966
967 `pagehide` is the reliable frame-detach signal in current Chrome;
968 `unload` is kept as a fallback for older engines. Deliberately NOT
969 wired to `visibilitychange` — a backgrounded Studio tab must come
970 back with a working stage. */
971 let gstDisposed = false ;
972 function gstDispose ( event ) {
973 /* A bfcache'd page is coming back with this canvas still on screen —
974 tearing the context down here would restore a dead stage. */
975 if (event && event.persisted === true ) return ;
976 if (gstDisposed) return ;
977 gstDisposed = true ;
978 try {
979 window. removeEventListener ( "pagehide" , gstDispose);
980 window. removeEventListener ( "unload" , gstDispose);
981 if (window.__gstRender === renderScene) window.__gstRender = null ;
982 if (window.__gstInstance && window.__gstInstance.dispose === gstDispose)
983 window.__gstInstance = null ;
984
985 scene. traverse ( function ( obj ) {
986 if (obj.geometry) obj.geometry. dispose ();
987 const mats = Array. isArray (obj.material) ? obj.material : obj.material ? [obj.material] : [];
988 for ( const m of mats) {
989 for ( const key in m) {
990 const v = m[key];
991 if (v && v.isTexture) v. dispose ();
992 }
993 m. dispose ();
994 }
995 });
996 headTex. dispose ();
997 matcapTex. dispose ();
998 envRT. dispose ();
999 scene.environment = null ;
1000 scene. clear ();
1001 shardGroups. length = 0 ;
1002 shards. length = 0 ;
1003 /* Free the 2D backing stores as well — the headline raster alone is
1004 4096 x 2304 x 4 B. */
1005 headCanvas.width = headCanvas.height = 1 ;
1006 maskCanvas.width = maskCanvas.height = 1 ;
1007 renderer. dispose ();
1008 /* Hands the context back immediately instead of waiting for the
1009 canvas to be collected; Chrome only allows a small number of live
1010 WebGL contexts per renderer process. */
1011 renderer. forceContextLoss ();
1012 } catch (e) {
1013 /* teardown is best-effort */
1014 }
1015 }
1016 window.__gstInstance = { dispose: gstDispose };
1017 window. addEventListener ( "pagehide" , gstDispose);
1018 window. addEventListener ( "unload" , gstDispose);