import type { MapProjection } from "../geometry/mapProjection"; import type { MapPolygon } from "../model/mapTypes"; type Pt = { x: number; y: number }; function unwrapX(points: Pt[], worldSize: number): Pt[] { if (points.length === 0) return []; const out: Pt[] = [points[0]]; let prevX = points[0].x; for (let i = 1; i < points.length; i++) { let x = points[i].x; while (x - prevX > worldSize / 2) x -= worldSize; while (prevX - x > worldSize / 2) x += worldSize; out.push({ x, y: points[i].y }); prevX = x; } return out; } /** * Фоновый слой тепловой карты приоритетов: заливка ячеек сетки заданий цветом по важности * (cool→hot). Важность нормируется к максимуму набора, поэтому шкала подстраивается под данные. * Рисуется ПОД контурами плана/заявок, без обводки и hit-теста — это фон, а не объекты. */ export function drawHeatmap( ctx: CanvasRenderingContext2D, cells: MapPolygon[], projection: MapProjection, viewport: { width: number; height: number } ) { if (cells.length === 0) return; const maxImportance = cells.reduce((max, cell) => Math.max(max, cell.importance ?? 0), 0); if (maxImportance <= 0) return; ctx.save(); for (const cell of cells) { if (cell.points.length < 3) continue; const raw = cell.points.map((p) => projection.project(p)); const pts = unwrapX(raw, projection.worldSize); // sqrt-нормировка слегка поднимает слабые ячейки, чтобы они не сливались с фоном. const t = Math.sqrt(Math.min(1, Math.max(0, (cell.importance ?? 0) / maxImportance))); ctx.fillStyle = heatColor(t); for (const dx of [0, projection.worldSize, -projection.worldSize]) { const visible = pts.some((p) => { const x = p.x + dx; return x >= -64 && x <= viewport.width + 64 && p.y >= -64 && p.y <= viewport.height + 64; }); if (!visible) continue; ctx.beginPath(); for (let i = 0; i < pts.length; i++) { if (i === 0) ctx.moveTo(pts[i].x + dx, pts[i].y); else ctx.lineTo(pts[i].x + dx, pts[i].y); } ctx.closePath(); ctx.fill(); } } ctx.restore(); } /** * Цвет тепловой карты по нормированной важности t∈[0,1]: бирюзовый (низкая) → * жёлтый → красный (высокая). Альфа растёт с важностью, чтобы «холодные» ячейки были легче. */ function heatColor(t: number): string { // Кусочно-линейная палитра: 0 → #2BD0C4 (cool), 0.5 → #F5D020 (warm), 1 → #F03B20 (hot). const cool = [43, 208, 196]; const warm = [245, 208, 32]; const hot = [240, 59, 32]; let rgb: number[]; if (t < 0.5) { rgb = mix(cool, warm, t / 0.5); } else { rgb = mix(warm, hot, (t - 0.5) / 0.5); } const alpha = 0.35 + 0.4 * t; return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.toFixed(3)})`; } function mix(a: number[], b: number[], k: number): number[] { return [ Math.round(a[0] + (b[0] - a[0]) * k), Math.round(a[1] + (b[1] - a[1]) * k), Math.round(a[2] + (b[2] - a[2]) * k), ]; }