Download index.html from eaglelandsonce/3d_example_google: direct link, hf CLI and curl.
- Browser
- Download file 23.9 kB
-
https://huggingface.co/spaces/eaglelandsonce/3d_example_google/resolve/main/index.html
- Command line
-
hf download hf://spaces/eaglelandsonce/3d_example_google/index.html
-
curl -L -o index.html https://huggingface.co/spaces/eaglelandsonce/3d_example_google/resolve/main/index.html
23.9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Agent Architecture 3D Interactive Simulation</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| } | |
| body, html { | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| background-color: #030712; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| color: #f8fafc; | |
| } | |
| #canvas3d { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: grab; | |
| z-index: 1; | |
| } | |
| #canvas3d:active { | |
| cursor: grabbing; | |
| } | |
| .hud-layer { | |
| position: relative; | |
| z-index: 10; | |
| pointer-events: none; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 16px; | |
| } | |
| .glass { | |
| background: rgba(15, 23, 42, 0.75); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid rgba(255, 255, 255, 0.12); | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5); | |
| pointer-events: auto; | |
| } | |
| .pill { | |
| border-radius: 9999px; | |
| padding: 6px 14px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .btn { | |
| background: rgba(30, 41, 59, 0.8); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| color: #cbd5e1; | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .btn:hover { | |
| background: rgba(51, 65, 85, 0.9); | |
| color: #ffffff; | |
| border-color: rgba(255, 255, 255, 0.25); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #2563eb, #1d4ed8); | |
| color: #ffffff; | |
| border: none; | |
| box-shadow: 0 4px 14px rgba(37, 99, 235, 0.4); | |
| } | |
| .btn-primary:hover { | |
| background: linear-gradient(135deg, #3b82f6, #2563eb); | |
| } | |
| .top-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .pulse-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: #38bdf8; | |
| border-radius: 50%; | |
| box-shadow: 0 0 10px #38bdf8; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.6); opacity: 0.5; } | |
| } | |
| .info-card { | |
| border-radius: 16px; | |
| padding: 16px; | |
| max-width: 440px; | |
| border-left: 4px solid #3b82f6; | |
| transition: transform 0.3s ease, opacity 0.3s ease; | |
| } | |
| .info-card.hidden { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| pointer-events: none; | |
| } | |
| .bottom-bar { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| align-items: center; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| .stage-track { | |
| display: flex; | |
| gap: 6px; | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| .stage-btn { | |
| flex: 1; | |
| padding: 8px 4px; | |
| text-align: center; | |
| font-size: 11px; | |
| font-weight: 600; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| background: rgba(15, 23, 42, 0.8); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| color: #94a3b8; | |
| transition: all 0.2s ease; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .stage-btn:hover { | |
| background: rgba(30, 41, 59, 0.9); | |
| color: #f1f5f9; | |
| } | |
| .stage-btn.active { | |
| background: #2563eb; | |
| color: #ffffff; | |
| border-color: #60a5fa; | |
| box-shadow: 0 0 15px rgba(37, 99, 235, 0.5); | |
| } | |
| .camera-presets { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Fullscreen 3D Canvas --> | |
| <canvas id="canvas3d"></canvas> | |
| <!-- Interactive HUD Overlay --> | |
| <div class="hud-layer"> | |
| <!-- Top Header & Controls --> | |
| <header class="top-bar"> | |
| <div class="glass pill"> | |
| <span class="pulse-dot"></span> | |
| <div style="font-size: 13px; font-weight: 700; letter-spacing: 0.5px;"> | |
| Agent Architecture <span style="color: #38bdf8; font-family: monospace;">3D</span> | |
| </div> | |
| <span style="font-size: 11px; color: #64748b;">| Drag to Rotate · Scroll to Zoom</span> | |
| </div> | |
| <div class="glass pill" style="gap: 6px;"> | |
| <button id="btn-prev" class="btn" title="Previous Stage">◀</button> | |
| <button id="btn-play" class="btn btn-primary" style="padding: 6px 14px;"> | |
| <span id="play-text">⏸ Pause</span> | |
| </button> | |
| <button id="btn-next" class="btn" title="Next Stage">▶</button> | |
| <div style="width: 1px; height: 16px; background: rgba(255,255,255,0.15); margin: 0 4px;"></div> | |
| <button id="btn-orbit" class="btn" style="color: #38bdf8;">Spin: ON</button> | |
| <button id="btn-toggle-info" class="btn">Info Box: ON</button> | |
| </div> | |
| <div class="glass pill camera-presets" style="padding: 4px 8px;"> | |
| <button onclick="setCamera('iso')" class="btn" style="font-size: 11px; padding: 4px 8px;">Isometric</button> | |
| <button onclick="setCamera('top')" class="btn" style="font-size: 11px; padding: 4px 8px;">Top</button> | |
| <button onclick="setCamera('front')" class="btn" style="font-size: 11px; padding: 4px 8px;">Front</button> | |
| </div> | |
| </header> | |
| <!-- Floating Component Inspector (Toggleable) --> | |
| <div id="inspector-card" class="glass info-card"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> | |
| <span id="card-tag" style="font-size: 10px; font-family: monospace; font-weight: 700; padding: 2px 8px; border-radius: 9999px; background: rgba(59, 130, 246, 0.2); color: #60a5fa;"> | |
| STAGE 1 | |
| </span> | |
| <span id="card-sub" style="font-size: 11px; font-family: monospace; color: #94a3b8;">GEMINI.md · AGENTS.md</span> | |
| </div> | |
| <h2 id="card-title" style="font-size: 16px; font-weight: 700; margin-bottom: 6px; color: #ffffff;">1. CONTEXT FILES</h2> | |
| <p id="card-desc" style="font-size: 12px; color: #cbd5e1; line-height: 1.5;"> | |
| The project's constitution loaded on every turn to ground the agent with workspace rules and project conventions. | |
| </p> | |
| <div id="card-detail" style="margin-top: 10px; padding: 8px 12px; background: rgba(2, 6, 23, 0.7); border-radius: 8px; border: 1px solid rgba(255,255,255,0.06); font-size: 11px; font-family: monospace; color: #38bdf8;"> | |
| ⚡ Immutable root constraints & user preferences | |
| </div> | |
| </div> | |
| <!-- Bottom Stage Timeline --> | |
| <footer class="bottom-bar"> | |
| <div class="stage-track glass" style="border-radius: 14px; padding: 6px;"> | |
| <div onclick="jumpToStep(0)" class="stage-btn active" data-step="0">1. Context</div> | |
| <div onclick="jumpToStep(1)" class="stage-btn" data-step="1">2. Skills</div> | |
| <div onclick="jumpToStep(2)" class="stage-btn" data-step="2">3. Subagents</div> | |
| <div onclick="jumpToStep(3)" class="stage-btn" data-step="3">4. MCP & Tools</div> | |
| <div onclick="jumpToStep(4)" class="stage-btn" data-step="4">5. Hooks</div> | |
| <div onclick="jumpToStep(5)" class="stage-btn" data-step="5">6. Output</div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById("canvas3d"); | |
| const ctx = canvas.getContext("2d"); | |
| let width = 0, height = 0, dpr = window.devicePixelRatio || 1; | |
| function resize() { | |
| width = window.innerWidth; | |
| height = window.innerHeight; | |
| canvas.width = width * dpr; | |
| canvas.height = height * dpr; | |
| ctx.scale(dpr, dpr); | |
| } | |
| window.addEventListener("resize", resize); | |
| resize(); | |
| let yaw = 0.55, pitch = 0.38, zoom = 1.0; | |
| let targetYaw = 0.55, targetPitch = 0.38, targetZoom = 1.0; | |
| let isDragging = false, lastMouseX = 0, lastMouseY = 0; | |
| let autoRotate = true, activeStep = 0, isPlaying = true, stepTimer = 0, showInfo = true; | |
| const nodesData = [ | |
| { | |
| id: "context", | |
| title: "1. CONTEXT FILES", | |
| sub: "GEMINI.md · AGENTS.md", | |
| tag: "CONSTITUTION", | |
| desc: "The project's constitution loaded on every turn to ground the agent with workspace rules and project conventions.", | |
| detail: "Immutable root constraints & user preferences", | |
| color: "#3b82f6", | |
| glow: "rgba(59, 130, 246, 0.7)", | |
| pos: { x: -190, y: -45, z: 110 }, | |
| radius: 30 | |
| }, | |
| { | |
| id: "skills", | |
| title: "2. SKILLS", | |
| sub: "Reusable capability packages", | |
| tag: "ON-DEMAND", | |
| desc: "Conditional knowledge packages loaded dynamically only when task intent requires specific workflows or domain knowledge.", | |
| detail: "Zero prompt bloat: loads only relevant guidelines", | |
| color: "#14b8a6", | |
| glow: "rgba(20, 184, 166, 0.7)", | |
| pos: { x: 0, y: -150, z: 140 }, | |
| radius: 30 | |
| }, | |
| { | |
| id: "subagents", | |
| title: "3. SUBAGENTS", | |
| sub: "Persona-driven specialists", | |
| tag: "DELEGATION", | |
| desc: "Narrow specialized agents (auditor, reviewer, doc-writer) spawned in isolated sandboxes to complete focused sub-tasks.", | |
| detail: "Autonomous parallel execution & focused scratchpads", | |
| color: "#f97316", | |
| glow: "rgba(249, 115, 22, 0.7)", | |
| pos: { x: 190, y: -45, z: 110 }, | |
| radius: 30 | |
| }, | |
| { | |
| id: "mcp", | |
| title: "4. MCP SERVERS & TOOLS", | |
| sub: "Model Context Protocol + Native Tools", | |
| tag: "EXECUTION", | |
| desc: "USB-C of agent tooling. Standardized protocol connecting cloud APIs, databases, git, and local bash execution seamlessly.", | |
| detail: "Universal integration layer (Postgres, GitHub, Slack, CLI)", | |
| color: "#6366f1", | |
| glow: "rgba(99, 102, 241, 0.7)", | |
| pos: { x: 170, y: 120, z: -70 }, | |
| radius: 30 | |
| }, | |
| { | |
| id: "hooks", | |
| title: "5. HOOKS", | |
| sub: "Deterministic guardrails", | |
| tag: "SAFETY SHIELD", | |
| desc: "Outside LLM cognition. Deterministic code that intercepts pre/post tool calls to block destructive commands and data leaks.", | |
| detail: "Hard security perimeter you never delegate to an LLM", | |
| color: "#f43f5e", | |
| glow: "rgba(244, 63, 94, 0.8)", | |
| pos: { x: 0, y: 170, z: -120 }, | |
| radius: 34 | |
| }, | |
| { | |
| id: "delivery", | |
| title: "6. SAFE EXECUTION", | |
| sub: "Verified Output & Artifacts", | |
| tag: "DELIVERY", | |
| desc: "Safe, scoped response delivered back to user with full audit transparency, rich artifacts, and verified changes.", | |
| detail: "Rendered interactive widgets, reports & code diffs", | |
| color: "#eab308", | |
| glow: "rgba(234, 179, 8, 0.7)", | |
| pos: { x: -170, y: 120, z: -70 }, | |
| radius: 30 | |
| } | |
| ]; | |
| const coreNode = { | |
| title: "ORCHESTRATOR", | |
| color: "#38bdf8", | |
| glow: "rgba(56, 189, 248, 0.8)", | |
| pos: { x: 0, y: 0, z: 0 }, | |
| radius: 38 | |
| }; | |
| const particles = []; | |
| const NUM_PARTICLES = 160; | |
| for (let i = 0; i < NUM_PARTICLES; i++) { | |
| const radius = 150 + Math.random() * 280; | |
| const theta = Math.random() * Math.PI * 2; | |
| const phi = (Math.random() - 0.5) * Math.PI; | |
| particles.push({ | |
| x: radius * Math.cos(theta) * Math.cos(phi), | |
| y: radius * Math.sin(phi), | |
| z: radius * Math.sin(theta) * Math.cos(phi), | |
| size: 1 + Math.random() * 2, | |
| color: Math.random() > 0.5 ? "rgba(56, 189, 248, 0.4)" : "rgba(147, 197, 253, 0.25)" | |
| }); | |
| } | |
| const flowPath = [ | |
| { from: 0, to: 1 }, | |
| { from: 1, to: 2 }, | |
| { from: 2, to: 3 }, | |
| { from: 3, to: 4 }, | |
| { from: 4, to: 5 }, | |
| { from: 5, to: 0 } | |
| ]; | |
| const energyPackets = []; | |
| for (let i = 0; i < flowPath.length; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| energyPackets.push({ | |
| pathIndex: i, | |
| progress: (j / 3) + Math.random() * 0.2, | |
| speed: 0.008 + Math.random() * 0.004, | |
| size: 4 + Math.random() * 2.5 | |
| }); | |
| } | |
| } | |
| function project3D(p, cx, cy) { | |
| const cosY = Math.cos(yaw), sinY = Math.sin(yaw); | |
| const cosP = Math.cos(pitch), sinP = Math.sin(pitch); | |
| let x1 = p.x * cosY - p.z * sinY; | |
| let z1 = p.x * sinY + p.z * cosY; | |
| let y2 = p.y * cosP - z1 * sinP; | |
| let z2 = p.y * sinP + z1 * cosP; | |
| const cameraDist = 520 / zoom; | |
| const fov = 440; | |
| const depth = cameraDist + z2; | |
| const scale = fov / Math.max(depth, 40); | |
| return { x: cx + x1 * scale, y: cy + y2 * scale, scale, depth: z2 }; | |
| } | |
| window.addEventListener("mousedown", (e) => { | |
| if (e.target === canvas) { | |
| isDragging = true; | |
| lastMouseX = e.clientX; | |
| lastMouseY = e.clientY; | |
| } | |
| }); | |
| window.addEventListener("mousemove", (e) => { | |
| if (isDragging) { | |
| const dx = e.clientX - lastMouseX; | |
| const dy = e.clientY - lastMouseY; | |
| targetYaw += dx * 0.006; | |
| targetPitch = Math.max(-1.1, Math.min(1.1, targetPitch - dy * 0.006)); | |
| lastMouseX = e.clientX; | |
| lastMouseY = e.clientY; | |
| autoRotate = false; | |
| document.getElementById("btn-orbit").textContent = "Spin: OFF"; | |
| } | |
| }); | |
| window.addEventListener("mouseup", () => { isDragging = false; }); | |
| window.addEventListener("wheel", (e) => { | |
| if (e.target === canvas) { | |
| e.preventDefault(); | |
| targetZoom = Math.max(0.6, Math.min(2.2, targetZoom - e.deltaY * 0.001)); | |
| } | |
| }, { passive: false }); | |
| function updateUI(stepIdx) { | |
| activeStep = stepIdx; | |
| const data = nodesData[stepIdx]; | |
| const inspector = document.getElementById("inspector-card"); | |
| if (inspector) { | |
| inspector.style.borderLeftColor = data.color; | |
| document.getElementById("card-tag").textContent = data.tag; | |
| document.getElementById("card-tag").style.backgroundColor = `${data.color}25`; | |
| document.getElementById("card-tag").style.color = data.color; | |
| document.getElementById("card-title").textContent = data.title; | |
| document.getElementById("card-sub").textContent = data.sub; | |
| document.getElementById("card-desc").textContent = data.desc; | |
| document.getElementById("card-detail").textContent = "⚡ " + data.detail; | |
| } | |
| document.querySelectorAll(".stage-btn").forEach((btn, idx) => { | |
| if (idx === stepIdx) btn.classList.add("active"); | |
| else btn.classList.remove("active"); | |
| }); | |
| } | |
| function jumpToStep(idx) { | |
| updateUI(idx); | |
| stepTimer = 0; | |
| } | |
| function nextStep() { | |
| jumpToStep((activeStep + 1) % nodesData.length); | |
| } | |
| function prevStep() { | |
| jumpToStep((activeStep - 1 + nodesData.length) % nodesData.length); | |
| } | |
| function setCamera(type) { | |
| autoRotate = false; | |
| document.getElementById("btn-orbit").textContent = "Spin: OFF"; | |
| if (type === 'iso') { targetYaw = 0.55; targetPitch = 0.38; targetZoom = 1.0; } | |
| else if (type === 'top') { targetYaw = 0; targetPitch = 1.45; targetZoom = 0.95; } | |
| else if (type === 'front') { targetYaw = 0; targetPitch = 0.05; targetZoom = 1.05; } | |
| } | |
| document.getElementById("btn-play").addEventListener("click", () => { | |
| isPlaying = !isPlaying; | |
| document.getElementById("play-text").textContent = isPlaying ? "⏸ Pause" : "▶ Play"; | |
| }); | |
| document.getElementById("btn-next").addEventListener("click", nextStep); | |
| document.getElementById("btn-prev").addEventListener("click", prevStep); | |
| document.getElementById("btn-orbit").addEventListener("click", () => { | |
| autoRotate = !autoRotate; | |
| document.getElementById("btn-orbit").textContent = autoRotate ? "Spin: ON" : "Spin: OFF"; | |
| }); | |
| document.getElementById("btn-toggle-info").addEventListener("click", () => { | |
| showInfo = !showInfo; | |
| const card = document.getElementById("inspector-card"); | |
| if (showInfo) { | |
| card.classList.remove("hidden"); | |
| document.getElementById("btn-toggle-info").textContent = "Info Box: ON"; | |
| } else { | |
| card.classList.add("hidden"); | |
| document.getElementById("btn-toggle-info").textContent = "Info Box: OFF"; | |
| } | |
| }); | |
| let lastTime = performance.now(); | |
| function render(currentTime) { | |
| const dt = (currentTime - lastTime) / 1000; | |
| lastTime = currentTime; | |
| yaw += (targetYaw - yaw) * 0.08; | |
| pitch += (targetPitch - pitch) * 0.08; | |
| zoom += (targetZoom - zoom) * 0.08; | |
| if (autoRotate) targetYaw += 0.003; | |
| if (isPlaying) { | |
| stepTimer += dt; | |
| if (stepTimer > 3.8) { | |
| stepTimer = 0; | |
| nextStep(); | |
| } | |
| } | |
| ctx.clearRect(0, 0, width, height); | |
| const cx = width / 2, cy = height / 2; | |
| ctx.save(); | |
| ctx.strokeStyle = "rgba(51, 65, 85, 0.22)"; | |
| ctx.lineWidth = 1; | |
| for (let r = 110; r <= 290; r += 60) { | |
| ctx.beginPath(); | |
| for (let angle = 0; angle <= Math.PI * 2; angle += 0.1) { | |
| const pt = project3D({ x: Math.cos(angle) * r, y: 40, z: Math.sin(angle) * r }, cx, cy); | |
| if (angle === 0) ctx.moveTo(pt.x, pt.y); | |
| else ctx.lineTo(pt.x, pt.y); | |
| } | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| } | |
| ctx.restore(); | |
| for (let p of particles) { | |
| const proj = project3D(p, cx, cy); | |
| if (proj.scale > 0) { | |
| ctx.beginPath(); | |
| ctx.arc(proj.x, proj.y, Math.max(0.5, p.size * proj.scale), 0, Math.PI * 2); | |
| ctx.fillStyle = p.color; | |
| ctx.fill(); | |
| } | |
| } | |
| const projCore = project3D(coreNode.pos, cx, cy); | |
| nodesData.forEach((node, i) => { | |
| const projNode = project3D(node.pos, cx, cy); | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.moveTo(projCore.x, projCore.y); | |
| ctx.lineTo(projNode.x, projNode.y); | |
| ctx.strokeStyle = (i === activeStep) ? "rgba(56, 189, 248, 0.55)" : "rgba(71, 85, 105, 0.18)"; | |
| ctx.lineWidth = (i === activeStep) ? 2.2 * projNode.scale : 1 * projNode.scale; | |
| if (i === activeStep) ctx.setLineDash([5, 5]); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| const nextNode = nodesData[(i + 1) % nodesData.length]; | |
| const projNext = project3D(nextNode.pos, cx, cy); | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.moveTo(projNode.x, projNode.y); | |
| ctx.lineTo(projNext.x, projNext.y); | |
| ctx.strokeStyle = (i === activeStep) ? "rgba(59, 130, 246, 0.65)" : "rgba(51, 65, 85, 0.28)"; | |
| ctx.lineWidth = (i === activeStep) ? 2.8 * projNode.scale : 1.2 * projNode.scale; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| }); | |
| energyPackets.forEach(ep => { | |
| ep.progress += ep.speed; | |
| if (ep.progress > 1) ep.progress = 0; | |
| const fromNode = nodesData[flowPath[ep.pathIndex].from]; | |
| const toNode = nodesData[flowPath[ep.pathIndex].to]; | |
| const interpPos = { | |
| x: fromNode.pos.x + (toNode.pos.x - fromNode.pos.x) * ep.progress, | |
| y: fromNode.pos.y + (toNode.pos.y - fromNode.pos.y) * ep.progress, | |
| z: fromNode.pos.z + (toNode.pos.z - fromNode.pos.z) * ep.progress | |
| }; | |
| const proj = project3D(interpPos, cx, cy); | |
| if (proj.scale > 0) { | |
| ctx.save(); | |
| ctx.beginPath(); | |
| const r = ep.size * proj.scale; | |
| ctx.arc(proj.x, proj.y, Math.max(1, r), 0, Math.PI * 2); | |
| ctx.fillStyle = (ep.pathIndex === activeStep) ? "#38bdf8" : "#93c5fd"; | |
| ctx.shadowColor = "#38bdf8"; | |
| ctx.shadowBlur = 8; | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| }); | |
| if (projCore.scale > 0) { | |
| ctx.save(); | |
| const coreR = coreNode.radius * projCore.scale; | |
| const glowGrad = ctx.createRadialGradient(projCore.x, projCore.y, coreR * 0.2, projCore.x, projCore.y, coreR * 2.2); | |
| glowGrad.addColorStop(0, "rgba(56, 189, 248, 0.35)"); | |
| glowGrad.addColorStop(1, "rgba(56, 189, 248, 0)"); | |
| ctx.fillStyle = glowGrad; | |
| ctx.beginPath(); | |
| ctx.arc(projCore.x, projCore.y, coreR * 2.2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| const pulse = 1 + Math.sin(currentTime * 0.004) * 0.1; | |
| ctx.beginPath(); | |
| ctx.arc(projCore.x, projCore.y, coreR * pulse, 0, Math.PI * 2); | |
| ctx.strokeStyle = "rgba(56, 189, 248, 0.6)"; | |
| ctx.lineWidth = 1.6 * projCore.scale; | |
| ctx.stroke(); | |
| const sphereGrad = ctx.createRadialGradient(projCore.x - coreR * 0.3, projCore.y - coreR * 0.3, coreR * 0.1, projCore.x, projCore.y, coreR); | |
| sphereGrad.addColorStop(0, "#e0f2fe"); | |
| sphereGrad.addColorStop(0.5, "#0284c7"); | |
| sphereGrad.addColorStop(1, "#0369a1"); | |
| ctx.fillStyle = sphereGrad; | |
| ctx.beginPath(); | |
| ctx.arc(projCore.x, projCore.y, coreR, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.font = `bold ${Math.max(8, 10 * projCore.scale)}px sans-serif`; | |
| ctx.fillStyle = "#ffffff"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| ctx.fillText("ORCHESTRATOR", projCore.x, projCore.y); | |
| ctx.restore(); | |
| } | |
| const sortedNodes = nodesData.map((n, idx) => { | |
| return { data: n, index: idx, proj: project3D(n.pos, cx, cy) }; | |
| }).sort((a, b) => a.proj.depth - b.proj.depth); | |
| sortedNodes.forEach(item => { | |
| const n = item.data; | |
| const proj = item.proj; | |
| const isActive = (item.index === activeStep); | |
| if (proj.scale > 0) { | |
| ctx.save(); | |
| const r = n.radius * proj.scale; | |
| if (isActive) { | |
| const activeGlow = ctx.createRadialGradient(proj.x, proj.y, r * 0.3, proj.x, proj.y, r * 2.4); | |
| activeGlow.addColorStop(0, n.glow); | |
| activeGlow.addColorStop(1, "rgba(0, 0, 0, 0)"); | |
| ctx.fillStyle = activeGlow; | |
| ctx.beginPath(); | |
| ctx.arc(proj.x, proj.y, r * 2.4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| const pulseR = r * (1.25 + Math.sin(currentTime * 0.006) * 0.15); | |
| ctx.beginPath(); | |
| ctx.arc(proj.x, proj.y, pulseR, 0, Math.PI * 2); | |
| ctx.strokeStyle = n.color; | |
| ctx.lineWidth = 2 * proj.scale; | |
| ctx.stroke(); | |
| } | |
| const nodeGrad = ctx.createRadialGradient(proj.x - r * 0.3, proj.y - r * 0.3, r * 0.1, proj.x, proj.y, r); | |
| nodeGrad.addColorStop(0, "#ffffff"); | |
| nodeGrad.addColorStop(0.4, n.color); | |
| nodeGrad.addColorStop(1, "#0f172a"); | |
| ctx.fillStyle = nodeGrad; | |
| ctx.beginPath(); | |
| ctx.arc(proj.x, proj.y, r, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.strokeStyle = isActive ? "#ffffff" : "rgba(255, 255, 255, 0.25)"; | |
| ctx.lineWidth = isActive ? 2 * proj.scale : 1 * proj.scale; | |
| ctx.stroke(); | |
| ctx.font = `bold ${Math.max(9, 11 * proj.scale)}px sans-serif`; | |
| ctx.fillStyle = isActive ? "#ffffff" : "#cbd5e1"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "bottom"; | |
| ctx.fillText(n.title.split(". ")[1] || n.title, proj.x, proj.y - r - 6); | |
| ctx.font = `${Math.max(8, 9 * proj.scale)}px monospace`; | |
| ctx.fillStyle = isActive ? n.color : "#94a3b8"; | |
| ctx.fillText(n.tag, proj.x, proj.y - r - 18); | |
| ctx.restore(); | |
| } | |
| }); | |
| requestAnimationFrame(render); | |
| } | |
| updateUI(0); | |
| requestAnimationFrame(render); | |
| </script> | |
| </body> | |
| </html> |