Update background animation to be more fluid and natural

This commit is contained in:
2025-04-17 12:28:25 -07:00
parent cfbe43ab8b
commit 0c2e7f505d

View File

@@ -30,9 +30,9 @@ interface BackgroundProps {
} }
const CELL_SIZE = 25; const CELL_SIZE = 25;
const TRANSITION_SPEED = 0.1; const TRANSITION_SPEED = 0.05; // Reduced from 0.1 for slower animations
const SCALE_SPEED = 0.15; const SCALE_SPEED = 0.05; // Reduced from 0.15 for slower animations
const CYCLE_FRAMES = 120; const CYCLE_FRAMES = 180; // Increased from 120 to give more time for transitions
const INITIAL_DENSITY = 0.15; const INITIAL_DENSITY = 0.15;
const SIDEBAR_WIDTH = 240; const SIDEBAR_WIDTH = 240;
@@ -119,6 +119,8 @@ const Background: React.FC<BackgroundProps> = ({
const col = (x + i + grid.cols) % grid.cols; const col = (x + i + grid.cols) % grid.cols;
const row = (y + j + grid.rows) % grid.rows; 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) { if (grid.cells[col][row].alive) {
neighbors.count++; neighbors.count++;
neighbors.colors.push(grid.cells[col][row].color); neighbors.colors.push(grid.cells[col][row].color);
@@ -144,11 +146,13 @@ const Background: React.FC<BackgroundProps> = ({
}; };
const computeNextState = (grid: Grid) => { const computeNextState = (grid: Grid) => {
// First, calculate the next state for all cells based on standard rules
for (let i = 0; i < grid.cols; i++) { for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; const cell = grid.cells[i][j];
const { count, colors } = countNeighbors(grid, i, j); const { count, colors } = countNeighbors(grid, i, j);
// Standard Conway's Game of Life rules
if (cell.alive) { if (cell.alive) {
cell.next = count === 2 || count === 3; cell.next = count === 2 || count === 3;
} else { } else {
@@ -157,14 +161,30 @@ const Background: React.FC<BackgroundProps> = ({
cell.color = averageColors(colors); cell.color = averageColors(colors);
} }
} }
}
}
// Then, set up animations for cells that need to change state
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
if (cell.alive !== cell.next && !cell.transitioning) { if (cell.alive !== cell.next && !cell.transitioning) {
cell.transitioning = true; cell.transitioning = true;
cell.transitionComplete = false; cell.transitionComplete = false;
// Random delay for staggered animation effect
const delay = Math.random() * 800;
setTimeout(() => {
if (!cell.next) { if (!cell.next) {
cell.targetScale = 0; cell.targetScale = 0;
cell.targetOpacity = 0; cell.targetOpacity = 0;
} else {
cell.targetScale = 1;
cell.targetOpacity = 1;
} }
}, delay);
} }
} }
} }
@@ -175,28 +195,24 @@ const Background: React.FC<BackgroundProps> = ({
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; const cell = grid.cells[i][j];
// Smooth transitions
cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED; cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED;
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED; cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED;
if (cell.transitioning) { if (cell.transitioning) {
if (!cell.next && cell.scale < 0.05) { // When a cell is completely faded out, update its alive state
if (!cell.next && cell.opacity < 0.01 && cell.scale < 0.01) {
cell.alive = false; cell.alive = false;
cell.transitioning = false; cell.transitioning = false;
cell.transitionComplete = true; cell.transitionComplete = true;
cell.scale = 0;
cell.opacity = 0; cell.opacity = 0;
cell.scale = 0;
} }
// When a new cell is born
else if (cell.next && !cell.alive && !cell.transitionComplete) { else if (cell.next && !cell.alive && !cell.transitionComplete) {
cell.alive = true; cell.alive = true;
cell.transitioning = false; cell.transitioning = false;
cell.transitionComplete = true; cell.transitionComplete = true;
cell.targetScale = 1;
cell.targetOpacity = 1;
}
else if (cell.next && !cell.alive && cell.transitionComplete) {
cell.transitioning = true;
cell.targetScale = 1;
cell.targetOpacity = 1;
} }
} }
} }
@@ -270,6 +286,7 @@ const Background: React.FC<BackgroundProps> = ({
frameCount.current++; frameCount.current++;
if (gridRef.current) { if (gridRef.current) {
// Every CYCLE_FRAMES, compute the next state
if (frameCount.current % CYCLE_FRAMES === 0) { if (frameCount.current % CYCLE_FRAMES === 0) {
computeNextState(gridRef.current); computeNextState(gridRef.current);
} }
@@ -289,7 +306,8 @@ const Background: React.FC<BackgroundProps> = ({
for (let i = 0; i < grid.cols; i++) { for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; const cell = grid.cells[i][j];
if (cell.opacity > 0.01) { // Draw all transitioning cells, even if they're fading out
if ((cell.alive || cell.targetOpacity > 0) && cell.opacity > 0.01) {
const [r, g, b] = cell.color; const [r, g, b] = cell.color;
ctx.fillStyle = `rgb(${r},${g},${b})`; ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.globalAlpha = cell.opacity * 0.8; ctx.globalAlpha = cell.opacity * 0.8;
@@ -336,7 +354,7 @@ const Background: React.FC<BackgroundProps> = ({
clearTimeout(resizeTimeoutRef.current); clearTimeout(resizeTimeoutRef.current);
} }
}; };
}, []); // Empty dependency array since we're managing state internally }, [layout]); // Added layout as a dependency since it's used in the effect
const getContainerClasses = () => { const getContainerClasses = () => {
if (layout === 'index') { if (layout === 'index') {