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

View File

@ -1,6 +1,27 @@
body { body {
margin: 50px; margin: 1em;
} }
canvas { 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;
} }

126
test.html
View File

@ -8,23 +8,119 @@
</head> </head>
<body> <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> <script>
const d = new Drawing(); let d1, d2, d3, d4;
d.setStroke('black', 4); {
d.polyline([0, 100], [0, 0], [100, 0]); const d = new Drawing('d1');
d.definePoint('p1', () => d.oscillatingPoint([25, 50], [100, 100], 5000)); d1 = d;
d.setStroke('red', 2); d.setTitle('Example Animation');
d.line([0, 0], 'p1'); d.setCaption('Lissajou figure (skewed)');
d.definePoint('p2', () => { d.addButtons();
const [x, y] = d.getPoint('p1'); d.setStroke('black', 4);
return [x, y - d.oscillatingValue(10, 40, 5000 / 3, Math.PI / 2)]; d.polyline([0, 100], [0, 0], [100, 0]);
}); d.definePoint('p1', () => d.oscillatingPoint([25, 50], [100, 100], 5000));
d.setFill('blue'); d.setStroke('red', 2);
d.square('p2', {trace: {age: 5000}}); d.line([0, 0], 'p1');
d.setFill('cyan'); d.definePoint('p2', () => {
d.circle('p1'); const [x, y] = d.getPoint('p1');
d.start(); return [x, y - d.oscillatingValue(10, 40, 5000 / 3, Math.PI / 2)];
});
d.setFill('blue');
d.square('p2', {trace: {age: 5000}});
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> </script>
</body> </body>
</html> </html>