@@ -750,7 +750,7 @@ <h1 data-hero>Backups with <em>deduplication</em>, <em>compression</em> and <em>
750750 < a class ="btn solid " href ="#install "> Install Borg</ a >
751751 < a class ="btn " href ="#demo "> Watch the demo</ a >
752752 < button class ="btn " id ="stopBtn " type ="button " aria-pressed ="true "
753- title ="Start the background animation " > Animate </ button >
753+ title ="Engage the warp drive " > Engage! </ button >
754754 </ div >
755755 < noscript >
756756 < div class ="hero-fallback ">
@@ -1197,7 +1197,8 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
11971197const scene = new THREE . Scene ( ) ;
11981198scene . fog = new THREE . FogExp2 ( 0x020503 , 0.012 ) ;
11991199
1200- const camera = new THREE . PerspectiveCamera ( 50 , 1 , 0.1 , 200 ) ;
1200+ const BASE_FOV = 50 ;
1201+ const camera = new THREE . PerspectiveCamera ( BASE_FOV , 1 , 0.1 , 200 ) ;
12011202camera . position . set ( 0 , 0 , 15 ) ;
12021203
12031204scene . add ( new THREE . AmbientLight ( 0x18301c , 1.4 ) ) ;
@@ -1260,8 +1261,23 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
12601261 starPos . set ( [ v . x , v . y , v . z ] , s * 3 ) ;
12611262}
12621263starGeo . setAttribute ( 'position' , new THREE . BufferAttribute ( starPos , 3 ) ) ;
1264+ // soft circular sprite so the points render as round dots instead of squares
1265+ // (white, so it still takes the material's colour tint)
1266+ const starSprite = ( ( ) => {
1267+ const sz = 64 , c = document . createElement ( 'canvas' ) ;
1268+ c . width = c . height = sz ;
1269+ const ctx = c . getContext ( '2d' ) ;
1270+ const g = ctx . createRadialGradient ( sz / 2 , sz / 2 , 0 , sz / 2 , sz / 2 , sz / 2 ) ;
1271+ g . addColorStop ( 0.0 , 'rgba(255,255,255,1)' ) ;
1272+ g . addColorStop ( 0.5 , 'rgba(255,255,255,0.92)' ) ;
1273+ g . addColorStop ( 1.0 , 'rgba(255,255,255,0)' ) ;
1274+ ctx . fillStyle = g ;
1275+ ctx . fillRect ( 0 , 0 , sz , sz ) ;
1276+ return new THREE . CanvasTexture ( c ) ;
1277+ } ) ( ) ;
12631278const stars = new THREE . Points ( starGeo , new THREE . PointsMaterial ( {
1264- color : 0x7fdb91 , size : 0.45 , sizeAttenuation : true , transparent : true , opacity : 0.7
1279+ color : 0x7fdb91 , size : 0.45 , sizeAttenuation : true , transparent : true , opacity : 0.7 ,
1280+ map : starSprite , depthWrite : false
12651281} ) ) ;
12661282scene . add ( stars ) ;
12671283
@@ -1294,7 +1310,7 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
12941310/* --- scroll-driven state --- */
12951311/* calm: 0 = full animation, 1 = cube gone, chunks stopped, stars only
12961312 starFade: 0 = stars at full brightness, 1 = stars gone (no animation left) */
1297- const state = { progress : 0 , mouseX : 0 , mouseY : 0 , calm : 1 , starFade : 1 } ;
1313+ const state = { progress : 0 , mouseX : 0 , mouseY : 0 , calm : 1 , starFade : 1 , warp : 1 , starWhite : 0 } ;
12981314
12991315window . addEventListener ( 'pointermove' , ( e ) => {
13001316 state . mouseX = ( e . clientX / window . innerWidth - 0.5 ) * 2 ;
@@ -1336,13 +1352,16 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
13361352 camera . aspect = w / h ;
13371353 camera . updateProjectionMatrix ( ) ;
13381354 // pull the camera back on narrow/portrait screens so the cube fits the width
1339- const halfTan = Math . tan ( THREE . MathUtils . degToRad ( camera . fov / 2 ) ) * camera . aspect ;
1355+ const halfTan = Math . tan ( THREE . MathUtils . degToRad ( BASE_FOV / 2 ) ) * camera . aspect ;
13401356 state . baseZ = Math . max ( 15 , 4.6 / halfTan ) ;
13411357} ;
13421358window . addEventListener ( 'resize' , resize ) ;
13431359resize ( ) ;
13441360
13451361const timer = new THREE . Timer ( ) ;
1362+ const starScratch = new THREE . Vector3 ( ) ; // reused when respawning wrapped stars
1363+ const starBaseColor = new THREE . Color ( 0x7fdb91 ) ; // cruise colour (matches the material)
1364+ const starWarpColor = new THREE . Color ( 0xffffff ) ; // full-warp white
13461365
13471366function render ( ) {
13481367 timer . update ( ) ;
@@ -1357,20 +1376,42 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
13571376
13581377 // stars drift past the camera — a slow cruise through space
13591378 // (a touch faster when calm, where the starfield is all there is)
1360- const starDrift = ( 1.2 + calm * 2.4 ) * dt ;
1379+ // state.warp multiplies the speed for the "Engage!" warp-drive burst
1380+ const starDrift = ( 1.2 + calm * 2.4 ) * state . warp * dt ;
13611381 const sp = starGeo . attributes . position ;
13621382 for ( let s = 0 ; s < starCount ; s ++ ) {
13631383 let z = sp . getZ ( s ) + starDrift ;
1364- if ( z > 40 ) z -= 140 ; // wrap from behind the camera to deep space
1384+ if ( z > 40 ) {
1385+ z -= 140 ; // wrap from behind the camera to deep space
1386+ // re-randomize the lateral position on each wrap so the finite field
1387+ // never visibly repeats — otherwise it tiles every 140 units, which at
1388+ // warp speed cycles ~3x in the burst and reads as a pulse
1389+ do { starScratch . randomDirection ( ) . multiplyScalar ( 40 + Math . random ( ) * 60 ) ; }
1390+ while ( Math . hypot ( starScratch . x , starScratch . y ) < 6 ) ;
1391+ sp . setX ( s , starScratch . x ) ;
1392+ sp . setY ( s , starScratch . y ) ;
1393+ }
13651394 sp . setZ ( s , z ) ;
13661395 }
13671396 sp . needsUpdate = true ;
13681397 // barely-there lateral sway; bounded so the wrap plane stays behind the camera
13691398 stars . rotation . y = Math . sin ( t * 0.05 ) * 0.04 ;
13701399 // second wind-down stage: once the cube is gone, the starfield fades to nothing
1371- stars . material . opacity = 0.7 * ( 1 - state . starFade ) ;
13721400 stars . visible = state . starFade < 0.999 ;
13731401
1402+ // warp drive: stars swell, brighten to white at full warp, then dim back —
1403+ // and the lens punches outward so the field rushes past. warpT tracks the
1404+ // warp curve (0 at cruise, 1 at full warp), so brightness ramps up while
1405+ // accelerating, holds at white through the constant phase, and fades on decel
1406+ const warpT = Math . min ( 1 , ( state . warp - 1 ) / 69 ) ;
1407+ stars . material . size = 0.45 + warpT * 1.0 ;
1408+ // colour follows its own track (starWhite): white the instant we engage,
1409+ // held through warp, greening only on deceleration
1410+ stars . material . color . copy ( starBaseColor ) . lerp ( starWarpColor , state . starWhite ) ;
1411+ stars . material . opacity = Math . min ( 1 , 0.7 * ( 1 - state . starFade ) + warpT * 0.5 ) ;
1412+ const warpFov = BASE_FOV + warpT * 28 ;
1413+ if ( Math . abs ( camera . fov - warpFov ) > 0.01 ) { camera . fov = warpFov ; camera . updateProjectionMatrix ( ) ; }
1414+
13741415 // lattice: home position pushed along explode direction by scroll
13751416 if ( cubeGroup . visible ) {
13761417 for ( let k = 0 ; k < COUNT ; k ++ ) {
@@ -1441,7 +1482,7 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
14411482let windTween = null ;
14421483stopBtn . addEventListener ( 'click' , ( ) => {
14431484 stopped = ! stopped ;
1444- stopBtn . textContent = stopped ? 'Animate ' : 'Stop' ;
1485+ stopBtn . textContent = stopped ? 'Engage! ' : 'Stop' ;
14451486 stopBtn . setAttribute ( 'aria-pressed' , String ( stopped ) ) ;
14461487 if ( reduced ) {
14471488 state . calm = state . starFade = stopped ? 1 : 0 ;
@@ -1451,19 +1492,32 @@ <h2>Trusted by sysadmins, hoarders and the merely paranoid.</h2>
14511492 if ( windTween ) windTween . kill ( ) ;
14521493 if ( stopped ) {
14531494 windTween = gsap . timeline ( )
1454- . to ( state , { calm : 1 , duration : 2.8 , ease : 'power2.inOut' } )
1495+ . to ( state , { warp : 1 , duration : 0.6 , ease : 'power2.out' } , 0 ) // drop out of warp if mid-burst
1496+ . to ( state , { starWhite : 0 , duration : 0.6 , ease : 'power2.out' } , 0 ) // and back to green
1497+ . to ( state , { calm : 1 , duration : 2.8 , ease : 'power2.inOut' } , 0 )
14551498 . to ( state , { starFade : 1 , duration : 1.6 , ease : 'power2.inOut' ,
14561499 onComplete : ( ) => renderer . setAnimationLoop ( null ) } ) ;
14571500 } else {
1458- renderer . setAnimationLoop ( render ) ; // wake the loop before reversing
1501+ renderer . setAnimationLoop ( render ) ; // wake the loop before engaging
1502+ state . warp = 1 ;
1503+ state . starWhite = 1 ; // stars flash to white the instant we engage
14591504 windTween = gsap . timeline ( )
1460- . to ( state , { starFade : 0 , duration : 1.0 , ease : 'power2.out' } )
1461- . to ( state , { calm : 0 , duration : 2.8 , ease : 'power2.inOut' } ) ;
1505+ // stars snap in (already white), then the warp drive winds up — accelerating hard…
1506+ . to ( state , { starFade : 0 , duration : 0.4 , ease : 'power1.out' } , 0 )
1507+ . to ( state , { warp : 70 , duration : 2.0 , ease : 'power3.in' } , 0 )
1508+ // …holds at full warp (constant speed)…
1509+ . to ( state , { warp : 70 , duration : 2.0 } )
1510+ // …then decelerates back to a normal cruise
1511+ . to ( state , { warp : 1 , duration : 2.0 , ease : 'power2.out' } )
1512+ // stars stay white through accel + cruise, then green back down over the decel
1513+ . to ( state , { starWhite : 0 , duration : 2.0 , ease : 'power2.out' } , 4 )
1514+ // dropping out of warp, the Borg cube reassembles into the familiar scene
1515+ . to ( state , { calm : 0 , duration : 2.4 , ease : 'power2.inOut' } , '-=1.5' ) ;
14621516 }
14631517} ) ;
14641518
14651519// debugging/testing handle (this page is a prototype)
1466- window . __borg = { state, drag, cubeGroup, renderer } ;
1520+ window . __borg = { state, drag, cubeGroup, camera , renderer } ;
14671521
14681522/* ================================================================
14691523 GSAP — choreography
0 commit comments