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) => ( -
- {link.label} -
- )); + const headerLinks = Links.map((link) => { + const isActive = checkIsActive(link.href); + + return ( +
+ + {link.label} +
+ {isClient && isActive && ( + + + + )} +
+
+
+ ); + }); return ( -
-
+
+
{headerLinks}
); -}; +} 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 ( -
- - {vines.map(vine => ( - - {renderBranch(vine.mainBranch, vine.opacity)} - {vine.subBranches.map(branch => renderBranch(branch, vine.opacity))} - - ))} - -
- ); -}; - -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";
- -
+