mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 02:53:51 +00:00
Absolutely beatiful game of life hero background
This commit is contained in:
@@ -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 isActive = checkIsActive(link.href);
|
||||
|
||||
const headerLinks = Links.map((link) => (
|
||||
<div
|
||||
key={link.id}
|
||||
className={`inline-block ${link.color}`}
|
||||
>
|
||||
<a href={link.href}>{link.label}</a>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div
|
||||
key={link.id}
|
||||
className={`relative inline-block ${link.color}`}
|
||||
>
|
||||
<a
|
||||
href={link.href}
|
||||
className="relative inline-block"
|
||||
>
|
||||
{link.label}
|
||||
<div className="absolute -bottom-1 left-0 w-full overflow-hidden">
|
||||
{isClient && isActive && (
|
||||
<svg
|
||||
className="w-full h-3 opacity-100"
|
||||
preserveAspectRatio="none"
|
||||
viewBox="0 0 100 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="stroke-current transition-[stroke-dashoffset] duration-[600ms] ease-out"
|
||||
d="M0,6
|
||||
C25,4 35,8 50,6
|
||||
S75,4 100,6"
|
||||
fill="none"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
style={{
|
||||
strokeDasharray: 100,
|
||||
strokeDashoffset: shouldAnimate ? 0 : 100
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<header className={`fixed z-50 top-0 left-0 right-0 font-bold transition-transform duration-300 ${
|
||||
visible ? "translate-y-0" : "-translate-y-full"
|
||||
}`}>
|
||||
<div className="flex flex-row pt-1 px-2 bg-black text-lg lg:pt-2 lg:text-3xl md:text-2xl items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">
|
||||
<header
|
||||
className={`
|
||||
fixed z-50 top-0 left-0 right-0
|
||||
bg-black font-bold
|
||||
transition-transform duration-300
|
||||
${visible ? "translate-y-0" : "-translate-y-full"}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-row pt-1 px-2 text-lg lg:pt-2 lg:text-3xl md:text-2xl items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">
|
||||
{headerLinks}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
328
src/src/components/hero/background.tsx
Normal file
328
src/src/components/hero/background.tsx
Normal file
@@ -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<HTMLCanvasElement>(null);
|
||||
const gridRef = useRef<Grid>();
|
||||
const animationFrameRef = useRef<number>();
|
||||
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 (
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full bg-black"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Background;
|
||||
@@ -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 (
|
||||
<path
|
||||
d={`
|
||||
M ${point.x} ${point.y}
|
||||
C ${baseCtrl1X} ${baseCtrl1Y} ${ctrl1X} ${ctrl1Y} ${tipX} ${tipY}
|
||||
C ${ctrl2X} ${ctrl2Y} ${baseCtrl2X} ${baseCtrl2Y} ${point.x} ${point.y}
|
||||
`}
|
||||
fill={VINE_COLOR}
|
||||
opacity={parentOpacity * 0.8}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<g key={branch.id}>
|
||||
{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 (
|
||||
<path
|
||||
key={`segment-${i}`}
|
||||
d={`M ${prev.x} ${prev.y} Q ${controlX} ${controlY} ${point.x} ${point.y}`}
|
||||
fill="none"
|
||||
stroke={VINE_COLOR}
|
||||
strokeWidth={getStrokeWidth(i)}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
opacity={parentOpacity * 0.9}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{branch.leaves.map((leaf, i) => {
|
||||
const point = points[Math.floor(leaf.position)];
|
||||
if (!point) return null;
|
||||
return (
|
||||
<g key={`${branch.id}-leaf-${i}`}>
|
||||
{renderLeaf(point, leaf.size, leaf.side, parentOpacity)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="fixed top-0 left-0 w-full h-full pointer-events-none">
|
||||
<svg
|
||||
className="w-full h-full"
|
||||
viewBox={`0 0 ${window.innerWidth} ${window.innerHeight}`}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
{vines.map(vine => (
|
||||
<g key={vine.id}>
|
||||
{renderBranch(vine.mainBranch, vine.opacity)}
|
||||
{vine.subBranches.map(branch => renderBranch(branch, vine.opacity))}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VineAnimation;
|
||||
@@ -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";
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
@@ -17,9 +17,8 @@ import VineAnimation from "@/components/vines";
|
||||
</head>
|
||||
<body class="bg-background text-foreground">
|
||||
<Header client:load />
|
||||
<VineAnimation side="left" client:only="react" />
|
||||
<VineAnimation side="right" client:only="react" />
|
||||
<main>
|
||||
<Background client:only="react" />
|
||||
<slot />
|
||||
</main>
|
||||
<Footer client:load fixed=true />
|
||||
|
||||
Reference in New Issue
Block a user