A guide on how to create interactive and animated graphics with HTML5 Canvas and JavaScript

Article directory

  • Preface
  • Effect demonstration
  • Code explanation
  • Complete code
  • end

Foreword

This article explains how to create an interactive bubble effect using HTML5’s Canvas and JavaScript. By moving with the mouse or touch, you can create flowing bubble trajectories on the canvas. This effect takes advantage of the spring effect between points, allowing the bubbles to naturally follow the movement of the mouse or touch.
The canvas element in the code is obtained, and the 2D drawing context is obtained through the getContext method. Mouse and touch events are listened to to update the position of the mouse or touch.

Effect demonstration

Code explanation

First use the document.querySelector() method to select the canvas in HTML and assign it to the variable canvas.

const canvas = document.querySelector("canvas");

Create a 2D drawing environment using the canvas.getContext(2d’) method and assign it to the variable ctx

const ctx = canvas.getContext('2d');

Define the mouse coordinate object, including initial coordinates and target coordinates.

let mouse = {<!-- -->
    x: .5 * window.innerWidth,
    y: .5 * window.innerHeight,
    tX: 0,
    tY: 0
}

Define parameter objects, including number of points, width coefficient, mouse sensitivity, elasticity and friction, etc.

let params = {<!-- -->
    pointsNumber: 40,
    widthFactor: .3,
    mouseThreshold: .6,
    spring: .4,
    friction: .5
};

Create an array to store the touch trajectory, and traverse to initialize the x and y coordinates, vx and vy speed of each point to 0.

const touchTrail = new Array(params.pointsNumber);
for (let i = 0; i < params.pointsNumber; i + + ) {<!-- -->
    touchTrail[i] = {<!-- -->
        x: mouse.x,
        y: mouse.y,
        vx: 0,
        vy: 0,
    }
}

Add click events, mouse movement events and touch events to the window through the addEventListener() method.

window.addEventListener("click", e => {<!-- -->
    updateMousePosition(e.pageX, e.pageY);
});
window.addEventListener("mousemove", e => {<!-- -->
    mouseMoved = true;
    updateMousePosition(e.pageX, e.pageY);
});
window.addEventListener("touchmove", e => {<!-- -->
    mouseMoved = true;
    updateMousePosition(e.targetTouches[0].pageX, e.targetTouches[0].pageY);
});

Define an event handling function updateMousePosition() to update mouse coordinate information.

function updateMousePosition(eX, eY) {<!-- -->
    mouse.tX = eX;
    mouse.tY = eY;
}

Call the setupCanvas() method to initialize the canvas and set the width and height of the canvas.

setupCanvas();

Call the window.addEventListener() method to add a resize event to the window, and call the setupCanvas() method in the event handling function to initialize the canvas.

window.addEventListener('resize', () => {<!-- -->
    setupCanvas();
});

Define the updateBubbles() method, which is used to update the bubble effect and continuously call the window.requestAnimationFrame() method to continuously render the picture.

function updateBubbles(t) {<!-- -->
    ...
    window.requestAnimationFrame(updateBubbles);
}

In the updateBubbles() method, the movement of bubbles is controlled by determining whether the mouse is moving. If the mouse is not moving, it is set to a regular dynamic pattern.

if (!mouseMoved) {<!-- -->
        mouse.tX = (.5 + .3 * Math.cos(.002 * t) * (Math.sin(.005 * t))) * window.innerWidth;
        mouse.tY = (.5 + .2 * (Math.cos(.005 * t)) + .1 * Math.cos(.01 * t)) * window.innerHeight;
}

Use the ctx.beginPath() method to start drawing the path. Each point in the touchTrail array represents a bubble effect.

ctx.beginPath();
touchTrail.forEach((p, pIdx) => {<!-- -->
    ...
});

Traverse each point in the touchTrail array. When the current point is the first point, set the coordinates of the point to the coordinates of the mouse and move the brush to the point. Otherwise, calculate the distance between the current point and the previous point, and Uses elasticity and friction to control its movement. vx and vy represent the speed in the x and y directions.

touchTrail.forEach((p, pIdx) => {<!-- -->
        if (pIdx === 0) {<!-- -->
            p.x = mouse.x;
            p.y = mouse.y;
            ctx.moveTo(p.x, p.y);
        } else {<!-- -->
            p.vx + = (touchTrail[pIdx - 1].x - p.x) * params.spring;
            p.vy + = (touchTrail[pIdx - 1].y - p.y) * params.spring;
            p.vx *= params.friction;
            p.vy *= params.friction;
            p.x + = p.vx;
            p.y + = p.vy;
        }
});

Use the ctx.quadraticCurveTo() method and ctx.lineTo() method to draw the bubble path, and control the change of line width through the params.widthFactor parameter.

for (let i = 1; i < touchTrail.length - 1; i + + ) {<!-- -->
        const xc = .5 * (touchTrail[i].x + touchTrail[i + 1].x);
        const yc = .5 * (touchTrail[i].y + touchTrail[i + 1].y);
        ctx.quadraticCurveTo(touchTrail[i].x, touchTrail[i].y, xc, yc);
        ctx.lineWidth = params.widthFactor * (params.pointsNumber - i);
        ctx.stroke();
    }
    ctx.lineTo(touchTrail[touchTrail.length - 1].x, touchTrail[touchTrail.length - 1].y);
    ctx.stroke();

Finally, the mouse sensitivity is controlled through the params.mouseThreshold parameter to achieve smooth motion effects.

mouse.x + = (mouse.tX - mouse.x) * params.mouseThreshold;
mouse.y + = (mouse.tY - mouse.y) * params.mouseThreshold;

Complete code

Explain the following code point by point. Each point needs a corresponding code.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext('2d');

// for intro motion
let mouseMoved = false;

let mouse = {<!-- -->
    x: .5 * window.innerWidth,
    y: .5 * window.innerHeight,
    tX: 0,
    tY: 0
}
let params = {<!-- -->
    pointsNumber: 40,
    widthFactor: .3,
    mouseThreshold: .6,
    spring: .4,
    friction: .5
};

const touchTrail = new Array(params.pointsNumber);
for (let i = 0; i < params.pointsNumber; i + + ) {<!-- -->
    touchTrail[i] = {<!-- -->
        x: mouse.x,
        y: mouse.y,
        vx: 0,
        vy: 0,
    }
}

window.addEventListener("click", e => {<!-- -->
    updateMousePosition(e.pageX, e.pageY);
});
window.addEventListener("mousemove", e => {<!-- -->
    mouseMoved = true;
    updateMousePosition(e.pageX, e.pageY);
});
window.addEventListener("touchmove", e => {<!-- -->
    mouseMoved = true;
    updateMousePosition(e.targetTouches[0].pageX, e.targetTouches[0].pageY);
});

function updateMousePosition(eX, eY) {<!-- -->
    mouse.tX = eX;
    mouse.tY = eY;
}

setupCanvas();
updateBubbles(0);
window.addEventListener('resize', () => {<!-- -->
    setupCanvas();
});


function updateBubbles(t) {

    // for intro motion
    if (!mouseMoved) {
        mouse.tX = (.5 + .3 * Math.cos(.002 * t) * (Math.sin(.005 * t))) * window.innerWidth;
        mouse.tY = (.5 + .2 * (Math.cos(.005 * t)) + .1 * Math.cos(.01 * t)) * window.innerHeight;
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();

    touchTrail.forEach((p, pIdx) => {
        if (pIdx === 0) {
            p.x = mouse.x;
            p.y = mouse.y;
            ctx.moveTo(p.x, p.y);
        } else {
            p.vx + = (touchTrail[pIdx - 1].x - p.x) * params.spring;
            p.vy + = (touchTrail[pIdx - 1].y - p.y) * params.spring;
            p.vx *= params.friction;
            p.vy *= params.friction;

            p.x + = p.vx;
            p.y + = p.vy;
        }
    });

    for (let i = 1; i < touchTrail.length - 1; i + + ) {<!-- -->
        const xc = .5 * (touchTrail[i].x + touchTrail[i + 1].x);
        const yc = .5 * (touchTrail[i].y + touchTrail[i + 1].y);
        ctx.quadraticCurveTo(touchTrail[i].x, touchTrail[i].y, xc, yc);
        ctx.lineWidth = params.widthFactor * (params.pointsNumber - i);
        ctx.stroke();
    }
    ctx.lineTo(touchTrail[touchTrail.length - 1].x, touchTrail[touchTrail.length - 1].y);
    ctx.stroke();

    mouse.x + = (mouse.tX - mouse.x) * params.mouseThreshold;
    mouse.y + = (mouse.tY - mouse.y) * params.mouseThreshold;

    window.requestAnimationFrame(updateBubbles);
}

function setupCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}

Complete