projectile

This commit is contained in:
Ladd Hoffman 2024-06-21 15:20:13 -05:00
parent e53d88c991
commit 4c093c1fad
3 changed files with 299 additions and 97 deletions

245
draw.js
View File

@ -1,19 +1,18 @@
class Drawing {
constructor() {
const div = document.createElement('div');
const buttonsDiv = document.createElement('div');
this.startButton = document.createElement('button');
this.startButton.onclick = () => this.start();
this.startButton.innerHTML = "Start";
this.stopButton = document.createElement('button');
this.stopButton.onclick = () => this.stop();
this.stopButton.innerHTML = "Stop";
constructor(divId) {
this.div = document.getElementById(divId);
this.div.classList.add('drawing');
this.div.classList.add('container');
this.buttonsDiv = document.createElement('div');
this.titleDiv = document.createElement('div');
this.titleDiv.classList.add('title');
this.captionDiv = document.createElement('div');
this.captionDiv.classList.add('caption');
this.canvas = document.createElement('canvas');
buttonsDiv.appendChild(this.startButton);
buttonsDiv.appendChild(this.stopButton);
div.appendChild(buttonsDiv);
div.appendChild(this.canvas);
document.body.appendChild(div);
this.div.appendChild(this.titleDiv);
this.div.appendChild(this.buttonsDiv);
this.div.appendChild(this.canvas);
this.div.appendChild(this.captionDiv);
this.ctx = this.canvas.getContext('2d');
this.sequence = [];
this.t = 0;
@ -21,34 +20,128 @@ class Drawing {
this.dt = 0;
this.points = {};
this.stopped = true;
this.frame = [[-50, -50], [150, 150]];
this.frame = [[-10, -10], [110, 110]];
this.frameMargin = 10;
this.scale = 1.0;
this.canvas.width = (this.frame[1][0] - this.frame[0][0]) * this.scale;
this.canvas.height = (this.frame[1][1] - this.frame[0][1]) * this.scale;
this.speed = 1.0;
this.rendered = false;
this.updateCanvasSize();
}
addButtons() {
if (!this.buttonsAdded) {
this.startButton = document.createElement('button');
this.startButton.onclick = () => this.start();
this.startButton.innerHTML = "Start";
this.stopButton = document.createElement('button');
this.stopButton.onclick = () => this.stop();
this.stopButton.innerHTML = "Stop";
this.buttonsDiv.appendChild(this.startButton);
this.buttonsDiv.appendChild(this.stopButton);
this.buttonsAdded = true;
this.updateButtonStates();
}
}
updateButtonStates() {
if (this.stopped) {
this.stopButton.disabled = true;
this.startButton.disabled = false;
} else {
this.stopButton.disabled = false;
this.startButton.disabled = true;
}
}
setTitle(title) {
this.titleDiv.innerHTML = title;
}
setCaption(caption) {
this.captionDiv.innerHTML = caption;
}
start() {
if (this.stopped) {
this.elideInterval = true;
this.stopped = false;
}
if (this.onStartFn) {
this.onStartFn();
}
this.addButtons();
this.updateButtonStates();
this.animate();
}
stop() {
this.stopped = true;
this.updateButtonStates();
}
render() {
for (let action of this.sequence) {
action();
}
this.rendered = true;
}
animate() {
this.ctx.reset();
this.render();
if (!this.stopped) {
requestAnimationFrame((prevt) => {
const rt = document.timeline.currentTime;
const elapsed = rt - prevt;
if (this.elideInterval) {
this.dt = 0;
this.elideInterval = false;
} else {
this.dt = (rt - this.rt + elapsed) * this.speed;
}
this.t += this.dt;
this.rt = rt;
this.animate();
});
}
}
pixel([x, y]) {
return [
(x - this.frame[0][0]) * this.scale,
this.canvas.height - (y - this.frame[0][1]) * this.scale
(x - this.frame[0][0]) * this.scale + this.frameMargin,
this.canvas.height - (y - this.frame[0][1]) * this.scale - this.frameMargin
];
}
onStart(fn) {
this.onStartFn = fn;
}
setSpeed(speed) {
this.speed = speed;
}
updateCanvasSize() {
this.canvas.width = (this.frame[1][0] - this.frame[0][0]) * this.scale + 2 * this.frameMargin;
this.canvas.height = (this.frame[1][1] - this.frame[0][1]) * this.scale + 2 * this.frameMargin;
if (this.rendered) {
this.render();
}
}
setFrame([x1, y1], [x2, y2]) {
this.frame = [[x1, y1], [x2, y2]];
this.canvas.width = (this.frame[1][0] - this.frame[0][0]) * this.scale;
this.canvas.height = (this.frame[1][1] - this.frame[0][1]) * this.scale;
this.updateCanvasSize();
}
setFrameMargin(frameMargin) {
this.frameMargin = frameMargin;
this.updateCanvasSize();
}
setScale(zoom) {
this.scale = zoom;
this.canvas.width = (this.frame[1][0] - this.frame[0][0]) * this.scale;
this.canvas.height = (this.frame[1][1] - this.frame[0][1]) * this.scale;
this.updateCanvasSize();
}
setStroke(style, width) {
@ -64,6 +157,24 @@ class Drawing {
});
}
definePoint(name, fn) {
this.points[name] = fn;
}
getPoint(p) {
if (typeof p === 'string') {
const fn = this.points[p];
if (!fn) {
const e = new Error;
e.message = `Point '${p}' is not defined`;
throw e;
}
return fn();
} else {
return p;
}
}
line(p1, p2) {
this.sequence.push(() => {
this.ctx.beginPath();
@ -73,6 +184,15 @@ class Drawing {
});
}
arrow(p1, p2) {
this.sequence.push(() => {
this.ctx.beginPath();
this.ctx.moveTo(...this.pixel(this.getPoint(p1)));
this.ctx.lineTo(...this.pixel(this.getPoint(p2)));
this.ctx.stroke();
});
}
polyline(...points) {
this.sequence.push(() => {
this.ctx.beginPath();
@ -115,7 +235,7 @@ class Drawing {
if (oldest >= 0) {
history = history.slice(oldest);
}
this.ctx.strokeStyle = 'black';
this.ctx.strokeStyle = 'gray';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(...this.pixel(history[0].p));
@ -128,78 +248,43 @@ class Drawing {
}
this.ctx.stroke();
fn(x, y);
})
});
return {
reset: () => {
history = [];
distance = 0;
}
};
}
square(p, opts) {
const size = opts?.size ?? 10;
this.object(p, opts, (x, y) => {
return this.object(p, opts, (x, y) => {
this.ctx.fillRect(...this.pixel([x - size / 2, y + size / 2]), size * this.scale, size * this.scale);
});
}
circle(p, opts) {
const radius = opts?.radius ?? 5;
this.object(p, opts, (x, y) => {
return this.object(p, opts, (x, y) => {
this.ctx.beginPath();
this.ctx.ellipse(...this.pixel([x, y]), radius * this.scale, radius * this.scale, 0, 0, 2*Math.PI);
this.ctx.fill();
});
}
definePoint(name, fn) {
this.points[name] = fn;
func(opts, fn) {
const origin = opts?.origin ?? [0, 0];
const domain = opts?.domain ?? [this.frame[0][0], this.frame[1][0]];
const step = opts?.step ?? 1;
this.sequence.push(() => {
this.ctx.beginPath();
this.ctx.moveTo(...this.pixel([domain[0], fn(domain[0])]));
for (let x = domain[0] + step; x <= domain[1]; x += step) {
this.ctx.lineTo(...this.pixel([x, fn(x)]));
}
getPoint(p) {
if (typeof p === 'string') {
const fn = this.points[p];
if (!fn) {
const e = new Error;
e.message = `Point '${p}' is not defined`;
throw e;
}
return fn();
} else {
return p;
}
}
render() {
for (let action of this.sequence) {
action();
}
}
animate() {
this.ctx.reset();
this.render();
if (!this.stopped) {
requestAnimationFrame((prevt) => {
const rt = document.timeline.currentTime;
const elapsed = rt - prevt;
if (this.elideInterval) {
this.dt = 0;
this.elideInterval = false;
} else {
this.dt = (rt - this.rt + elapsed) * this.speed;
}
this.t += this.dt;
this.rt = rt;
this.animate();
});
}
}
stop() {
this.stopped = true;
}
start() {
if (this.stopped) {
this.elideInterval = true;
this.stopped = false;
}
this.animate();
this.ctx.lineTo(...this.pixel([domain[1], fn(domain[1])]));
this.ctx.stroke();
})
}
}

View File

@ -1,6 +1,27 @@
body {
margin: 50px;
margin: 1em;
}
canvas {
border: 1px red dashed;
/* border: 1px red dashed; */
margin: 0.5em;
}
button {
margin: 0.25em;
}
.drawing.container {
/* border: 1px black solid; */
display: inline-block;
margin: 1em;
}
.drawing > .title {
font-size: large;
}
.drawing > .caption {
font-size: small;
text-align: center;
}

View File

@ -8,8 +8,22 @@
</head>
<body>
<div id="d1"></div>
<p>
Some body text
</p>
<div id="d2"></div>
<div id="d3"></div>
<div id="d4"></div>
<div id="d5"></div>
<script>
const d = new Drawing();
let d1, d2, d3, d4;
{
const d = new Drawing('d1');
d1 = d;
d.setTitle('Example Animation');
d.setCaption('Lissajou figure (skewed)');
d.addButtons();
d.setStroke('black', 4);
d.polyline([0, 100], [0, 0], [100, 0]);
d.definePoint('p1', () => d.oscillatingPoint([25, 50], [100, 100], 5000));
@ -24,7 +38,89 @@
d.setFill('cyan');
d.circle('p1');
d.start();
}
{
const d = new Drawing('d2');
d2 = d;
d.setTitle('Sine Wave');
d.setCaption('y = sin(x)');
d.setStroke('black', 4);
d.line([0, 0], [2*Math.PI, 0]);
d.line([0, -1], [0, 1]);
d.setStroke('red', 2);
d.setFrame([0, -1], [2*Math.PI, 1]);
d.setScale(100 / Math.PI);
d.func({step: 0.1}, (x) => Math.sin(x));
d.render();
}
{
const d = new Drawing('d3');
d3 = d;
d.setTitle('Oscillating Sine Wave');
d.setCaption('y = sin(x) * sin(t)');
d.setStroke('black', 4);
d.line([0, 0], [2*Math.PI, 0]);
d.line([0, -1], [0, 1]);
d.setStroke('red', 2);
d.setFrame([0, -1], [2*Math.PI, 1]);
d.setScale(100 / Math.PI);
d.func({step: 0.1}, (x) => Math.sin(x) * d.oscillatingValue(-1, 1, 500));
d.start();
}
{
const d = new Drawing('d4');
d4 = d;
d.setTitle('Travelling Sine Wave');
d.setCaption('y = sin(x + t)');
d.setStroke('black', 4);
d.line([0, 0], [2*Math.PI, 0]);
d.line([0, -1], [0, 1]);
d.setStroke('red', 2);
d.setFrame([0, -1], [2*Math.PI, 1]);
d.setScale(100 / Math.PI);
d.func({step: 0.1}, (x) => Math.sin(x + 2*Math.PI*d.t / 1000));
d.start();
}
{
const d = new Drawing('d5');
d5 = d;
d.setTitle('Projectile');
d.setCaption('y = v0y * t - g * t^2<br>x = v0x * t');
d.setFrame([0, 0], [300, 100]);
d.setStroke('black', 4);
d.polyline([0, 100], [0, 0], [300, 0]);
const v0 = 80;
const angle = Math.PI / 4;
const v0x = v0 * Math.cos(angle);
const v0y = v0 * Math.sin(angle);
d.definePoint('p1', () => {
const t = d.t / 1000;
const x = v0x * t;
const y = v0y * t - 9.81 * t**2;
if (t > 0 && (y <= 0 || x >= d.frame[1][0])) {
d.stop();
d.t = 0;
}
return [x, y];
})
d.setStroke('green', 1);
d.line([0, 0], [v0x, v0y]);
d.setFill('blue');
const projectile = d.circle('p1', {trace: {age: -1}})
d.onStart(() => {
if (d.t == 0) {
projectile.reset();
}
})
d.start();
}
</script>
</body>
</html>