DEV Community

Cover image for Code a cannon to shoot colored balls when clicked using Vanilla JS
Pujaa M
Pujaa M

Posted on • Edited on

Code a cannon to shoot colored balls when clicked using Vanilla JS

Created a shooting cannon using the trajectory of projectile motion with just a click.

In this tutorial, we'll see how to create an engaging visual experience: a cannon launching colorful projectiles upon a click, each trajectory influenced by the user's mouse movement using CSS and JS. It's a fun and interactive way to play with angles and animation on the web!

To achieve the cannon shoot, follow the below steps.

1: Create a canvas

The entire process will be working on a canvas.

HTML

<canvas class="canvas"></canvas>
Enter fullscreen mode Exit fullscreen mode

CSS

* {
  margin: 0;
  padding: 0;
}

html, body {
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode

JS

let canvas = document.querySelector(".canvas");
let pen = canvas.getContext("2d");

function setupCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    canvas.style.backgroundColor = "#000000";
}

setupCanvas();

window.addEventListener("resize", setupCanvas);
Enter fullscreen mode Exit fullscreen mode

Canvas

We have created a canvas to work on.

2: Find a point from where you to launch and fix it as the origin

In my case, I fixed the origin to somewhere in the bottom left. What I did was, I positioned my cannon image first then figured out the new origin.

Selecting origin

I placed the origin at the point I marked with a blue dot.

My Origin

{
  x: 100,
  y: canvas.height -108
}
Enter fullscreen mode Exit fullscreen mode

Cannon position

From now on, this point will be playing a vital role.

3: Rotate the Cannon and find the angle using 'mousemove' EventListener

From the mousemove EventListener we can figure out the x and y coordinates of the cursor pointer but how to find the angle? This is where some mathematics comes into play!

The below formula is for finding the angle between two lines:

Angle between two lines

Where m1 and m2 are the slopes of the given lines.

Let us consider that the first line is passing through the origin fixed by us and the cursor pointer and the second line is through that origin and any one point lying on the x-axis.

Angle between two lines

JS

/* ... */

let cursorPointer = {
    x: 100,
    y: canvas.height - 108
}

/* ... */

function findAngle() {
    let x1 = 100;
    let y1 = canvas.height - 108;
    let x2 = cursorPointer.x;
    let y2 = cursorPointer.y;
    let tempX = 500;
    let tempY = canvas.height - 108;

    let m1 = (tempY - y1) / (tempX - x1);
    let m2 = (y2 - y1) / (x2 - x1);

    let angle = Math.atan(Math.abs((m2 - m1) / (1 + (m1 * m2))));

    return angle;
}

function rotateCannon(pointerX, pointerY) {
    cursorPointer.x = pointerX;
    cursorPointer.y = pointerY;

        // Start of code snippet for restricting the angle from 30deg to 75deg
    angle = findAngle() * (180 / Math.PI);
    if(cursorPointer.x < 100) {
        angle += 90;
    }

    if(cursorPointer.y > canvas.height - 108) {
        angle = -angle;
    }

    if(angle > 75) {
        angle = 75;
    }

    if(angle < 30) {
        angle = 30;
    }
        // End of code snippet

        // Rotating the cannon to the angle calculated
    /* let cannon = document.querySelector("#cannon");
    cannon.style.transform = "translateX(22px) rotate(-" + angle + "deg)"; */ 
}

document.addEventListener("mousemove", function(event) {
    rotateCannon(event.clientX, event.clientY);
});

/* ... */
Enter fullscreen mode Exit fullscreen mode

At this moment, the angle changes according to the movement of the mouse. In my case, I rotate the cannon to the angle calculated along with the mouse movement.

Rotating cannon

4: Find the trajectory of the ball

Trajectory of a projectile is the path of an object moving in the air that is affected by external force such as gravity.

Trajectory of a projectile

As we can see, the trajectory is parabolic.

For simplicity, we will be considering the acceleration of the ball as zero.

The equation for the trajectory of a projectile is:

Equation of Trajectory of a Projectile

The y-coordinates, we will be finding them using the above equation. To find them we will require:

x-coordinates: Roughly consider from 1 to 200
g: Acceleration due to gravity(~10m/s²)
V₀: Initial velocity
θ: Angle at which the ball is launched

JS

/* ... */

function initializePathAndTrail() {
    for(let i = 0; i < numberOfPoints; i++) {
        path[i] = {
            x: 0,
            y: 0
        }
    }
}

initializePathAndTrail();

/* ... */

function getYTrajectoryPoint(pointerX) {
    let g = 10;
    let initialVelocity = 100;
    let radians = angle * (Math.PI / 180);

    let pointerY = (pointerX * Math.tan(radians)) - ((g * pointerX * pointerX) / (2 * initialVelocity * initialVelocity * Math.cos(radians) * Math.cos(radians)));

    return pointerY;
}

function createTrajectory() {
    let x = 10;
    for(let i = 0; i < numberOfPoints; i++) {
        let y = getYTrajectoryPoint(x);
        path[i].x = x;
        path[i].y = y;
        if(angle <= 60) { //To increase the distance between the points since the distance between the points for an angle above 60deg is greater while increasing by 15.
            x += 20;
        }
        else {
            x += 15;
        }
    }
}

/* ... */

function draw() {
    pen.fillStyle = "cyan";
    for(let i = 0; i < path.length - 1; i++) {
        pen.beginPath();
        pen.arc(path[i].x, path[i].y, 10, 0, Math.PI * 2);
        pen.fill();
    }
}

document.addEventListener("click", function() {
    cancelAnimationFrame(animationId);
    initializePathAndTrail();
    createTrajectory();
}
Enter fullscreen mode Exit fullscreen mode

We will get something like this:

Create trajectory

As you can see, the trajectory projected is based on the initial origin. So we need to translate all these points according to the new origin.

JS

/* ... */
function transformOrigin() {
    for(let i = 0; i < numberOfPoints; i++) {
        path[i].x = 100 + path[i].x;
        path[i].y = canvas.height - 108 - path[i].y;
    }
}

/* ... */

document.addEventListener("click", function() {

/* ... */

    transformOrigin();

/* ... */
});
Enter fullscreen mode Exit fullscreen mode

Transformed origin

5: Animating the ball

We have almost reached the result. We have figured out how to rotate the cannon, found the trajectory and now we will be animating the ball to travel along the trajectory whenever clicked.

To achieve this, we will be using requestAnimationFrame().

JS

/* ... */

function draw() {
    pen.clearRect(0, 0, canvas.width, canvas.height);
    pen.beginPath();
    pen.arc(path[params.index].x, path[params.index].y, 10, 0, Math.PI * 2);
    pen.fill();
        params.index ++;
    if(params.index >= numberOfPoints) {
        params.index = numberOfPoints - 1;
    }

    animationId = requestAnimationFrame(draw);
}

document.addEventListener("click", function() {
    cancelAnimationFrame(animationId);
    initializePathAndTrail();
    createTrajectory();
    transformOrigin();
    params.index = 0;

    let num = Math.floor((Math.random() * 10) % 4);
    pen.fillStyle = color[num];

    draw();
});
Enter fullscreen mode Exit fullscreen mode

Animating the ball

And done! Now just click and launch the ball in various angles and play!

6: Creating a trail behind the ball(optional)

I am sharing a tutorial here on how to create a trail by @uuuuuulala

I am also sharing the code below:

JS

/* ... */

function initializePathAndTrail() {
    for(let i = 0; i < numberOfPoints; i++) {
        path[i] = {
            x: 0,
            y: 0
        }
    }

    for(let i = 0; i < trailNumber; i++) {
        trail[i] = {
            x: 100,
            y: canvas.height - 108,
            dx: 0,
            dy: 0
        }
    }
}

/* ... */

function draw() {
    pen.clearRect(0, 0, canvas.width, canvas.height);
    let tempPointer = {
        x: 0,
        y: 0
    };

    tempPointer.x = path[params.index].x;
    tempPointer.y = path[params.index].y;

    trail.forEach((pointer, index) => {
        const previousPointer = (index === 0) ? tempPointer : trail[index - 1];

        const spring = params.spring;

        pointer.dx += (previousPointer.x - pointer.x) * spring;
        pointer.dy += (previousPointer.y - pointer.y) * spring;

        pointer.dx *= params.friction
        pointer.dy *= params.friction

        pointer.x += pointer.dx;
        pointer.y += pointer.dy;
    })

    pen.lineCap = "round";
    pen.beginPath();
    pen.moveTo(trail[0].x, trail[0].y);
    for(let i = 0; i < trail.length - 1; i++) {
        pen.lineWidth = trailNumber * 2 - i * 2;
        pen.globalAlpha = (i === 0) ? 1 : 0.1;
        pen.lineTo(trail[i].x, trail[i].y);
        pen.stroke();
    }
    params.index ++;
    if(params.index >= numberOfPoints) {
        params.index = numberOfPoints - 1;
    }

    animationId = requestAnimationFrame(draw);
}

document.addEventListener("click", function() {
    cancelAnimationFrame(animationId);
    initializePathAndTrail();
    createTrajectory();
    transformOrigin();
    params.index = 0;

    let num = Math.floor((Math.random() * 10) % 4);
    pen.strokeStyle = color[num];

    draw();
});
Enter fullscreen mode Exit fullscreen mode

Trail behind the ball

Hope you enjoyed this tutorial. Have a nice day.

SVG for the cannon and its support
Cannon
Cannon

<svg id="cannon" width="200" height="129" viewBox="0 0 200 129" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect x="14.2857" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="42.8571" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="28.5714" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="14.2857" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="42.8571" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="28.5714" y="71.4286" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="14.2857" y="42.8571" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="42.8571" y="42.8571" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="28.5714" y="42.8571" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect y="57.1429" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect y="71.4286" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect y="42.8571" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="42.8571" y="14.2857" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="28.5714" y="14.2857" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="14.2857" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="42.8571" y="28.5714" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="28.5714" y="28.5714" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="42.8571" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="42.8571" y="100" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="28.5714" y="100" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="42.8571" y="114.286" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="14.2857" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="42.8571" y="85.7143" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="28.5714" y="85.7143" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="185.714" y="57.1429" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="185.714" y="71.4286" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="185.714" y="42.8571" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="185.714" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="185.714" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="57.1429" y="57.1429" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="85.7143" y="57.1429" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="71.4286" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="57.1429" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="85.7143" y="71.4286" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="71.4286" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="57.1429" y="42.8571" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="85.7143" y="42.8571" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="71.4286" y="42.8571" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="142.857" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="171.429" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="157.143" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="142.857" y="71.4286" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="171.429" y="71.4286" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="157.143" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="142.857" y="42.8571" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="171.429" y="42.8571" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="157.143" y="42.8571" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="100" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="128.571" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="114.286" y="57.1429" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="100" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="128.571" y="71.4286" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="114.286" y="71.4286" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="100" y="42.8571" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="128.571" y="42.8571" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="114.286" y="42.8571" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="57.1429" y="14.2857" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="85.7143" y="14.2857" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="71.4286" y="14.2857" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="57.1429" y="28.5714" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="85.7143" y="28.5714" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="71.4286" y="28.5714" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="57.1429" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="71.4286" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="171.429" y="14.2857" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="142.857" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="171.429" y="28.5714" width="14.2857" height="14.2857" fill="#F7E1D4"/>
        <rect x="157.143" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="100" y="14.2857" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="100" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="128.571" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="114.286" y="28.5714" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="57.1429" y="100" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="85.7143" y="100" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="71.4286" y="100" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="57.1429" y="114.286" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="71.4286" y="114.286" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="57.1429" y="85.7143" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="85.7143" y="85.7143" width="14.2857" height="14.2857" fill="#FFA2C4"/>
        <rect x="71.4286" y="85.7143" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="171.429" y="100" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="142.857" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="171.429" y="85.7143" width="14.2857" height="14.2857" fill="#F365FF"/>
        <rect x="157.143" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="100" y="100" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="100" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="128.571" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
        <rect x="114.286" y="85.7143" width="14.2857" height="14.2857" fill="#FF357E"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

Support
Support

<svg id="support" width="200" height="110" viewBox="0 0 200 110" fill="none" xmlns="http://www.w3.org/2000/svg">
        <rect x="36.3636" y="54.5455" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="72.7273" y="54.5455" width="18.1818" height="18.1818" fill="#F365FF"/>
        <rect x="54.5455" y="54.5455" width="18.1818" height="18.1818" fill="#F7E1D4"/>
        <rect x="36.3636" y="72.7273" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="72.7273" y="72.7273" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="54.5455" y="72.7273" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="72.7273" y="36.3636" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="54.5455" y="36.3636" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="145.455" y="54.5455" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="145.455" y="72.7273" width="18.1818" height="18.1818" fill="#F7E1D4"/>
        <rect x="163.636" y="72.7273" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="90.9091" y="54.5455" width="18.1818" height="18.1818" fill="#F365FF"/>
        <rect x="127.273" y="54.5455" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="109.091" y="54.5455" width="18.1818" height="18.1818" fill="#F365FF"/>
        <rect x="90.9091" y="72.7273" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="127.273" y="72.7273" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="109.091" y="72.7273" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="90.9091" y="36.3636" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="127.273" y="36.3636" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="109.091" y="36.3636" width="18.1818" height="18.1818" fill="#F7E1D4"/>
        <rect x="72.7273" y="18.1818" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="90.9091" y="18.1818" width="18.1818" height="18.1818" fill="#FFA2C4"/>
        <rect x="109.091" y="18.1818" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="36.3636" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="72.7273" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="54.5455" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="145.455" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="181.818" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="163.636" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="90.9091" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="127.273" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="109.091" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="18.1818" y="72.7273" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect x="18.1818" y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
        <rect y="90.9091" width="18.1818" height="18.1818" fill="#FF357E"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)