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"; 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.ctx = this.canvas.getContext('2d'); this.sequence = []; this.t = 0; this.rt = 0; this.dt = 0; this.points = {}; this.stopped = true; this.frame = [[-50, -50], [150, 150]]; 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; } pixel([x, y]) { return [ (x - this.frame[0][0]) * this.scale, this.canvas.height - (y - this.frame[0][1]) * this.scale ]; } setSpeed(speed) { this.speed = speed; } 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; } 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; } setStroke(style, width) { this.sequence.push(() => { this.ctx.strokeStyle = style; this.ctx.lineWidth = width; }); } setFill(style) { this.sequence.push(() => { this.ctx.fillStyle = style; }); } 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(); }); } 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 = 'black'; 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); }) } square(p, opts) { const size = opts?.size ?? 10; 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) => { 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; } 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(); } }