// === COMMIT HEATMAP === import * as THREE from 'three'; import { scene } from './scene-setup.js'; import { GLASS_RADIUS } from './platform.js'; import { S } from './state.js'; const HEATMAP_SIZE = 512; const HEATMAP_REFRESH_MS = 5 * 60 * 1000; const HEATMAP_DECAY_MS = 24 * 60 * 60 * 1000; export const HEATMAP_ZONES = [ { name: 'Claude', color: [255, 100, 60], authorMatch: /^claude$/i, angleDeg: 0 }, { name: 'Timmy', color: [ 60, 160, 255], authorMatch: /^timmy/i, angleDeg: 90 }, { name: 'Kimi', color: [ 60, 255, 140], authorMatch: /^kimi/i, angleDeg: 180 }, { name: 'Perplexity', color: [200, 60, 255], authorMatch: /^perplexity/i, angleDeg: 270 }, ]; const HEATMAP_ZONE_SPAN_RAD = Math.PI / 2; const heatmapCanvas = document.createElement('canvas'); heatmapCanvas.width = HEATMAP_SIZE; heatmapCanvas.height = HEATMAP_SIZE; export const heatmapTexture = new THREE.CanvasTexture(heatmapCanvas); export const heatmapMat = new THREE.MeshBasicMaterial({ map: heatmapTexture, transparent: true, opacity: 0.9, depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, }); const 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 const zoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0])); 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 = 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 async function updateHeatmap() { let commits = []; try { const res = await fetch( 'http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/the-nexus/commits?limit=50', { headers: { 'Authorization': 'token dc0517a965226b7a0c5ffdd961b1ba26521ac592' } } ); if (res.ok) commits = await res.json(); } catch { /* silently use zero-activity baseline */ } S._matrixCommitHashes = commits.slice(0, 20).map(c => (c.sha || '').slice(0, 7)).filter(h => h.length > 0); const now = Date.now(); const rawWeights = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0])); for (const commit of commits) { const author = commit.commit?.author?.name || commit.author?.login || ''; const ts = new Date(commit.commit?.author?.date || 0).getTime(); const age = now - ts; if (age > HEATMAP_DECAY_MS) continue; const weight = 1 - age / HEATMAP_DECAY_MS; for (const zone of HEATMAP_ZONES) { if (zone.authorMatch.test(author)) { rawWeights[zone.name] += weight; break; } } } const MAX_WEIGHT = 8; for (const zone of HEATMAP_ZONES) { zoneIntensity[zone.name] = Math.min(rawWeights[zone.name] / MAX_WEIGHT, 1.0); } drawHeatmap(); } updateHeatmap(); setInterval(updateHeatmap, HEATMAP_REFRESH_MS);