diff --git a/src/src/components/header/index.tsx b/src/src/components/header/index.tsx
index 76cc6a1..701dba8 100644
--- a/src/src/components/header/index.tsx
+++ b/src/src/components/header/index.tsx
@@ -1,38 +1,98 @@
-import React, { useState, useEffect } from "react";
-
+// components/header/index.tsx
+import React, { useState, useEffect, useRef } from "react";
import { Links } from "@/components/header/links";
export default function Header() {
+ const [isClient, setIsClient] = useState(false);
const [visible, setVisible] = useState(true);
const [lastScrollY, setLastScrollY] = useState(0);
+ const [currentPath, setCurrentPath] = useState("");
+ const [shouldAnimate, setShouldAnimate] = useState(false);
+
+ // Handle client-side initialization
+ useEffect(() => {
+ setIsClient(true);
+ setCurrentPath(document.location.pathname);
+ // Trigger initial animation after a brief delay
+ setTimeout(() => setShouldAnimate(true), 50);
+ }, []);
useEffect(() => {
+ if (!isClient) return;
+
const handleScroll = () => {
- const currentScrollY = window.scrollY;
- setVisible(currentScrollY < lastScrollY || currentScrollY < 10);
- setLastScrollY(currentScrollY);
+ const currentScrollY = document.documentElement.scrollTop;
+ setVisible(currentScrollY < lastScrollY || currentScrollY < 10);
+ setLastScrollY(currentScrollY);
+ };
+
+ document.addEventListener("scroll", handleScroll);
+ return () => document.removeEventListener("scroll", handleScroll);
+ }, [lastScrollY, isClient]);
+
+ const checkIsActive = (linkHref: string): boolean => {
+ if (!isClient) return false;
+
+ const path = document.location.pathname;
+ if (linkHref === "/") return path === "/";
+
+ return linkHref !== "/" && path.startsWith(linkHref);
};
- window.addEventListener("scroll", handleScroll);
- return () => window.removeEventListener("scroll", handleScroll);
- }, [lastScrollY]);
-
- const headerLinks = Links.map((link) => (
-
- ));
+ const headerLinks = Links.map((link) => {
+ const isActive = checkIsActive(link.href);
+
+ return (
+
+ );
+ });
return (
-
-
+
);
-};
+}
diff --git a/src/src/components/hero/background.tsx b/src/src/components/hero/background.tsx
new file mode 100644
index 0000000..7d31162
--- /dev/null
+++ b/src/src/components/hero/background.tsx
@@ -0,0 +1,328 @@
+import { useEffect, useRef } from 'react';
+
+interface Cell {
+ alive: boolean;
+ next: boolean;
+ color: [number, number, number];
+ currentX: number;
+ currentY: number;
+ targetX: number;
+ targetY: number;
+ opacity: number;
+ targetOpacity: number;
+ scale: number;
+ targetScale: number;
+ transitioning: boolean;
+ transitionComplete: boolean;
+}
+
+interface Grid {
+ cells: Cell[][];
+ cols: number;
+ rows: number;
+ offsetX: number;
+ offsetY: number;
+}
+
+const CELL_SIZE = 25;
+const TRANSITION_SPEED = 0.1;
+const SCALE_SPEED = 0.15;
+const CYCLE_FRAMES = 120;
+const INITIAL_DENSITY = 0.15;
+
+const Background: React.FC = () => {
+ const canvasRef = useRef
(null);
+ const gridRef = useRef();
+ const animationFrameRef = useRef();
+ const frameCount = useRef(0);
+ const isInitialized = useRef(false);
+
+ const randomColor = (): [number, number, number] => {
+ const colors = [
+ [204, 36, 29], // red
+ [152, 151, 26], // green
+ [215, 153, 33], // yellow
+ [69, 133, 136], // blue
+ [177, 98, 134], // purple
+ [104, 157, 106] // aqua
+ ];
+ return colors[Math.floor(Math.random() * colors.length)];
+ };
+
+ const calculateGridDimensions = (width: number, height: number) => {
+ // Calculate number of complete cells that fit in the viewport
+ const cols = Math.floor(width / CELL_SIZE);
+ const rows = Math.floor(height / CELL_SIZE);
+
+ // Calculate offsets to center the grid
+ const offsetX = Math.floor((width - (cols * CELL_SIZE)) / 2);
+ const offsetY = Math.floor((height - (rows * CELL_SIZE)) / 2);
+
+ return { cols, rows, offsetX, offsetY };
+ };
+
+ const initGrid = (width: number, height: number): Grid => {
+ const { cols, rows, offsetX, offsetY } = calculateGridDimensions(width, height);
+
+ const cells = Array(cols).fill(0).map((_, i) =>
+ Array(rows).fill(0).map((_, j) => ({
+ alive: Math.random() < INITIAL_DENSITY,
+ next: false,
+ color: randomColor(),
+ currentX: i,
+ currentY: j,
+ targetX: i,
+ targetY: j,
+ opacity: 0,
+ targetOpacity: 0,
+ scale: 0,
+ targetScale: 0,
+ transitioning: false,
+ transitionComplete: false
+ }))
+ );
+
+ const grid = { cells, cols, rows, offsetX, offsetY };
+ computeNextState(grid);
+
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < rows; j++) {
+ const cell = cells[i][j];
+ if (cell.next) {
+ cell.alive = true;
+ setTimeout(() => {
+ cell.targetOpacity = 1;
+ cell.targetScale = 1;
+ }, Math.random() * 1000);
+ } else {
+ cell.alive = false;
+ }
+ }
+ }
+
+ return grid;
+ };
+
+ const countNeighbors = (grid: Grid, x: number, y: number): { count: number, colors: [number, number, number][] } => {
+ const neighbors = { count: 0, colors: [] as [number, number, number][] };
+
+ for (let i = -1; i <= 1; i++) {
+ for (let j = -1; j <= 1; j++) {
+ if (i === 0 && j === 0) continue;
+
+ const col = (x + i + grid.cols) % grid.cols;
+ const row = (y + j + grid.rows) % grid.rows;
+
+ if (grid.cells[col][row].alive) {
+ neighbors.count++;
+ neighbors.colors.push(grid.cells[col][row].color);
+ }
+ }
+ }
+
+ return neighbors;
+ };
+
+ const averageColors = (colors: [number, number, number][]): [number, number, number] => {
+ if (colors.length === 0) return [0, 0, 0];
+ const sum = colors.reduce((acc, color) => [
+ acc[0] + color[0],
+ acc[1] + color[1],
+ acc[2] + color[2]
+ ], [0, 0, 0]);
+ return [
+ Math.round(sum[0] / colors.length),
+ Math.round(sum[1] / colors.length),
+ Math.round(sum[2] / colors.length)
+ ];
+ };
+
+ const computeNextState = (grid: Grid) => {
+ // First pass: compute next state without applying it
+ for (let i = 0; i < grid.cols; i++) {
+ for (let j = 0; j < grid.rows; j++) {
+ const cell = grid.cells[i][j];
+ const { count, colors } = countNeighbors(grid, i, j);
+
+ if (cell.alive) {
+ cell.next = count === 2 || count === 3;
+ } else {
+ cell.next = count === 3;
+ if (cell.next) {
+ cell.color = averageColors(colors);
+ }
+ }
+
+ // Mark cells that need to transition
+ if (cell.alive !== cell.next && !cell.transitioning) {
+ cell.transitioning = true;
+ cell.transitionComplete = false;
+ // For dying cells, start the shrinking animation
+ if (!cell.next) {
+ cell.targetScale = 0;
+ cell.targetOpacity = 0;
+ }
+ }
+ }
+ }
+ };
+
+ const updateCellAnimations = (grid: Grid) => {
+ for (let i = 0; i < grid.cols; i++) {
+ for (let j = 0; j < grid.rows; j++) {
+ const cell = grid.cells[i][j];
+
+ // Update animation properties
+ cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED;
+ cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED;
+
+ // Handle transition states
+ if (cell.transitioning) {
+ // Check if shrinking animation is complete for dying cells
+ if (!cell.next && cell.scale < 0.05) {
+ cell.alive = false;
+ cell.transitioning = false;
+ cell.transitionComplete = true;
+ cell.scale = 0;
+ cell.opacity = 0;
+ }
+ // Check if growing animation is complete for new cells
+ else if (cell.next && !cell.alive && !cell.transitionComplete) {
+ cell.alive = true;
+ cell.transitioning = false;
+ cell.transitionComplete = true;
+ cell.targetScale = 1;
+ cell.targetOpacity = 1;
+ }
+ // Start growing animation for new cells once old cells have shrunk
+ else if (cell.next && !cell.alive && cell.transitionComplete) {
+ cell.transitioning = true;
+ cell.targetScale = 1;
+ cell.targetOpacity = 1;
+ }
+ }
+ }
+ }
+ };
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ const resizeCanvas = () => {
+ const dpr = window.devicePixelRatio || 1;
+ const displayWidth = window.innerWidth;
+ const displayHeight = window.innerHeight;
+
+ // Set canvas size accounting for device pixel ratio
+ canvas.width = displayWidth * dpr;
+ canvas.height = displayHeight * dpr;
+
+ // Scale the context to ensure correct drawing operations
+ ctx.scale(dpr, dpr);
+
+ // Set CSS size
+ 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) {
+ // Update grid dimensions and offsets on resize
+ 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;
+
+ // Add grid offsets to center the animation
+ 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 = () => {
+ frameCount.current++;
+
+ if (gridRef.current) {
+ if (frameCount.current % CYCLE_FRAMES === 0) {
+ computeNextState(gridRef.current);
+ }
+
+ updateCellAnimations(gridRef.current);
+ }
+
+ drawGrid();
+ animationFrameRef.current = requestAnimationFrame(animate);
+ };
+
+ window.addEventListener('resize', resizeCanvas);
+ resizeCanvas();
+ animate();
+
+ return () => {
+ window.removeEventListener('resize', resizeCanvas);
+ if (animationFrameRef.current) {
+ cancelAnimationFrame(animationFrameRef.current);
+ }
+ };
+ }, []);
+
+ return (
+
+ );
+};
+
+export default Background;
diff --git a/src/src/components/vines/index.tsx b/src/src/components/vines/index.tsx
deleted file mode 100644
index e2bdb85..0000000
--- a/src/src/components/vines/index.tsx
+++ /dev/null
@@ -1,305 +0,0 @@
-import React, { useState, useEffect, useCallback } from 'react';
-
-const VineAnimation = ({ side }) => {
- const [vines, setVines] = useState([]);
- const VINE_COLOR = '#b8bb26'; // Gruvbox green
- const VINE_LIFETIME = 8000; // Time before fade starts
- const FADE_DURATION = 3000; // How long the fade takes
- const MAX_VINES = Math.max(3, Math.floor(window.innerWidth / 400)); // Adjust to screen width
- const isMobile = window.innerWidth <= 768;
-
- const getDistance = (pointA, pointB) => {
- return Math.sqrt(
- Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)
- );
- };
-
- // Function to create a new branch
- const createBranch = (startX, startY, baseRotation) => ({
- id: Date.now() + Math.random(),
- points: [{
- x: startX,
- y: startY,
- rotation: baseRotation
- }],
- leaves: [],
- growing: true,
- phase: Math.random() * Math.PI * 2,
- amplitude: Math.random() * 0.5 + 1.2
- });
-
- // Function to create a new vine
- const createNewVine = useCallback(() => ({
- id: Date.now() + Math.random(),
- mainBranch: createBranch(
- side === 'left' ? 0 : window.innerWidth,
- Math.random() * (window.innerHeight * 0.8) + (window.innerHeight * 0.1),
- side === 'left' ? 0 : Math.PI
- ),
- subBranches: [],
- growing: true,
- createdAt: Date.now(),
- fadingOut: false,
- opacity: 1
- }), [side]);
-
- // Update branch function
- const updateBranch = (branch, isSubBranch = false) => {
- if (!branch.growing) return branch;
-
- const lastPoint = branch.points[branch.points.length - 1];
- const progress = branch.points.length * 0.15;
-
- const safetyMargin = window.innerWidth * 0.2;
- const minX = safetyMargin;
- const maxX = window.innerWidth - safetyMargin;
-
- const baseAngle = side === 'left' ? 0 : Math.PI;
- let curve = Math.sin(progress + branch.phase) * branch.amplitude;
-
- const distanceFromCenter = Math.abs(window.innerWidth/2 - lastPoint.x);
- const centerRepulsion = Math.max(0, 1 - (distanceFromCenter / (window.innerWidth/4)));
- curve += (side === 'left' ? -1 : 1) * centerRepulsion * 0.5;
-
- if (side === 'left' && lastPoint.x > minX) {
- curve -= Math.pow((lastPoint.x - minX) / safetyMargin, 2);
- } else if (side === 'right' && lastPoint.x < maxX) {
- curve += Math.pow((maxX - lastPoint.x) / safetyMargin, 2);
- }
-
- const currentAngle = baseAngle + curve;
- const distance = isSubBranch ? 12 : 18;
- const newX = lastPoint.x + Math.cos(currentAngle) * distance;
- const newY = lastPoint.y + Math.sin(currentAngle) * distance;
-
- const newPoint = {
- x: newX,
- y: newY,
- rotation: currentAngle
- };
-
- let newLeaves = [...branch.leaves];
- if (Math.random() < 0.2 && branch.points.length > 2) {
- const maxLength = isSubBranch ? 15 : 30;
- const progress = branch.points.length / maxLength;
- const baseSize = isSubBranch ? 20 : 35;
- const sizeGradient = Math.pow(1 - progress, 2);
- const leafSize = Math.max(8, baseSize * sizeGradient);
-
- // Ensure leaves don't grow on the last point
- const leafPosition = Math.min(branch.points.length - 2, Math.floor(Math.random() * branch.points.length));
-
- newLeaves.push({
- position: leafPosition,
- size: leafSize,
- side: Math.random() > 0.5 ? 'left' : 'right'
- });
- }
-
- return {
- ...branch,
- points: [...branch.points, newPoint],
- leaves: newLeaves,
- growing: branch.points.length < (isSubBranch ? 15 : 30)
- };
- };
-
- // Update vine function
- const updateVine = useCallback((vine) => {
- const now = Date.now();
- const age = now - vine.createdAt;
-
- // Calculate opacity based on age
- let newOpacity = vine.opacity;
- if (age > VINE_LIFETIME) {
- const fadeProgress = (age - VINE_LIFETIME) / FADE_DURATION;
- newOpacity = Math.max(0, 1 - fadeProgress);
- }
-
- // Update main branch
- const newMainBranch = updateBranch(vine.mainBranch);
- let newSubBranches = [...vine.subBranches];
-
- // Add new branches with random probability
- if (
- !vine.fadingOut &&
- age < VINE_LIFETIME &&
- Math.random() < 0.05 &&
- newMainBranch.points.length > 4
- ) {
- // Choose a random point, excluding the last few points of any branch
- const allBranches = [newMainBranch, ...newSubBranches];
- const sourceBranch = allBranches[Math.floor(Math.random() * allBranches.length)];
-
- // Calculate the valid range for branching
- const minPoints = 4; // Minimum points needed before branching
- const reservedTipPoints = 5; // Points to reserve at the tip
- const maxBranchPoint = Math.max(
- minPoints,
- sourceBranch.points.length - reservedTipPoints
- );
-
- // Only create new branch if there's a valid spot
- if (maxBranchPoint > minPoints) {
- const branchPointIndex = Math.floor(
- Math.random() * (maxBranchPoint - minPoints) + minPoints
- );
- const branchPoint = sourceBranch.points[branchPointIndex];
-
- // Add some randomness to the branching angle
- const rotationOffset = (Math.random() * 0.8 - 0.4) +
- (Math.random() > 0.5 ? Math.PI/4 : -Math.PI/4);
-
- newSubBranches.push(
- createBranch(
- branchPoint.x,
- branchPoint.y,
- branchPoint.rotation + rotationOffset
- )
- );
- }
- }
-
- // Update existing branches
- newSubBranches = newSubBranches.map(branch => updateBranch(branch, true));
-
- return {
- ...vine,
- mainBranch: newMainBranch,
- subBranches: newSubBranches,
- growing: newMainBranch.growing || newSubBranches.some(b => b.growing),
- opacity: newOpacity,
- fadingOut: age > VINE_LIFETIME
- };
- }, [side]);
-
- // [Rest of the component code remains the same...]
-
- const renderLeaf = (point, size, leafSide, parentOpacity = 1) => {
- const sideMultiplier = leafSide === 'left' ? -1 : 1;
- const angle = point.rotation + (Math.PI / 3) * sideMultiplier;
-
- const tipX = point.x + Math.cos(angle) * size * 2;
- const tipY = point.y + Math.sin(angle) * size * 2;
-
- const ctrl1X = point.x + Math.cos(angle - Math.PI/8) * size * 1.8;
- const ctrl1Y = point.y + Math.sin(angle - Math.PI/8) * size * 1.8;
-
- const ctrl2X = point.x + Math.cos(angle + Math.PI/8) * size * 1.8;
- const ctrl2Y = point.y + Math.sin(angle + Math.PI/8) * size * 1.8;
-
- const baseCtrl1X = point.x + Math.cos(angle - Math.PI/4) * size * 0.5;
- const baseCtrl1Y = point.y + Math.sin(angle - Math.PI/4) * size * 0.5;
-
- const baseCtrl2X = point.x + Math.cos(angle + Math.PI/4) * size * 0.5;
- const baseCtrl2Y = point.y + Math.sin(angle + Math.PI/4) * size * 0.5;
-
- return (
-
- );
- };
-
- const renderBranch = (branch, parentOpacity = 1) => {
- if (branch.points.length < 2) return null;
-
- const points = branch.points;
-
- const getStrokeWidth = (index) => {
- const maxWidth = 5;
- const progress = index / (points.length - 1);
- const startTaper = Math.min(1, index / 3);
- const endTaper = Math.pow(1 - progress, 1.5);
- return maxWidth * startTaper * endTaper;
- };
-
- return (
-
- {points.map((point, i) => {
- if (i === 0) return null;
- const prev = points[i - 1];
- const dx = point.x - prev.x;
- const dy = point.y - prev.y;
- const controlX = prev.x + dx * 0.7;
- const controlY = prev.y + dy * 0.7;
-
- return (
-
- );
- })}
- {branch.leaves.map((leaf, i) => {
- const point = points[Math.floor(leaf.position)];
- if (!point) return null;
- return (
-
- {renderLeaf(point, leaf.size, leaf.side, parentOpacity)}
-
- );
- })}
-
- );
- };
-
- useEffect(() => {
- if (isMobile) return;
-
- if (vines.length === 0) {
- setVines([
- createNewVine(),
- { ...createNewVine(), createdAt: Date.now() - 2000 },
- { ...createNewVine(), createdAt: Date.now() - 4000 }
- ]);
- }
-
- const interval = setInterval(() => {
- setVines(currentVines => {
- const updatedVines = currentVines
- .map(vine => updateVine(vine))
- .filter(vine => vine.opacity > 0.01);
-
- if (updatedVines.length < 3) {
- return [...updatedVines, createNewVine()];
- }
-
- return updatedVines;
- });
- }, 40);
-
- return () => clearInterval(interval);
- }, [createNewVine, updateVine]);
-
- return (
-
-
-
- );
-};
-
-export default VineAnimation;
diff --git a/src/src/layouts/index.astro b/src/src/layouts/index.astro
index 838a1b4..2071e0f 100644
--- a/src/src/layouts/index.astro
+++ b/src/src/layouts/index.astro
@@ -5,7 +5,7 @@ import "@/style/globals.css";
import Header from "@/components/header";
import Footer from "@/components/footer";
-import VineAnimation from "@/components/vines";
+import Background from "@/components/hero/background";
---
@@ -17,9 +17,8 @@ import VineAnimation from "@/components/vines";
-
-
+