🌟 Animated Particle Constellations

A Complete Tutorial Using Rust and Macroquad

📚 What You'll Learn

In this tutorial, you'll create a mesmerizing particle constellation effect where particles drift across the screen and connect to nearby neighbors with glowing lines. This is a great project for learning:

🚀 Step 1: Project Setup

Create a New Project

Open your terminal and run:

cargo new particle_constellation
cd particle_constellation

Add Macroquad Dependency

Open Cargo.toml and add macroquad:

[dependencies]
macroquad = "0.4"

Macroquad is a simple game framework that handles window creation, rendering, and input for us.

You can download the complete source code here: main.rs or follow the steps below.

🎨 Step 2: Understanding the Particle Structure

Each particle needs to track its position, velocity, and size. Let's build the Particle struct:

use macroquad::prelude::*;

struct Particle {
    pos: Vec2,    // 2D position (x, y)
    vel: Vec2,    // 2D velocity (speed in x and y directions)
    size: f32,    // Radius of the particle
}
💡 Tip: Vec2 is Macroquad's 2D vector type. It's perfect for positions and velocities because it bundles x and y coordinates together.

✨ Step 3: Creating Particles

We need a function to create particles with random positions and velocities:

impl Particle {
    fn new() -> Self {
        Self {
            // Random position anywhere on screen
            pos: vec2(
                rand::gen_range(0.0, screen_width()),
                rand::gen_range(0.0, screen_height()),
            ),
            // Random velocity between -1 and 1 pixels per frame
            vel: vec2(
                rand::gen_range(-1.0, 1.0),
                rand::gen_range(-1.0, 1.0),
            ),
            // Random size between 2 and 4 pixels
            size: rand::gen_range(2.0, 4.0),
        }
    }
}
⚠️ Important: rand::gen_range() is provided by Macroquad's built-in random number generator. You don't need to add the rand crate separately!

🔄 Step 4: Updating Particle Positions

Each frame, we need to move particles and handle screen wrapping:

    fn update(&mut self) {
        // Move particle by its velocity
        self.pos += self.vel;

        // Wrap around screen edges (like Pac-Man)
        if self.pos.x < 0.0 {
            self.pos.x = screen_width();
        } else if self.pos.x > screen_width() {
            self.pos.x = 0.0;
        }

        if self.pos.y < 0.0 {
            self.pos.y = screen_height();
        } else if self.pos.y > screen_height() {
            self.pos.y = 0.0;
        }
    }

The wrapping creates seamless, continuous motion where particles that exit one side reappear on the opposite side.

🎯 Step 5: Drawing Particles

Simple rendering method to draw each particle as a white circle:

    fn draw(&self) {
        draw_circle(self.pos.x, self.pos.y, self.size, WHITE);
    }
}

🌌 Step 6: The Main Game Loop

Now for the core of the application. The game loop runs every frame:

#[macroquad::main("Particle Constellations")]
async fn main() {
    // Configuration
    let particle_count = 100;
    let connection_distance = 120.0;
    
    // Create all particles
    let mut particles: Vec<Particle> = (0..particle_count)
        .map(|_| Particle::new())
        .collect();

    loop {
        // Clear screen with dark blue background
        clear_background(Color::from_rgba(10, 10, 30, 255));

        // Update all particles
        for particle in particles.iter_mut() {
            particle.update();
        }

        // ... (connection drawing - next step)

        // Draw all particles
        for particle in &particles {
            particle.draw();
        }

        // Wait for next frame
        next_frame().await
    }
}

🔗 Step 7: Drawing Connections (The Magic!)

This is where the constellation effect comes alive. We check every pair of particles and draw lines between nearby ones:

        // Draw connections between nearby particles
        for i in 0..particles.len() {
            for j in (i + 1)..particles.len() {
                // Calculate distance between particles
                let dx = particles[i].pos.x - particles[j].pos.x;
                let dy = particles[i].pos.y - particles[j].pos.y;
                let distance = (dx * dx + dy * dy).sqrt();

                // Only draw if particles are close enough
                if distance < connection_distance {
                    // Fade line based on distance (closer = more opaque)
                    let alpha = 1.0 - (distance / connection_distance);
                    let color = Color::from_rgba(
                        100, 150, 255, 
                        (alpha * 100.0) as u8
                    );
                    
                    draw_line(
                        particles[i].pos.x,
                        particles[i].pos.y,
                        particles[j].pos.x,
                        particles[j].pos.y,
                        1.0,
                        color,
                    );
                }
            }
        }
🧮 Math Breakdown:

📊 Step 8: Adding Debug Info

Let's add FPS and particle count to the screen:

        // Display info
        draw_text(
            &format!("FPS: {}", get_fps()),
            10.0, 20.0, 20.0, WHITE,
        );
        draw_text(
            &format!("Particles: {}", particle_count),
            10.0, 40.0, 20.0, WHITE,
        );

🎮 Step 9: Run Your Constellation!

Build and Run

cargo run --release

The --release flag enables optimizations for smooth performance.

Next Rust tutorial:

Interactive fractal graphics zoom