continue re-design
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
"use client";
|
||||
|
||||
import { animate } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface AnimatedDotIconProps {
|
||||
active?: boolean;
|
||||
alwaysHeat?: boolean;
|
||||
triggerOnHover?: boolean;
|
||||
size?: number;
|
||||
className?: string;
|
||||
pattern?:
|
||||
| "usage"
|
||||
| "api-keys"
|
||||
| "settings"
|
||||
| "overview"
|
||||
| "team"
|
||||
| "billing"
|
||||
| "account-settings"
|
||||
| "admin"
|
||||
| "domain-checker"
|
||||
| "extract-playground"
|
||||
| "extract"
|
||||
| "logs"
|
||||
| "playground"
|
||||
| "teams";
|
||||
}
|
||||
|
||||
const initCanvas = (canvas: HTMLCanvasElement) => {
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
const upscaleCanvas = () => {
|
||||
const scale = window.visualViewport?.scale || 1;
|
||||
const dpr = (window.devicePixelRatio || 1) * scale;
|
||||
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
canvas.dispatchEvent(new Event("resize"));
|
||||
};
|
||||
|
||||
upscaleCanvas();
|
||||
|
||||
const handleResize = () => {
|
||||
setTimeout(upscaleCanvas, 500);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.visualViewport?.addEventListener("resize", handleResize);
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// Pattern definitions for different pages
|
||||
const patterns = {
|
||||
usage: {
|
||||
grid: [
|
||||
[10, 11, 12, 14, 15, 16],
|
||||
[3, 7, 19, 23],
|
||||
[0, 2, 24, 26],
|
||||
[27, 28, 29, 31, 32, 33],
|
||||
],
|
||||
gridSize: 7,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"api-keys": {
|
||||
grid: [[12], [10, 14], [8, 16], [6, 18], [4, 5, 19, 20]],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
settings: {
|
||||
grid: [
|
||||
[0, 1, 2, 3, 4],
|
||||
[5, 9],
|
||||
[10, 14],
|
||||
[15, 19],
|
||||
[20, 21, 22, 23, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
overview: {
|
||||
grid: [
|
||||
[24],
|
||||
[16, 18, 30, 32],
|
||||
[8, 12, 36, 40],
|
||||
[0, 3, 6, 21, 27, 42, 45, 48],
|
||||
],
|
||||
gridSize: 7,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
team: {
|
||||
grid: [
|
||||
[6, 7, 8],
|
||||
[11, 12, 13],
|
||||
[16, 17, 18],
|
||||
[0, 4, 20, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
teams: {
|
||||
grid: [
|
||||
[6, 7, 8],
|
||||
[11, 12, 13],
|
||||
[16, 17, 18],
|
||||
[0, 4, 20, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
billing: {
|
||||
grid: [
|
||||
[0, 4],
|
||||
[5, 6, 8, 9],
|
||||
[10, 11, 13, 14],
|
||||
[15, 19],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"account-settings": {
|
||||
grid: [
|
||||
[2, 7, 12, 17, 22],
|
||||
[5, 10, 15, 20],
|
||||
[8, 13, 18],
|
||||
[11, 16],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
admin: {
|
||||
grid: [
|
||||
[0, 1, 2, 3, 4],
|
||||
[5, 14],
|
||||
[10, 11, 12, 13],
|
||||
[15, 24],
|
||||
[20, 21, 22, 23, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"domain-checker": {
|
||||
grid: [
|
||||
[12, 13, 14],
|
||||
[7, 11, 15, 19],
|
||||
[2, 6, 20, 24],
|
||||
[0, 1, 25, 26],
|
||||
],
|
||||
gridSize: 6,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
"extract-playground": {
|
||||
grid: [
|
||||
[5, 10, 15, 20],
|
||||
[6, 11, 16, 21],
|
||||
[7, 12, 17, 22],
|
||||
[8, 13, 18, 23],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
extract: {
|
||||
grid: [[12], [7, 17], [2, 6, 18, 22], [0, 1, 3, 4, 20, 21, 23, 24]],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
logs: {
|
||||
grid: [
|
||||
[0, 5, 10, 15, 20],
|
||||
[1, 6, 11, 16, 21],
|
||||
[2, 7, 12, 17, 22],
|
||||
[3, 8, 13, 18, 23],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
playground: {
|
||||
grid: [
|
||||
[6, 8, 16, 18],
|
||||
[10, 11, 12, 13, 14],
|
||||
[5, 9, 15, 19],
|
||||
[0, 4, 20, 24],
|
||||
],
|
||||
gridSize: 5,
|
||||
cellSize: 2,
|
||||
spacing: 2,
|
||||
offset: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export function AnimatedDotIcon({
|
||||
active = true,
|
||||
alwaysHeat = false,
|
||||
triggerOnHover = false,
|
||||
size = 20,
|
||||
className,
|
||||
pattern = "usage",
|
||||
}: AnimatedDotIconProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const fnRefs = useRef<{
|
||||
activate: () => void;
|
||||
deactivate: () => void;
|
||||
}>({ activate: () => {}, deactivate: () => {} });
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = initCanvas(canvas);
|
||||
const config = patterns[pattern];
|
||||
|
||||
let isRunning = false;
|
||||
let isActive = false;
|
||||
|
||||
let activeGroup = 0;
|
||||
const rowAlphas = [0.2, 0.4, 1, 0.04];
|
||||
|
||||
const scaler = size / 20;
|
||||
|
||||
const render = () => {
|
||||
ctx.fillStyle = "#fa5d19";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (const group of config.grid.slice(0, 4)) {
|
||||
const groupIndex = config.grid.indexOf(group);
|
||||
ctx.globalAlpha = rowAlphas[groupIndex];
|
||||
|
||||
for (const index of group) {
|
||||
ctx.fillRect(
|
||||
(config.offset + (index % config.gridSize) * config.spacing) *
|
||||
scaler,
|
||||
(config.offset +
|
||||
Math.floor(index / config.gridSize) * config.spacing) *
|
||||
scaler,
|
||||
config.cellSize * scaler,
|
||||
config.cellSize * scaler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: number[] = [];
|
||||
let runCount = 0;
|
||||
|
||||
const cycle = () => {
|
||||
isRunning = true;
|
||||
activeGroup = (activeGroup + 1) % 5;
|
||||
|
||||
rowAlphas.forEach((alpha, index) => {
|
||||
let targetAlpha = alpha;
|
||||
|
||||
if (index === activeGroup) targetAlpha = 1;
|
||||
else if (index === (activeGroup + 1) % 4) targetAlpha = 0.12;
|
||||
else if (index === (activeGroup + 2) % 4) targetAlpha = 0.2;
|
||||
else if (index === (activeGroup + 3) % 4) targetAlpha = 0.4;
|
||||
|
||||
animate(alpha, targetAlpha, {
|
||||
duration: 0.05,
|
||||
onUpdate: (value) => {
|
||||
rowAlphas[index] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
timeouts.forEach((timeout) => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
isRunning = false;
|
||||
}, 300),
|
||||
);
|
||||
|
||||
if (activeGroup === 3) runCount += 1;
|
||||
|
||||
if ((runCount === 2 || !isActive) && activeGroup === 2) return;
|
||||
|
||||
timeouts.push(
|
||||
window.setTimeout(() => {
|
||||
cycle();
|
||||
}, 50),
|
||||
);
|
||||
};
|
||||
|
||||
fnRefs.current = {
|
||||
activate: () => {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
runCount = 0;
|
||||
cycle();
|
||||
render();
|
||||
},
|
||||
deactivate: () => {
|
||||
if (!isActive) return;
|
||||
isActive = false;
|
||||
},
|
||||
};
|
||||
|
||||
render();
|
||||
canvas.addEventListener("resize", render);
|
||||
|
||||
if (triggerOnHover) {
|
||||
const group = canvasRef.current!.closest(".group");
|
||||
|
||||
if (group) {
|
||||
group.addEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.addEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
|
||||
return () => {
|
||||
group.removeEventListener("mouseenter", fnRefs.current.activate);
|
||||
group.removeEventListener("mouseleave", fnRefs.current.deactivate);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [triggerOnHover, size, pattern]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerOnHover) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && active) {
|
||||
fnRefs.current.activate();
|
||||
} else {
|
||||
fnRefs.current.deactivate();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
observer.observe(canvasRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [active, triggerOnHover]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cn(
|
||||
alwaysHeat
|
||||
? ""
|
||||
: [
|
||||
"[&.grayscale]:opacity-60 transition-[filter,opacity]",
|
||||
!active && "grayscale",
|
||||
],
|
||||
className,
|
||||
)}
|
||||
ref={canvasRef}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user