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
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:
// 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.
// 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.