@@ -1298,8 +1298,9 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
12981298}
12991299
13001300/* --- scroll-driven state --- */
1301- /* calm: 0 = full animation, 1 = cube gone, chunks stopped, stars only */
1302- const state = { progress : 0 , mouseX : 0 , mouseY : 0 , calm : 0 } ;
1301+ /* calm: 0 = full animation, 1 = cube gone, chunks stopped, stars only
1302+ starFade: 0 = stars at full brightness, 1 = stars gone (no animation left) */
1303+ const state = { progress : 0 , mouseX : 0 , mouseY : 0 , calm : 0 , starFade : 0 } ;
13031304
13041305window . addEventListener ( 'pointermove' , ( e ) => {
13051306 state . mouseX = ( e . clientX / window . innerWidth - 0.5 ) * 2 ;
@@ -1371,6 +1372,9 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
13711372 sp . needsUpdate = true ;
13721373 // barely-there lateral sway; bounded so the wrap plane stays behind the camera
13731374 stars . rotation . y = Math . sin ( t * 0.05 ) * 0.04 ;
1375+ // second wind-down stage: once the cube is gone, the starfield fades to nothing
1376+ stars . material . opacity = 0.7 * ( 1 - state . starFade ) ;
1377+ stars . visible = state . starFade < 0.999 ;
13741378
13751379 // lattice: home position pushed along explode direction by scroll
13761380 if ( cubeGroup . visible ) {
@@ -1435,14 +1439,34 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
14351439 renderer . setAnimationLoop ( render ) ;
14361440}
14371441
1438- /* --- stop button: wind the scene down to a calm starfield --- */
1442+ /* --- stop button: wind the scene down in two stages, then to nothing ---
1443+ stop: cube + chunks recede (as before), then the starfield fades out and
1444+ the render loop is parked, leaving no animated background at all.
1445+ resume: restart the loop, fade the stars back, then bring the cube back. */
14391446const stopBtn = document . getElementById ( 'stopBtn' ) ;
1447+ let stopped = false ;
1448+ let windTween = null ;
14401449stopBtn . addEventListener ( 'click' , ( ) => {
1441- const toCalm = state . calm < 0.5 ;
1442- stopBtn . textContent = toCalm ? 'Resume' : 'Stop' ;
1443- stopBtn . setAttribute ( 'aria-pressed' , String ( toCalm ) ) ;
1444- if ( reduced ) { state . calm = toCalm ? 1 : 0 ; render ( ) ; return ; }
1445- gsap . to ( state , { calm : toCalm ? 1 : 0 , duration : 2.8 , ease : 'power2.inOut' , overwrite : 'auto' } ) ;
1450+ stopped = ! stopped ;
1451+ stopBtn . textContent = stopped ? 'Resume' : 'Stop' ;
1452+ stopBtn . setAttribute ( 'aria-pressed' , String ( stopped ) ) ;
1453+ if ( reduced ) {
1454+ state . calm = state . starFade = stopped ? 1 : 0 ;
1455+ render ( ) ;
1456+ return ;
1457+ }
1458+ if ( windTween ) windTween . kill ( ) ;
1459+ if ( stopped ) {
1460+ windTween = gsap . timeline ( )
1461+ . to ( state , { calm : 1 , duration : 2.8 , ease : 'power2.inOut' } )
1462+ . to ( state , { starFade : 1 , duration : 1.6 , ease : 'power2.inOut' ,
1463+ onComplete : ( ) => renderer . setAnimationLoop ( null ) } ) ;
1464+ } else {
1465+ renderer . setAnimationLoop ( render ) ; // wake the loop before reversing
1466+ windTween = gsap . timeline ( )
1467+ . to ( state , { starFade : 0 , duration : 1.0 , ease : 'power2.out' } )
1468+ . to ( state , { calm : 0 , duration : 2.8 , ease : 'power2.inOut' } ) ;
1469+ }
14461470} ) ;
14471471
14481472// debugging/testing handle (this page is a prototype)
0 commit comments