summaryrefslogtreecommitdiff
path: root/src/relabi
diff options
context:
space:
mode:
Diffstat (limited to 'src/relabi')
-rw-r--r--src/relabi/canvas.js155
-rw-r--r--src/relabi/index.js55
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;
}
/**