|
174 | 174 | Work:"168,160,255", Event:"94,234,212", Entity:"148,163,184" |
175 | 175 | }; |
176 | 176 | const colorOf = t => TYPE_COLORS[t] || "148,163,184"; |
| 177 | +const TAU = Math.PI * 2; // full circle for ctx.arc |
177 | 178 |
|
178 | 179 | /* ===================== canvas + state ===================== */ |
179 | 180 | const canvas = document.getElementById("c"); |
|
189 | 190 | const nodes = GRAPH.nodes.map(n => ({ |
190 | 191 | ...n, x:0, y:0, z:0, vx:0, vy:0, vz:0, |
191 | 192 | r: 8 + Math.min(10, (n.in || 0) + (n.out || 0)), // radius scales with degree |
| 193 | + col: colorOf(n.type), // cached neon color → no per-frame lookup |
192 | 194 | alpha: 1, // per-node fade (legend toggle) |
193 | 195 | sx:0, sy:0, rz:0, pp:1, hl:1, pinned:false // projection cache + smoothed highlight + pinned (drag to pull out) |
194 | 196 | })); |
195 | 197 | const byId = Object.fromEntries(nodes.map(n => [n.id, n])); |
196 | 198 | const edges = GRAPH.edges.filter(e => byId[e.source] && byId[e.target]); |
197 | | -const adj = {}; nodes.forEach(n => adj[n.id] = []); |
198 | | -edges.forEach(e => { adj[e.source].push(e.target); adj[e.target].push(e.source); }); |
199 | | - |
200 | | -// the few highest-degree hubs get an idle breathing glow |
201 | | -const hubIds = new Set( |
202 | | - [...nodes].sort((a,b) => ((b.in+b.out)-(a.in+a.out))).slice(0, Math.min(3, nodes.length)).map(n => n.id) |
203 | | -); |
204 | | -// labels shown at rest = the most-connected nodes; the rest reveal on hover/zoom |
205 | | -const labelIds = new Set( |
206 | | - [...nodes].sort((a,b) => ((b.in+b.out)-(a.in+a.out))) |
207 | | - .slice(0, Math.max(6, Math.round(nodes.length*0.18))).map(n => n.id) |
208 | | -); |
| 199 | +const adj = {}; nodes.forEach(n => adj[n.id] = new Set()); |
| 200 | +edges.forEach(e => { adj[e.source].add(e.target); adj[e.target].add(e.source); }); // Set → O(1) neighbour test |
| 201 | + |
| 202 | +// rank nodes by degree once; top 3 hubs breathe at rest, top ~18% keep labels at rest |
| 203 | +const byDeg = [...nodes].sort((a,b) => (b.in+b.out)-(a.in+a.out)).map(n => n.id); |
| 204 | +const hubIds = new Set(byDeg.slice(0, Math.min(3, nodes.length))); |
| 205 | +const labelIds = new Set(byDeg.slice(0, Math.max(6, Math.round(nodes.length*0.18)))); |
209 | 206 |
|
210 | 207 | const hiddenTypes = new Set(); // legend filter |
211 | 208 | const typeVisible = t => !hiddenTypes.has(t); |
|
261 | 258 | const a = byId[e.source], b = byId[e.target]; |
262 | 259 | if(!visible(a) || !visible(b)) return; |
263 | 260 | let dx = b.x-a.x, dy = b.y-a.y, dz = b.z-a.z, d = Math.hypot(dx,dy,dz) || 1; |
264 | | - // normalize the pull by degree: a hub with N springs shouldn't feel N× the force |
265 | | - const s = 1 / Math.min(adj[e.source].length || 1, adj[e.target].length || 1); |
| 261 | + // soften by the lower-degree endpoint so dense hub↔hub edges don't dominate |
| 262 | + // (a leaf↔hub edge stays full strength — that's intended) |
| 263 | + const s = 1 / Math.min(adj[e.source].size || 1, adj[e.target].size || 1); |
266 | 264 | const f = (d-118)*0.012 * s * alpha; |
267 | 265 | a.vx += dx/d*f; a.vy += dy/d*f; a.vz += dz/d*f; |
268 | 266 | b.vx -= dx/d*f; b.vy -= dy/d*f; b.vz -= dz/d*f; |
|
365 | 363 | const fx = ax + (bx-ax)*k, fy = ay + (by-ay)*k; |
366 | 364 | ctx.shadowColor = "rgba(45,212,191,.9)"; ctx.shadowBlur = 8; |
367 | 365 | ctx.fillStyle = "rgba(170,255,240,.98)"; |
368 | | - ctx.beginPath(); ctx.arc(fx, fy, 2.8, 0, 6.2832); ctx.fill(); |
| 366 | + ctx.beginPath(); ctx.arc(fx, fy, 2.8, 0, TAU); ctx.fill(); |
369 | 367 | ctx.shadowBlur = 0; |
370 | 368 | } |
371 | 369 | ctx.globalAlpha = 1; |
372 | 370 | } |
373 | 371 |
|
374 | 372 | function drawNode(n, t){ |
375 | 373 | if(n.alpha < 0.02) return; |
376 | | - const col = colorOf(n.type); |
| 374 | + const col = n.col; |
377 | 375 | const hl = n.hl; // smoothed highlight: 1 = lit; eases down (not snaps) when another node is hovered |
378 | 376 | const breathe = (!hover && hubIds.has(n.id)) ? (1 + 0.05*Math.sin(t*0.0024)) : 1; |
379 | 377 | const pulse = (n.id===hover) ? (1.16 + 0.03*Math.sin(t*0.006)) : 1; |
|
383 | 381 | ctx.globalAlpha = n.alpha * fog * (0.34 + 0.66*hl); // dim non-neighbors gently, never to black |
384 | 382 | // selected ring |
385 | 383 | if(n.id === selected){ |
386 | | - ctx.beginPath(); ctx.arc(x, y, r+6, 0, 6.2832); |
| 384 | + ctx.beginPath(); ctx.arc(x, y, r+6, 0, TAU); |
387 | 385 | ctx.strokeStyle = `rgba(${col},.85)`; ctx.lineWidth = 2; ctx.stroke(); |
388 | 386 | } |
389 | 387 | // pinned marker (dashed ring → "pulled out and held") |
390 | 388 | if(n.pinned){ |
391 | | - ctx.beginPath(); ctx.arc(x, y, r+4, 0, 6.2832); |
| 389 | + ctx.beginPath(); ctx.arc(x, y, r+4, 0, TAU); |
392 | 390 | ctx.strokeStyle = "rgba(255,255,255,.65)"; ctx.lineWidth = 1.5; |
393 | 391 | ctx.setLineDash([3,3]); ctx.stroke(); ctx.setLineDash([]); |
394 | 392 | } |
395 | 393 | // hover halo |
396 | 394 | if(n.id === hover){ |
397 | | - ctx.beginPath(); ctx.arc(x, y, r+11, 0, 6.2832); |
| 395 | + ctx.beginPath(); ctx.arc(x, y, r+11, 0, TAU); |
398 | 396 | ctx.fillStyle = `rgba(${col},.16)`; ctx.fill(); |
399 | 397 | } |
400 | 398 | // disc + glow (glow eases with highlight so hovering doesn't flash the whole scene) |
401 | | - ctx.beginPath(); ctx.arc(x, y, r, 0, 6.2832); |
| 399 | + ctx.beginPath(); ctx.arc(x, y, r, 0, TAU); |
402 | 400 | ctx.fillStyle = `rgba(${col},${0.5 + 0.5*hl})`; |
403 | 401 | ctx.shadowColor = `rgba(${col},.8)`; |
404 | 402 | ctx.shadowBlur = (n.id===hover ? 17 : 11) * fog * hl; |
405 | 403 | ctx.fill(); |
406 | 404 | ctx.shadowBlur = 0; |
407 | 405 | // bright inner core |
408 | 406 | if(hl > 0.35){ |
409 | | - ctx.beginPath(); ctx.arc(x-r*0.28, y-r*0.28, r*0.34, 0, 6.2832); |
| 407 | + ctx.beginPath(); ctx.arc(x-r*0.28, y-r*0.28, r*0.34, 0, TAU); |
410 | 408 | ctx.fillStyle = `rgba(255,255,255,${0.5*hl})`; ctx.fill(); |
411 | 409 | } |
412 | 410 | // label — declutter: hubs at rest; hovered node + neighbors; selection; zoomed in; near depth only |
|
428 | 426 | // ease each node's highlight toward its target so hover brightens/dims smoothly (no flash) |
429 | 427 | const focus = hover || selected; // hovering OR an open inspector keeps that node's connections lit |
430 | 428 | for(const n of nodes){ |
431 | | - const near = !focus || n.id===focus || adj[focus].includes(n.id); |
| 429 | + const near = !focus || n.id===focus || adj[focus].has(n.id); |
432 | 430 | n.hl += ((near ? 1 : 0.22) - n.hl) * 0.12; |
433 | 431 | } |
434 | 432 | ctx.setTransform(DPR,0,0,DPR,0,0); |
|
0 commit comments