Building an Animated Timeline with Conway's Game of Life

Combining career visualization with cellular automata

Overview

Visualizing career timelines can be boring with static bars. In this post, we'll explore how to create an engaging timeline visualization that combines career data with Conway's Game of Life cellular automaton, creating an organic, living representation of professional experience.

Implementation Details

The timeline is built using HTML Canvas and implements a modified version of Conway's Game of Life. Each cell represents one month of career history, with different colors indicating different positions or companies.

Canvas Setup and Grid Initialization

timeline.ts
    const SQUARE_SIZE = 10;
    const SQUARE_GAP = 3;
    const SQUARES_PER_ROW = Math.floor(width / (SQUARE_SIZE + SQUARE_GAP));
    const SQUARES_PER_COL = Math.floor(height / (SQUARE_SIZE + SQUARE_GAP));

    // Create grid to track square states
    let grid = new Array(SQUARES_PER_COL)
    .fill(null)
    .map(() => new Array(SQUARES_PER_ROW).fill(false));
  

Modified Game of Life Rules

The traditional Game of Life rules have been modified to create a more subtle animation effect:

  • Cells survive with 1-4 neighbors (instead of 2-3)
  • Dead cells come alive with 2-3 neighbors
  • Random cell activation to prevent stagnation
  • Color preservation based on career period

Animation System

The animation combines two elements: the Game of Life simulation and a pulsing highlight effect:

timeline.ts
    // Pulsing highlight effect
ctx.fillStyle = `rgba(255, 255, 255, ${
  Math.sin(time * 2) * 0.3 + 0.4
})`;

// Random cell activation
if (!newGrid[row][col] && Math.random() < 0.001) {
  newGrid[row][col] = true;
}
  

Performance Considerations

Several key optimizations ensure smooth animation:

Core Optimizations

Frame Throttling

Grid updates are throttled to every 30 frames using requestAnimationFrame. This balance ensures smooth visual updates while reducing unnecessary calculations. While the animation still runs at 60fps, the Game of Life logic only updates every half second, which is imperceptible to users but significantly reduces CPU usage.

Canvas-Based Rendering

Instead of using DOM elements, the entire visualization is rendered on an HTML Canvas. This approach eliminates the overhead of managing thousands of individual DOM elements and their state changes. Canvas operations are hardware-accelerated and perfect for this kind of pixel-based animation.

Memory Management

Proper cleanup of animation frames on component unmount prevents memory leaks. This is crucial for single-page applications where components may be mounted and unmounted frequently. We use React's useEffect hook to ensure all resources are properly released when the component is removed from the DOM.

Optimized Neighbor Checking

The neighbor checking algorithm includes early bounds checking and optimized array access patterns. By checking boundaries before accessing array elements, we prevent out-of-bounds errors without try-catch blocks, leading to better performance. The algorithm also minimizes array lookups by using direct index access.

timeline.ts
    // Throttle updates to every 30 frames
let frameCount = 0;
const animate = (time: number) => {
  frameCount++;
  if (frameCount % 30 === 0) {
    updateGrid();
  }
  render(time);
  animationFrame = requestAnimationFrame(animate);
};

// Cleanup on unmount
useEffect(() => {
  return () => {
    if (animationFrame) {
      cancelAnimationFrame(animationFrame);
    }
  };
}, []);

// Optimized neighbor checking
const getNeighborCount = (row: number, col: number): number => {
  let count = 0;
  for (let i = -1; i <= 1; i++) {
    for (let j = -1; j <= 1; j++) {
      if (i === 0 && j === 0) continue;
      const newRow = row + i;
      const newCol = col + j;
      if (
        newRow >= 0 && 
        newRow < SQUARES_PER_COL && 
        newCol >= 0 && 
        newCol < SQUARES_PER_ROW
      ) {
        count += grid[newRow][newCol] ? 1 : 0;
      }
    }
  }
  return count;
};
  

Additional Performance Optimizations

TypedArray Implementation

For larger grids exceeding 10,000 cells, we switch to Uint8Array for grid storage. TypedArrays provide better memory efficiency and faster numerical operations compared to standard JavaScript arrays. This is particularly important when dealing with large datasets or when running on memory-constrained devices.

Double Buffering

To prevent visual artifacts and ensure smooth transitions, we implement double buffering. This technique uses two grid buffers - one for reading the current state and another for writing the next state. This prevents partial updates from being visible and ensures frame consistency during rendering.

Value Caching

Frequently accessed values and calculations are cached at the appropriate scope level. Grid dimensions, color values, and complex calculations are stored rather than recomputed. This includes caching date calculations for timeline positions and color interpolation values used in the visualization.

Optimized Animation Timing

requestAnimationFrame is used instead of setInterval or setTimeout for animation timing. This browser API provides better integration with the display's refresh rate and automatically pauses when the tab is inactive, reducing battery consumption and CPU usage on mobile devices.

Boundary Optimization

The implementation includes sophisticated bounds checking that prevents unnecessary calculations for cells at the grid edges. This reduces the number of neighbor calculations needed for edge cells and eliminates the need for wrap-around checks, improving performance for larger grids.

Conclusion

By combining career data visualization with cellular automata, we've created an engaging and unique way to display timeline information. The living, breathing nature of the visualization adds interest while maintaining clarity of the underlying data.