mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 00:34:35 -05:00
Improve network map with floating bubble nodes and 6-ring layout
- Replace tiny dot nodes with frosted-glass bubbles with ambient glow and float animation - Add 6th ring for netscan-discovered network devices (cap 28) - Split named/DB devices and discovered devices into separate rings - Push rFrac to 0.82 to fill the overlay window - Increase all ring caps and node radii - Add FortiGate NAT IP to providers ACL - Fix TCP SIP drop issue via transport=udp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+150
-74
@@ -1142,8 +1142,9 @@ body::after{
|
||||
<div id="nmLegend">
|
||||
<span><span class="nm-leg-dot" style="background:#00ff88;box-shadow:0 0 4px #00ff88"></span>PROXMOX</span>
|
||||
<span><span class="nm-leg-dot" style="background:#ffd700;box-shadow:0 0 4px #ffd700"></span>SERVICES</span>
|
||||
<span><span class="nm-leg-dot" style="background:#00d4ff;box-shadow:0 0 4px #00d4ff"></span>AGENTS</span>
|
||||
<span><span class="nm-leg-dot" style="background:rgba(0,140,180,0.8)"></span>DEVICES</span>
|
||||
<span><span class="nm-leg-dot" style="background:#00beff;box-shadow:0 0 4px #00beff"></span>AGENTS</span>
|
||||
<span><span class="nm-leg-dot" style="background:rgba(0,160,200,0.9)"></span>DEVICES</span>
|
||||
<span><span class="nm-leg-dot" style="background:rgba(0,110,170,0.9)"></span>NETWORK</span>
|
||||
<span><span class="nm-leg-dot" style="background:#ff2244;box-shadow:0 0 4px #ff2244"></span>OFFLINE</span>
|
||||
<span style="margin-left:auto;opacity:0.4;font-size:0.5rem">CYAN = DATA IN · ORANGE = CMD OUT</span>
|
||||
</div>
|
||||
@@ -1582,14 +1583,14 @@ function renderTopology(devices) {
|
||||
const canvas = document.getElementById('topoCanvas');
|
||||
if (!canvas) return;
|
||||
const W = canvas.parentElement?.clientWidth || 260;
|
||||
canvas.width = W; canvas.height = 100;
|
||||
canvas.width = W; canvas.height = 118;
|
||||
|
||||
_topoNodes = devices.slice(0, 18).map((d, i, arr) => {
|
||||
const angle = (i / arr.length) * Math.PI * 2 - Math.PI / 2;
|
||||
const rx = W * 0.36, ry = 36;
|
||||
return {
|
||||
x: W/2 + Math.cos(angle) * rx * (0.6 + (i%3)*0.18),
|
||||
y: 50 + Math.sin(angle) * ry * (0.7 + (i%2)*0.3),
|
||||
y: 52 + Math.sin(angle) * ry * (0.65 + (i%2)*0.25),
|
||||
label: (d.name || d.ip || '?').split('.')[0].substring(0, 9),
|
||||
on: !!(d.alive || d.status === 'online'),
|
||||
agent: d.source === 'agent',
|
||||
@@ -1609,37 +1610,62 @@ function _drawTopo() {
|
||||
_topoT += 0.018;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
const floatY = n => Math.sin(_topoT * 0.9 + n.phase) * 2.5;
|
||||
|
||||
// Connections
|
||||
for (let i = 0; i < _topoNodes.length; i++) {
|
||||
for (let j = i+1; j < _topoNodes.length; j++) {
|
||||
const a = _topoNodes[i], b = _topoNodes[j];
|
||||
if (!a.on || !b.on) continue;
|
||||
const dx = b.x-a.x, dy = b.y-a.y, dist = Math.hypot(dx,dy);
|
||||
const ax = a.x, ay = a.y + floatY(a), bx = b.x, by = b.y + floatY(b);
|
||||
const dist = Math.hypot(bx-ax, by-ay);
|
||||
if (dist > W * 0.55) continue;
|
||||
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
|
||||
ctx.strokeStyle = 'rgba(0,180,220,0.13)'; ctx.lineWidth = 0.5; ctx.stroke();
|
||||
const lg = ctx.createLinearGradient(ax, ay, bx, by);
|
||||
const col = a.agent ? '0,255,136' : '0,212,255';
|
||||
lg.addColorStop(0, `rgba(${col},0.25)`); lg.addColorStop(0.5, `rgba(${col},0.1)`); lg.addColorStop(1, `rgba(${col},0.25)`);
|
||||
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by);
|
||||
ctx.strokeStyle = lg; ctx.lineWidth = 0.8; ctx.stroke();
|
||||
// travelling pulse
|
||||
const p = (_topoT * 0.4 + a.phase) % 1;
|
||||
ctx.beginPath(); ctx.arc(a.x+dx*p, a.y+dy*p, 1.5, 0, Math.PI*2);
|
||||
ctx.fillStyle = 'rgba(0,212,255,0.7)'; ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(ax+(bx-ax)*p, ay+(by-ay)*p, 1.8, 0, Math.PI*2);
|
||||
ctx.fillStyle = 'rgba(0,212,255,0.85)'; ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Nodes
|
||||
// Bubble nodes
|
||||
for (const n of _topoNodes) {
|
||||
const pulse = 0.5 + Math.sin(_topoT*1.8 + n.phase) * 0.3;
|
||||
const pulse = 0.5 + Math.sin(_topoT * 1.4 + n.phase) * 0.3;
|
||||
const col = n.on ? (n.agent ? '0,255,136' : '0,212,255') : '255,50,80';
|
||||
const a = n.on ? pulse * 0.7 : 0.2;
|
||||
const r = n.agent ? 10 : 7;
|
||||
const nx = n.x, ny = n.y + floatY(n);
|
||||
|
||||
if (n.on) {
|
||||
const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,9);
|
||||
g.addColorStop(0,`rgba(${col},${(a*0.5).toFixed(2)})`); g.addColorStop(1,`rgba(${col},0)`);
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y,9,0,Math.PI*2); ctx.fillStyle=g; ctx.fill();
|
||||
// Ambient bloom
|
||||
const bloom = ctx.createRadialGradient(nx, ny, r*0.4, nx, ny, r*3);
|
||||
bloom.addColorStop(0, `rgba(${col},${(pulse*0.18).toFixed(3)})`);
|
||||
bloom.addColorStop(1, `rgba(${col},0)`);
|
||||
ctx.beginPath(); ctx.arc(nx, ny, r*3, 0, Math.PI*2); ctx.fillStyle = bloom; ctx.fill();
|
||||
}
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y, n.agent ? 4 : 3, 0, Math.PI*2);
|
||||
ctx.fillStyle = `rgba(${col},${a.toFixed(2)})`; ctx.fill();
|
||||
ctx.font = '6.5px Share Tech Mono,monospace';
|
||||
ctx.fillStyle = n.on ? 'rgba(180,230,255,0.65)' : 'rgba(255,100,100,0.45)';
|
||||
ctx.fillText(n.label, n.x+5, n.y+3);
|
||||
|
||||
// Frosted glass fill
|
||||
const fg = ctx.createRadialGradient(nx, ny - r*0.3, 0, nx, ny, r);
|
||||
const fa = n.on ? 0.18 + pulse*0.1 : 0.07;
|
||||
fg.addColorStop(0, `rgba(${col},${(fa*1.8).toFixed(3)})`);
|
||||
fg.addColorStop(0.65, `rgba(${col},${fa.toFixed(3)})`);
|
||||
fg.addColorStop(1, `rgba(${col},${(fa*0.2).toFixed(3)})`);
|
||||
ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2); ctx.fillStyle = fg; ctx.fill();
|
||||
|
||||
// Border
|
||||
ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2);
|
||||
ctx.strokeStyle = `rgba(${col},${n.on ? (0.5 + pulse*0.32).toFixed(3) : '0.2'})`;
|
||||
ctx.lineWidth = 1; ctx.stroke();
|
||||
|
||||
// Label below bubble
|
||||
ctx.font = '6px Share Tech Mono,monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = n.on ? `rgba(${col},0.82)` : 'rgba(255,100,100,0.5)';
|
||||
ctx.fillText(n.label, nx, ny + r + 7);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1841,32 +1867,39 @@ function _focusWindow() {
|
||||
|
||||
// ── NETWORK MAP ──────────────────────────────────────────────────────────────
|
||||
var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null;
|
||||
var _nmRot=[0,0,0,0,0];
|
||||
var _nmRot=[0,0,0,0,0,0];
|
||||
var NM_RINGS=[
|
||||
{name:'hub', label:'', rFrac:0, speed:0, rgb:'0,212,255', nodeR:20, cap:1 },
|
||||
{name:'proxmox', label:'PROXMOX', rFrac:0.18, speed:0.006, rgb:'0,255,136', nodeR:13, cap:4 },
|
||||
{name:'services',label:'SERVICES',rFrac:0.33, speed:-0.004, rgb:'255,215,0', nodeR:12, cap:5 },
|
||||
{name:'agents', label:'AGENTS', rFrac:0.50, speed:0.0025, rgb:'0,190,255', nodeR:10, cap:8 },
|
||||
{name:'devices', label:'NETWORK', rFrac:0.68, speed:-0.002, rgb:'0,140,180', nodeR:7, cap:9 },
|
||||
{name:'hub', label:'', rFrac:0, speed:0, rgb:'0,212,255', nodeR:30, cap:1 },
|
||||
{name:'proxmox', label:'PROXMOX', rFrac:0.16, speed:0.006, rgb:'0,255,136', nodeR:22, cap:4 },
|
||||
{name:'services',label:'SERVICES', rFrac:0.30, speed:-0.004, rgb:'255,215,0', nodeR:20, cap:7 },
|
||||
{name:'agents', label:'AGENTS', rFrac:0.45, speed:0.0025, rgb:'0,190,255', nodeR:17, cap:12 },
|
||||
{name:'devices', label:'DEVICES', rFrac:0.62, speed:-0.002, rgb:'0,160,200', nodeR:14, cap:14 },
|
||||
{name:'network', label:'NETWORK', rFrac:0.82, speed:0.0015, rgb:'0,110,170', nodeR:11, cap:28 },
|
||||
];
|
||||
var NM_OPEN_RE = /\b(show|open|display|launch|pull\s*up|bring\s*up)\b.*\b(network\s*(map|topology|viz|visual|graph)|topology|node\s*map)\b|\bnetwork\s*(map|topology|viz|visual|graph)\b/i;
|
||||
var NM_CLOSE_RE = /\b(close|hide|dismiss|exit|collapse)\b.*\b(network|map|topology|overlay)\b|\b(close|hide|dismiss)\s*map\b/i;
|
||||
|
||||
function _nmClassify(d){
|
||||
var h=(d.name||'').toLowerCase();
|
||||
if(d.source!=='agent') return 'devices';
|
||||
if(d.agent_type==='proxmox'||h.indexOf('pve')>=0||h.indexOf('proxmox')>=0) return 'proxmox';
|
||||
if(d.agent_type==='homeassistant'||h.indexOf('homeassist')>=0||h.indexOf('_ha')>=0||
|
||||
h.indexOf('ollama')>=0||h.indexOf('ai')>=0||h.indexOf('fusion')>=0||h.indexOf('pbx')>=0||
|
||||
h.indexOf('jellyfin')>=0||h.indexOf('homebridge')>=0) return 'services';
|
||||
return 'agents';
|
||||
if(d.source==='agent'){
|
||||
if(d.agent_type==='proxmox'||h.indexOf('pve')>=0||h.indexOf('proxmox')>=0) return 'proxmox';
|
||||
if(d.agent_type==='homeassistant'||h.indexOf('homeassist')>=0||h.indexOf('_ha')>=0||
|
||||
h.indexOf('ollama')>=0||h.indexOf('ai')>=0||h.indexOf('fusion')>=0||h.indexOf('pbx')>=0||
|
||||
h.indexOf('jellyfin')>=0||h.indexOf('homebridge')>=0) return 'services';
|
||||
return 'agents';
|
||||
}
|
||||
// Named/pinned DB devices and static hosts get the inner device ring
|
||||
if(d.source==='db'||d.source==='static') return 'devices';
|
||||
// Netscan-discovered devices go to the outer network ring
|
||||
return 'network';
|
||||
}
|
||||
function _nmRgb(n){
|
||||
if(!n.online) return '255,50,80';
|
||||
if(n.ringIdx===1) return '0,255,136';
|
||||
if(n.ringIdx===2) return '255,215,0';
|
||||
if(n.ringIdx===3) return '0,190,255';
|
||||
if(n.ringIdx===4) return '0,140,180';
|
||||
if(n.ringIdx===4) return '0,160,200';
|
||||
if(n.ringIdx===5) return '0,110,170';
|
||||
return '0,212,255';
|
||||
}
|
||||
function _nmNodePos(n,W,H){
|
||||
@@ -1892,11 +1925,19 @@ function closeNetMap(){
|
||||
function _nmBuild(devices){
|
||||
_nmNodes=[]; _nmEdges=[]; _nmParticles=[];
|
||||
// Hub
|
||||
_nmNodes.push({id:'jarvis',label:'JARVIS',sub:'165.22.1.228',online:true,agent:true,ringIdx:0,angle:0,r:20,pulse:0});
|
||||
_nmNodes.push({id:'jarvis',label:'JARVIS',sub:'165.22.1.228',online:true,agent:true,ringIdx:0,angle:0,r:NM_RINGS[0].nodeR,pulse:0});
|
||||
// Bucket
|
||||
var buckets={proxmox:[],services:[],agents:[],devices:[]};
|
||||
var buckets={proxmox:[],services:[],agents:[],devices:[],network:[]};
|
||||
for(var i=0;i<devices.length;i++) buckets[_nmClassify(devices[i])].push(devices[i]);
|
||||
var rings=['proxmox','services','agents','devices'];
|
||||
// Sort netscan devices: online first, then those with meaningful hostnames
|
||||
buckets.network.sort(function(a,b){
|
||||
var sa=a.alive?1:0, sb=b.alive?1:0;
|
||||
if(sb!==sa) return sb-sa;
|
||||
var ha=(a.name&&a.name!==a.ip&&a.name.indexOf('10.48')!==0)?1:0;
|
||||
var hb=(b.name&&b.name!==b.ip&&b.name.indexOf('10.48')!==0)?1:0;
|
||||
return hb-ha;
|
||||
});
|
||||
var rings=['proxmox','services','agents','devices','network'];
|
||||
for(var ri=0;ri<rings.length;ri++){
|
||||
var rname=rings[ri], rd=NM_RINGS[ri+1], list=buckets[rname].slice(0,rd.cap);
|
||||
for(var j=0;j<list.length;j++){
|
||||
@@ -1935,7 +1976,7 @@ function _nmDraw(){
|
||||
var canvas=document.getElementById('nmCanvas'); if(!canvas) return;
|
||||
var ov=document.getElementById('netMapOverlay');
|
||||
canvas.width = ov ? ov.clientWidth : 820;
|
||||
canvas.height= ov ? ov.clientHeight-62 : 478;
|
||||
canvas.height= ov ? ov.clientHeight-48 : 490;
|
||||
var W=canvas.width, H=canvas.height, cx=W/2, cy=H/2, minR=Math.min(cx,cy);
|
||||
var ctx=canvas.getContext('2d');
|
||||
|
||||
@@ -1980,9 +2021,14 @@ function _nmDraw(){
|
||||
for(var ei=0;ei<_nmEdges.length;ei++){
|
||||
var e=_nmEdges[ei], pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) continue;
|
||||
var n=_nmNodes[e.from], rgb=_nmRgb(n);
|
||||
ctx.beginPath();ctx.moveTo(pa.x,pa.y);ctx.lineTo(pb.x,pb.y);
|
||||
ctx.strokeStyle=n.online?'rgba('+rgb+',0.1)':'rgba(80,20,30,0.07)';
|
||||
ctx.lineWidth=e.strength*0.9; ctx.stroke();
|
||||
// Apply float offset to spoke endpoints
|
||||
var fya=Math.sin(_nmT*0.85+_nmNodes[e.from].pulse)*4;
|
||||
var fyb=Math.sin(_nmT*0.85+_nmNodes[e.to].pulse)*4;
|
||||
var lg=ctx.createLinearGradient(pa.x,pa.y+fya,pb.x,pb.y+fyb);
|
||||
if(n.online){lg.addColorStop(0,'rgba('+rgb+',0.22)');lg.addColorStop(0.5,'rgba('+rgb+',0.08)');lg.addColorStop(1,'rgba(0,212,255,0.15)');}
|
||||
else{lg.addColorStop(0,'rgba(80,20,30,0.07)');lg.addColorStop(1,'rgba(80,20,30,0.07)');}
|
||||
ctx.beginPath();ctx.moveTo(pa.x,pa.y+fya);ctx.lineTo(pb.x,pb.y+fyb);
|
||||
ctx.strokeStyle=lg;ctx.lineWidth=e.strength*1.1;ctx.stroke();
|
||||
}
|
||||
|
||||
// Particles
|
||||
@@ -2000,47 +2046,77 @@ function _nmDraw(){
|
||||
ctx.shadowBlur=6;ctx.fill();ctx.shadowBlur=0;
|
||||
}
|
||||
|
||||
// Nodes
|
||||
// Bubble nodes
|
||||
for(var ni=0;ni<_nmNodes.length;ni++){
|
||||
var n=_nmNodes[ni], p=pos[ni], rgb=_nmRgb(n);
|
||||
var pulse=0.5+Math.sin(_nmT*1.5+n.pulse)*0.3;
|
||||
var isHub=n.ringIdx===0, isHov=_nmHoverNode===ni, baseR=n.r+(isHov?5:0);
|
||||
// Glow
|
||||
var pulse=0.5+Math.sin(_nmT*1.4+n.pulse)*0.3;
|
||||
var isHub=n.ringIdx===0, isHov=_nmHoverNode===ni;
|
||||
// Float offset — each node drifts on Y with unique phase
|
||||
var fy=Math.sin(_nmT*0.85+n.pulse)*4;
|
||||
var px=p.x, py=p.y+fy;
|
||||
var baseR=n.r*(isHov?1.28:1.0);
|
||||
|
||||
// Ambient glow bloom
|
||||
if(n.online){
|
||||
var gr=baseR+10+Math.sin(_nmT+n.pulse)*3;
|
||||
var g=ctx.createRadialGradient(p.x,p.y,baseR*0.3,p.x,p.y,gr);
|
||||
g.addColorStop(0,'rgba('+rgb+','+(pulse*0.22).toFixed(3)+')');g.addColorStop(1,'rgba('+rgb+',0)');
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,gr,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
|
||||
// Rotating arc
|
||||
var arcSpd=isHub?0.6:0.38,arcLen=isHub?Math.PI*1.5:Math.PI*0.85;
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,baseR+4,_nmT*arcSpd,_nmT*arcSpd+arcLen);
|
||||
ctx.strokeStyle='rgba('+rgb+','+(0.2+pulse*0.15).toFixed(3)+')';ctx.lineWidth=isHub?1.5:0.8;ctx.stroke();
|
||||
if(isHub){ctx.beginPath();ctx.arc(p.x,p.y,baseR+8,-_nmT*0.35,-_nmT*0.35+Math.PI*1.1);ctx.strokeStyle='rgba('+rgb+',0.12)';ctx.lineWidth=0.6;ctx.stroke();}
|
||||
var bloomR=baseR*2.6+Math.sin(_nmT*1.1+n.pulse)*3;
|
||||
var bloom=ctx.createRadialGradient(px,py,baseR*0.4,px,py,bloomR);
|
||||
bloom.addColorStop(0,'rgba('+rgb+','+(pulse*0.2).toFixed(3)+')');
|
||||
bloom.addColorStop(1,'rgba('+rgb+',0)');
|
||||
ctx.beginPath();ctx.arc(px,py,bloomR,0,Math.PI*2);ctx.fillStyle=bloom;ctx.fill();
|
||||
|
||||
// Sonar ping — expanding ring that fades out
|
||||
var pingR=baseR+(((_nmT*0.6+n.pulse)%1)*baseR*2.5);
|
||||
var pingA=(1-(pingR-baseR)/(baseR*2.5))*0.3;
|
||||
ctx.beginPath();ctx.arc(px,py,pingR,0,Math.PI*2);
|
||||
ctx.strokeStyle='rgba('+rgb+','+pingA.toFixed(3)+')';
|
||||
ctx.lineWidth=0.7;ctx.stroke();
|
||||
}
|
||||
// Fill + border
|
||||
var fg=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,baseR);
|
||||
var fa=n.online?pulse*0.4:0.09;
|
||||
fg.addColorStop(0,'rgba('+rgb+','+fa.toFixed(3)+')');fg.addColorStop(1,'rgba('+rgb+','+(fa*0.2).toFixed(3)+')');
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,baseR,0,Math.PI*2);ctx.fillStyle=fg;ctx.fill();
|
||||
ctx.strokeStyle='rgba('+rgb+','+(n.online?0.58+pulse*0.3:0.18).toFixed(3)+')';ctx.lineWidth=isHub?2:1;ctx.stroke();
|
||||
// Hub crosshairs
|
||||
|
||||
// Frosted glass fill
|
||||
var fg=ctx.createRadialGradient(px,py-baseR*0.28,0,px,py,baseR);
|
||||
var fa=n.online?0.17+pulse*0.1:0.06;
|
||||
fg.addColorStop(0,'rgba('+rgb+','+(fa*2.0).toFixed(3)+')');
|
||||
fg.addColorStop(0.55,'rgba('+rgb+','+fa.toFixed(3)+')');
|
||||
fg.addColorStop(1,'rgba('+rgb+','+(fa*0.15).toFixed(3)+')');
|
||||
ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);ctx.fillStyle=fg;ctx.fill();
|
||||
|
||||
// Glassy highlight sheen (top-left arc)
|
||||
if(n.online){
|
||||
var sh=ctx.createRadialGradient(px-baseR*0.3,py-baseR*0.35,0,px,py,baseR);
|
||||
sh.addColorStop(0,'rgba(255,255,255,0.12)');sh.addColorStop(0.45,'rgba(255,255,255,0.03)');sh.addColorStop(1,'rgba(255,255,255,0)');
|
||||
ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);ctx.fillStyle=sh;ctx.fill();
|
||||
}
|
||||
|
||||
// Border
|
||||
ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);
|
||||
ctx.strokeStyle='rgba('+rgb+','+(n.online?(0.45+pulse*0.35).toFixed(3):'0.18')+')';
|
||||
ctx.lineWidth=isHub?1.8:1.1;ctx.stroke();
|
||||
|
||||
// Hub crosshairs (softer)
|
||||
if(isHub){
|
||||
ctx.strokeStyle='rgba('+rgb+',0.2)';ctx.lineWidth=0.6;
|
||||
var lines=[[p.x-42,p.y,p.x-baseR-3,p.y],[p.x+baseR+3,p.y,p.x+42,p.y],[p.x,p.y-42,p.x,p.y-baseR-3],[p.x,p.y+baseR+3,p.x,p.y+42]];
|
||||
for(var li=0;li<lines.length;li++){ctx.beginPath();ctx.moveTo(lines[li][0],lines[li][1]);ctx.lineTo(lines[li][2],lines[li][3]);ctx.stroke();}
|
||||
ctx.strokeStyle='rgba('+rgb+',0.15)';ctx.lineWidth=0.6;
|
||||
var ext=50;
|
||||
var hlines=[[px-ext,py,px-baseR-3,py],[px+baseR+3,py,px+ext,py],[px,py-ext,px,py-baseR-3],[px,py+baseR+3,px,py+ext]];
|
||||
for(var li=0;li<hlines.length;li++){ctx.beginPath();ctx.moveTo(hlines[li][0],hlines[li][1]);ctx.lineTo(hlines[li][2],hlines[li][3]);ctx.stroke();}
|
||||
}
|
||||
|
||||
// Status dot
|
||||
if(!isHub){ctx.beginPath();ctx.arc(p.x+baseR*0.65,p.y-baseR*0.65,2.2,0,Math.PI*2);ctx.fillStyle=n.online?'rgba(0,255,120,0.9)':'rgba(255,50,80,0.9)';ctx.fill();}
|
||||
// Label outward
|
||||
var la=isHub?-Math.PI/2:Math.atan2(p.y-cy,p.x-cx);
|
||||
var ld=baseR+(isHub?14:10);
|
||||
var lx=p.x+Math.cos(la)*ld, ly=p.y+Math.sin(la)*ld+3;
|
||||
ctx.font=(isHub?'700 10':'7.5')+'px Share Tech Mono,monospace';
|
||||
var ca=Math.cos(la);
|
||||
ctx.textAlign=isHub?'center':ca>0.2?'left':ca<-0.2?'right':'center';
|
||||
ctx.fillStyle=n.online?'rgba('+rgb+',0.92)':'rgba(200,80,80,0.65)';
|
||||
ctx.fillText(n.label,lx,ly);
|
||||
if(n.sub&&(isHub||isHov)){ctx.font='6px Share Tech Mono,monospace';ctx.fillStyle='rgba(130,185,205,0.5)';ctx.fillText(n.sub,lx,ly+10);}
|
||||
if(!isHub){
|
||||
ctx.beginPath();ctx.arc(px+baseR*0.62,py-baseR*0.62,2.5,0,Math.PI*2);
|
||||
ctx.fillStyle=n.online?'rgba(0,255,120,0.95)':'rgba(255,50,80,0.95)';ctx.fill();
|
||||
}
|
||||
|
||||
// Label — centered below bubble
|
||||
var lblY=py+baseR+11;
|
||||
ctx.font=(isHub?'700 11':'8')+'px Share Tech Mono,monospace';
|
||||
ctx.textAlign='center';
|
||||
ctx.fillStyle=n.online?'rgba('+rgb+',0.95)':'rgba(220,90,90,0.7)';
|
||||
ctx.fillText(n.label,px,lblY);
|
||||
if(n.sub&&(isHub||isHov)){
|
||||
ctx.font='6.5px Share Tech Mono,monospace';
|
||||
ctx.fillStyle='rgba(140,195,215,0.55)';
|
||||
ctx.fillText(n.sub,px,lblY+10);
|
||||
}
|
||||
ctx.textAlign='left';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user