Files
the-nexus/modules/panels/heatmap.js
Claude (Opus 4.6) 481a0790d2
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
[claude] Phase 3: Panel modules — Heatmap, Agent Board, Dual-Brain, LoRA, Sovereignty, Earth (#422) (#446)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-24 18:22:34 +00:00

126 lines
4.0 KiB
JavaScript

// modules/panels/heatmap.js — Commit heatmap floor overlay
// Canvas-texture circle on the glass platform floor.
// Each agent occupies a polar sector; recent commits make that sector glow brighter.
// Activity decays over 24 h (driven by state.zoneIntensity, written by data/gitea.js).
//
// Data category: DATA-TETHERED AESTHETIC
// Data source: state.zoneIntensity (populated from Gitea commits API by data/gitea.js)
import * as THREE from 'three';
import { state } from '../core/state.js';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
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_SIZE = 512;
const HEATMAP_ZONE_SPAN_RAD = Math.PI / 2; // 90° per zone
const GLASS_RADIUS = 4.55; // matches terrain/island.js platform radius
let _canvas, _ctx, _texture, _mesh;
let _scene;
function _draw() {
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 lx = cx + Math.cos(baseRad) * r * 0.62;
const ly = cy + Math.sin(baseRad) * r * 0.62;
_ctx.font = `bold ${Math.round(13 * intensity + 7)}px ${NEXUS.theme.fontMono}`;
_ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`;
_ctx.textAlign = 'center';
_ctx.textBaseline = 'middle';
_ctx.fillText(zone.name, lx, ly);
}
}
_ctx.restore();
_texture.needsUpdate = true;
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_canvas = document.createElement('canvas');
_canvas.width = HEATMAP_SIZE;
_canvas.height = HEATMAP_SIZE;
_ctx = _canvas.getContext('2d');
_texture = new THREE.CanvasTexture(_canvas);
const mat = new THREE.MeshBasicMaterial({
map: _texture,
transparent: true,
opacity: 0.9,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
_mesh = new THREE.Mesh(new THREE.CircleGeometry(GLASS_RADIUS, 64), mat);
_mesh.rotation.x = -Math.PI / 2;
_mesh.position.y = 0.005;
_mesh.userData.zoomLabel = 'Activity Heatmap';
scene.add(_mesh);
// Draw initial empty state
_draw();
subscribe(update);
}
let _lastDrawElapsed = 0;
const REDRAW_INTERVAL = 0.5; // redraw at most every 500 ms (data changes slowly)
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
if (elapsed - _lastDrawElapsed < REDRAW_INTERVAL) return;
_lastDrawElapsed = elapsed;
_draw();
}
export function dispose() {
if (_mesh) { _scene.remove(_mesh); _mesh.geometry.dispose(); _mesh.material.dispose(); }
if (_texture) _texture.dispose();
}