diff --git a/app.js b/app.js index 3f9b2cd..52bfcbf 100644 --- a/app.js +++ b/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(); }