[claude] Commit heatmap on Nexus floor (#469) #493
107
app.js
107
app.js
@@ -45,6 +45,21 @@ let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
let performanceTier = 'high';
|
||||
|
||||
// ═══ COMMIT HEATMAP ═══
|
||||
let heatmapMesh = null;
|
||||
let heatmapMat = null;
|
||||
let heatmapTexture = null;
|
||||
const _heatmapCanvas = document.createElement('canvas');
|
||||
_heatmapCanvas.width = 512;
|
||||
_heatmapCanvas.height = 512;
|
||||
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 _heatZoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
|
||||
|
||||
// ═══ NAVIGATION SYSTEM ═══
|
||||
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
||||
let navModeIdx = 0;
|
||||
@@ -92,6 +107,7 @@ async function init() {
|
||||
createLighting();
|
||||
updateLoad(40);
|
||||
createFloor();
|
||||
createCommitHeatmap();
|
||||
updateLoad(50);
|
||||
createBatcaveTerminal();
|
||||
updateLoad(60);
|
||||
@@ -332,6 +348,94 @@ function createFloor() {
|
||||
scene.add(ring);
|
||||
}
|
||||
|
||||
// ═══ COMMIT HEATMAP FUNCTIONS ═══
|
||||
function createCommitHeatmap() {
|
||||
heatmapTexture = new THREE.CanvasTexture(_heatmapCanvas);
|
||||
heatmapMat = new THREE.MeshBasicMaterial({
|
||||
map: heatmapTexture,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
heatmapMesh = new THREE.Mesh(new THREE.CircleGeometry(24, 64), heatmapMat);
|
||||
heatmapMesh.rotation.x = -Math.PI / 2;
|
||||
heatmapMesh.position.y = 0.005;
|
||||
scene.add(heatmapMesh);
|
||||
// Kick off first fetch; subsequent updates every 5 min
|
||||
updateHeatmap();
|
||||
setInterval(updateHeatmap, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
function drawHeatmap() {
|
||||
const ctx = _heatmapCanvas.getContext('2d');
|
||||
const cx = 256, cy = 256, r = 246;
|
||||
const SPAN = Math.PI / 2;
|
||||
ctx.clearRect(0, 0, 512, 512);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
const intensity = _heatZoneIntensity[zone.name] || 0;
|
||||
if (intensity < 0.01) continue;
|
||||
const [rr, gg, bb] = zone.color;
|
||||
const baseRad = zone.angleDeg * (Math.PI / 180);
|
||||
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, baseRad - SPAN / 2, baseRad + SPAN / 2);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
if (intensity > 0.05) {
|
||||
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, cx + Math.cos(baseRad) * r * 0.62, cy + Math.sin(baseRad) * r * 0.62);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
if (heatmapTexture) heatmapTexture.needsUpdate = true;
|
||||
}
|
||||
|
||||
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 { /* network error — use zero baseline */ }
|
||||
|
||||
const DECAY_MS = 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const raw = 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 > DECAY_MS) continue;
|
||||
const weight = 1 - age / DECAY_MS;
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
if (zone.authorMatch.test(author)) { raw[zone.name] += weight; break; }
|
||||
}
|
||||
}
|
||||
const MAX_W = 8;
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
_heatZoneIntensity[zone.name] = Math.min(raw[zone.name] / MAX_W, 1.0);
|
||||
}
|
||||
drawHeatmap();
|
||||
}
|
||||
|
||||
// ═══ BATCAVE TERMINAL ═══
|
||||
function createBatcaveTerminal() {
|
||||
const terminalGroup = new THREE.Group();
|
||||
@@ -1408,6 +1512,9 @@ function gameLoop() {
|
||||
const sky = scene.getObjectByName('skybox');
|
||||
if (sky) sky.material.uniforms.uTime.value = elapsed;
|
||||
|
||||
// Pulse heatmap opacity
|
||||
if (heatmapMat) heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2;
|
||||
|
||||
batcaveTerminals.forEach(t => {
|
||||
if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user