Files
the-nexus/modules/panels/heatmap.js

82 lines
2.7 KiB
JavaScript
Raw Normal View History

// 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;
}