Code a cannon to shoot colored balls when clicked using Vanilla JS
Tags: codepen, webdev, javascript, tutorial
Published: 2024-02-09 08:47:23

<p>Created a shooting cannon using the trajectory of projectile motion with just a click.</p>

{% codepen https://codepen.io/Pujaa-M/pen/KKEoLqq %}

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>
```

***CSS***
```
* {
  margin: 0;
  padding: 0;
}

html, body {
  height: 100%;
}
```

***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);
```

![Canvas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/olasrhdh68smhd6oft2b.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/maifi1vpx1qr162mfktn.png)

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

***My Origin***
```
{
  x: 100,
  y: canvas.height -108
}
```

![Cannon position](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fozevltxd72p1oufy290.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jyssm4yv0rz2us588y42.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/75ppi3c70podvdkefaec.png)

***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);
});

/* ... */
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d71wlo006uto4zkpben1.gif)

##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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cig0zlbifm195u5uqrlt.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/161vvg8dpf2ida9tclkw.png)

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();
}
```

We will get something like this:

![Create trajectory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/892j9q6cwtnwcwtv0o12.gif)

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();

/* ... */
});
```

![Transformed origin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vvbha4bdb2e8xkw795wq.gif)

##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();
});
```

![Animating the ball](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59tfkzvffymict3nymtm.gif)

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](https://dev.to/uuuuuulala/coding-an-interactive-and-damn-satisfying-cursor-7-simple-steps-2kb-of-code-1c8b) 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();
});
```

![Trail behind the ball](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/67tfg4ib91j6osdembud.gif)

Hope you enjoyed this tutorial. Have a nice day.

***SVG for the cannon and its support***
Cannon
![Cannon](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kuebtk7xe71hjqukwch3.png)
```
<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>
```

Support
![Support](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fhn5i6fnu4eeip2119e.png)
```
<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>
```