Split the monolithic 5393-line app.js into 32 focused ES modules under modules/ with a thin ~330-line orchestrator. No bundler required — runs in-browser via import maps. Module structure: core/ — scene, ticker, state, theme, audio data/ — gitea, weather, bitcoin, loaders terrain/ — stars, clouds, island effects/ — matrix-rain, energy-beam, lightning, shockwave, rune-ring, gravity-zones panels/ — heatmap, sigil, sovereignty, dual-brain, batcave, earth, agent-board, lora-panel portals/ — portal-system, commit-banners narrative/ — bookshelves, oath, chat utils/ — perlin All files pass node --check. No new dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
63 lines
2.5 KiB
JavaScript
63 lines
2.5 KiB
JavaScript
// modules/portals/commit-banners.js — Floating commit banner sprites
|
|
import * as THREE from 'three';
|
|
import { fetchRecentCommitsForBanners } from '../data/gitea.js';
|
|
|
|
const commitBanners = [];
|
|
|
|
function createCommitTexture(hash, message) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 512; canvas.height = 64;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = 'rgba(0, 0, 16, 0.75)'; ctx.fillRect(0, 0, 512, 64);
|
|
ctx.strokeStyle = '#4488ff'; ctx.lineWidth = 1; ctx.strokeRect(0.5, 0.5, 511, 63);
|
|
ctx.font = 'bold 11px "Courier New", monospace'; ctx.fillStyle = '#4488ff'; ctx.fillText(hash, 10, 20);
|
|
ctx.font = '12px "Courier New", monospace'; ctx.fillStyle = '#ccd6f6';
|
|
const displayMsg = message.length > 54 ? message.slice(0, 54) + '\u2026' : message;
|
|
ctx.fillText(displayMsg, 10, 46);
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
export async function init(scene) {
|
|
const commits = await fetchRecentCommitsForBanners();
|
|
const spreadX = [-7, -3.5, 0, 3.5, 7];
|
|
const spreadY = [1.0, -1.5, 2.2, -0.8, 1.6];
|
|
const spreadZ = [-1.5, -2.5, -1.0, -2.0, -1.8];
|
|
commits.forEach((commit, i) => {
|
|
const texture = createCommitTexture(commit.hash, commit.message);
|
|
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0, depthWrite: false });
|
|
const sprite = new THREE.Sprite(material);
|
|
sprite.scale.set(12, 1.5, 1);
|
|
sprite.position.set(spreadX[i % spreadX.length], spreadY[i % spreadY.length], spreadZ[i % spreadZ.length]);
|
|
sprite.userData = {
|
|
baseY: spreadY[i % spreadY.length],
|
|
floatPhase: (i / commits.length) * Math.PI * 2,
|
|
floatSpeed: 0.25 + i * 0.07,
|
|
startDelay: i * 2.5,
|
|
lifetime: 12 + i * 1.5,
|
|
spawnTime: null,
|
|
zoomLabel: `Commit: ${commit.hash}`,
|
|
};
|
|
scene.add(sprite);
|
|
commitBanners.push(sprite);
|
|
});
|
|
}
|
|
|
|
export function update(elapsed) {
|
|
const FADE_DUR = 1.5;
|
|
commitBanners.forEach(banner => {
|
|
const ud = banner.userData;
|
|
if (ud.spawnTime === null) {
|
|
if (elapsed < ud.startDelay) return;
|
|
ud.spawnTime = elapsed;
|
|
}
|
|
const age = elapsed - ud.spawnTime;
|
|
let opacity;
|
|
if (age < FADE_DUR) opacity = age / FADE_DUR;
|
|
else if (age < ud.lifetime - FADE_DUR) opacity = 1;
|
|
else if (age < ud.lifetime) opacity = (ud.lifetime - age) / FADE_DUR;
|
|
else { ud.spawnTime = elapsed + 3; opacity = 0; }
|
|
banner.material.opacity = opacity * 0.85;
|
|
banner.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.4;
|
|
});
|
|
}
|