/** * ResearchIcons — procedural tech glyphs for the tree nodes and the detail * readout (128×128 canvas textures, neon stroke + soft glow, transparent * background). Names come from each node's `icon` key; unknown names fall * back to `diamond`. Tintable at use (Phaser tint multiplies). */ import { toColor } from '../utils/Color.js'; export const ICON_NAMES = ['tether', 'tether2', 'anchor', 'signal', 'diamond', 'map', 'gate']; export function iconKey(name, color = 0x00e5ff) { const n = ICON_NAMES.includes(name) ? name : 'diamond'; const c = toColor(color); return `__research_icon_${n}_${c.toString(16).padStart(6, '0')}`; } export function ensureIcon(scene, name, color = 0x00e5ff) { const key = iconKey(name, color); if (scene.textures.exists(key)) return key; const S = 128; const canvas = document.createElement('canvas'); canvas.width = S; canvas.height = S; const ctx = canvas.getContext('2d'); const hex = `#${toColor(color).toString(16).padStart(6, '0')}`; const cx = S / 2; const cy = S / 2; ctx.clearRect(0, 0, S, S); ctx.strokeStyle = hex; ctx.fillStyle = hex; ctx.shadowColor = hex; ctx.shadowBlur = 9; ctx.lineWidth = 5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; const diamond = (x, y, r) => { ctx.beginPath(); ctx.moveTo(x, y - r); ctx.lineTo(x + r, y); ctx.lineTo(x, y + r); ctx.lineTo(x - r, y); ctx.closePath(); ctx.fill(); }; const n = ICON_NAMES.includes(name) ? name : 'diamond'; if (n === 'tether') { // Concentric tether rings + core + tick marks + a satellite on the mid ring. const rings = [ [52, 0.5], [38, 0.75], [24, 1.0], ]; for (const [r, a] of rings) { ctx.globalAlpha = a; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke(); } ctx.globalAlpha = 1; ctx.beginPath(); ctx.arc(cx, cy, 7, 0, Math.PI * 2); ctx.fill(); for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) { ctx.beginPath(); ctx.moveTo(cx + Math.cos(a) * 55, cy + Math.sin(a) * 55); ctx.lineTo(cx + Math.cos(a) * 61, cy + Math.sin(a) * 61); ctx.stroke(); } diamond(cx + 38, cy, 8); } else if (n === 'tether2') { // Level-2 tether: two bright concentric level rings + core + tick // marks + TWO satellites riding the outer ring (L1 shows one). const rings = [ [52, 1.0], [34, 0.85], ]; for (const [r, a] of rings) { ctx.globalAlpha = a; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke(); } ctx.globalAlpha = 1; ctx.beginPath(); ctx.arc(cx, cy, 8, 0, Math.PI * 2); ctx.fill(); for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) { ctx.beginPath(); ctx.moveTo(cx + Math.cos(a) * 55, cy + Math.sin(a) * 55); ctx.lineTo(cx + Math.cos(a) * 61, cy + Math.sin(a) * 61); ctx.stroke(); } diamond(cx + 52 * Math.cos(-Math.PI / 3), cy + 52 * Math.sin(-Math.PI / 3), 8); diamond(cx + 52 * Math.cos(Math.PI / 3), cy + 52 * Math.sin(Math.PI / 3), 8); } else if (n === 'anchor') { // Anchor ring + stock + crossbar + flukes (the relay bolted to a body). ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.arc(cx, cy, 46, 0, Math.PI * 2); ctx.stroke(); ctx.globalAlpha = 1; ctx.beginPath(); ctx.moveTo(cx, cy - 24); ctx.lineTo(cx, cy + 20); ctx.stroke(); ctx.beginPath(); ctx.arc(cx, cy - 24, 7, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cx - 15, cy - 8); ctx.lineTo(cx + 15, cy - 8); ctx.stroke(); ctx.beginPath(); ctx.arc(cx, cy + 4, 20, Math.PI * 0.12, Math.PI * 0.88); ctx.stroke(); diamond(cx - 19, cy + 15, 6); diamond(cx + 19, cy + 15, 6); } else if (n === 'signal') { // Long-range array: source dot + rising arcs. ctx.beginPath(); ctx.arc(cx, cy + 22, 7, 0, Math.PI * 2); ctx.fill(); const arcs = [ [19, 0.95], [33, 0.7], [47, 0.45], ]; for (const [r, a] of arcs) { ctx.globalAlpha = a; ctx.beginPath(); ctx.arc(cx, cy + 22, r, -Math.PI * 0.82, -Math.PI * 0.18); ctx.stroke(); } ctx.globalAlpha = 1; } else if (n === 'map') { // The SYSTEM category's chart glyph — a folded map sheet with a plotted // route between two NAV points (the per-system '{System} Map' tech). const w = 62, h = 50, x = cx - w / 2, y = cy - h / 2; ctx.beginPath(); ctx.roundRect(x, y, w, h, 7); ctx.stroke(); // fold line ctx.beginPath(); ctx.moveTo(cx, y); ctx.lineTo(cx, y + h); ctx.stroke(); // route: two plotted points + the line between them ctx.beginPath(); ctx.moveTo(x + w * 0.22, y + h * 0.68); ctx.lineTo(x + w * 0.78, y + h * 0.32); ctx.stroke(); diamond(x + w * 0.22, y + h * 0.68, 5); diamond(x + w * 0.78, y + h * 0.32, 5); } else if (n === 'gate') { // The SYSTEM category's jumpgate glyph — the ring + its inner aperture // (+ the four aperture seams), like the world's gate entity minus field. ctx.globalAlpha = 0.95; ctx.beginPath(); ctx.arc(cx, cy, 44, 0, Math.PI * 2); ctx.stroke(); ctx.globalAlpha = 1; ctx.lineWidth = 3.5; ctx.beginPath(); ctx.arc(cx, cy, 19, 0, Math.PI * 2); ctx.stroke(); ctx.lineWidth = 5; ctx.globalAlpha = 0.6; for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) { ctx.beginPath(); ctx.moveTo(cx + Math.cos(a) * 28, cy + Math.sin(a) * 28); ctx.lineTo(cx + Math.cos(a) * 37, cy + Math.sin(a) * 37); ctx.stroke(); } ctx.globalAlpha = 1; } else { // diamond — the default glyph ctx.beginPath(); ctx.moveTo(cx, cy - 42); ctx.lineTo(cx + 42, cy); ctx.lineTo(cx, cy + 42); ctx.lineTo(cx - 42, cy); ctx.closePath(); ctx.stroke(); ctx.globalAlpha = 0.55; ctx.beginPath(); ctx.moveTo(cx, cy - 22); ctx.lineTo(cx + 22, cy); ctx.lineTo(cx, cy + 22); ctx.lineTo(cx - 22, cy); ctx.closePath(); ctx.stroke(); ctx.globalAlpha = 1; } scene.textures.addCanvas(key, canvas); return key; }