[claude] Git commit heatmap on Nexus floor (#201) (#220)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #220.
This commit is contained in:
147
app.js
147
app.js
@@ -232,6 +232,150 @@ glassPlatformGroup.add(voidLight);
|
||||
|
||||
scene.add(glassPlatformGroup);
|
||||
|
||||
// === COMMIT HEATMAP ===
|
||||
// Canvas-texture overlay on the floor. Each agent occupies a polar sector;
|
||||
// recent commits make that sector glow brighter. Activity decays over 24 h.
|
||||
|
||||
const HEATMAP_SIZE = 512;
|
||||
const HEATMAP_REFRESH_MS = 5 * 60 * 1000; // 5 min between API polls
|
||||
const HEATMAP_DECAY_MS = 24 * 60 * 60 * 1000; // 24 h full decay
|
||||
|
||||
// Agent zones — angle in canvas degrees (0 = east/right, clockwise)
|
||||
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; // 90° per zone
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const heatmapMesh = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(GLASS_RADIUS, 64),
|
||||
heatmapMat
|
||||
);
|
||||
heatmapMesh.rotation.x = -Math.PI / 2;
|
||||
heatmapMesh.position.y = 0.005;
|
||||
scene.add(heatmapMesh);
|
||||
|
||||
// Per-zone intensity [0..1], updated by updateHeatmap()
|
||||
const zoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
|
||||
|
||||
/**
|
||||
* Redraws the heatmap canvas from current zoneIntensity values.
|
||||
*/
|
||||
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);
|
||||
|
||||
// Clip drawing to the circular platform boundary
|
||||
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;
|
||||
|
||||
// Glow origin sits at 55% radius in the zone's direction
|
||||
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();
|
||||
|
||||
// Zone label — only when active
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches recent commits, maps them to agent zones via author, and redraws.
|
||||
*/
|
||||
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 */ }
|
||||
|
||||
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; // linear decay
|
||||
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
if (zone.authorMatch.test(author)) {
|
||||
rawWeights[zone.name] += weight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise: 8 recent weighted commits = full brightness
|
||||
const MAX_WEIGHT = 8;
|
||||
for (const zone of HEATMAP_ZONES) {
|
||||
zoneIntensity[zone.name] = Math.min(rawWeights[zone.name] / MAX_WEIGHT, 1.0);
|
||||
}
|
||||
|
||||
drawHeatmap();
|
||||
}
|
||||
|
||||
// Kick off and schedule periodic refresh
|
||||
updateHeatmap();
|
||||
setInterval(updateHeatmap, HEATMAP_REFRESH_MS);
|
||||
|
||||
// === MOUSE-DRIVEN ROTATION ===
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
@@ -471,6 +615,9 @@ function animate() {
|
||||
// Pulse the void light below
|
||||
voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2;
|
||||
|
||||
// Heatmap floor: subtle breathing glow
|
||||
heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2;
|
||||
|
||||
if (photoMode) {
|
||||
orbitControls.update();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user