Skip to content

Commit 4a5a4cc

Browse files
Merge pull request #757 from vedansht2211/main
Improve projectile motion graph visualization
2 parents cf95205 + 14711cd commit 4a5a4cc

1 file changed

Lines changed: 162 additions & 21 deletions

File tree

web-app/js/projects/projectile-motion.js

Lines changed: 162 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function getProjectileMotionHTML() {
1010
</div>
1111
<div class="control-group">
1212
<label for="projAngle">Launch Angle (°)</label>
13-
<input id="projAngle" type="number" min="1" max="89" value="45">
13+
<input id="projAngle" type="number" min="1" max="90" value="45">
1414
</div>
1515
</div>
1616
@@ -121,7 +121,6 @@ function getProjectileMotionHTML() {
121121
</style>
122122
`;
123123
}
124-
125124
function initProjectileMotion() {
126125
const g = 9.81;
127126
const canvas = document.getElementById('projectileCanvas');
@@ -168,17 +167,22 @@ function initProjectileMotion() {
168167
const marginBottom = 35;
169168
const marginTop = 20;
170169

171-
const xMax = Math.max(...x, 10) * 1.1;
172-
const yMax = Math.max(...y, 10) * 1.1;
170+
const maxValue = Math.max(
171+
Math.max(...x, 10),
172+
Math.max(...y, 10)
173+
) * 1.1;
174+
175+
const xMax = maxValue;
176+
const yMax = maxValue;
173177

174178
const usableWidth = width - marginLeft - 20;
175179
const usableHeight = height - marginTop - marginBottom;
176180

177-
// Compute a uniform scale (pixels per meter) so vertical and horizontal scaling is identical
178-
const scale = Math.min(usableWidth / xMax, usableHeight / yMax);
181+
const scaleX = usableWidth / xMax;
182+
const scaleY = usableHeight / yMax;
179183

180-
const mapX = v => marginLeft + v * scale;
181-
const mapY = v => height - marginBottom - v * scale;
184+
const mapX = v => marginLeft + v * scaleX;
185+
const mapY = v => height - marginBottom - v * scaleY;
182186

183187
ctx.clearRect(0, 0, width, height);
184188

@@ -196,8 +200,49 @@ function initProjectileMotion() {
196200

197201
ctx.fillStyle = "#64748b";
198202
ctx.font = "12px Arial";
199-
ctx.fillText("Height (m)", 8, marginTop + 12);
200-
ctx.fillText("Distance (m)", width - 95, height - 10);
203+
204+
const xTicks = 5;
205+
const yTicks = 5;
206+
207+
for (let t = 0; t <= xTicks; t++) {
208+
const value = (xMax / xTicks) * t;
209+
const xPos = marginLeft + value * scaleX;
210+
211+
ctx.beginPath();
212+
ctx.moveTo(xPos, height - marginBottom);
213+
ctx.lineTo(xPos, height - marginBottom + 6);
214+
ctx.stroke();
215+
216+
ctx.fillText(
217+
value.toFixed(0),
218+
xPos - 10,
219+
height - marginBottom + 20
220+
);
221+
}
222+
223+
for (let t = 0; t <= yTicks; t++) {
224+
const value = (yMax / yTicks) * t;
225+
const yPos = height - marginBottom - value * scaleY;
226+
227+
ctx.beginPath();
228+
ctx.moveTo(marginLeft - 6, yPos);
229+
ctx.lineTo(marginLeft, yPos);
230+
ctx.stroke();
231+
232+
ctx.fillText(
233+
value.toFixed(0),
234+
marginLeft - 25,
235+
yPos + 4
236+
);
237+
}
238+
239+
ctx.fillText("Distance (m)", width / 2 - 40, height);
240+
241+
ctx.save();
242+
ctx.translate(15, height / 2 + 40);
243+
ctx.rotate(-Math.PI / 2);
244+
ctx.fillText("Height (m)", 0, 0);
245+
ctx.restore();
201246

202247
if (x.length > 1) {
203248
ctx.strokeStyle = "#2563eb";
@@ -224,27 +269,30 @@ function initProjectileMotion() {
224269
const speed = Number(speedInput.value);
225270
const angle = Number(angleInput.value);
226271

227-
// Reset previous states
228272
resultEl.style.color = "";
229273

230-
// Comprehensive Validation Check
231274
if (isNaN(speed) || speed < 1 || speed > 200) {
232275
resultEl.textContent = "❌ Error: Speed must be between 1 and 200 m/s.";
233276
resultEl.style.color = "#ef4444";
234277
return;
235278
}
236279

237-
if (isNaN(angle) || angle < 1 || angle > 89) {
238-
resultEl.textContent = "❌ Error: Angle must be between 1° and 89°.";
280+
if (isNaN(angle) || angle < 1 || angle > 90) {
281+
resultEl.textContent = "❌ Error: Angle must be between 1° and 90°.";
239282
resultEl.style.color = "#ef4444";
240283
return;
241284
}
242285

243286
const stats = projectileStats(speed, angle);
244287
const points = trajectoryPoints(speed, angle);
245288

246-
const xMax = Math.max(...points.x, 10) * 1.1;
247-
const yMax = Math.max(...points.y, 10) * 1.1;
289+
const maxValue = Math.max(
290+
Math.max(...points.x, 10),
291+
Math.max(...points.y, 10)
292+
) * 1.1;
293+
294+
const xMax = Math.max(maxValue, 10);
295+
const yMax = Math.max(maxValue, 10);
248296

249297
const marginLeft = 50;
250298
const marginBottom = 35;
@@ -256,11 +304,16 @@ function initProjectileMotion() {
256304
const usableWidth = width - marginLeft - 20;
257305
const usableHeight = height - marginTop - marginBottom;
258306

259-
260-
const scale = Math.min(usableWidth / xMax, usableHeight / yMax);
307+
const scaleX = usableWidth / xMax;
308+
const scaleY = usableHeight / yMax;
309+
310+
const mapX = v => marginLeft + v * scaleX;
311+
const mapY = v => height - marginBottom - v * scaleY;
261312

262-
const mapX = v => marginLeft + v * scale;
263-
const mapY = v => height - marginBottom - v * scale;
313+
const maxY = Math.max(...points.y);
314+
const peakIndex = points.y.indexOf(maxY);
315+
316+
let showPeakGuides = false;
264317

265318
drawTrajectory(points.x, points.y);
266319

@@ -287,6 +340,52 @@ function initProjectileMotion() {
287340
ctx.lineTo(width - 20, height - marginBottom);
288341
ctx.stroke();
289342

343+
ctx.fillStyle = "#64748b";
344+
ctx.font = "12px Arial";
345+
346+
const xTicks = 5;
347+
const yTicks = 5;
348+
349+
for (let t = 0; t <= xTicks; t++) {
350+
const value = (xMax / xTicks) * t;
351+
const xPos = marginLeft + value * scaleX;
352+
353+
ctx.beginPath();
354+
ctx.moveTo(xPos, height - marginBottom);
355+
ctx.lineTo(xPos, height - marginBottom + 6);
356+
ctx.stroke();
357+
358+
ctx.fillText(
359+
value.toFixed(0),
360+
xPos - 10,
361+
height - marginBottom + 20
362+
);
363+
}
364+
365+
for (let t = 0; t <= yTicks; t++) {
366+
const value = (yMax / yTicks) * t;
367+
const yPos = height - marginBottom - value * scaleY;
368+
369+
ctx.beginPath();
370+
ctx.moveTo(marginLeft - 6, yPos);
371+
ctx.lineTo(marginLeft, yPos);
372+
ctx.stroke();
373+
374+
ctx.fillText(
375+
value.toFixed(0),
376+
marginLeft - 25,
377+
yPos + 4
378+
);
379+
}
380+
381+
ctx.fillText("Distance (m)", width / 2 - 40, height);
382+
383+
ctx.save();
384+
ctx.translate(15, height / 2 + 40);
385+
ctx.rotate(-Math.PI / 2);
386+
ctx.fillText("Height (m)", 0, 0);
387+
ctx.restore();
388+
290389
ctx.strokeStyle = "#2563eb";
291390
ctx.lineWidth = 3;
292391
ctx.beginPath();
@@ -303,11 +402,53 @@ function initProjectileMotion() {
303402

304403
if (i > 0) {
305404
const last = i - 1;
405+
const ballX = mapX(points.x[last]);
406+
const ballY = mapY(points.y[last]);
306407

307408
ctx.fillStyle = "#ef4444";
308409
ctx.beginPath();
309-
ctx.arc(mapX(points.x[last]), mapY(points.y[last]), 6, 0, Math.PI * 2);
410+
ctx.arc(ballX, ballY, 6, 0, Math.PI * 2);
310411
ctx.fill();
412+
413+
if (last >= peakIndex) {
414+
showPeakGuides = true;
415+
}
416+
417+
if (showPeakGuides) {
418+
const peakX = mapX(points.x[peakIndex]);
419+
const peakY = mapY(points.y[peakIndex]);
420+
421+
ctx.setLineDash([5, 5]);
422+
ctx.strokeStyle = "#ef4444";
423+
ctx.lineWidth = 1.5;
424+
425+
ctx.beginPath();
426+
ctx.moveTo(peakX, peakY);
427+
ctx.lineTo(peakX, height - marginBottom);
428+
ctx.stroke();
429+
430+
ctx.beginPath();
431+
ctx.moveTo(marginLeft, peakY);
432+
ctx.lineTo(peakX, peakY);
433+
ctx.stroke();
434+
435+
ctx.setLineDash([]);
436+
437+
ctx.fillStyle = "#ef4444";
438+
ctx.font = "12px Arial";
439+
440+
ctx.fillText(
441+
`Hmax: ${maxY.toFixed(2)} m`,
442+
peakX + 10,
443+
peakY - 10
444+
);
445+
446+
ctx.fillText(
447+
`${points.x[peakIndex].toFixed(2)} m`,
448+
peakX - 20,
449+
height - marginBottom + 20
450+
);
451+
}
311452
}
312453

313454
i++;

0 commit comments

Comments
 (0)