mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +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";
|
import { Links } from "@/components/header/links";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
const [lastScrollY, setLastScrollY] = useState(0);
|
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(() => {
|
useEffect(() => {
|
||||||
|
if (!isClient) return;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const currentScrollY = window.scrollY;
|
const currentScrollY = document.documentElement.scrollTop;
|
||||||
setVisible(currentScrollY < lastScrollY || currentScrollY < 10);
|
setVisible(currentScrollY < lastScrollY || currentScrollY < 10);
|
||||||
setLastScrollY(currentScrollY);
|
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);
|
const headerLinks = Links.map((link) => {
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
const isActive = checkIsActive(link.href);
|
||||||
}, [lastScrollY]);
|
|
||||||
|
|
||||||
const headerLinks = Links.map((link) => (
|
return (
|
||||||
<div
|
<div
|
||||||
key={link.id}
|
key={link.id}
|
||||||
className={`inline-block ${link.color}`}
|
className={`relative inline-block ${link.color}`}
|
||||||
>
|
>
|
||||||
<a href={link.href}>{link.label}</a>
|
<a
|
||||||
</div>
|
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 (
|
return (
|
||||||
<header className={`fixed z-50 top-0 left-0 right-0 font-bold transition-transform duration-300 ${
|
<header
|
||||||
visible ? "translate-y-0" : "-translate-y-full"
|
className={`
|
||||||
}`}>
|
fixed z-50 top-0 left-0 right-0
|
||||||
<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">
|
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}
|
{headerLinks}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 Header from "@/components/header";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import VineAnimation from "@/components/vines";
|
import Background from "@/components/hero/background";
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -17,9 +17,8 @@ import VineAnimation from "@/components/vines";
|
|||||||
</head>
|
</head>
|
||||||
<body class="bg-background text-foreground">
|
<body class="bg-background text-foreground">
|
||||||
<Header client:load />
|
<Header client:load />
|
||||||
<VineAnimation side="left" client:only="react" />
|
|
||||||
<VineAnimation side="right" client:only="react" />
|
|
||||||
<main>
|
<main>
|
||||||
|
<Background client:only="react" />
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<Footer client:load fixed=true />
|
<Footer client:load fixed=true />
|
||||||
|
|||||||
Reference in New Issue
Block a user