128 lines
4.0 KiB
JavaScript
128 lines
4.0 KiB
JavaScript
// === COMMIT HEATMAP ===
|
|
import * as THREE from 'three';
|
|
import { scene } from './scene-setup.js';
|
|
import { GLASS_RADIUS } from './platform.js';
|
|
import { S } from './state.js';
|
|
import { refreshCommitData } from './data/gitea.js';
|
|
|
|
const HEATMAP_SIZE = 512;
|
|
const HEATMAP_REFRESH_MS = 5 * 60 * 1000;
|
|
const HEATMAP_DECAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
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_ZONE_SPAN_RAD = Math.PI / 2;
|
|
|
|
const heatmapCanvas = document.createElement('canvas');
|
|
heatmapCanvas.width = HEATMAP_SIZE;
|
|
heatmapCanvas.height = HEATMAP_SIZE;
|
|
export const heatmapTexture = new THREE.CanvasTexture(heatmapCanvas);
|
|
|
|
export 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;
|
|
heatmapMesh.userData.zoomLabel = 'Activity Heatmap';
|
|
scene.add(heatmapMesh);
|
|
|
|
export const zoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
|
|
|
|
export 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);
|
|
|
|
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;
|
|
|
|
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 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;
|
|
}
|
|
|
|
export async function updateHeatmap() {
|
|
const commits = await refreshCommitData();
|
|
|
|
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;
|
|
|
|
for (const zone of HEATMAP_ZONES) {
|
|
if (zone.authorMatch.test(author)) {
|
|
rawWeights[zone.name] += weight;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const MAX_WEIGHT = 8;
|
|
for (const zone of HEATMAP_ZONES) {
|
|
zoneIntensity[zone.name] = Math.min(rawWeights[zone.name] / MAX_WEIGHT, 1.0);
|
|
}
|
|
|
|
drawHeatmap();
|
|
}
|
|
|
|
updateHeatmap();
|
|
setInterval(updateHeatmap, HEATMAP_REFRESH_MS);
|