DEV Community

Cover image for Flow Field Screen
ibra-kdbra
ibra-kdbra

Posted on • Edited on

Flow Field Screen

Dynamic Flow Fields with Vanilla JS and HTML Canvas

Have you ever been mesmerized by abstract particle animations? These flowing, dynamic visuals can be achieved with surprisingly simple techniques using plain JavaScript and the HTML canvas element. In this article, we will break down the process of creating a flow field that animates thousands of particles, giving them a natural movement.

1. Setting Up the Project

To start, we need three files: an HTML file to set up the canvas, a CSS file for styling, and a JavaScript file for handling the logic.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flow Fields</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <canvas id="canvas1"></canvas>
    <script src="script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We define a <canvas> element where all our animations will take place.
  • The styles.css will be linked to style the canvas.
  • The main animation logic is contained in script.js.

2. Styling the Canvas with CSS

Let’s add a simple style to give our canvas a black background and make sure all padding and margins are removed.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

canvas {
    background-color: black;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Setting margin and padding to zero ensures that the canvas fills the entire screen.
  • The black background provides a nice contrast for the white particles.

3. Setup the Canvas: using library benefit

/** @type{HTMLCanvasElement} **/

const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

ctx.fillStyle = 'white';
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • /** @type{HTMLCanvasElement} **/: This JSDoc comment specifies that the following variable is an HTMLCanvasElement. It helps with type checking and enhances code editor IntelliSense.
  • canvas: Selects the element from the DOM with the ID canvas1.
  • ctx: Obtains the 2D rendering context, which provides methods and properties for drawing on the canvas.
  • Sets the canvas width and height to match the current window's inner width and height, ensuring the canvas fills the viewport.

4. Particle Class: Creating the Magic

The Particle class is where the core of the animation lies. Each particle moves across the canvas, leaving a trail of its past locations, creating the flowing effect.

class Particle {
    constructor(effect) {
        this.effect = effect;
        this.x = Math.floor(Math.random() * this.effect.width);
        this.y = Math.floor(Math.random() * this.effect.height);
        this.speedModifier = Math.floor(Math.random() * 5 + 1);
        this.history = [{ x: this.x, y: this.y }];
        this.maxLength = Math.floor(Math.random() * 200 + 10);
        this.timer = this.maxLength * 2;
        this.colors = ['#4C026B', '#8E0E00', '#9D0208', '#BA1A1A', '#730D9E'];
        this.color = this.colors[Math.floor(Math.random() * this.colors.length)];
    }

    draw(context) {
        context.beginPath();
        context.moveTo(this.history[0].x, this.history[0].y);
        for (let i = 1; i < this.history.length; i++) {
            context.lineTo(this.history[i].x, this.history[i].y);
        }
        context.strokeStyle = this.color;
        context.stroke();
    }

    update() {
        this.timer--;
        if (this.timer >= 1) {
            let x = Math.floor(this.x / this.effect.cellSize);
            let y = Math.floor(this.y / this.effect.cellSize);
            let index = y * this.effect.cols + x;
            let angle = this.effect.flowField[index];

            this.speedX = Math.cos(angle);
            this.speedY = Math.sin(angle);
            this.x += this.speedX * this.speedModifier;
            this.y += this.speedY * this.speedModifier;

            this.history.push({ x: this.x, y: this.y });
            if (this.history.length > this.maxLength) {
                this.history.shift();
            }
        } else if (this.history.length > 1) {
            this.history.shift();
        } else {
            this.reset();
        }
    }

    reset() {
        this.x = Math.floor(Math.random() * this.effect.width);
        this.y = Math.floor(Math.random() * this.effect.height);
        this.history = [{ x: this.x, y: this.y }];
        this.timer = this.maxLength * 2;
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Constructor: Each particle is initialized with a random position and movement speed. The history array tracks past positions to create trails.
  • draw(): This function draws the particle’s path based on its history. The particle leaves a colorful trail that adds to the visual effect.
  • update(): Here, the particle's position is updated by calculating the angle from the flow field. The speed and direction are controlled by trigonometric functions.
  • reset(): When the particle finishes its trail, it is reset to a new random location.

5. Effect Class: Organizing the Animation

The Effect class handles the creation of particles and the flow field itself, which controls the movement of the particles.

class Effect {
    constructor(canvas) {
        this.canvas = canvas;
        this.width = this.canvas.width;
        this.height = this.canvas.height;
        this.particles = [];
        this.numberOfParticles = 3000;
        this.cellSize = 20;
        this.flowField = [];
        this.curve = 5;
        this.zoom = 0.12;
        this.debug = true;
        this.init();
    }

    init() {
        this.rows = Math.floor(this.height / this.cellSize);
        this.cols = Math.floor(this.width / this.cellSize);
        for (let y = 0; y < this.rows; y++) {
            for (let x = 0; x < this.cols; x++) {
                let angle = (Math.cos(x * this.zoom) + Math.sin(y * this.zoom)) * this.curve;
                this.flowField.push(angle);
            }
        }
        for (let i = 0; i < this.numberOfParticles; i++) {
            this.particles.push(new Particle(this));
        }
    }

    drawGrid(context) {
        context.save();
        context.strokeStyle = 'white';
        context.lineWidth = 0.3;
        for (let c = 0; c < this.cols; c++) {
            context.beginPath();
            context.moveTo(c * this.cellSize, 0);
            context.lineTo(c * this.cellSize, this.height);
            context.stroke();
        }
        for (let r = 0; r < this.rows; r++) {
            context.beginPath();
            context.moveTo(0, r * this.cellSize);
            context.lineTo(this.width, r * this.cellSize);
            context.stroke();
        }
        context.restore();
    }

    render(context) {
        if (this.debug) this.drawGrid(context);
        this.particles.forEach(particle => {
            particle.draw(context);
            particle.update();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Constructor: Initializes the canvas dimensions, the number of particles, and the flow field.
  • init(): Calculates the angles for the flow field by combining trigonometric functions for each grid cell. This field influences how particles move.
  • drawGrid(): Draws the grid that divides the canvas into cells, used when debugging.
  • render(): Calls the draw and update methods for each particle to animate the particles across the canvas.

6. Bringing it to Life with the Animation Loop

To make everything work, we need an animation loop that continuously clears the canvas and re-renders the particles:

const effect = new Effect(canvas);

function animate() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    effect.render(ctx);
    requestAnimationFrame(animate);
}
animate();
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • clearRect(): Clears the canvas on each frame to avoid drawing over previous frames.
  • requestAnimationFrame: Keeps the animation smooth by recursively calling the animate() function.

Conclusion

By breaking down the Particle and Effect classes, we have created a fluid and dynamic flow field animation using only vanilla JavaScript. The simplicity of the HTML canvas, combined with JavaScript's trigonometric functions, allows us to build these mesmerizing visual effects.

Feel free to play around with the particle count, colors, or the flow field formula to create your own unique effects!

Top comments (0)