/* HERO D — confete físico que cai, quica, acumula até a metade e revela o logo original da Cheer quando o mouse passa. */ const BRAND_DOTS = ["#FECB39", "#D0A4CC", "#2864AE", "#74C9DE"]; const LOGO_SRC = "cheer-ds/assets/logo/cheer-logo-stacked-white.png"; function HeroD({ fill = 0.5, density = 56, reveal = 150 }) { const canvasRef = React.useRef(null); const sectionRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; const section = sectionRef.current; if (!canvas || !section) return; const ctx = canvas.getContext("2d"); const reduced = window.Motion && window.Motion.REDUCED; // ---- logo image (stickers) ---- const logo = new Image(); let logoReady = false; logo.onload = () => { logoReady = true; }; logo.src = LOGO_SRC; // ---- config ---- const GRAVITY = 0.30, BOUNCE = 0.5, FRICTION = 0.986, SUBSTEPS = 4; const MOUSE_F = 13, SPAWN_INTERVAL = 60, AVG_AREA = 7400; let W = 0, H = 0, dpr = Math.min(2, window.devicePixelRatio || 1); let circles = [], spawned = 0, lastSpawn = 0, raf = null, running = true; const mouse = { x: -9999, y: -9999, active: false }; // alvo de quantidade ~ área da "piscina" (largura × altura × fração) function targetCount() { const t = Math.round((W * H * fill * 0.8 / AVG_AREA) * (density / 56)); return Math.max(10, Math.min(140, t)); } function resize() { W = section.offsetWidth; H = section.offsetHeight; dpr = Math.min(2, window.devicePixelRatio || 1); 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); } function makeCircle() { const isSticker = Math.random() < 0.3; const r = isSticker ? 30 + Math.random() * 26 : 26 + Math.random() * 50; return { x: r + Math.random() * Math.max(1, W - 2 * r), y: -r - Math.random() * 80, r, vx: (Math.random() - 0.5) * 2.4, vy: 0.5 + Math.random() * 1.4, isSticker, color: BRAND_DOTS[(Math.random() * BRAND_DOTS.length) | 0], opacity: 0, glow: 0, }; } function repel(c) { const dx = c.x - mouse.x, dy = c.y - mouse.y; const range = reveal + c.r, d2 = dx * dx + dy * dy; if (d2 < range * range && d2 > 0.01) { const d = Math.sqrt(d2), t = 1 - d / range, f = t * t * MOUSE_F; c.vx += (dx / d) * f; c.vy += (dy / d) * f; c.glow = Math.max(c.glow, t * 0.8); } } function bounds(c) { if (c.y + c.r > H) { c.y = H - c.r; c.vy = -Math.abs(c.vy) * BOUNCE; c.vx *= 0.82; } if (c.x - c.r < 0) { c.x = c.r; c.vx = Math.abs(c.vx) * BOUNCE; } if (c.x + c.r > W) { c.x = W - c.r; c.vx = -Math.abs(c.vx) * BOUNCE; } } function collide(a, b) { const dx = b.x - a.x, dy = b.y - a.y, min = a.r + b.r, d2 = dx * dx + dy * dy; if (d2 >= min * min || d2 < 0.0001) return; const d = Math.sqrt(d2), nx = dx / d, ny = dy / d, pen = (min - d) * 0.5; a.x -= nx * pen; a.y -= ny * pen; b.x += nx * pen; b.y += ny * pen; const rv = (b.vx - a.vx) * nx + (b.vy - a.vy) * ny; if (rv > 0) return; const imp = rv * (1 + BOUNCE) * 0.5; a.vx += imp * nx; a.vy += imp * ny; b.vx -= imp * nx; b.vy -= imp * ny; } function step() { const dt = 1 / SUBSTEPS; for (let s = 0; s < SUBSTEPS; s++) { for (const c of circles) { if (mouse.active) repel(c); c.vy += GRAVITY * dt; c.vx *= Math.pow(FRICTION, dt); c.vy *= Math.pow(FRICTION, dt); c.x += c.vx; c.y += c.vy; bounds(c); } for (let i = 0; i < circles.length - 1; i++) for (let j = i + 1; j < circles.length; j++) collide(circles[i], circles[j]); } for (const c of circles) { c.opacity = Math.min(1, c.opacity + 0.04); c.glow = Math.max(0, c.glow - 0.05); } } // fill target: topo da pilha que queremos atingir (até `fill` da altura) function settledTop() { let top = H; for (const c of circles) { if (Math.abs(c.vy) < 0.8 && c.y > H * 0.25) top = Math.min(top, c.y - c.r); } return top; } void settledTop; function drawPlain(c) { ctx.save(); ctx.globalAlpha = c.opacity; if (c.glow > 0.01) { const g = ctx.createRadialGradient(c.x, c.y, c.r * 0.5, c.x, c.y, c.r * 1.7); g.addColorStop(0, c.color + Math.round(c.glow * 90).toString(16).padStart(2, "0")); g.addColorStop(1, c.color + "00"); ctx.beginPath(); ctx.arc(c.x, c.y, c.r * 1.7, 0, 6.2832); ctx.fillStyle = g; ctx.fill(); } ctx.beginPath(); ctx.arc(c.x, c.y, c.r, 0, 6.2832); ctx.fillStyle = c.color; ctx.fill(); ctx.restore(); } function drawSticker(c) { ctx.save(); ctx.globalAlpha = c.opacity; // corpo preto ctx.beginPath(); ctx.arc(c.x, c.y, c.r, 0, 6.2832); ctx.fillStyle = "#0a0a0b"; ctx.fill(); // anel degradê (brand) ctx.lineWidth = Math.max(2.5, c.r * 0.1); if (ctx.createConicGradient) { const ring = ctx.createConicGradient(0, c.x, c.y); ring.addColorStop(0.00, "#FECB39"); ring.addColorStop(0.33, "#D0A4CC"); ring.addColorStop(0.66, "#2864AE"); ring.addColorStop(0.85, "#74C9DE"); ring.addColorStop(1.00, "#FECB39"); ctx.strokeStyle = ring; } else { ctx.strokeStyle = "#74C9DE"; } ctx.beginPath(); ctx.arc(c.x, c.y, c.r - ctx.lineWidth / 2, 0, 6.2832); ctx.stroke(); // logo dentro (clip) if (logoReady) { ctx.save(); ctx.beginPath(); ctx.arc(c.x, c.y, c.r * 0.82, 0, 6.2832); ctx.clip(); const box = c.r * 1.3, ar = logo.naturalWidth / logo.naturalHeight; let w = box, h = box / ar; if (h > box) { h = box; w = box * ar; } ctx.drawImage(logo, c.x - w / 2, c.y - h / 2, w, h); ctx.restore(); } ctx.restore(); } function drawRing() { if (!mouse.active) return; ctx.save(); ctx.beginPath(); ctx.arc(mouse.x, mouse.y, reveal, 0, 6.2832); ctx.strokeStyle = "rgba(255,255,255,0.08)"; ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); } function draw() { ctx.clearRect(0, 0, W, H); for (const c of circles) c.isSticker ? drawSticker(c) : drawPlain(c); drawRing(); } function loop(t) { if (!running) return; if (circles.length < targetCount() && t - lastSpawn > SPAWN_INTERVAL) { circles.push(makeCircle()); spawned++; lastSpawn = t; } step(); draw(); canvas.setAttribute("data-n", circles.length); raf = requestAnimationFrame(loop); } window.__heroD = { get circles() { return circles; }, dims: () => ({ W, H }), mouse }; // ---- events ---- function onMove(e) { const r = section.getBoundingClientRect(); const pt = e.touches ? e.touches[0] : e; mouse.x = pt.clientX - r.left; mouse.y = pt.clientY - r.top; mouse.active = true; } function onLeave() { mouse.active = false; mouse.x = mouse.y = -9999; } function onResize() { resize(); for (const c of circles) { c.x = Math.max(c.r, Math.min(W - c.r, c.x)); if (c.y > H - c.r) c.y = H - c.r; } } resize(); section.addEventListener("mousemove", onMove); section.addEventListener("mouseleave", onLeave); section.addEventListener("touchmove", onMove, { passive: true }); section.addEventListener("touchend", onLeave); window.addEventListener("resize", onResize); if (reduced) { // sem animação: preenche estaticamente const N = targetCount(); for (let i = 0; i < N; i++) { const c = makeCircle(); c.y = H - c.r - Math.random() * H * fill; c.opacity = 1; circles.push(c); } let tries = 0; const settle = () => { step(); tries++; if (tries < 60) requestAnimationFrame(settle); else draw(); }; logo.complete ? settle() : (logo.onload = () => { logoReady = true; settle(); }); } else { raf = requestAnimationFrame(loop); } return () => { running = false; if (raf) cancelAnimationFrame(raf); section.removeEventListener("mousemove", onMove); section.removeEventListener("mouseleave", onLeave); section.removeEventListener("touchmove", onMove); section.removeEventListener("touchend", onLeave); window.removeEventListener("resize", onResize); }; }, [fill, density, reveal]); return (
cheer design
Estúdio de branding e estratégia

Somos o parceiro estratégico de negócios que entendem que marca não é só estética

Posicionamos fundadores, experts e empresas como a escolha óbvia para o cliente certo.

Mova o cursor para revelar o logo
); } window.HeroD = HeroD;