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