Forem

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

Posted on • Edited on

2

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!

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

Top comments (0)

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay