// Premium hero graph variants. Selected via TWEAKS.graphStyle. // All variants share the same node taxonomy but render very differently. // Robust alpha injection for any oklch / rgb / hex string. // Strategy: paint a 1px swatch into a hidden canvas, then read back rgba bytes. const _swatchCtx = (() => { if (typeof document === 'undefined') return null; const c = document.createElement('canvas'); c.width = c.height = 1; return c.getContext('2d', { willReadFrequently: true }); })(); const _swatchCache = new Map(); function colorWithAlpha(color, alpha) { if (!_swatchCtx) return color; const cacheKey = color + '|base'; let rgb = _swatchCache.get(cacheKey); if (!rgb) { try { _swatchCtx.clearRect(0, 0, 1, 1); _swatchCtx.fillStyle = '#000'; _swatchCtx.fillStyle = color; // browser parses oklch/hex/etc. _swatchCtx.fillRect(0, 0, 1, 1); const d = _swatchCtx.getImageData(0, 0, 1, 1).data; rgb = [d[0], d[1], d[2]]; _swatchCache.set(cacheKey, rgb); } catch (e) { return color; } } const a = Math.max(0, Math.min(1, alpha)); return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${a})`; } const GRAPH_DATA = (() => { // Categorias principais (CORE) — espelham as cards de Integrações const CORE = [ { id:'crm', label:'CRM' }, { id:'erp', label:'ERP' }, { id:'canal',label:'Canais' }, { id:'help', label:'Helpdesk' }, { id:'ai', label:'IA' }, { id:'acs', label:'ACS' }, { id:'db', label:'Dados' }, { id:'voice',label:'Voz' }, ]; // RING1: marcas reais que conectamos hoje (alinhado com a seção Integrações) const RING1 = [ 'IXC Provedor','Hubsoft','Trello','Opa! Suite', 'WhatsApp Business','Conecta SIP-AI','Twilio','OpenAI', 'Anthropic','IXC ACS','Postgres','E-mail', ]; // RING2: capacidades operacionais que rodam por cima das integrações const RING2 = [ '2ª via','Status do plano','Agendamento','Cobrança', 'Suporte L1','Reset CPE','Velocidade','Diagnóstico', 'Pré-venda','Onboarding','Upsell','CSAT', 'Triagem','Roteamento','Transcrição','Resumo', ]; // RING3: protocolos / blocos técnicos que tornam tudo isso possível const RING3 = [ 'Webhooks','REST API','SDK','SIP/RTP', 'TR-069','SNMP','RAG','Embeddings', 'LGPD','Logs auditáveis','SSO','Guardrails', 'Cron','Eventos','Filas','Fallback humano', ]; return { CORE, RING1, RING2, RING3 }; })(); // ───────────────────────────────────────────────────────────────────────── // VARIANT 1: CONSTELLATION // Obsidian-graph-view inspired. Curved bezier edges, individual node halos, // floating particles, additive glow, radial gradient backdrop. // ───────────────────────────────────────────────────────────────────────── const HeroGraphConstellation = ({ accent = "oklch(0.82 0.18 125)" }) => { const canvasRef = React.useRef(null); const wrapRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; const wrap = wrapRef.current; if (!canvas || !wrap) return; const ctx = canvas.getContext('2d'); const dpr = Math.min(window.devicePixelRatio || 1, 2); let W = 0, H = 0; const { CORE, RING1, RING2, RING3 } = GRAPH_DATA; // Build nodes const nodes = []; nodes.push({ id:'hub', label:'ConectaAI', kind:'hub', r: 18, baseAngle: 0, ring: 0, orbitSpeed: 0 }); CORE.forEach((c, i) => nodes.push({ id:c.id, label:c.label, kind:'core', r: 8, baseAngle: (i / CORE.length) * Math.PI * 2 - Math.PI/2, ring: 1, orbitSpeed: 0.00006 })); RING1.forEach((label, i) => nodes.push({ id:'r1_'+i, label, kind:'leaf1', r: 5.5, baseAngle: (i / RING1.length) * Math.PI * 2, ring: 2, orbitSpeed: -0.00004 })); RING2.forEach((label, i) => nodes.push({ id:'r2_'+i, label, kind:'leaf2', r: 4, baseAngle: (i / RING2.length) * Math.PI * 2 + 0.1, ring: 3, orbitSpeed: 0.00003 })); RING3.forEach((label, i) => nodes.push({ id:'r3_'+i, label, kind:'leaf3', r: 3, baseAngle: (i / RING3.length) * Math.PI * 2 - 0.15, ring: 4, orbitSpeed: -0.000022 })); nodes.forEach(n => { n.x = 0; n.y = 0; }); const byId = Object.fromEntries(nodes.map(n => [n.id, n])); // Edges const EDGES = []; CORE.forEach(c => EDGES.push(['hub', c.id])); for (let i = 0; i < RING1.length; i++) { const coreIdx = Math.floor(i / 2) % CORE.length; EDGES.push([CORE[coreIdx].id, 'r1_'+i]); } for (let i = 0; i < RING2.length; i++) { const r1Idx = Math.floor(i / 1.5) % RING1.length; EDGES.push(['r1_'+r1Idx, 'r2_'+i]); } for (let i = 0; i < RING3.length; i++) { EDGES.push(['r2_'+(i % RING2.length), 'r3_'+i]); } // Tangential connections within each ring (textura) for (let i = 0; i < RING1.length; i++) { EDGES.push(['r1_'+i, 'r1_'+((i+1) % RING1.length)]); } const edges = EDGES.filter(([a,b]) => byId[a] && byId[b]) .map(([a,b]) => ({ a: byId[a], b: byId[b], pulses: [] })); // Floating particles (background dust) const particles = Array.from({ length: 60 }, () => ({ x: Math.random(), y: Math.random(), vx: (Math.random() - 0.5) * 0.0002, vy: (Math.random() - 0.5) * 0.0002, r: Math.random() * 1.4 + 0.3, a: Math.random() * 0.4 + 0.1, })); function getRadii() { const isFB = wrap.classList.contains('hero-graph-fullbleed'); // Em modo card: círculo (usa min). Em fullbleed: elipse (rx baseado em W, ry baseado em H) // pra preencher melhor a largura do hero. if (isFB) { const mx = W * 0.5; const my = H * 0.5; return { rCore: Math.min(mx, my) * 0.18, r1: { rx: mx * 0.36, ry: my * 0.46 }, r2: { rx: mx * 0.62, ry: my * 0.72 }, r3: { rx: mx * 0.88, ry: my * 0.94 }, }; } const m = Math.min(W, H); return { rCore: m*0.18, r1: m*0.32, r2: m*0.42, r3: m*0.51 }; } function spawnPulse() { const hubEdges = edges.filter(e => e.a.kind==='hub' || e.b.kind==='hub'); const pool = Math.random() < 0.6 ? hubEdges : edges; const e = pool[Math.floor(Math.random() * pool.length)]; if (!e) return; e.pulses.push({ t: 0, dir: Math.random()<0.5?1:-1, speed: 0.005 + Math.random()*0.012 }); } function resize() { const rect = wrap.getBoundingClientRect(); const isFullbleed = wrap.classList.contains('hero-graph-fullbleed'); W = Math.max(320, Math.min(2400, rect.width || 800)); H = Math.max(320, Math.min(isFullbleed ? 2000 : 900, rect.height || 600)); canvas.width = W * dpr; canvas.height = H * dpr; canvas.style.width = W+'px'; canvas.style.height = H+'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } resize(); const ro = new ResizeObserver(resize); ro.observe(wrap); const mouse = { x:-9999, y:-9999, active:false }; const isFB = wrap.classList.contains('hero-graph-fullbleed'); const onMove = (e) => { const r = canvas.getBoundingClientRect(); const x = e.clientX - r.left, y = e.clientY - r.top; mouse.x = x; mouse.y = y; mouse.active = x >= 0 && x <= r.width && y >= 0 && y <= r.height; }; const onLeave = () => { mouse.active = false; mouse.x = -9999; mouse.y = -9999; }; // Em fullbleed o backdrop é pointer-events:none, então escutamos no window // pra continuar reagindo ao mouse mesmo com elementos por cima. const target = isFB ? window : canvas; target.addEventListener('mousemove', onMove); if (!isFB) canvas.addEventListener('mouseleave', onLeave); function step(ts) { const cx = W/2, cy = H/2; const { rCore, r1, r2, r3 } = getRadii(); for (const n of nodes) { if (n.kind === 'hub') { n.x = cx; n.y = cy; continue; } const radius = n.kind==='core' ? rCore : n.ring===2 ? r1 : n.ring===3 ? r2 : r3; const rx = typeof radius === 'object' ? radius.rx : radius; const ry = typeof radius === 'object' ? radius.ry : radius; const a = n.baseAngle + ts * n.orbitSpeed; const wobble = Math.sin(ts*0.0008 + n.baseAngle*3) * 5; const tx = cx + Math.cos(a) * (rx + wobble); const ty = cy + Math.sin(a) * (ry + wobble); let mx = 0, my = 0; if (mouse.active) { const dx = n.x - mouse.x, dy = n.y - mouse.y; const d2 = dx*dx + dy*dy + 0.01; if (d2 < 18000) { const d = Math.sqrt(d2); const f = 35 / Math.max(1, d/8); mx = (dx/d) * f; my = (dy/d) * f; } } n.x += (tx + mx - n.x) * 0.07; n.y += (ty + my - n.y) * 0.07; } for (const e of edges) { e.pulses = e.pulses.filter(p => { p.t += p.speed; return p.t <= 1; }); } for (const p of particles) { p.x += p.vx; p.y += p.vy; if (p.x < 0) p.x = 1; if (p.x > 1) p.x = 0; if (p.y < 0) p.y = 1; if (p.y > 1) p.y = 0; } } function draw(ts) { const cx = W/2, cy = H/2; const { rCore, r1, r2, r3 } = getRadii(); // Backdrop: transparente (deixa o fundo da página passar) ctx.clearRect(0, 0, W, H); // Floating dust for (const p of particles) { ctx.fillStyle = `rgba(255,255,255,${p.a})`; ctx.beginPath(); ctx.arc(p.x*W, p.y*H, p.r, 0, Math.PI*2); ctx.fill(); } // Subtle ring guides ctx.strokeStyle = 'rgba(255,255,255,.035)'; ctx.lineWidth = 1; [rCore, r1, r2, r3].forEach(r => { const rx = typeof r === 'object' ? r.rx : r; const ry = typeof r === 'object' ? r.ry : r; ctx.beginPath(); if (ctx.ellipse) ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI*2); else ctx.arc(cx, cy, rx, 0, Math.PI*2); ctx.stroke(); }); // EDGES with bezier curves ctx.lineCap = 'round'; for (const e of edges) { const dx = e.b.x - e.a.x, dy = e.b.y - e.a.y; const d = Math.sqrt(dx*dx + dy*dy); if (d < 0.1) continue; const ux = dx/d, uy = dy/d; const x1 = e.a.x + ux * e.a.r, y1 = e.a.y + uy * e.a.r; const x2 = e.b.x - ux * e.b.r, y2 = e.b.y - uy * e.b.r; // Bezier control point: pulled slightly toward center for organic curve const mx = (x1+x2)/2, my = (y1+y2)/2; const tcx = cx + (mx - cx) * 0.55; const tcy = cy + (my - cy) * 0.55; const isHub = e.a.kind==='hub' || e.b.kind==='hub'; const isCore = e.a.kind==='core' || e.b.kind==='core'; ctx.strokeStyle = isHub ? 'rgba(255,255,255,.28)' : isCore ? 'rgba(255,255,255,.13)' : 'rgba(255,255,255,.07)'; ctx.lineWidth = isHub ? 1.2 : 0.7; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.quadraticCurveTo(tcx, tcy, x2, y2); ctx.stroke(); // Pulses traveling along bezier for (const p of e.pulses) { const t = p.dir === 1 ? p.t : 1 - p.t; // Quadratic bezier point const bp = (t) => { const u = 1 - t; return [ u*u*x1 + 2*u*t*tcx + t*t*x2, u*u*y1 + 2*u*t*tcy + t*t*y2, ]; }; const t0 = Math.max(0, t - 0.18); const [px, py] = bp(t); const [tx, ty] = bp(t0); const grad = ctx.createLinearGradient(tx, ty, px, py); grad.addColorStop(0, 'rgba(255,255,255,0)'); grad.addColorStop(1, accent); ctx.strokeStyle = grad; ctx.lineWidth = 1.8; ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(px, py); ctx.stroke(); // Glowing dot ctx.shadowBlur = 12; ctx.shadowColor = accent; ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(px, py, 2.6, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; } } // NODES with individual glow for (const n of nodes) { if (n.kind === 'hub') { // Massive halo const pulse = (Math.sin(ts*0.0018) + 1) / 2; const haloR = 70 + pulse * 18; const grad = ctx.createRadialGradient(n.x, n.y, n.r, n.x, n.y, haloR); grad.addColorStop(0, colorWithAlpha(accent, 0.55)); grad.addColorStop(0.4, colorWithAlpha(accent, 0.18)); grad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(n.x, n.y, haloR, 0, Math.PI*2); ctx.fill(); // Inner ring ctx.strokeStyle = accent; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 8 + pulse*3, 0, Math.PI*2); ctx.stroke(); } else if (n.kind === 'core') { // Soft halo const grad = ctx.createRadialGradient(n.x, n.y, n.r, n.x, n.y, n.r + 14); grad.addColorStop(0, 'rgba(255,255,255,.25)'); grad.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 14, 0, Math.PI*2); ctx.fill(); } else if (n.ring === 2) { const grad = ctx.createRadialGradient(n.x, n.y, n.r, n.x, n.y, n.r + 8); grad.addColorStop(0, 'rgba(255,255,255,.12)'); grad.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 8, 0, Math.PI*2); ctx.fill(); } // Solid dot ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI*2); if (n.kind === 'hub') { ctx.shadowBlur = 24; ctx.shadowColor = accent; ctx.fillStyle = accent; ctx.fill(); ctx.shadowBlur = 0; ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(255,255,255,.9)'; ctx.stroke(); } else if (n.kind === 'core') { ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.fill(); } else if (n.ring === 2) { ctx.fillStyle = 'rgba(255,255,255,.78)'; ctx.fill(); } else if (n.ring === 3) { ctx.fillStyle = 'rgba(255,255,255,.55)'; ctx.fill(); } else { ctx.fillStyle = 'rgba(255,255,255,.38)'; ctx.fill(); } // Labels: hub + core + ring2 (RING1) sempre. Ring 3 (RING2) também em modo fullbleed. const isFB = wrap.classList.contains('hero-graph-fullbleed'); if (n.kind === 'hub' || n.kind === 'core' || n.ring === 2 || (isFB && n.ring === 3)) { const text = n.kind === 'hub' ? 'ConectaAI' : n.label; ctx.font = n.kind === 'hub' ? '600 13px ui-sans-serif, system-ui' : n.kind === 'core' ? '500 11px ui-monospace, monospace' : n.ring === 2 ? '400 10px ui-monospace, monospace' : '400 9px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; const ty = n.y + n.r + 6; if (n.kind === 'hub' || n.kind === 'core') { const w = ctx.measureText(text).width; ctx.fillStyle = 'rgba(0,0,0,.55)'; ctx.fillRect(n.x - w/2 - 4, ty - 1, w + 8, n.kind==='hub'?17:14); } ctx.fillStyle = n.kind === 'hub' ? '#fff' : n.kind === 'core' ? 'rgba(255,255,255,.92)' : n.ring === 2 ? 'rgba(255,255,255,.55)' : 'rgba(255,255,255,.32)'; ctx.fillText(text, n.x, ty); } } } let raf = 0, lastPulse = 0; const tick = (ts) => { if (ts - lastPulse > 200) { spawnPulse(); if (Math.random() < 0.7) spawnPulse(); if (Math.random() < 0.4) spawnPulse(); lastPulse = ts; } step(ts); draw(ts); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); ro.disconnect(); target.removeEventListener('mousemove', onMove); if (!isFB) canvas.removeEventListener('mouseleave', onLeave); }; }, [accent]); return (
● contexto sendo puxado em tempo real 72 nós · 100+ conexões ativas
); }; // ───────────────────────────────────────────────────────────────────────── // VARIANT 2: ORBITAL 3D // Rings tilted in perspective, planetary feel. Parallax via mouse. // ───────────────────────────────────────────────────────────────────────── const HeroGraphOrbital = ({ accent = "oklch(0.82 0.18 125)" }) => { const canvasRef = React.useRef(null); const wrapRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; const wrap = wrapRef.current; if (!canvas || !wrap) return; const ctx = canvas.getContext('2d'); const dpr = Math.min(window.devicePixelRatio || 1, 2); let W = 0, H = 0; const { CORE, RING1, RING2, RING3 } = GRAPH_DATA; // Each ring orbits at a tilted angle (perspective by squashing y) const RINGS = [ { items: CORE.map(c => c.label), labelKind: 'core', tilt: 0.42, baseRadius: 0.20, speed: 0.00006, r: 8 }, { items: RING1, labelKind: 'leaf1', tilt: 0.38, baseRadius: 0.32, speed: -0.00004, r: 5.5 }, { items: RING2, labelKind: 'leaf2', tilt: 0.34, baseRadius: 0.42, speed: 0.000028, r: 4 }, { items: RING3, labelKind: 'leaf3', tilt: 0.30, baseRadius: 0.52, speed: -0.000018, r: 3 }, ]; // Build flat node list const nodes = [{ id:'hub', kind:'hub', r: 18, label: 'ConectaAI' }]; RINGS.forEach((ring, ri) => { ring.items.forEach((label, i) => { nodes.push({ id: `r${ri}_${i}`, kind: ring.labelKind, label, r: ring.r, ringIdx: ri, indexInRing: i, totalInRing: ring.items.length, }); }); }); nodes.forEach(n => { n.x = 0; n.y = 0; n.depth = 0; }); // Edges: hub→core, then chain const edges = []; // hub to all core nodes.filter(n => n.ringIdx === 0).forEach(n => edges.push({ a: nodes[0], b: n, pulses: [] })); // core → ring1 (each core to 2) const coreNodes = nodes.filter(n => n.ringIdx === 0); const r1Nodes = nodes.filter(n => n.ringIdx === 1); r1Nodes.forEach((n, i) => { const c = coreNodes[Math.floor(i/2) % coreNodes.length]; edges.push({ a: c, b: n, pulses: [] }); }); function getCenter() { return { cx: W/2, cy: H * 0.5 }; } function project(ringIdx, t, ts) { const ring = RINGS[ringIdx]; const m = Math.min(W, H); const radius = ring.baseRadius * m; const a = t * Math.PI * 2 + ts * ring.speed; const { cx, cy } = getCenter(); const x = cx + Math.cos(a) * radius; const y = cy + Math.sin(a) * radius * ring.tilt; // squash y for perspective const depth = Math.sin(a); // -1 (back) to 1 (front) return { x, y, depth }; } function spawnPulse() { const e = edges[Math.floor(Math.random() * edges.length)]; e.pulses.push({ t: 0, dir: Math.random()<0.5?1:-1, speed: 0.006 + Math.random()*0.012 }); } function resize() { const rect = wrap.getBoundingClientRect(); W = Math.max(320, Math.min(2200, rect.width || 800)); H = Math.max(320, Math.min(900, rect.height || 600)); canvas.width = W * dpr; canvas.height = H * dpr; canvas.style.width = W+'px'; canvas.style.height = H+'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } resize(); const ro = new ResizeObserver(resize); ro.observe(wrap); const mouse = { x: 0, y: 0 }; const onMove = (e) => { const r = canvas.getBoundingClientRect(); mouse.x = (e.clientX - r.left - W/2) / W; mouse.y = (e.clientY - r.top - H/2) / H; }; const onLeave = () => { mouse.x = 0; mouse.y = 0; }; canvas.addEventListener('mousemove', onMove); canvas.addEventListener('mouseleave', onLeave); function draw(ts) { const { cx, cy } = getCenter(); // Backdrop const bg = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(W,H)*0.65); bg.addColorStop(0, 'rgba(22, 26, 32, 1)'); bg.addColorStop(0.5, 'rgba(10, 12, 16, 1)'); bg.addColorStop(1, 'rgba(3, 4, 6, 1)'); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); // Update positions const parallaxX = mouse.x * 24; const parallaxY = mouse.y * 14; nodes[0].x = cx + parallaxX * 0.3; nodes[0].y = cy + parallaxY * 0.3; nodes[0].depth = 1; for (const n of nodes) { if (n.kind === 'hub') continue; const t = n.indexInRing / n.totalInRing; const p = project(n.ringIdx, t, ts); // Apply parallax stronger to outer rings n.x = p.x + parallaxX * (0.4 + n.ringIdx * 0.15); n.y = p.y + parallaxY * (0.4 + n.ringIdx * 0.15); n.depth = p.depth; } // Draw orbital ellipses (faint) ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.lineWidth = 1; RINGS.forEach((ring) => { const m = Math.min(W, H); const rx = ring.baseRadius * m; const ry = rx * ring.tilt; ctx.beginPath(); ctx.ellipse(cx + parallaxX * 0.5, cy + parallaxY * 0.5, rx, ry, 0, 0, Math.PI*2); ctx.stroke(); }); // Draw edges (sorted by avg depth so back ones first) const sortedEdges = [...edges].sort((a, b) => (a.a.depth + a.b.depth) - (b.a.depth + b.b.depth)); for (const e of sortedEdges) { const dx = e.b.x - e.a.x, dy = e.b.y - e.a.y; const d = Math.sqrt(dx*dx + dy*dy); if (d < 0.1) continue; const ux = dx/d, uy = dy/d; const x1 = e.a.x + ux * e.a.r, y1 = e.a.y + uy * e.a.r; const x2 = e.b.x - ux * e.b.r, y2 = e.b.y - uy * e.b.r; const avgDepth = (e.a.depth + e.b.depth) / 2; const opacity = 0.08 + (avgDepth + 1) * 0.10; const isHub = e.a.kind === 'hub' || e.b.kind === 'hub'; ctx.strokeStyle = `rgba(255,255,255,${isHub ? opacity*1.6 : opacity})`; ctx.lineWidth = isHub ? 1.1 : 0.7; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); for (const p of e.pulses) { const t = p.dir === 1 ? p.t : 1 - p.t; const px = x1 + (x2-x1)*t, py = y1 + (y2-y1)*t; const t0 = Math.max(0, t - 0.16); const tx = x1 + (x2-x1)*t0, ty = y1 + (y2-y1)*t0; const grad = ctx.createLinearGradient(tx, ty, px, py); grad.addColorStop(0, 'rgba(255,255,255,0)'); grad.addColorStop(1, accent); ctx.strokeStyle = grad; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(px, py); ctx.stroke(); ctx.shadowBlur = 12; ctx.shadowColor = accent; ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(px, py, 2.4, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; } e.pulses = e.pulses.filter(p => { p.t += p.speed; return p.t <= 1; }); } // Draw nodes back-to-front (sort by depth) const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth); for (const n of sortedNodes) { const depthFactor = (n.depth + 1) / 2; // 0 (back) to 1 (front) const sizeMul = 0.6 + depthFactor * 0.6; // back=0.6x, front=1.2x const r = n.r * sizeMul; if (n.kind === 'hub') { const pulse = (Math.sin(ts*0.0018) + 1) / 2; const haloR = 75 + pulse * 18; const grad = ctx.createRadialGradient(n.x, n.y, n.r, n.x, n.y, haloR); grad.addColorStop(0, colorWithAlpha(accent, 0.5)); grad.addColorStop(0.4, colorWithAlpha(accent, 0.18)); grad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(n.x, n.y, haloR, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 24; ctx.shadowColor = accent; ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(255,255,255,.9)'; ctx.stroke(); } else { const alpha = 0.35 + depthFactor * 0.55; ctx.fillStyle = `rgba(255,255,255,${alpha})`; ctx.beginPath(); ctx.arc(n.x, n.y, r, 0, Math.PI*2); ctx.fill(); } // Labels for hub + core + front-half ring1 const showLabel = n.kind === 'hub' || (n.ringIdx === 0) || (n.ringIdx === 1 && n.depth > -0.2); if (showLabel) { const text = n.kind === 'hub' ? 'ConectaAI' : n.label; ctx.font = n.kind === 'hub' ? '600 13px ui-sans-serif, system-ui' : n.ringIdx === 0 ? '500 11px ui-monospace, monospace' : '400 10px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; const ty = n.y + r + 6; if (n.kind === 'hub' || n.ringIdx === 0) { const w = ctx.measureText(text).width; ctx.fillStyle = 'rgba(0,0,0,.55)'; ctx.fillRect(n.x - w/2 - 4, ty - 1, w + 8, n.kind==='hub'?17:14); } ctx.fillStyle = n.kind === 'hub' ? '#fff' : n.ringIdx === 0 ? 'rgba(255,255,255,.92)' : `rgba(255,255,255,${0.4 + depthFactor*0.4})`; ctx.fillText(text, n.x, ty); } } } let raf = 0, lastPulse = 0; const tick = (ts) => { if (ts - lastPulse > 220) { spawnPulse(); if (Math.random() < 0.6) spawnPulse(); lastPulse = ts; } draw(ts); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener('mousemove', onMove); canvas.removeEventListener('mouseleave', onLeave); }; }, [accent]); return (
● contexto sendo puxado em tempo real órbita planetária · 72 nós ativos
); }; // ───────────────────────────────────────────────────────────────────────── // VARIANT 3: DATAFLOW HORIZONTAL // Left: source systems. Center: ConectaAI agent core. Right: actions. // Pulses travel left→right and right→left through the agent. // ───────────────────────────────────────────────────────────────────────── const HeroGraphDataflow = ({ accent = "oklch(0.82 0.18 125)" }) => { const canvasRef = React.useRef(null); const wrapRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; const wrap = wrapRef.current; if (!canvas || !wrap) return; const ctx = canvas.getContext('2d'); const dpr = Math.min(window.devicePixelRatio || 1, 2); let W = 0, H = 0; const SOURCES = [ 'IXC Provedor','Hubsoft','IXC ACS', 'WhatsApp Business','Conecta SIP-AI','Twilio', 'Opa! Suite','Trello','E-mail', 'OpenAI','Anthropic','Postgres', 'Webhooks','REST API','SDK', ]; const ACTIONS = [ 'Resolver L1', 'Emitir 2ª via', 'Reset do CPE', 'Diagnóstico de link', 'Status do plano', 'Agendar visita', 'Negociar acordo', 'Atualizar CRM', 'Abrir chamado', 'Encaminhar humano', 'Notificar técnico', 'Pesquisa CSAT', ]; let sources = [], actions = []; function layout() { sources = SOURCES.map((label, i) => ({ label, x: W * 0.12, y: 0, idx: i, total: SOURCES.length, })); actions = ACTIONS.map((label, i) => ({ label, x: W * 0.88, y: 0, idx: i, total: ACTIONS.length, })); const padTop = 30, padBot = 30; sources.forEach((s, i) => { s.y = padTop + (i / (SOURCES.length - 1)) * (H - padTop - padBot); }); actions.forEach((a, i) => { a.y = padTop + (i / (ACTIONS.length - 1)) * (H - padTop - padBot); }); } // Pulses: source → hub → action const pulses = []; function spawnPulse() { const src = sources[Math.floor(Math.random() * sources.length)]; const act = actions[Math.floor(Math.random() * actions.length)]; if (!src || !act) return; pulses.push({ src, act, t: 0, speed: 0.004 + Math.random() * 0.005 }); } function resize() { const rect = wrap.getBoundingClientRect(); W = Math.max(320, Math.min(2200, rect.width || 800)); H = Math.max(320, Math.min(900, rect.height || 600)); canvas.width = W * dpr; canvas.height = H * dpr; canvas.style.width = W+'px'; canvas.style.height = H+'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); layout(); } resize(); const ro = new ResizeObserver(resize); ro.observe(wrap); function bezierPoint(t, p0, p1, p2, p3) { const u = 1 - t; return [ u*u*u*p0[0] + 3*u*u*t*p1[0] + 3*u*t*t*p2[0] + t*t*t*p3[0], u*u*u*p0[1] + 3*u*u*t*p1[1] + 3*u*t*t*p2[1] + t*t*t*p3[1], ]; } function draw(ts) { // Backdrop const cx = W/2, cy = H/2; const bg = ctx.createLinearGradient(0, 0, W, 0); bg.addColorStop(0, 'rgba(8, 10, 14, 1)'); bg.addColorStop(0.5, 'rgba(15, 18, 24, 1)'); bg.addColorStop(1, 'rgba(8, 10, 14, 1)'); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); // Hub area glow const hubGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.min(W, H) * 0.35); hubGrad.addColorStop(0, colorWithAlpha(accent, 0.10)); hubGrad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = hubGrad; ctx.fillRect(0, 0, W, H); // Column labels ctx.font = '500 10px ui-monospace, monospace'; ctx.textBaseline = 'top'; ctx.fillStyle = 'rgba(255,255,255,.4)'; ctx.textAlign = 'left'; ctx.fillText('SISTEMAS · 26', 24, 14); ctx.textAlign = 'right'; ctx.fillText('AÇÕES · 12', W - 24, 14); ctx.textAlign = 'center'; ctx.fillText('AGENTE', cx, 14); // Faint divider lines ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(W * 0.30, 28); ctx.lineTo(W * 0.30, H - 24); ctx.stroke(); ctx.beginPath(); ctx.moveTo(W * 0.70, 28); ctx.lineTo(W * 0.70, H - 24); ctx.stroke(); // Background connection lines (faint, all sources to hub, hub to all actions) ctx.lineCap = 'round'; for (const s of sources) { ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.lineWidth = 0.6; const p0 = [s.x + 6, s.y]; const p1 = [W * 0.32, s.y]; const p2 = [W * 0.42, cy]; const p3 = [cx - 30, cy]; ctx.beginPath(); ctx.moveTo(p0[0], p0[1]); ctx.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]); ctx.stroke(); } for (const a of actions) { ctx.strokeStyle = 'rgba(255,255,255,.05)'; ctx.lineWidth = 0.6; const p0 = [cx + 30, cy]; const p1 = [W * 0.58, cy]; const p2 = [W * 0.68, a.y]; const p3 = [a.x - 6, a.y]; ctx.beginPath(); ctx.moveTo(p0[0], p0[1]); ctx.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]); ctx.stroke(); } // Pulses for (let i = pulses.length - 1; i >= 0; i--) { const p = pulses[i]; p.t += p.speed; if (p.t > 1) { pulses.splice(i, 1); continue; } // 0 → 0.5: source → hub. 0.5 → 1: hub → action. let pos, segT; if (p.t < 0.5) { segT = p.t * 2; const p0 = [p.src.x + 6, p.src.y]; const p1 = [W * 0.32, p.src.y]; const p2 = [W * 0.42, cy]; const p3 = [cx - 30, cy]; pos = bezierPoint(segT, p0, p1, p2, p3); } else { segT = (p.t - 0.5) * 2; const p0 = [cx + 30, cy]; const p1 = [W * 0.58, cy]; const p2 = [W * 0.68, p.act.y]; const p3 = [p.act.x - 6, p.act.y]; pos = bezierPoint(segT, p0, p1, p2, p3); } // Trail const trail = 8; for (let k = 0; k < trail; k++) { const tk = Math.max(0, p.t - k * 0.012); let pk; if (tk < 0.5) { const stk = tk * 2; const p0 = [p.src.x + 6, p.src.y]; const p1 = [W * 0.32, p.src.y]; const p2 = [W * 0.42, cy]; const p3 = [cx - 30, cy]; pk = bezierPoint(stk, p0, p1, p2, p3); } else { const stk = (tk - 0.5) * 2; const p0 = [cx + 30, cy]; const p1 = [W * 0.58, cy]; const p2 = [W * 0.68, p.act.y]; const p3 = [p.act.x - 6, p.act.y]; pk = bezierPoint(stk, p0, p1, p2, p3); } const alpha = (1 - k / trail) * 0.35; ctx.fillStyle = colorWithAlpha(accent, alpha); ctx.beginPath(); ctx.arc(pk[0], pk[1], 1.8, 0, Math.PI*2); ctx.fill(); } ctx.shadowBlur = 12; ctx.shadowColor = accent; ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(pos[0], pos[1], 2.6, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; } // Source nodes (left column) for (const s of sources) { ctx.fillStyle = 'rgba(255,255,255,.85)'; ctx.beginPath(); ctx.arc(s.x, s.y, 3.5, 0, Math.PI*2); ctx.fill(); ctx.font = '400 11px ui-monospace, monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgba(255,255,255,.78)'; ctx.fillText(s.label, s.x - 10, s.y); } // Action nodes (right column) for (const a of actions) { ctx.fillStyle = accent; ctx.shadowBlur = 6; ctx.shadowColor = accent; ctx.beginPath(); ctx.arc(a.x, a.y, 3.8, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; ctx.font = '400 11px ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgba(255,255,255,.85)'; ctx.fillText(a.label, a.x + 10, a.y); } // Hub: large central pill const pulse = (Math.sin(ts*0.002) + 1) / 2; const hubR = 38 + pulse * 4; const haloR = hubR + 30 + pulse * 12; const halo = ctx.createRadialGradient(cx, cy, hubR, cx, cy, haloR); halo.addColorStop(0, colorWithAlpha(accent, 0.4)); halo.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(cx, cy, haloR, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 28; ctx.shadowColor = accent; ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(cx, cy, hubR, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(255,255,255,.9)'; ctx.beginPath(); ctx.arc(cx, cy, hubR, 0, Math.PI*2); ctx.stroke(); // Hub label ctx.font = '600 16px ui-sans-serif, system-ui'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#0a0a0a'; ctx.fillText('ConectaAI', cx, cy - 3); ctx.font = '400 9px ui-monospace, monospace'; ctx.fillStyle = 'rgba(0,0,0,.7)'; ctx.fillText('72 nós · agente ativo', cx, cy + 13); } let raf = 0, lastPulse = 0; const tick = (ts) => { if (ts - lastPulse > 140) { spawnPulse(); if (Math.random() < 0.7) spawnPulse(); lastPulse = ts; } draw(ts); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, [accent]); return (
● dados fluindo em tempo real 26 sistemas → ConectaAI → 12 ações
); }; // ───────────────────────────────────────────────────────────────────────── // Selector // ───────────────────────────────────────────────────────────────────────── const HeroGraphPicker = ({ style = 'constellation', accent }) => { if (style === 'orbital') return ; if (style === 'dataflow') return ; return ; }; Object.assign(window, { HeroGraphConstellation, HeroGraphOrbital, HeroGraphDataflow, HeroGraphPicker, });