feat: control room hero, SEO/AEO optimization and design system fixes

Hero: replace particle field with cobe globe (Capricorn view, multi-axis drag, reduced-motion safe), telemetry HUD with pointer parallax, particle text build effect, Space Grotesk display font. SEO: short titles with template, PT-BR accents, twitter cards on dynamic cases, escaped JSON-LD via lib/jsonld.ts, CSP report-only, 25 service FAQs answer-first. Design: fix TW4 layered reset killing spacing utilities, literal @theme tokens, mobile hero overflow, header overlap, empty case cards. Remove framer-motion (unified on motion/react), header version badge, phone number from hero CTA. Production build verified (24 static pages).
This commit is contained in:
Evandro Fonseca Junior committed 2026-08-27 10:55:21 -03:00
1 parent 0920370f30
commit b791b7de9e
94 files changed
+2621 -2889

No files matched your search

@@ -0,0 +1,397 @@
"use client";
import { useEffect, useRef } from "react";
const DEFAULT_TEXT =
"Engenharia de software para sistemas que precisam operar de verdade.";
const MAX_DPR = 1.5;
const ASSEMBLE_MS = 2000;
const REPEL_RADIUS_PX = 80;
const MIN_PARTICLES = 1200;
const MAX_PARTICLES = 2500;
const OFFSCREEN_POINTER = -100000;
type Particle = {
sx: number;
sy: number;
tx: number;
ty: number;
ox: number;
oy: number;
delay: number;
color: string;
size: number;
phase: number;
speed: number;
amp: number;
};
type TrackingCapableContext = CanvasRenderingContext2D & {
letterSpacing?: string;
};
type Rgb = [number, number, number];
const FALLBACK_SLASH: Rgb = [204, 255, 0];
const FALLBACK_PAPER: Rgb = [244, 242, 237];
function hexToRgb(hex: string): Rgb | null {
const value = hex.trim().replace("#", "");
const full =
value.length === 3
? value
.split("")
.map((char) => char + char)
.join("")
: value;
if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;
return [
Number.parseInt(full.slice(0, 2), 16),
Number.parseInt(full.slice(2, 4), 16),
Number.parseInt(full.slice(4, 6), 16),
];
}
function easeOutQuint(progress: number) {
return 1 - (1 - progress) ** 5;
}
function wrapLines(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
) {
const lines: string[] = [];
let current = "";
for (const word of text.split(" ")) {
const candidate = current ? `${current} ${word}` : word;
if (!current || ctx.measureText(candidate).width <= maxWidth) {
current = candidate;
} else {
lines.push(current);
current = word;
}
}
if (current) lines.push(current);
return lines;
}
export function ParticleBuildText({ text = DEFAULT_TEXT }: { text?: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const container = containerRef.current;
const canvas = canvasRef.current;
if (!container || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
let rafId = 0;
let running = false;
let cancelled = false;
let resampleTimer = 0;
let particles: Particle[] = [];
let hasAssembled = false;
let elapsed = 0;
let lastTime = 0;
let dpr = 1;
let intersecting = true;
const pointer = { x: OFFSCREEN_POINTER, y: OFFSCREEN_POINTER };
let rect = container.getBoundingClientRect();
const rootStyles = getComputedStyle(document.documentElement);
const slashRgb =
hexToRgb(rootStyles.getPropertyValue("--slash")) ?? FALLBACK_SLASH;
const paperRgb =
hexToRgb(rootStyles.getPropertyValue("--paper")) ?? FALLBACK_PAPER;
// next/font gera nome de família com hash: resolver pela classe font-display real
const probe = document.createElement("span");
probe.className = "font-display";
probe.style.position = "absolute";
probe.style.opacity = "0";
document.body.appendChild(probe);
const fontFamily = getComputedStyle(probe).fontFamily || "sans-serif";
document.body.removeChild(probe);
const fontsReady =
typeof document.fonts === "undefined"
? Promise.resolve()
: document.fonts.ready;
const sample = () => {
const width = container.clientWidth;
const height = container.clientHeight;
if (width < 60 || height < 60) return;
dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
rect = container.getBoundingClientRect();
const offscreen = document.createElement("canvas");
offscreen.width = canvas.width;
offscreen.height = canvas.height;
const offCtx = offscreen.getContext("2d");
if (!offCtx) return;
// Backing store é device-pixel: escala p/ que coordenadas CSS preencham o painel inteiro
offCtx.scale(dpr, dpr);
const maxWidth = width * 0.92;
let fontSize = Math.min(height * 0.22, width / 7);
let lines: string[] = [];
while (fontSize >= 12) {
offCtx.font = `800 ${fontSize}px ${fontFamily}`;
const trackingCtx = offCtx as TrackingCapableContext;
if (trackingCtx.letterSpacing !== undefined) {
trackingCtx.letterSpacing = "-0.04em";
}
lines = wrapLines(offCtx, text, maxWidth);
if (lines.length * fontSize * 0.98 <= height * 0.8) break;
fontSize *= 0.94;
}
fontSize = Math.max(fontSize, 12);
const lineHeight = fontSize * 0.98;
const startY = (height - lines.length * lineHeight) / 2 + fontSize * 0.74;
offCtx.fillStyle = "#ffffff";
offCtx.textBaseline = "alphabetic";
lines.forEach((line, index) => {
const lineWidth = offCtx.measureText(line).width;
offCtx.fillText(
line,
(width - lineWidth) / 2,
startY + index * lineHeight,
);
});
const pixels = offCtx.getImageData(
0,
0,
offscreen.width,
offscreen.height,
).data;
const targets: Array<[number, number]> = [];
const stride = 3;
for (let py = 0; py < offscreen.height; py += stride) {
for (let px = 0; px < offscreen.width; px += stride) {
if (pixels[(py * offscreen.width + px) * 4 + 3] > 128) {
targets.push([px, py]);
}
}
}
if (targets.length === 0) return;
const desired = Math.round(
Math.min(
MAX_PARTICLES,
Math.max(MIN_PARTICLES, (width * height) / 180),
),
);
particles = Array.from({ length: desired }, () => {
const target = targets[Math.floor(Math.random() * targets.length)];
const useSlash = Math.random() < 0.72;
const rgb = useSlash ? slashRgb : paperRgb;
const alpha = useSlash
? 0.28 + Math.random() * 0.22
: 0.25 + Math.random() * 0.15;
return {
tx: target[0],
ty: target[1],
sx: hasAssembled
? target[0]
: (Math.random() * 1.3 - 0.15) * canvas.width,
sy: hasAssembled
? target[1]
: (Math.random() * 1.3 - 0.15) * canvas.height,
ox: 0,
oy: 0,
delay: Math.random() * 0.35,
color: `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha.toFixed(2)})`,
size: (Math.random() < 0.85 ? 1 : 2) * dpr,
phase: Math.random() * Math.PI * 2,
speed: 0.0005 + Math.random() * 0.0007,
amp: (0.6 + Math.random()) * dpr,
};
});
};
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const pointerX = (pointer.x - rect.left) * dpr;
const pointerY = (pointer.y - rect.top) * dpr;
const radius = REPEL_RADIUS_PX * dpr;
const radiusSq = radius * radius;
let allSettled = true;
for (const particle of particles) {
const progress = Math.min(
Math.max(
(elapsed - particle.delay * ASSEMBLE_MS) / (ASSEMBLE_MS * 0.75),
0,
),
1,
);
if (progress < 1) allSettled = false;
const eased = easeOutQuint(progress);
const baseX = particle.sx + (particle.tx - particle.sx) * eased;
const baseY = particle.sy + (particle.ty - particle.sy) * eased;
const driftX =
Math.sin(elapsed * particle.speed + particle.phase) *
particle.amp *
progress;
const driftY =
Math.cos(elapsed * particle.speed * 0.9 + particle.phase) *
particle.amp *
progress;
const dx = baseX + driftX - pointerX;
const dy = baseY + driftY - pointerY;
const distSq = dx * dx + dy * dy;
if (distSq < radiusSq) {
const dist = Math.sqrt(distSq) || 1;
const force = (1 - dist / radius) * 10 * dpr;
particle.ox += (dx / dist) * force;
particle.oy += (dy / dist) * force;
}
particle.ox *= 0.86;
particle.oy *= 0.86;
ctx.fillStyle = particle.color;
ctx.fillRect(
baseX + driftX + particle.ox,
baseY + driftY + particle.oy,
particle.size,
particle.size,
);
}
if (allSettled && particles.length > 0) hasAssembled = true;
};
const drawStaticFrame = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const particle of particles) {
ctx.fillStyle = particle.color;
ctx.fillRect(particle.tx, particle.ty, particle.size, particle.size);
}
};
const tick = (now: number) => {
elapsed += Math.min(now - lastTime, 50);
lastTime = now;
render();
};
const syncLoop = () => {
const shouldRun = !reducedMotion && intersecting && !document.hidden;
if (shouldRun && !running) {
running = true;
lastTime = performance.now();
rafId = window.requestAnimationFrame(tick);
} else if (!shouldRun && running) {
running = false;
window.cancelAnimationFrame(rafId);
}
};
const scheduleResample = () => {
window.clearTimeout(resampleTimer);
resampleTimer = window.setTimeout(() => {
sample();
if (reducedMotion) drawStaticFrame();
}, 150);
};
const handlePointerMove = (event: PointerEvent) => {
pointer.x = event.clientX;
pointer.y = event.clientY;
};
const handlePointerLeave = () => {
pointer.x = OFFSCREEN_POINTER;
pointer.y = OFFSCREEN_POINTER;
};
const handleScroll = () => {
rect = container.getBoundingClientRect();
};
const handleVisibilityChange = () => {
syncLoop();
};
const resizeObserver = new ResizeObserver(scheduleResample);
resizeObserver.observe(container);
if (reducedMotion) {
// Variante estática: quadro final montado, sem rAF e sem listeners de ponteiro
fontsReady.then(() => {
if (cancelled) return;
hasAssembled = true;
sample();
drawStaticFrame();
});
return () => {
cancelled = true;
window.clearTimeout(resampleTimer);
resizeObserver.disconnect();
};
}
const intersectionObserver = new IntersectionObserver((entries) => {
intersecting = entries.some((entry) => entry.isIntersecting);
syncLoop();
});
intersectionObserver.observe(container);
window.addEventListener("pointermove", handlePointerMove, {
passive: true,
});
document.documentElement.addEventListener(
"pointerleave",
handlePointerLeave,
);
window.addEventListener("scroll", handleScroll, { passive: true });
document.addEventListener("visibilitychange", handleVisibilityChange);
fontsReady.then(() => {
if (cancelled) return;
sample();
syncLoop();
});
return () => {
cancelled = true;
window.cancelAnimationFrame(rafId);
window.clearTimeout(resampleTimer);
resizeObserver.disconnect();
intersectionObserver.disconnect();
window.removeEventListener("pointermove", handlePointerMove);
document.documentElement.removeEventListener(
"pointerleave",
handlePointerLeave,
);
window.removeEventListener("scroll", handleScroll);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [text]);
return (
<div
ref={containerRef}
aria-hidden="true"
className="pointer-events-none absolute inset-0"
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
);
}