diff options
Diffstat (limited to 'src/relabi')
| -rw-r--r-- | src/relabi/canvas.js | 155 | ||||
| -rw-r--r-- | src/relabi/index.js | 55 |
2 files changed, 197 insertions, 13 deletions
diff --git a/src/relabi/canvas.js b/src/relabi/canvas.js new file mode 100644 index 0000000..dfbffa0 --- /dev/null +++ b/src/relabi/canvas.js @@ -0,0 +1,155 @@ +/** + * Relabi waveform display + * @module src/relabi/canvas.js; + */ + +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; + + // Attach to the DOM + this.parent = parent = document.body; + this.canvas = document.createElement("canvas"); + this.ctx = this.canvas.getContext("2d"); + this.parent.appendChild(this.canvas); + + // Initialize array + this.values = new Array(window.innerWidth).fill(0); + this.append([]); + + // Clear the canvas + this.resize(); + + // Start drawing + this.requestAnimationFrame(0); + } + + /** + * 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) { + this.lastAppendTime = time; + this.lastAppendFrame = this.lastFrame; + this.values = this.values.concat(values).slice(-this.canvas.width); + } + + /** + * Paint the canvas + */ + paint(frame) { + this.clear(); + this.drawRelabiWave(frame); + this.drawBounds(); + } + + /** + * 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([10, 5]); + ctx.strokeStyle = bound.color || "#888"; + ctx.moveTo(0, y); + ctx.lineTo(width, y); + 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 = "white"; + 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 - 10; + + if (x < 0) { + continue; + } + + const y = getWaveHeight(value, height); + if (index === 0) { + ctx.moveTo(x, y); + } + ctx.lineTo(x, y); + index += 1; + } + + // Paint the line + ctx.stroke(); + } +} + +/** + * Get the height of the wave at a given value + */ +const getWaveHeight = (value, height) => ((value + 1) / 2) * height; diff --git a/src/relabi/index.js b/src/relabi/index.js index ad5b11d..e392a27 100644 --- a/src/relabi/index.js +++ b/src/relabi/index.js @@ -1,4 +1,9 @@ +/** + * Relabi event generator + */ + import * as Tone from "tone"; +import RelabiCanvas from "./canvas"; const TWO_PI = 2 * Math.PI; @@ -24,8 +29,8 @@ export default class Relabi { /** * Initialize generator */ - constructor({ waves, bounds }) { - this.updateTime = 0.5; + constructor({ waves, bounds, parent }) { + this.updateTime = 1; this.steps = 50; this.waves = waves || [ { type: "sine", frequency: randrange(0.5, 1.5) }, @@ -34,6 +39,8 @@ export default class Relabi { { type: "sine", frequency: randrange(2, 4) }, ]; this.bounds = bounds; + this.previousValue = 0; + this.canvas = new RelabiCanvas({ relabi: this, parent }); } /** @@ -42,7 +49,11 @@ export default class Relabi { start() { console.log("Start Relabi"); this.stop(); - this.clock = new Tone.Clock((time) => this.step(time), this.updateTime); + this.clock = new Tone.Clock((time) => { + const values = this.generate(time); + this.canvas.append(time, values); + this.play(values); + }, this.updateTime); this.clock.start(); } @@ -59,19 +70,17 @@ export default class Relabi { /** * Generate relabi events */ - step(time) { + generate(time) { const waveCount = this.waves.length; - const boundsCount = this.bounds.length; - let previousValue = this.previousValue; let index; let step; let value; - let noteCount = 0; + let values = []; // Generate several events per second for (step = 0; step < this.steps; step += 1) { // Time offset for this event - const offset = time + (step * this.updateTime) / this.steps; + const timeOffset = time + (step * this.updateTime) / this.steps; // Initialize value value = 0; @@ -79,22 +88,42 @@ export default class Relabi { // Compute the wave functions for this event for (index = 0; index < waveCount; index += 1) { const wave = this.waves[index]; - value += WAVE_FUNCTIONS[wave.type](offset * wave.frequency); + value += WAVE_FUNCTIONS[wave.type](timeOffset * wave.frequency); } // Scale to [-1, 1] - value /= waveCount / Math.PI; + value /= waveCount; + + values.push([timeOffset, value]); + } + + return values; + } + + /** + * Schedule relabi events + */ + play(values) { + const boundsCount = this.bounds.length; + let previousValue = this.previousValue; + let index; + let step; + let noteCount = 0; + + for (step = 0; step < this.steps; step += 1) { + // Get the next value + const [time, value] = values[step]; // Compute whether we crossed a boundary, and which direction for (index = 0; index < boundsCount; index += 1) { const bound = this.bounds[index]; if (value < bound.level && bound.level < previousValue) { // Going down - this.trigger(offset, bound.sounds[0]); + this.trigger(time, bound.sounds[0]); noteCount += 1; } else if (value > bound.level && bound.level > previousValue) { // Going up - this.trigger(offset, bound.sounds[1]); + this.trigger(time, bound.sounds[1]); noteCount += 1; } } @@ -104,7 +133,7 @@ export default class Relabi { } // Store the latest value - this.previousValue = value; + this.previousValue = previousValue; } /** |
