diff --git a/ui/app.js b/ui/app.js index cd301bf..7a69138 100644 --- a/ui/app.js +++ b/ui/app.js @@ -10,6 +10,7 @@ clusterHubs: [], laneGuides: [], labelAnchors: [], + bloomPetals: [], subnet: "all", model: null, groupHint: null, @@ -250,6 +251,7 @@ state.clusterHubs = []; state.laneGuides = []; state.labelAnchors = []; + state.bloomPetals = []; setTargets(nodes); } @@ -280,6 +282,7 @@ state.clusterHubs = []; state.laneGuides = []; state.labelAnchors = []; + state.bloomPetals = []; setTargets(nodes); } @@ -332,6 +335,79 @@ state.clusterHubs = hubs; state.laneGuides = []; state.labelAnchors = anchors; + state.bloomPetals = []; + setTargets(nodes); + } + + function layoutBloom(devices, cx, cy, w, h) { + const groups = groupBySubnet(devices); + const nodes = []; + const anchors = []; + const petals = []; + const R = Math.min(w, h) * 0.38; + const omeHub = { x: cx, y: cy }; + const nGroups = Math.max(groups.length, 1); + + groups.forEach((g, si) => { + const a = (si / nGroups) * Math.PI * 2 - Math.PI / 2; + const info = subnetLabelInfo(g.cidr); + const tipX = cx + Math.cos(a) * R; + const tipY = cy + Math.sin(a) * R; + const midX = cx + Math.cos(a) * R * 0.52; + const midY = cy + Math.sin(a) * R * 0.52; + const petalWidth = 22 + Math.min(48, g.members.length * 2.8); + + petals.push({ + angle: a, + tipX, + tipY, + midX, + midY, + width: petalWidth, + color: info.color, + cidr: g.cidr, + label: info.label, + count: g.members.length, + }); + + anchors.push( + makeLabelAnchor({ + x: tipX, + y: tipY, + cidr: g.cidr, + localR: 10, + dirX: Math.cos(a), + dirY: Math.sin(a), + pipeExtra: 28, + }) + ); + + g.members.forEach((n, i) => { + const t = g.members.length <= 1 ? 0.55 : 0.18 + (i / Math.max(g.members.length - 1, 1)) * 0.72; + const alongX = cx + Math.cos(a) * R * t; + const alongY = cy + Math.sin(a) * R * t; + // Petal bulge: widest mid-petal + const bulge = Math.sin(Math.min(1, Math.max(0, (t - 0.12) / 0.76)) * Math.PI); + const lane = (i % 2 === 0 ? 1 : -1) * (0.4 + ((i * 7) % 5) * 0.11); + const px = -Math.sin(a); + const py = Math.cos(a); + const spread = bulge * petalWidth * lane; + nodes.push({ + ...n, + tx: alongX + px * spread, + ty: alongY + py * spread, + r: nodeRadius(n), + depth: 0.65 + t * 0.55, + bloom: si, + bloomT: t, + }); + }); + }); + + state.clusterHubs = []; + state.laneGuides = []; + state.labelAnchors = anchors; + state.bloomPetals = petals; setTargets(nodes); } @@ -438,6 +514,9 @@ case "clusters": layoutClusters(devices, cx, cy, w, h); break; + case "bloom": + layoutBloom(devices, cx, cy, w, h); + break; case "lanes": layoutLanes(devices, cx, cy, w, h); break; @@ -626,6 +705,60 @@ ctx.globalAlpha = 1; drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55); } + } else if (mode === "bloom") { + // Soft petal outlines + spine pulse + for (const p of state.bloomPetals || []) { + const rgb = hexToRgb(p.color || "#00a8e8"); + const a = p.angle || 0; + const nx = -Math.sin(a); + const ny = Math.cos(a); + // Petal path: hub → left bulge → tip → right bulge → hub + const c1x = hub.x + Math.cos(a) * (Math.hypot(p.midX - hub.x, p.midY - hub.y) || 80); + const c1y = hub.y + Math.sin(a) * (Math.hypot(p.midX - hub.x, p.midY - hub.y) || 80); + ctx.beginPath(); + ctx.moveTo(hub.x, hub.y); + ctx.quadraticCurveTo( + c1x + nx * p.width, + c1y + ny * p.width, + p.tipX, + p.tipY + ); + ctx.quadraticCurveTo( + c1x - nx * p.width, + c1y - ny * p.width, + hub.x, + hub.y + ); + ctx.closePath(); + ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.06)`; + ctx.fill(); + ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.28)`; + ctx.lineWidth = 1.2; + ctx.stroke(); + + // Spine + ctx.beginPath(); + ctx.moveTo(hub.x, hub.y); + ctx.lineTo(p.tipX, p.tipY); + ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.35)`; + ctx.lineWidth = 1.4; + ctx.stroke(); + + // Traveling bloom spark along spine + const u = (t * 0.012 + (p.angle || 0)) % 1; + const sx = hub.x + (p.tipX - hub.x) * u; + const sy = hub.y + (p.tipY - hub.y) * u; + ctx.beginPath(); + ctx.arc(sx, sy, 2.6, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.85)`; + ctx.fill(); + } + // Inner halo + ctx.beginPath(); + ctx.arc(hub.x, hub.y, 42, 0, Math.PI * 2); + ctx.strokeStyle = canvasTheme().decor(2); + ctx.lineWidth = 1; + ctx.stroke(); } else if (mode === "lanes") { for (const g of state.laneGuides || []) { ctx.beginPath(); @@ -769,6 +902,19 @@ } return; } + if (mode === "bloom") { + // Node → petal mid (spine), plus faint hub link + for (const n of state.nodes) { + const petal = (state.bloomPetals || [])[n.bloom]; + if (petal) { + drawPulseLink(petal.midX, petal.midY, n.x, n.y, n.connected, t, n.id, 0.75); + if (n.connected) drawPulseLink(hub.x, hub.y, petal.midX, petal.midY, true, t, n.id + 9000, 0.35); + } else { + drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.8); + } + } + return; + } if (mode === "lanes") { for (const n of state.nodes) { drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.55); diff --git a/ui/index.html b/ui/index.html index 591260d..05d6d55 100644 --- a/ui/index.html +++ b/ui/index.html @@ -58,6 +58,7 @@ + @@ -633,7 +634,7 @@ - +