physics/draw.js

290 lines
7.3 KiB
JavaScript

class Drawing {
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');
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;
this.rt = 0;
this.dt = 0;
this.points = {};
this.stopped = true;
this.frame = [[-10, -10], [110, 110]];
this.frameMargin = 10;
this.scale = 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]) {
return [
(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.updateCanvasSize();
}
setFrameMargin(frameMargin) {
this.frameMargin = frameMargin;
this.updateCanvasSize();
}
setScale(zoom) {
this.scale = zoom;
this.updateCanvasSize();
}
setStroke(style, width) {
this.sequence.push(() => {
this.ctx.strokeStyle = style;
this.ctx.lineWidth = width;
});
}
setFill(style) {
this.sequence.push(() => {
this.ctx.fillStyle = style;
});
}
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();
this.ctx.moveTo(...this.pixel(this.getPoint(p1)));
this.ctx.lineTo(...this.pixel(this.getPoint(p2)));
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) {
this.sequence.push(() => {
this.ctx.beginPath();
this.ctx.moveTo(...this.pixel(points[0]));
for (let i = 1; i < points.length; i++) {
this.ctx.lineTo(...this.pixel(points[i]));
}
this.ctx.stroke();
});
}
oscillatingValue(x1, x2, period, initialPhase = 0) {
const center = (x1 + x2) / 2;
const amplitude = (x2 - x1) / 2;
return center + amplitude * Math.sin(2*Math.PI*this.t/period + initialPhase);
}
oscillatingPoint([x1, y1], [x2, y2], period) {
const x = this.oscillatingValue(x1, x2, period);
const y = this.oscillatingValue(y1, y2, period);
return [x, y];
}
object(p, opts, fn) {
let history = [];
const maxAge = opts?.trace?.age ?? 0;
const dashLength = 5;
let distance = 0;
this.sequence.push(() => {
const [x, y] = this.getPoint(p);
let ds = 0;
if (history.length) {
const dx = x - history[history.length - 1].p[0];
const dy = y - history[history.length - 1].p[1];
ds = Math.sqrt(dx**2 + dy**2);
}
distance += ds;
history.push({t: this.t, p: [x, y], distance});
const oldest = history.findIndex(({t}) => this.t - t <= maxAge);
if (oldest >= 0) {
history = history.slice(oldest);
}
this.ctx.strokeStyle = 'gray';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(...this.pixel(history[0].p));
for (let i = 1; i < history.length; i++) {
if (Math.floor(history[i].distance / dashLength) % 2) {
this.ctx.moveTo(...this.pixel(history[i].p));
} else {
this.ctx.lineTo(...this.pixel(history[i].p));
}
}
this.ctx.stroke();
fn(x, y);
});
return {
reset: () => {
history = [];
distance = 0;
}
};
}
square(p, opts) {
const size = opts?.size ?? 10;
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;
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();
});
}
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)]));
}
this.ctx.lineTo(...this.pixel([domain[1], fn(domain[1])]));
this.ctx.stroke();
})
}
}