mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
Add interactivity to the background animation:
This commit is contained in:
@@ -4,6 +4,7 @@ interface Cell {
|
||||
alive: boolean;
|
||||
next: boolean;
|
||||
color: [number, number, number];
|
||||
baseColor: [number, number, number]; // Original color
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
targetX: number;
|
||||
@@ -12,8 +13,11 @@ interface Cell {
|
||||
targetOpacity: number;
|
||||
scale: number;
|
||||
targetScale: number;
|
||||
elevation: number; // For 3D effect
|
||||
targetElevation: number;
|
||||
transitioning: boolean;
|
||||
transitionComplete: boolean;
|
||||
rippleEffect: number; // For ripple animation
|
||||
}
|
||||
|
||||
interface Grid {
|
||||
@@ -24,17 +28,30 @@ interface Grid {
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
isDown: boolean;
|
||||
lastClickTime: number;
|
||||
cellX: number;
|
||||
cellY: number;
|
||||
}
|
||||
|
||||
interface BackgroundProps {
|
||||
layout?: 'index' | 'sidebar';
|
||||
position?: 'left' | 'right';
|
||||
}
|
||||
|
||||
const CELL_SIZE = 25;
|
||||
const TRANSITION_SPEED = 0.05; // Reduced from 0.1 for slower animations
|
||||
const SCALE_SPEED = 0.05; // Reduced from 0.15 for slower animations
|
||||
const CYCLE_FRAMES = 180; // Increased from 120 to give more time for transitions
|
||||
const TRANSITION_SPEED = 0.05;
|
||||
const SCALE_SPEED = 0.05;
|
||||
const CYCLE_FRAMES = 180;
|
||||
const INITIAL_DENSITY = 0.15;
|
||||
const SIDEBAR_WIDTH = 240;
|
||||
const MOUSE_INFLUENCE_RADIUS = 150; // Radius of mouse influence in pixels
|
||||
const COLOR_SHIFT_AMOUNT = 30; // Maximum color shift amount
|
||||
const RIPPLE_SPEED = 0.2; // Speed of ripple propagation
|
||||
const ELEVATION_FACTOR = 15; // Max height for 3D effect
|
||||
|
||||
const Background: React.FC<BackgroundProps> = ({
|
||||
layout = 'index',
|
||||
@@ -45,6 +62,14 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const animationFrameRef = useRef<number>();
|
||||
const frameCount = useRef(0);
|
||||
const resizeTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const mouseRef = useRef<MousePosition>({
|
||||
x: -1000,
|
||||
y: -1000,
|
||||
isDown: false,
|
||||
lastClickTime: 0,
|
||||
cellX: -1,
|
||||
cellY: -1
|
||||
});
|
||||
|
||||
const randomColor = (): [number, number, number] => {
|
||||
const colors = [
|
||||
@@ -70,10 +95,13 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const { cols, rows, offsetX, offsetY } = calculateGridDimensions(width, height);
|
||||
|
||||
const cells = Array(cols).fill(0).map((_, i) =>
|
||||
Array(rows).fill(0).map((_, j) => ({
|
||||
Array(rows).fill(0).map((_, j) => {
|
||||
const baseColor = randomColor();
|
||||
return {
|
||||
alive: Math.random() < INITIAL_DENSITY,
|
||||
next: false,
|
||||
color: randomColor(),
|
||||
color: [...baseColor] as [number, number, number],
|
||||
baseColor: baseColor,
|
||||
currentX: i,
|
||||
currentY: j,
|
||||
targetX: i,
|
||||
@@ -82,9 +110,13 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
targetOpacity: 0,
|
||||
scale: 0,
|
||||
targetScale: 0,
|
||||
elevation: 0,
|
||||
targetElevation: 0,
|
||||
transitioning: false,
|
||||
transitionComplete: false
|
||||
}))
|
||||
transitionComplete: false,
|
||||
rippleEffect: 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const grid = { cells, cols, rows, offsetX, offsetY };
|
||||
@@ -119,11 +151,9 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const col = (x + i + grid.cols) % grid.cols;
|
||||
const row = (y + j + grid.rows) % grid.rows;
|
||||
|
||||
// Only count cells that are actually alive according to the game state
|
||||
// Not cells that are just visually transitioning
|
||||
if (grid.cells[col][row].alive) {
|
||||
neighbors.count++;
|
||||
neighbors.colors.push(grid.cells[col][row].color);
|
||||
neighbors.colors.push(grid.cells[col][row].baseColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +188,8 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
} else {
|
||||
cell.next = count === 3;
|
||||
if (cell.next) {
|
||||
cell.color = averageColors(colors);
|
||||
cell.baseColor = averageColors(colors);
|
||||
cell.color = [...cell.baseColor];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,9 +211,11 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
if (!cell.next) {
|
||||
cell.targetScale = 0;
|
||||
cell.targetOpacity = 0;
|
||||
cell.targetElevation = 0;
|
||||
} else {
|
||||
cell.targetScale = 1;
|
||||
cell.targetOpacity = 1;
|
||||
cell.targetElevation = 0;
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
@@ -190,7 +223,59 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const createRippleEffect = (grid: Grid, centerX: number, centerY: number) => {
|
||||
const maxDistance = Math.max(grid.cols, grid.rows) / 2;
|
||||
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
|
||||
// Calculate distance from cell to ripple center
|
||||
const dx = i - centerX;
|
||||
const dy = j - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Only apply ripple to visible cells
|
||||
if (cell.opacity > 0.1) {
|
||||
// Delayed animation based on distance from center
|
||||
setTimeout(() => {
|
||||
cell.rippleEffect = 1; // Start ripple
|
||||
|
||||
// After a short time, reset ripple
|
||||
setTimeout(() => {
|
||||
cell.rippleEffect = 0;
|
||||
}, 300 + distance * 50);
|
||||
}, distance * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const spawnCellAtPosition = (grid: Grid, x: number, y: number) => {
|
||||
if (x >= 0 && x < grid.cols && y >= 0 && y < grid.rows) {
|
||||
const cell = grid.cells[x][y];
|
||||
|
||||
if (!cell.alive && !cell.transitioning) {
|
||||
cell.alive = true;
|
||||
cell.next = true;
|
||||
cell.transitioning = true;
|
||||
cell.transitionComplete = false;
|
||||
cell.baseColor = randomColor();
|
||||
cell.color = [...cell.baseColor];
|
||||
cell.targetScale = 1;
|
||||
cell.targetOpacity = 1;
|
||||
cell.targetElevation = 0;
|
||||
|
||||
// Create a small ripple from the new cell
|
||||
createRippleEffect(grid, x, y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateCellAnimations = (grid: Grid) => {
|
||||
const mouseX = mouseRef.current.x;
|
||||
const mouseY = mouseRef.current.y;
|
||||
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
@@ -198,7 +283,44 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
// Smooth transitions
|
||||
cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED;
|
||||
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED;
|
||||
cell.elevation += (cell.targetElevation - cell.elevation) * SCALE_SPEED;
|
||||
|
||||
// Apply mouse interaction
|
||||
const cellCenterX = grid.offsetX + i * CELL_SIZE + CELL_SIZE / 2;
|
||||
const cellCenterY = grid.offsetY + j * CELL_SIZE + CELL_SIZE / 2;
|
||||
const dx = cellCenterX - mouseX;
|
||||
const dy = cellCenterY - mouseY;
|
||||
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Color wave effect based on mouse position
|
||||
if (distanceToMouse < MOUSE_INFLUENCE_RADIUS && cell.opacity > 0.1) {
|
||||
// Calculate color adjustment based on distance
|
||||
const influenceFactor = 1 - (distanceToMouse / MOUSE_INFLUENCE_RADIUS);
|
||||
|
||||
// Wave effect with sine function
|
||||
const waveOffset = (frameCount.current * 0.05 + distanceToMouse * 0.05) % (Math.PI * 2);
|
||||
const waveFactor = (Math.sin(waveOffset) * 0.5 + 0.5) * influenceFactor;
|
||||
|
||||
// Adjust color based on wave
|
||||
cell.color = [
|
||||
Math.min(255, Math.max(0, cell.baseColor[0] + COLOR_SHIFT_AMOUNT * waveFactor)),
|
||||
Math.min(255, Math.max(0, cell.baseColor[1] - COLOR_SHIFT_AMOUNT * waveFactor)),
|
||||
Math.min(255, Math.max(0, cell.baseColor[2] + COLOR_SHIFT_AMOUNT * waveFactor))
|
||||
] as [number, number, number];
|
||||
|
||||
// 3D elevation effect when mouse is close
|
||||
cell.targetElevation = ELEVATION_FACTOR * influenceFactor;
|
||||
} else {
|
||||
// Gradually return to base color when mouse is away
|
||||
cell.color[0] += (cell.baseColor[0] - cell.color[0]) * 0.1;
|
||||
cell.color[1] += (cell.baseColor[1] - cell.color[1]) * 0.1;
|
||||
cell.color[2] += (cell.baseColor[2] - cell.color[2]) * 0.1;
|
||||
|
||||
// Reset elevation when mouse moves away
|
||||
cell.targetElevation = 0;
|
||||
}
|
||||
|
||||
// Handle cell state transitions
|
||||
if (cell.transitioning) {
|
||||
// When a cell is completely faded out, update its alive state
|
||||
if (!cell.next && cell.opacity < 0.01 && cell.scale < 0.01) {
|
||||
@@ -207,6 +329,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
cell.transitionComplete = true;
|
||||
cell.opacity = 0;
|
||||
cell.scale = 0;
|
||||
cell.elevation = 0;
|
||||
}
|
||||
// When a new cell is born
|
||||
else if (cell.next && !cell.alive && !cell.transitionComplete) {
|
||||
@@ -215,8 +338,66 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
cell.transitionComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Gradually decrease ripple effect
|
||||
if (cell.rippleEffect > 0) {
|
||||
cell.rippleEffect = Math.max(0, cell.rippleEffect - RIPPLE_SPEED);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (!gridRef.current || !canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
mouseRef.current.isDown = true;
|
||||
mouseRef.current.lastClickTime = Date.now();
|
||||
|
||||
const grid = gridRef.current;
|
||||
|
||||
// Calculate which cell was clicked
|
||||
const cellX = Math.floor((mouseX - grid.offsetX) / CELL_SIZE);
|
||||
const cellY = Math.floor((mouseY - grid.offsetY) / CELL_SIZE);
|
||||
|
||||
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
|
||||
mouseRef.current.cellX = cellX;
|
||||
mouseRef.current.cellY = cellY;
|
||||
|
||||
const cell = grid.cells[cellX][cellY];
|
||||
|
||||
if (cell.alive) {
|
||||
// Create ripple effect from existing cell
|
||||
createRippleEffect(grid, cellX, cellY);
|
||||
} else {
|
||||
// Spawn new cell at empty position
|
||||
spawnCellAtPosition(grid, cellX, cellY);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
mouseRef.current.x = e.clientX - rect.left;
|
||||
mouseRef.current.y = e.clientY - rect.top;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
mouseRef.current.isDown = false;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouseRef.current.isDown = false;
|
||||
mouseRef.current.x = -1000;
|
||||
mouseRef.current.y = -1000;
|
||||
};
|
||||
|
||||
const setupCanvas = (canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||
@@ -280,6 +461,12 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
gridRef.current = initGrid(displayWidth, displayHeight);
|
||||
}
|
||||
|
||||
// Add mouse event listeners
|
||||
canvas.addEventListener('mousedown', handleMouseDown, { signal });
|
||||
canvas.addEventListener('mousemove', handleMouseMove, { signal });
|
||||
canvas.addEventListener('mouseup', handleMouseUp, { signal });
|
||||
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
|
||||
|
||||
const animate = () => {
|
||||
if (signal.aborted) return;
|
||||
|
||||
@@ -310,16 +497,71 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
if ((cell.alive || cell.targetOpacity > 0) && cell.opacity > 0.01) {
|
||||
const [r, g, b] = cell.color;
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.globalAlpha = cell.opacity * 0.8;
|
||||
|
||||
// Apply ripple and elevation effects to opacity
|
||||
const rippleBoost = cell.rippleEffect * 0.4; // Boost opacity during ripple
|
||||
ctx.globalAlpha = Math.min(1, cell.opacity * 0.8 + rippleBoost);
|
||||
|
||||
const scaledSize = cellSize * cell.scale;
|
||||
const xOffset = (cellSize - scaledSize) / 2;
|
||||
const yOffset = (cellSize - scaledSize) / 2;
|
||||
|
||||
// Apply 3D elevation effect
|
||||
const elevationOffset = cell.elevation;
|
||||
|
||||
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 y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset - elevationOffset;
|
||||
const scaledRoundness = roundness * cell.scale;
|
||||
|
||||
// Draw shadow for 3D effect if cell has elevation
|
||||
if (elevationOffset > 1) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y + elevationOffset + 5);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + elevationOffset + 5);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset + 5, x + scaledSize, y + elevationOffset + 5 + scaledRoundness);
|
||||
ctx.lineTo(x + scaledSize, y + elevationOffset + 5 + scaledSize - scaledRoundness);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset + 5 + scaledSize, x + scaledSize - scaledRoundness, y + elevationOffset + 5 + scaledSize);
|
||||
ctx.lineTo(x + scaledRoundness, y + elevationOffset + 5 + scaledSize);
|
||||
ctx.quadraticCurveTo(x, y + elevationOffset + 5 + scaledSize, x, y + elevationOffset + 5 + scaledSize - scaledRoundness);
|
||||
ctx.lineTo(x, y + elevationOffset + 5 + scaledRoundness);
|
||||
ctx.quadraticCurveTo(x, y + elevationOffset + 5, x + scaledRoundness, y + elevationOffset + 5);
|
||||
ctx.fill();
|
||||
|
||||
// Draw side of elevated cell
|
||||
const sideHeight = elevationOffset;
|
||||
ctx.fillStyle = `rgba(${r*0.7}, ${g*0.7}, ${b*0.7}, ${ctx.globalAlpha})`;
|
||||
|
||||
// Left side
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + scaledRoundness);
|
||||
ctx.lineTo(x, y + scaledSize - scaledRoundness + sideHeight);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x, y + scaledSize - scaledRoundness);
|
||||
ctx.fill();
|
||||
|
||||
// Right side
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledSize, y + scaledRoundness);
|
||||
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness + sideHeight);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
|
||||
ctx.fill();
|
||||
|
||||
// Bottom side
|
||||
ctx.fillStyle = `rgba(${r*0.5}, ${g*0.5}, ${b*0.5}, ${ctx.globalAlpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Draw main cell with original color
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y);
|
||||
@@ -331,6 +573,38 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
ctx.lineTo(x, y + scaledRoundness);
|
||||
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
|
||||
ctx.fill();
|
||||
|
||||
// Draw highlight on top for 3D effect
|
||||
if (elevationOffset > 1) {
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${0.2 * elevationOffset / ELEVATION_FACTOR})`;
|
||||
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/3);
|
||||
ctx.lineTo(x, y + scaledSize/3);
|
||||
ctx.lineTo(x, y + scaledRoundness);
|
||||
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Draw ripple effect
|
||||
if (cell.rippleEffect > 0) {
|
||||
const rippleRadius = cell.rippleEffect * cellSize * 2;
|
||||
const rippleAlpha = (1 - cell.rippleEffect) * 0.5;
|
||||
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${rippleAlpha})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x + scaledSize / 2,
|
||||
y + scaledSize / 2,
|
||||
rippleRadius,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,7 +635,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
return 'fixed inset-0 -z-10';
|
||||
}
|
||||
|
||||
const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10 pointer-events-none';
|
||||
const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10';
|
||||
return position === 'left'
|
||||
? `${baseClasses} left-0`
|
||||
: `${baseClasses} right-0`;
|
||||
@@ -371,9 +645,9 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
<div className={getContainerClasses()}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full bg-black"
|
||||
className="w-full h-full bg-black cursor-pointer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user