projectile-motion.js (8167B)
1 const App = { 2 data: function() { 3 return { 4 // Data 5 height: 1, // The projectile's initial height (m) 6 initialVelocity: 5, // The projectile's initial velocity (N) 7 angle: 0, // The projectile's intial trajectory (degrees) 8 gravity: 9.8, // The acceleration due to gravity (m/s/s) 9 time: 0, // The time (s) 10 positions: [], // A list of the projectile's positions 11 12 // Simulation properties 13 active: false, // Whether the simulation is active 14 refreshRate: 0.01, // The simulation refresh rate (s) 15 intervalId: null, // The value returned by setInterval 16 infoVisible: false, 17 svg: null, 18 } 19 }, 20 computed: { 21 /** 22 * The position of the projectile 23 */ 24 position: function() { 25 let x = this.getAdj(this.angle, this.initialVelocity) * this.time; 26 let y = this.height + (this.getOpp(this.angle, this.initialVelocity) * this.time) - (0.5 * this.gravity * this.time * this.time); 27 return {x, y}; 28 }, 29 30 /** 31 * The current velocity of the projectile 32 */ 33 velocity: function() { 34 let x = this.getAdj(this.angle, this.initialVelocity); 35 let y = this.getOpp(this.angle, this.initialVelocity) - (this.gravity * this.time); 36 let total = Math.sqrt(x*x + y*y); 37 let angle = total != 0 ? this.getAng(y, x) : 0; 38 return {x, y, total, angle}; 39 }, 40 41 /** 42 * The coordinates of the launch vector arrow 43 */ 44 launchArrow: function() { 45 if (this.time !== 0 || this.initialVelocity === 0) { 46 return [ 47 this.position, 48 this.position, 49 this.position 50 ]; 51 } 52 else { 53 let x = this.position.x + this.getAdj(this.angle, this.initialVelocity / 4 + 0.4); 54 let y = this.position.y + this.getOpp(this.angle, this.initialVelocity / 4 + 0.4); 55 let arrow1x = x - this.getAdj(this.angle + 20, 0.25); 56 let arrow1y = y - this.getOpp(this.angle + 20, 0.25); 57 let arrow2x = x - this.getAdj(this.angle - 20, 0.25); 58 let arrow2y = y - this.getOpp(this.angle - 20, 0.25); 59 return [ 60 { x: arrow1x, y: arrow1y }, // Arrow side #1 61 { x: x, y: y }, // Center point 62 { x: arrow2x, y: arrow2y }, // Arrow side #2 63 ]; 64 } 65 } 66 }, 67 methods: { 68 /** 69 * Handle a keyup event (implements keyboard shortcuts) 70 * @param {object} e - The event args 71 */ 72 keyup: function(e) { 73 if (e.key === "Escape") { 74 if (this.infoVisible) this.infoVisible = false; 75 else window.location.href = "../"; 76 } 77 }, 78 79 /** 80 * Toggle whether the simulation is active 81 */ 82 toggle: function() { 83 this.active = !this.active; 84 if (this.active) this.intervalID = setInterval(this.update, this.refreshRate * 1000); 85 else clearInterval(this.intervalID); 86 }, 87 88 /** 89 * Reset the simulation 90 */ 91 reset: function() { 92 this.time = 0; 93 this.positions = []; 94 }, 95 96 /** 97 * Update the simulation 98 */ 99 update: function() { 100 // Update time 101 this.time += this.refreshRate; 102 103 // Update positions 104 this.positions.push(this.position); 105 106 // Stop simulation when the projectile hits the ground 107 if (this.position.y <= 0) this.toggle(); 108 }, 109 110 /** 111 * Get the length of the opposite side of a triangle 112 * @param {Number} angle The angle in degrees 113 * @param {Number} distance The length of the hypotenuse 114 * @returns {Number} The length of the opposite side 115 */ 116 getOpp: function(angle, distance) { 117 return Math.sin(angle / 360 * 2 * Math.PI) * distance; 118 }, 119 120 /** 121 * Get the length of the adjacent side of a triangle 122 * @param {Number} angle The angle in degrees 123 * @param {Number} distance The length of the hypotenuse 124 * @returns {Number} The length of the adjacent side 125 */ 126 getAdj: function(angle, distance) { 127 return Math.cos(angle / 360 * 2 * Math.PI) * distance; 128 }, 129 130 /** 131 * Get the angle of a triangle from two of it's sides 132 * @param {Number} opposite The length of the opposite side 133 * @param {Number} adjacent The length of the adjacent side 134 * @returns {Number} The angle in degrees 135 */ 136 getAng: function(opposite, adjacent) { 137 return Math.atan(opposite / adjacent) / (2 * Math.PI) * 360; 138 }, 139 }, 140 created: function() { 141 // Add keyup handler 142 window.addEventListener("keyup", this.keyup); 143 }, 144 destroyed: function() { 145 // Remove keyup handler 146 window.removeEventListener("keyup", this.keyup); 147 }, 148 } 149 150 151 152 // Create Vue app 153 function createApp() { 154 // Create app 155 let app = Vue.createApp(App).mount("#app"); 156 157 // Unhide app divs 158 document.getElementById("input").hidden = false; 159 document.getElementById("output").hidden = false; 160 document.getElementById("data").hidden = false; 161 document.getElementById("info").hidden = false; 162 163 // Enable zooming 164 let mobileZoomPanHandler = { 165 haltEventListeners: ["touchstart", "touchend", "touchmove", "touchleave", "touchcancel"], 166 init: function(options) { 167 var instance = options.instance; 168 let initialScale = 1; 169 let pannedX = 0; 170 let pannedY = 0; 171 172 // Init Hammer 173 // Listen only for pointer and touch events 174 this.hammer = Hammer(options.svgElement, { 175 inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput 176 }); 177 178 // Enable pinch 179 this.hammer.get("pinch").set({enable: true}); 180 181 // Handle double tap 182 this.hammer.on("doubletap", function(ev){ 183 instance.zoomIn() 184 }); 185 186 // Handle pan 187 this.hammer.on("panstart panmove", function(ev){ 188 // On pan start reset panned variables 189 if (ev.type === "panstart") { 190 pannedX = 0; 191 pannedY = 0; 192 } 193 194 // Pan only the difference 195 instance.panBy({x: ev.deltaX - pannedX, y: ev.deltaY - pannedY}); 196 pannedX = ev.deltaX; 197 pannedY = ev.deltaY; 198 }) 199 200 // Handle pinch 201 this.hammer.on("pinchstart pinchmove", function(ev) { 202 // Calculate true zoom center 203 const el = ev.target; 204 const rect = el.getBoundingClientRect(); 205 const pos = { 206 x: (ev.center.x - rect.left), 207 y: (ev.center.y - rect.top), 208 }; 209 210 // On pinch start remember initial zoom 211 if (ev.type === "pinchstart") { 212 initialScale = instance.getZoom(); 213 instance.zoomAtPoint(initialScale * ev.scale, {x: pos.x, y: pos.y}); 214 } 215 216 instance.zoomAtPoint(initialScale * ev.scale, {x: pos.x, y: pos.y}); 217 }); 218 219 // Prevent moving the page on some devices when panning over SVG 220 options.svgElement.addEventListener("touchmove", function(e){ e.preventDefault(); }); 221 }, 222 223 destroy: function(){ 224 this.hammer.destroy(); 225 } 226 } 227 app.svg = svgPanZoom("svg", { 228 dblClickZoomEnabled: false, 229 minZoom: 0.1, 230 maxZoom: 10, 231 customEventsHandler: mobileZoomPanHandler, 232 }); 233 }