mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
optimize view persistence & header
This commit is contained in:
@@ -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`;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user