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).
159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
"use client";
|
|
import type React from "react";
|
|
import { useEffect, useRef } from "react";
|
|
|
|
type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern;
|
|
|
|
interface GridOffset {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface SquaresProps {
|
|
direction?: "diagonal" | "up" | "right" | "down" | "left";
|
|
speed?: number;
|
|
borderColor?: CanvasStrokeStyle;
|
|
squareSize?: number;
|
|
hoverFillColor?: CanvasStrokeStyle;
|
|
}
|
|
|
|
const Squares: React.FC<SquaresProps> = ({
|
|
direction = "right",
|
|
speed = 1,
|
|
borderColor = "#999",
|
|
squareSize = 40,
|
|
hoverFillColor = "#222",
|
|
}) => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const requestRef = useRef<number | null>(null);
|
|
const numSquaresX = useRef<number>(0);
|
|
const numSquaresY = useRef<number>(0);
|
|
const gridOffset = useRef<GridOffset>({ x: 0, y: 0 });
|
|
const hoveredSquareRef = useRef<GridOffset | null>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
const resizeCanvas = () => {
|
|
canvas.width = canvas.offsetWidth;
|
|
canvas.height = canvas.offsetHeight;
|
|
numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;
|
|
numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;
|
|
};
|
|
|
|
window.addEventListener("resize", resizeCanvas);
|
|
resizeCanvas();
|
|
|
|
const drawGrid = () => {
|
|
if (!ctx) return;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
|
|
const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
|
|
|
|
for (let x = startX; x < canvas.width + squareSize; x += squareSize) {
|
|
for (let y = startY; y < canvas.height + squareSize; y += squareSize) {
|
|
const squareX = x - (gridOffset.current.x % squareSize);
|
|
const squareY = y - (gridOffset.current.y % squareSize);
|
|
|
|
if (
|
|
hoveredSquareRef.current &&
|
|
hoveredSquareRef.current.x >= squareX &&
|
|
hoveredSquareRef.current.x < squareX + squareSize &&
|
|
hoveredSquareRef.current.y >= squareY &&
|
|
hoveredSquareRef.current.y < squareY + squareSize
|
|
) {
|
|
ctx.fillStyle = hoverFillColor;
|
|
ctx.fillRect(squareX, squareY, squareSize, squareSize);
|
|
}
|
|
|
|
ctx.strokeStyle = borderColor;
|
|
ctx.strokeRect(squareX, squareY, squareSize, squareSize);
|
|
}
|
|
}
|
|
|
|
const gradient = ctx.createRadialGradient(
|
|
canvas.width / 2,
|
|
canvas.height / 2,
|
|
0,
|
|
canvas.width / 2,
|
|
canvas.height / 2,
|
|
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2,
|
|
);
|
|
gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
|
|
gradient.addColorStop(1, "#060010");
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
};
|
|
|
|
const updateAnimation = () => {
|
|
const effectiveSpeed = Math.max(speed, 0.1);
|
|
switch (direction) {
|
|
case "right":
|
|
gridOffset.current.x =
|
|
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
|
|
break;
|
|
case "left":
|
|
gridOffset.current.x =
|
|
(gridOffset.current.x + effectiveSpeed + squareSize) % squareSize;
|
|
break;
|
|
case "up":
|
|
gridOffset.current.y =
|
|
(gridOffset.current.y + effectiveSpeed + squareSize) % squareSize;
|
|
break;
|
|
case "down":
|
|
gridOffset.current.y =
|
|
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
|
|
break;
|
|
case "diagonal":
|
|
gridOffset.current.x =
|
|
(gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
|
|
gridOffset.current.y =
|
|
(gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
drawGrid();
|
|
requestRef.current = requestAnimationFrame(updateAnimation);
|
|
};
|
|
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
if (!canvas) return;
|
|
const rect = canvas.getBoundingClientRect();
|
|
const mouseX = event.clientX - rect.left;
|
|
const mouseY = event.clientY - rect.top;
|
|
hoveredSquareRef.current = { x: mouseX, y: mouseY };
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
hoveredSquareRef.current = null;
|
|
};
|
|
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseleave", handleMouseLeave); // Use document for leaving window
|
|
requestRef.current = requestAnimationFrame(updateAnimation);
|
|
|
|
return () => {
|
|
window.removeEventListener("resize", resizeCanvas);
|
|
if (requestRef.current) cancelAnimationFrame(requestRef.current);
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
};
|
|
}, [direction, speed, borderColor, hoverFillColor, squareSize]);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-full border-none block max-w-full"
|
|
></canvas>
|
|
);
|
|
};
|
|
|
|
export default Squares;
|