// 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,
});