/** * Relabi waveform display * @module src/relabi/canvas.js; */ const NOTE_RADIUS = 7; export default class RelabiCanvas { /** * Initialize relabi wave renderer */ constructor({ relabi, parent }) { this.relabi = relabi; this.height = 400; this.lastFrame = 0; this.lastAppendTime = 0; this.lastAppendFrame = 0; // Speed of the wave in pixels per second this.speed = 1 / 5; // Position of current time on the screen, from the left this.tZeroOffset = 20; // Attach to the DOM this.appendCanvas(parent); // Initialize array this.values = new Array(window.innerWidth).fill(0); this.notes = []; this.append([]); // Clear the canvas this.resize(); // Start drawing this.requestAnimationFrame(0); } /** * Append the canvas */ appendCanvas(parent) { this.parent = parent || document.body; this.canvas = this.canvas || document.createElement("canvas"); this.ctx = this.ctx || this.canvas.getContext("2d"); this.parent.appendChild(this.canvas); } /** * Draw the next frame */ requestAnimationFrame(frame) { requestAnimationFrame((frame) => { this.requestAnimationFrame(frame); }); this.paint(frame); this.lastFrame = frame; } /** * Handle window resize */ resize() { this.canvas.width = window.innerWidth; this.canvas.height = this.height; } /** * Clear the canvas */ clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } /** * Append values */ append(time, values, notes) { this.lastAppendTime = time; this.lastAppendFrame = this.lastFrame; this.values = this.values .filter((value) => value && value[0] < time) .concat(values) .slice(-this.canvas.width) .sort((a, b) => a[0] - b[0]); if (notes?.length) { this.notes = this.notes .concat(notes) .slice(-50) .sort((a, b) => a[0] - b[0]); } } /** * Paint the canvas */ paint(frame) { this.clear(); this.drawRelabiWave(frame); this.drawBounds(); this.drawNotes(frame); } /** * Draw the bounds */ drawBounds() { const { canvas, ctx } = this; const { width, height } = canvas; // Draw dashed lines for all bounds for (const bound of this.relabi.bounds) { const y = getWaveHeight(bound.level, height); ctx.beginPath(); ctx.setLineDash([4, 4]); ctx.lineWidth = 1; ctx.strokeStyle = bound.color || "#888"; ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // Draw the zero position ctx.beginPath(); ctx.setLineDash([1, 1]); ctx.lineWidth = 2; ctx.strokeStyle = "#666"; ctx.moveTo(width - this.tZeroOffset, 0); ctx.lineTo(width - this.tZeroOffset, height); ctx.stroke(); } /** * Draw the relabi wave */ drawRelabiWave(frame) { const { canvas, ctx } = this; const { width, height } = canvas; const pointCount = this.values.length; let index = 0; // Start the path ctx.beginPath(); ctx.strokeStyle = "#dff"; ctx.lineWidth = 1.1; ctx.setLineDash([]); // This is the offset in seconds from the last frame we computed const frameOffset = (frame - this.lastAppendFrame) / 1000; // Make a path connecting all values for (const point of this.values) { if (!point) { index += 1; continue; } const [time, value] = point; // This is the offset of this time from current time // If this is in the future, it will be positive. // Subtracting the frame offset pulls it negative. const timeOffset = this.lastAppendTime + frameOffset - time; const x = width - timeOffset * this.speed * width - this.tZeroOffset; const y = getWaveHeight(value, height); if (x < 0) { continue; } if (index === 0) { ctx.moveTo(x, y); } ctx.lineTo(x, y); index += 1; } // Paint the line ctx.stroke(); } /** * Draw the notes */ drawNotes(frame) { const { canvas, ctx } = this; const { width, height } = canvas; // This is the offset in seconds from the last frame we computed const frameOffset = (frame - this.lastAppendFrame) / 1000; // Make a path connecting all values for (const note of this.notes) { const [time, value, direction] = note; const timeOffset = this.lastAppendTime + frameOffset - time; const x = width - timeOffset * this.speed * width - this.tZeroOffset; const y = getWaveHeight(value, height); // Don't draw notes that haven't played yet if (x > width - this.tZeroOffset) { continue; } // Draw notes as a dot that fades out const opacity = 1 - (timeOffset * this.speed * width) / 1000; if (opacity < 0.001) { continue; } ctx.beginPath(); ctx.arc(x - NOTE_RADIUS / 4, y, NOTE_RADIUS, 0, 2 * Math.PI); ctx.fillStyle = direction ? `rgba(255,96,128,${opacity})` : `rgba(128,255,96,${opacity})`; ctx.fill(); } } } /** * Get the height of the wave at a given value */ const getWaveHeight = (value, height) => ((value + 1) / 2) * height;