<main>
  <canvas id="canvas">
    <p>Your browser does not support the HTML5 canvas tag.</p>
  </canvas>

  <div>
    <input type="number" name="angle" min="0" max="90" placeholder="Angle (deg)" />
    <input type="number" name="velocity" min="0" placeholder="Velocity (m/s)" />
  </div>

  <div>
    <button data-start>Start</button>
    <button data-reset>Reset</button>
  </div>

  <p data-results>
    <span data-time></span>
    <span data-height></span>
    <span data-distance></span>
  </p>
</main>
@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");

:root {
  --f-family: "Poppins", sans-serif;
  --f-size: 16px;

  --c-primary: #0d6efd;
  --c-primary-light: #3d8bfb;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: var(--f-family);
  font-size: var(--f-size);
}

html,
body {
  height: 100%;
  display: grid;
  place-items: center;
}

main {
  display: flex;
  gap: 1rem;
  flex-direction: column;
  align-items: flex-start;
}

@media screen and (max-width: 768px) {
  main {
    max-width: 80vw;
    align-items: center;
  }
}

[data-result]:empty {
  display: none;
}

canvas {
  border: 2px solid transparent;
  border-left-color: #000;
  border-bottom-color: #000;
  background-image: url("data:image/svg+xml,%0A%3Csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='smallGrid' width='8' height='8' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 8 0 L 0 0 0 8' fill='none' stroke='gray' stroke-width='0.5' /%3E%3C/pattern%3E%3Cpattern id='grid' width='80' height='80' patternUnits='userSpaceOnUse'%3E%3Crect width='80' height='80' fill='url(%23smallGrid)' /%3E%3Cpath d='M 80 0 L 0 0 0 80' fill='none' stroke='gray' stroke-width='1' /%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23smallGrid)' /%3E%3C/svg%3E%0A");
}

main > div {
  display: flex;
  gap: 10px;
}

input {
  width: 100%;
  padding: 10px;
  outline: none;
  border: 1px solid #ccc;
  border-radius: 5px;
}

button {
  width: 100%;
  border: none;
  outline: none;
  color: #fff;
  padding: 10px 20px;
  border-radius: 5px;
  background-color: var(--c-primary);
  cursor: pointer;
}

button:hover {
  background-color: var(--c-primary-light);
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

[data-results] {
  display: flex;
  gap: 10px;
}

[data-results] span:not(:last-child) {
  padding-right: 10px;
  border-right: 1px solid #ccc;
}
/*******************************************
 * Utilities                               *
 *******************************************/

const toRad = (deg) => {
  return (deg * Math.PI) / 180;
};

const randomColor = () => {
  const MAX_COLORS = 16777215;
  return `#${Math.floor(Math.random() * MAX_COLORS).toString(16)}`;
};

const drawPoint = (ctx, x, y, color = "#dc3545") => {
  ctx.beginPath();
  ctx.arc(x, y, 1, 0, 2 * Math.PI);
  ctx.fillStyle = color;
  ctx.fill();
};

/*******************************************
 * Projectile                              *
 *******************************************/

const GRAVITY = 9.8;

const getFlightTime = (velocity, rad) => {
  return (2 * velocity * Math.sin(rad)) / GRAVITY;
};

const getMaxHeight = (velocity, rad) => {
  return Math.pow(velocity * Math.sin(rad), 2) / (2 * GRAVITY);
};

const getDistance = (velocity, rad) => {
  return (Math.pow(velocity, 2) / GRAVITY) * Math.sin(rad * 2);
};

const getPlot = (velocity, rad, time) => {
  return {
    x: velocity * time * Math.cos(rad),
    y: velocity * time * Math.sin(rad) - (GRAVITY * Math.pow(time, 2)) / 2
  };
};

/*******************************************
 * Main Script                             *
 *******************************************/

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

const startButton = document.querySelector("[data-start]");
const resetButton = document.querySelector("[data-reset]");

const angleField = document.querySelector('[name="angle"]');
const velocityField = document.querySelector('[name="velocity"]');

const resultsField = document.querySelector("[data-results]");
const timeField = resultsField.querySelector("[data-time]");
const heightField = resultsField.querySelector("[data-height]");
const distanceField = resultsField.querySelector("[data-distance]");

document.addEventListener("DOMContentLoaded", () => {
  startButton.addEventListener("click", start);
  resetButton.addEventListener("click", reset);
  window.addEventListener("resize", setCanvasSize);
  setCanvasSize();
  reset();
});

const start = () => {
  const angle = +angleField.value;
  const velocity = +velocityField.value;

  if (angle > 90 || angle < 0 || velocity < 0) {
    return alert("Invalid input");
  }

  const radial = toRad(angle);
  const flightTime = getFlightTime(velocity, radial);
  const maxHeight = getMaxHeight(velocity, radial);
  const distance = getDistance(velocity, radial);

  timeField.innerHTML = `Flight time: ${flightTime.toFixed(2)}s`;
  heightField.innerHTML = `Max height: ${maxHeight.toFixed(2)}m`;
  distanceField.innerHTML = `Distance: ${distance.toFixed(2)}m`;

  drawProjectile(radial, velocity, flightTime);
  startButton.setAttribute("disabled", true);
};

const drawProjectile = (radial, velocity, flightTime) => {
  let time = 0;
  const color = randomColor();

  window.interval = setInterval(() => {
    const { x, y } = getPlot(velocity, radial, time);
    drawPoint(ctx, x, canvas.height - y, color);
    time += 0.01;

    if (time >= flightTime) {
      clearInterval(window.interval);
      startButton.removeAttribute("disabled");
    }
  }, 10);
};

const reset = () => {
  window.clearInterval(window?.interval);
  startButton.removeAttribute("disabled");
  angleField.value = velocityField.value = "";
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  timeField.innerHTML = "Flight time: 0.00s";
  heightField.innerHTML = "Max height: 0.00m";
  distanceField.innerHTML = "Distance: 0.00m";
};

const setCanvasSize = () => {
  canvas.width = window.innerWidth * 0.8;
  canvas.height = window.innerHeight / 2;
};

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.