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>
82 lines
2.7 KiB
JavaScript
82 lines
2.7 KiB
JavaScript
// modules/panels/heatmap.js — Commit heatmap floor overlay
|
|
import * as THREE from 'three';
|
|
import { state } from '../core/state.js';
|
|
import { HEATMAP_ZONES } from '../data/gitea.js';
|
|
import { GLASS_RADIUS } from '../terrain/island.js';
|
|
|
|
const HEATMAP_SIZE = 512;
|
|
const HEATMAP_ZONE_SPAN_RAD = Math.PI / 2;
|
|
|
|
const heatmapCanvas = document.createElement('canvas');
|
|
heatmapCanvas.width = HEATMAP_SIZE;
|
|
heatmapCanvas.height = HEATMAP_SIZE;
|
|
const heatmapTexture = new THREE.CanvasTexture(heatmapCanvas);
|
|
|
|
const heatmapMat = new THREE.MeshBasicMaterial({
|
|
map: heatmapTexture, transparent: true, opacity: 0.9,
|
|
depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
|
|
});
|
|
|
|
let heatmapMesh;
|
|
|
|
export function drawHeatmap() {
|
|
const ctx = heatmapCanvas.getContext('2d');
|
|
const cx = HEATMAP_SIZE / 2;
|
|
const cy = HEATMAP_SIZE / 2;
|
|
const r = cx * 0.96;
|
|
|
|
ctx.clearRect(0, 0, HEATMAP_SIZE, HEATMAP_SIZE);
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
ctx.clip();
|
|
|
|
for (const zone of HEATMAP_ZONES) {
|
|
const intensity = state.zoneIntensity[zone.name] || 0;
|
|
if (intensity < 0.01) continue;
|
|
const [rr, gg, bb] = zone.color;
|
|
const baseRad = zone.angleDeg * (Math.PI / 180);
|
|
const startRad = baseRad - HEATMAP_ZONE_SPAN_RAD / 2;
|
|
const endRad = baseRad + HEATMAP_ZONE_SPAN_RAD / 2;
|
|
const gx = cx + Math.cos(baseRad) * r * 0.55;
|
|
const gy = cy + Math.sin(baseRad) * r * 0.55;
|
|
const grad = ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.75);
|
|
grad.addColorStop(0, `rgba(${rr},${gg},${bb},${0.65 * intensity})`);
|
|
grad.addColorStop(0.45, `rgba(${rr},${gg},${bb},${0.25 * intensity})`);
|
|
grad.addColorStop(1, `rgba(${rr},${gg},${bb},0)`);
|
|
ctx.beginPath();
|
|
ctx.moveTo(cx, cy);
|
|
ctx.arc(cx, cy, r, startRad, endRad);
|
|
ctx.closePath();
|
|
ctx.fillStyle = grad;
|
|
ctx.fill();
|
|
if (intensity > 0.05) {
|
|
const labelX = cx + Math.cos(baseRad) * r * 0.62;
|
|
const labelY = cy + Math.sin(baseRad) * r * 0.62;
|
|
ctx.font = `bold ${Math.round(13 * intensity + 7)}px "Courier New", monospace`;
|
|
ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`;
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(zone.name, labelX, labelY);
|
|
}
|
|
}
|
|
|
|
ctx.restore();
|
|
heatmapTexture.needsUpdate = true;
|
|
}
|
|
|
|
export function init(scene) {
|
|
heatmapMesh = new THREE.Mesh(
|
|
new THREE.CircleGeometry(GLASS_RADIUS, 64),
|
|
heatmapMat
|
|
);
|
|
heatmapMesh.rotation.x = -Math.PI / 2;
|
|
heatmapMesh.position.y = 0.005;
|
|
heatmapMesh.userData.zoomLabel = 'Activity Heatmap';
|
|
scene.add(heatmapMesh);
|
|
}
|
|
|
|
export function update(elapsed) {
|
|
heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2;
|
|
}
|