commit c7e7481f439a745dc4ea663066ba974a640f758d
parent 864a1f61ba8051553fe9d713dafb9ae107aa6369
Author: AsherMorgan <59518073+AsherMorgan@users.noreply.github.com>
Date: Wed, 17 Mar 2021 10:12:07 -0700
Add projectile motion simulation
Diffstat:
4 files changed, 194 insertions(+), 1 deletion(-)
diff --git a/index.html b/index.html
@@ -22,6 +22,7 @@
<li><a title="Atwood Machine" href="simulations/atwood-machine.html">Atwood Machine</a></li>
<li><a title="Circular Motion" href="simulations/circular-motion.html">Circular Motion</a></li>
<li><a title="Horizontal Motion" href="simulations/horizontal-motion.html">Horizontal Motion</a></li>
+ <li><a title="Projectile Motion" href="simulations/projectile-motion.html">Projectile Motion</a></li>
</ul>
</body>
</html>
diff --git a/simulations/projectile-motion.html b/simulations/projectile-motion.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>Projectile Motion Simulation</title>
+ <meta name="Description" content="Projectile motion physics simulation and calculator">
+ <meta name="viewport" content="width=device-width">
+ <link rel="icon" type="image/png" href="../images/favicon-32.png">
+ <link rel="apple-touch-icon" href="../images/favicon-180.png">
+ <script src="https://cdn.jsdelivr.net/npm/vue@3.0.6"></script>
+ <link rel="stylesheet" href="styles.css">
+ <script src="projectile-motion.js"></script>
+ </head>
+ <body onload="createApp()">
+ <div id="app">
+ <header>
+ <a title="Home" href="../" class="icon"><img alt="" src="../images/home.svg"></a>
+ <h1>Projectile Motion</h1>
+ </header>
+
+ <noscript>
+ <p>This simulation requires JavaScript</p>
+ </noscript>
+
+ <div id="input" hidden>
+ <section>
+ <label for="heightInput"><b>Height:</b> {{ height.toFixed(1) }} m</label>
+ <input type="range" min="0" max="10" step="0.1" v-model.number="height" @input="reset" :disabled="active" @dblclick="height=1" id="heightInput">
+ </section>
+ <section>
+ <label for="initialVelocityInput"><b>Velocity:</b> {{ initialVelocity.toFixed(1) }} m/s</label>
+ <input type="range" min="0" max="10" step="0.1" v-model.number="initialVelocity" @input="reset" :disabled="active" @dblclick="initialVelocity=5" id="initialVelocityInput">
+ </section>
+ <section>
+ <label for="angleInput"><b>Angle:</b> {{ angle.toFixed(0) }}<sup>o</sup></label>
+ <input type="range" min="-90" max="90" step="1" v-model.number="angle" @input="reset" :disabled="active" @dblclick="angle=0" id="angleInput">
+ </section>
+ <section>
+ <label for="gravityInput"><b>Gravity:</b> {{ gravity.toFixed(1) }} m/s<sup>2</sup></label>
+ <input type="range" min="0" max="10" step="0.1" v-model.number="gravity" @input="reset" :disabled="active" @dblclick="gravity=9.8" id="gravityInput">
+ </section>
+ </div>
+
+ <div id="output" hidden>
+ <div id="controls">
+ <button @click="toggle" class="icon" :title="active ? 'Pause' : (time === 0 ? 'Start' : 'Resume')" :disabled="time !== 0 && position.y <= 0">
+ <img alt="" :src="active ? '../images/pause.svg' : '../images/play.svg'">
+ </button>
+ <button @click="update" class="icon" title="Step Forward" :disabled="(time !== 0 && position.y <= 0) || active">
+ <img alt="" src="../images/step-forward.svg">
+ </button>
+ <button @click="reset" class="icon" title="Reset" :disabled="time === 0">
+ <img alt="" src="../images/reset.svg">
+ </button>
+ </div>
+ <svg width="400px" viewBox="-0.1 -0.1 10.1 10.2">
+ <!-- Ground -->
+ <line x1="-0.1" y1="10.1" x2="10" y2="10.1" stroke-width="0.1" stroke="#404040"></line>
+
+ <!-- Path -->
+ <circle v-for="position in positions" :cx="position.x" :cy="10 - position.y" r="0.05" fill="#808080"></circle>
+
+ <!-- Projectile -->
+ <circle :cx="position.x" :cy="10 - position.y" r="0.1" fill="#ff0000"></circle>
+ </svg>
+ </div>
+
+ <div id="data" hidden>
+ <label><b>Time:</b> {{ time.toFixed(2) }} s</label>
+ <label><b>Position:</b> {{ position.x.toFixed(2) }} m, {{ position.y > 0 ? position.y.toFixed(2) : 0.0.toFixed(2) }} m</label>
+ <label><b>Velocity:</b> {{ velocity.total.toFixed(2) }} m/s at {{ velocity.angle.toFixed(1) }}<sup>o</sup></label>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/simulations/projectile-motion.js b/simulations/projectile-motion.js
@@ -0,0 +1,114 @@
+const App = {
+ data: function() {
+ return {
+ // Data
+ height: 1, // The projectile's initial height (m)
+ initialVelocity: 5, // The projectile's initial velocity (N)
+ angle: 0, // The projectile's intial trajectory (degrees)
+ gravity: 9.8, // The acceleration due to gravity (m/s/s)
+ time: 0, // The time (s)
+ positions: [], // A list of the projectile's positions
+
+ // Simulation properties
+ active: false, // Whether the simulation is active
+ refreshRate: 0.01, // The simulation refresh rate (s)
+ intervalId: null, // The value returned by setInterval
+ }
+ },
+ computed: {
+ /**
+ * The position of the projectile
+ */
+ position: function() {
+ let x = this.getAdj(this.angle, this.initialVelocity) * this.time;
+ let y = this.height + (this.getOpp(this.angle, this.initialVelocity) * this.time) - (0.5 * this.gravity * this.time * this.time);
+ return {x, y};
+ },
+
+ /**
+ * The current velocity of the projectile.
+ */
+ velocity: function() {
+ let x = this.getAdj(this.angle, this.initialVelocity);
+ let y = this.getOpp(this.angle, this.initialVelocity) - (this.gravity * this.time);
+ let total = Math.sqrt(x*x + y*y);
+ let angle = total != 0 ? this.getAng(y, x) : 0;
+ return {x, y, total, angle};
+ },
+ },
+ methods: {
+ /**
+ * Toggle whether the simulation is active
+ */
+ toggle: function() {
+ this.active = !this.active;
+ if (this.active) this.intervalID = setInterval(this.update, this.refreshRate * 1000);
+ else clearInterval(this.intervalID);
+ },
+
+ /**
+ * Reset the simulation
+ */
+ reset: function() {
+ this.time = 0;
+ this.positions = [];
+ },
+
+ /**
+ * Update the simulation
+ */
+ update: function() {
+ // Update time
+ this.time += this.refreshRate;
+
+ // Update positions
+ this.positions.push(this.position);
+
+ // Stop simulation when the projectile hits the ground
+ if (this.position.y <= 0) this.toggle();
+ },
+
+ /**
+ * Get the length of the opposite side of a triangle
+ * @param {Number} angle The angle in degrees
+ * @param {Number} distance The length of the hypotenuse
+ * @returns {Number} The length of the opposite side
+ */
+ getOpp: function(angle, distance) {
+ return Math.sin(angle / 360 * 2 * Math.PI) * distance;
+ },
+
+ /**
+ * Get the length of the adjacent side of a triangle
+ * @param {Number} angle The angle in degrees
+ * @param {Number} distance The length of the hypotenuse
+ * @returns {Number} The length of the adjacent side
+ */
+ getAdj: function(angle, distance) {
+ return Math.cos(angle / 360 * 2 * Math.PI) * distance;
+ },
+
+ /**
+ * Get the angle of a triangle from two of it's sides
+ * @param {Number} opposite The length of the opposite side
+ * @param {Number} adjacent The length of the adjacent side
+ * @returns {Number} The angle in degrees
+ */
+ getAng: function(opposite, adjacent) {
+ return Math.atan(opposite / adjacent) / (2 * Math.PI) * 360;
+ },
+ },
+}
+
+
+
+// Create Vue app
+function createApp() {
+ // Create app
+ Vue.createApp(App).mount("#app");
+
+ // Unhide app divs
+ document.getElementById("input").hidden = false;
+ document.getElementById("output").hidden = false;
+ document.getElementById("data").hidden = false;
+}
diff --git a/simulations/styles.css b/simulations/styles.css
@@ -66,12 +66,15 @@ noscript {
}
#output {
+ position: relative;
+ left: 0px;
margin: 10px;
width: min-content;
border: 1px solid #000000;
}
#output>#controls {
- float: right;
+ position: absolute;
+ right: 0px;
padding: 5px;
padding-bottom: 0px;
}