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>
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);
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.
I placed the origin at the point I marked with a blue dot.
My Origin
{
x: 100,
y: canvas.height -108
}
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:
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.
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.
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.
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:
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:
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();
/* ... */
});
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();
});
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();
});
Hope you enjoyed this tutorial. Have a nice day.
SVG for the cannon and its support
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>
<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>
Top comments (0)