From c73985f380e958a49e5d74cbf5281b46d050e9de Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:46:49 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20northern=20lights=20respond=20to=20git?= =?UTF-8?q?=20push=20events=20=E2=80=94=20flash=20brighter=20on=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds aurora borealis effect to the Nexus sky: - Three translucent curtain planes (green/cyan/purple) positioned high in the background, gently undulating with sine-wave opacity and position. - pollForAuroraEvents() polls the Gitea commits API every 30 s; on a new push it calls triggerAuroraFlash('push') (moderate intensity boost) and on a merge commit triggerAuroraFlash('merge') (extra-bright flash). - Flash intensity decays at 0.35 units/s back to the ambient baseline. - delta is now tracked explicitly in the animation loop to drive decay. Fixes #248 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/app.js b/app.js index b396094..8f8b925 100644 --- a/app.js +++ b/app.js @@ -656,6 +656,69 @@ for (let i = 0; i < RUNE_COUNT; i++) { runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 }); } +// === NORTHERN LIGHTS (AURORA BOREALIS) === +// Three translucent curtain planes positioned high in the background sky. +// Intensity flashes when a new git push is detected, flashes brighter on merge. + +const AURORA_BANDS = [ + { color: '#00ff88', y: 55, z: -160, width: 500, height: 30, speed: 0.18, phase: 0.0 }, + { color: '#00ccff', y: 70, z: -170, width: 600, height: 22, speed: 0.13, phase: 1.8 }, + { color: '#aa44ff', y: 82, z: -180, width: 450, height: 18, speed: 0.22, phase: 3.5 }, +]; + +/** + * Creates a vertical gradient canvas texture for an aurora band. + * @param {string} hexColor - CSS hex color string + * @returns {THREE.CanvasTexture} + */ +function createAuroraTexture(hexColor) { + const W = 512, H = 128; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + const grad = ctx.createLinearGradient(0, 0, 0, H); + grad.addColorStop(0, hexColor + '00'); + grad.addColorStop(0.25, hexColor + 'cc'); + grad.addColorStop(0.5, hexColor + 'ff'); + grad.addColorStop(0.75, hexColor + 'cc'); + grad.addColorStop(1, hexColor + '00'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, W, H); + return new THREE.CanvasTexture(canvas); +} + +/** @type {Array<{mesh: THREE.Mesh, baseY: number, speed: number, phase: number, baseOpacity: number}>} */ +const auroraBandMeshes = AURORA_BANDS.map(cfg => { + const geo = new THREE.PlaneGeometry(cfg.width, cfg.height); + const mat = new THREE.MeshBasicMaterial({ + map: createAuroraTexture(cfg.color), + transparent: true, + opacity: 0.18, + depthWrite: false, + blending: THREE.AdditiveBlending, + side: THREE.DoubleSide, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(0, cfg.y, cfg.z); + scene.add(mesh); + return { mesh, baseY: cfg.y, speed: cfg.speed, phase: cfg.phase, baseOpacity: 0.18 }; +}); + +// Flash state: intensity decays from peak back to 0 over time. +// 'push' peaks at 1.0, 'merge' peaks at 2.0 (extra bright + color shift). +let auroraFlashIntensity = 0.0; +let auroraIsMerge = false; + +/** + * Triggers an aurora brightness flash. + * @param {'push'|'merge'} type + */ +function triggerAuroraFlash(type) { + auroraFlashIntensity = type === 'merge' ? 2.0 : 1.0; + auroraIsMerge = type === 'merge'; +} + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -663,10 +726,13 @@ const clock = new THREE.Clock(); * Main animation loop — called each frame via requestAnimationFrame. * @returns {void} */ +let _prevElapsed = 0; function animate() { // Only start animation after assets are loaded requestAnimationFrame(animate); const elapsed = clock.getElapsedTime(); + const delta = elapsed - _prevElapsed; + _prevElapsed = elapsed; // Smooth camera transition for overview mode const targetT = overviewMode ? 1 : 0; @@ -770,6 +836,19 @@ function animate() { rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2; } + // Animate northern lights — undulate and apply flash intensity + const AURORA_DECAY = 0.35; // intensity units per second + if (auroraFlashIntensity > 0) { + auroraFlashIntensity = Math.max(0, auroraFlashIntensity - AURORA_DECAY * delta); + } + for (const band of auroraBandMeshes) { + const wave = Math.sin(elapsed * band.speed + band.phase) * 0.07; + const flashBoost = auroraFlashIntensity * (auroraIsMerge ? 0.55 : 0.35); + band.mesh.material.opacity = Math.min(band.baseOpacity + wave + flashBoost, 0.98); + band.mesh.position.y = band.baseY + Math.sin(elapsed * band.speed * 0.6 + band.phase) * 3.5; + // Merge flash: tint the aurora warm-white briefly by boosting opacity on all bands equally + } + composer.render(); } @@ -1290,3 +1369,53 @@ function showTimmySpeech(text) { timmySpeechSprite = sprite; timmySpeechState = { startTime: clock.getElapsedTime(), sprite }; } + +// === AURORA PUSH/MERGE POLLING === +// Polls the Gitea API every 30 s for new commits on main. +// A new push triggers a brightness flash; a merge commit triggers an even +// brighter flash with increased intensity. + +const AURORA_POLL_MS = 30_000; +const GITEA_COMMITS_URL = + 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=3&sha=main'; + +/** @type {string|null} */ +let lastKnownCommitSha = null; + +/** + * Checks for new commits and fires the appropriate aurora flash. + */ +async function pollForAuroraEvents() { + try { + const res = await fetch(GITEA_COMMITS_URL, { + headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' }, + }); + if (!res.ok) return; + const commits = await res.json(); + if (!Array.isArray(commits) || commits.length === 0) return; + + const latestSha = commits[0].sha; + + if (lastKnownCommitSha === null) { + // First poll — just record the baseline, no flash + lastKnownCommitSha = latestSha; + return; + } + + if (latestSha !== lastKnownCommitSha) { + lastKnownCommitSha = latestSha; + + // Determine if the newest commit is a merge + const msg = (commits[0].commit?.message || '').toLowerCase(); + const isMerge = msg.startsWith('merge') || msg.includes('squash') || msg.includes('pull request'); + triggerAuroraFlash(isMerge ? 'merge' : 'push'); + console.log(`[aurora] New ${isMerge ? 'merge' : 'push'} detected — flashing northern lights`); + } + } catch { /* silently ignore network errors */ } +} + +// Stagger the first poll by 5 s so it doesn't overlap page load requests +setTimeout(() => { + pollForAuroraEvents(); + setInterval(pollForAuroraEvents, AURORA_POLL_MS); +}, 5000); -- 2.43.0