Skip to content

Commit 8f9d219

Browse files
Merge pull request #115 from borgbackup/engage
index2.html: "Engage!" warp-drive intro
2 parents 5ee395b + 38efbe8 commit 8f9d219

1 file changed

Lines changed: 68 additions & 14 deletions

File tree

index2.html

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
11971197
const scene = new THREE.Scene();
11981198
scene.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);
12011202
camera.position.set(0, 0, 15);
12021203

12031204
scene.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
}
12621263
starGeo.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+
})();
12631278
const 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
}));
12661282
scene.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

12991315
window.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
};
13421358
window.addEventListener('resize', resize);
13431359
resize();
13441360

13451361
const 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

13471366
function 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>
14411482
let windTween = null;
14421483
stopBtn.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

Comments
 (0)