optimize view persistence & header

This commit is contained in:
Timothy Pidashev
2025-01-08 17:26:05 -08:00
parent 5f06079b5b
commit b2455cb1e2
4 changed files with 146 additions and 104 deletions

View File

@@ -44,7 +44,7 @@ const Background: React.FC<BackgroundProps> = ({
const gridRef = useRef<Grid>(); const gridRef = useRef<Grid>();
const animationFrameRef = useRef<number>(); const animationFrameRef = useRef<number>();
const frameCount = useRef(0); const frameCount = useRef(0);
const isInitialized = useRef(false); const resizeTimeoutRef = useRef<NodeJS.Timeout>();
const randomColor = (): [number, number, number] => { const randomColor = (): [number, number, number] => {
const colors = [ const colors = [
@@ -90,6 +90,7 @@ const Background: React.FC<BackgroundProps> = ({
const grid = { cells, cols, rows, offsetX, offsetY }; const grid = { cells, cols, rows, offsetX, offsetY };
computeNextState(grid); computeNextState(grid);
// Initialize cells with staggered animation
for (let i = 0; i < cols; i++) { for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) { for (let j = 0; j < rows; j++) {
const cell = cells[i][j]; const cell = cells[i][j];
@@ -202,89 +203,55 @@ const Background: React.FC<BackgroundProps> = ({
} }
}; };
const setupCanvas = (canvas: HTMLCanvasElement, width: number, height: number) => {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
return ctx;
};
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); const handleResize = () => {
// Clear the previous timeout
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
// Debounce resize event
resizeTimeoutRef.current = setTimeout(() => {
const displayWidth = layout === 'index' ? window.innerWidth : SIDEBAR_WIDTH;
const displayHeight = window.innerHeight;
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
if (!ctx) return;
// Reset animation state
frameCount.current = 0;
// Initialize new grid with new dimensions
gridRef.current = initGrid(displayWidth, displayHeight);
}, 250); // Debounce time of 250ms
};
// Initial setup
const displayWidth = layout === 'index' ? window.innerWidth : SIDEBAR_WIDTH;
const displayHeight = window.innerHeight;
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
if (!ctx) return; if (!ctx) return;
const resizeCanvas = () => { gridRef.current = initGrid(displayWidth, displayHeight);
const dpr = window.devicePixelRatio || 1;
let displayWidth: number;
let displayHeight: number;
if (layout === 'index') {
displayWidth = window.innerWidth;
displayHeight = window.innerHeight;
} else {
displayWidth = SIDEBAR_WIDTH;
displayHeight = window.innerHeight;
}
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
ctx.scale(dpr, dpr);
canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;
if (!isInitialized.current) {
gridRef.current = initGrid(displayWidth, displayHeight);
isInitialized.current = true;
} else if (gridRef.current) {
const { cols, rows, offsetX, offsetY } = calculateGridDimensions(displayWidth, displayHeight);
gridRef.current.cols = cols;
gridRef.current.rows = rows;
gridRef.current.offsetX = offsetX;
gridRef.current.offsetY = offsetY;
gridRef.current.cells = gridRef.current.cells.slice(0, cols).map(col => col.slice(0, rows));
}
};
const drawGrid = () => {
if (!ctx || !canvas || !gridRef.current) return;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const grid = gridRef.current;
const cellSize = CELL_SIZE * 0.8;
const roundness = cellSize * 0.2;
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
if (cell.opacity > 0.01) {
const [r, g, b] = cell.color;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.globalAlpha = cell.opacity * 0.8;
const scaledSize = cellSize * cell.scale;
const xOffset = (cellSize - scaledSize) / 2;
const yOffset = (cellSize - scaledSize) / 2;
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset;
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset;
const scaledRoundness = roundness * cell.scale;
ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y);
ctx.lineTo(x + scaledSize - scaledRoundness, y);
ctx.quadraticCurveTo(x + scaledSize, y, x + scaledSize, y + scaledRoundness);
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
ctx.quadraticCurveTo(x + scaledSize, y + scaledSize, x + scaledSize - scaledRoundness, y + scaledSize);
ctx.lineTo(x + scaledRoundness, y + scaledSize);
ctx.quadraticCurveTo(x, y + scaledSize, x, y + scaledSize - scaledRoundness);
ctx.lineTo(x, y + scaledRoundness);
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
ctx.fill();
}
}
}
ctx.globalAlpha = 1;
};
const animate = () => { const animate = () => {
frameCount.current++; frameCount.current++;
@@ -297,19 +264,63 @@ const Background: React.FC<BackgroundProps> = ({
updateCellAnimations(gridRef.current); updateCellAnimations(gridRef.current);
} }
drawGrid(); // Draw frame
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (gridRef.current) {
const grid = gridRef.current;
const cellSize = CELL_SIZE * 0.8;
const roundness = cellSize * 0.2;
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
if (cell.opacity > 0.01) {
const [r, g, b] = cell.color;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.globalAlpha = cell.opacity * 0.8;
const scaledSize = cellSize * cell.scale;
const xOffset = (cellSize - scaledSize) / 2;
const yOffset = (cellSize - scaledSize) / 2;
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset;
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset;
const scaledRoundness = roundness * cell.scale;
ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y);
ctx.lineTo(x + scaledSize - scaledRoundness, y);
ctx.quadraticCurveTo(x + scaledSize, y, x + scaledSize, y + scaledRoundness);
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
ctx.quadraticCurveTo(x + scaledSize, y + scaledSize, x + scaledSize - scaledRoundness, y + scaledSize);
ctx.lineTo(x + scaledRoundness, y + scaledSize);
ctx.quadraticCurveTo(x, y + scaledSize, x, y + scaledSize - scaledRoundness);
ctx.lineTo(x, y + scaledRoundness);
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
ctx.fill();
}
}
}
ctx.globalAlpha = 1;
}
animationFrameRef.current = requestAnimationFrame(animate); animationFrameRef.current = requestAnimationFrame(animate);
}; };
window.addEventListener('resize', resizeCanvas); window.addEventListener('resize', handleResize);
resizeCanvas();
animate(); animate();
return () => { return () => {
window.removeEventListener('resize', resizeCanvas); window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animationFrameRef.current);
} }
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
}; };
}, [layout]); }, [layout]);
@@ -318,7 +329,7 @@ const Background: React.FC<BackgroundProps> = ({
return 'fixed inset-0 -z-10'; return 'fixed inset-0 -z-10';
} }
const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10'; const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10 pointer-events-none';
return position === 'left' return position === 'left'
? `${baseClasses} left-0` ? `${baseClasses} left-0`
: `${baseClasses} right-0`; : `${baseClasses} right-0`;

View File

@@ -35,7 +35,6 @@ export default function Header() {
}; };
const isIndexPage = checkIsActive("/"); const isIndexPage = checkIsActive("/");
const headerLinks = Links.map((link) => { const headerLinks = Links.map((link) => {
const isActive = checkIsActive(link.href); const isActive = checkIsActive(link.href);
@@ -45,7 +44,7 @@ export default function Header() {
className={` className={`
relative inline-block relative inline-block
${link.color} ${link.color}
${!isIndexPage ? 'bg-black rounded' : ''} ${!isIndexPage ? 'bg-black' : ''}
`} `}
> >
<a <a
@@ -91,12 +90,14 @@ export default function Header() {
${visible ? "translate-y-0" : "-translate-y-full"} ${visible ? "translate-y-0" : "-translate-y-full"}
`} `}
> >
<div className="flex flex-row items-center justify-center h-full"> <div className={`
w-full flex flex-row items-center justify-center
${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
`}>
<div className={` <div className={`
flex flex-row pt-1 px-2 text-lg lg:pt-2 lg:text-3xl md:text-2xl w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl
items-center justify-between md:justify-center items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2
space-x-2 md:space-x-10 lg:space-x-20 ${!isIndexPage ? 'bg-black md:px-20' : ''}
${!isIndexPage ? 'bg-black' : ''}
`}> `}>
{headerLinks} {headerLinks}
</div> </div>

View File

@@ -1,5 +1,6 @@
--- ---
import "@/style/globals.css"; import "@/style/globals.css";
import { ClientRouter } from "astro:transitions";
import Header from "@/components/header"; import Header from "@/components/header";
import Footer from "@/components/footer"; import Footer from "@/components/footer";
import Background from "@/components/background"; import Background from "@/components/background";
@@ -13,22 +14,48 @@ export interface Props {
const { title, description, permalink, current } = Astro.props; const { title, description, permalink, current } = Astro.props;
--- ---
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title> <title>{title}</title>
</head> <ClientRouter
defaultTransition={false}
handleFocus={false}
/>
<style>
::view-transition-new(:root) {
animation: none;
}
::view-transition-old(:root) {
animation: 90ms ease-out both fade-out;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
</head>
<body class="bg-background text-foreground"> <body class="bg-background text-foreground">
<Header client:load /> <Header client:load />
<main> <main>
<div class="max-w-5xl mx-auto pt-12 px-4 py-8"> <div class="max-w-5xl mx-auto pt-12 px-4 py-8">
<Background layout="content" position="right" client:only="react" /> <Background layout="content" position="right" client:only="react" transition:persist />
<slot /> <slot />
<Background layout="content" position="left" client:only="react" /> <Background layout="content" position="left" client:only="react" transition:persist />
</div> </div>
</main> </main>
<Footer client:load /> <Footer client:load transition:persist />
</body>
<script>
document.addEventListener("astro:after-navigation", () => {
window.scrollTo(0, 0);
});
</script>
</body>
</html> </html>

View File

@@ -3,6 +3,8 @@ const { content } = Astro.props;
import "@/style/globals.css"; import "@/style/globals.css";
import { ClientRouter } from "astro:transitions";
import Header from "@/components/header"; import Header from "@/components/header";
import Footer from "@/components/footer"; import Footer from "@/components/footer";
import Background from "@/components/background"; import Background from "@/components/background";
@@ -14,14 +16,15 @@ import Background from "@/components/background";
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<title>{content.title}</title> <title>{content.title}</title>
<ClientRouter />
</head> </head>
<body class="bg-background text-foreground"> <body class="bg-background text-foreground">
<Header client:load /> <Header client:load />
<main> <main transition:animate="fade">
<Background layout="index" client:only="react" /> <Background layout="index" client:only="react" />
<slot /> <slot />
</main> </main>
<Footer client:load fixed=true /> <Footer client:load transition:persist fixed=true />
</body> </body>
</html> </html>