Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
126 lines
4.0 KiB
JavaScript
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();
|
|
}
|