summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/index.jsx14
-rw-r--r--src/relabi/canvas.js90
-rw-r--r--src/relabi/index.js37
-rw-r--r--src/ui/App.jsx34
4 files changed, 145 insertions, 30 deletions
diff --git a/src/index.jsx b/src/index.jsx
index b41c7da..cc5b7a2 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -8,9 +8,19 @@ document.body.style.backgroundColor = "#111";
document.body.style.color = "#fff";
document.body.style.margin = 0;
document.body.style.padding = 0;
+document.body.style.height = "100%";
+document.body.style.width = "100%";
+document.body.parentNode.style.margin = 0;
+document.body.parentNode.style.padding = 0;
+document.body.parentNode.style.height = "100%";
+document.body.parentNode.style.width = "100%";
requestAudioContext(() => {
- document.body.innerHTML = '<div id="app"></div>';
- const root = createRoot(document.getElementById("app"));
+ const container = document.createElement("div");
+ container.style.height = "100%";
+ container.style.width = "100%";
+ document.body.innerHTML = "";
+ document.body.appendChild(container);
+ const root = createRoot(container);
root.render(<App />);
});
diff --git a/src/relabi/canvas.js b/src/relabi/canvas.js
index dfbffa0..d2c55a2 100644
--- a/src/relabi/canvas.js
+++ b/src/relabi/canvas.js
@@ -17,14 +17,14 @@ export default class RelabiCanvas {
// 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);
+ // 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
@@ -35,6 +35,16 @@ export default class RelabiCanvas {
}
/**
+ * 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) {
@@ -63,10 +73,20 @@ export default class RelabiCanvas {
/**
* Append values
*/
- append(time, values) {
+ append(time, values, notes) {
this.lastAppendTime = time;
this.lastAppendFrame = this.lastFrame;
- this.values = this.values.concat(values).slice(-this.canvas.width);
+ this.values = this.values
+ .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]);
+ }
}
/**
@@ -76,6 +96,7 @@ export default class RelabiCanvas {
this.clear();
this.drawRelabiWave(frame);
this.drawBounds();
+ this.drawNotes(frame);
}
/**
@@ -90,12 +111,22 @@ export default class RelabiCanvas {
const y = getWaveHeight(bound.level, height);
ctx.beginPath();
- ctx.setLineDash([10, 5]);
+ 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();
}
/**
@@ -110,7 +141,8 @@ export default class RelabiCanvas {
// Start the path
ctx.beginPath();
- ctx.strokeStyle = "white";
+ ctx.strokeStyle = "#dff";
+ ctx.lineWidth = 0.75;
ctx.setLineDash([]);
// This is the offset in seconds from the last frame we computed
@@ -130,13 +162,13 @@ export default class RelabiCanvas {
// Subtracting the frame offset pulls it negative.
const timeOffset = this.lastAppendTime + frameOffset - time;
- const x = width - timeOffset * this.speed * width - 10;
+ const x = width - timeOffset * this.speed * width - this.tZeroOffset;
+ const y = getWaveHeight(value, height);
if (x < 0) {
continue;
}
- const y = getWaveHeight(value, height);
if (index === 0) {
ctx.moveTo(x, y);
}
@@ -147,6 +179,42 @@ export default class RelabiCanvas {
// 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 - 2.5, y, 5, 0, 2 * Math.PI);
+ ctx.fillStyle = direction
+ ? `rgba(255,128,192,${opacity})`
+ : `rgba(192,255,128,${opacity})`;
+ ctx.fill();
+ }
+ }
}
/**
diff --git a/src/relabi/index.js b/src/relabi/index.js
index e392a27..89e4ce9 100644
--- a/src/relabi/index.js
+++ b/src/relabi/index.js
@@ -33,13 +33,13 @@ export default class Relabi {
this.updateTime = 1;
this.steps = 50;
this.waves = waves || [
- { type: "sine", frequency: randrange(0.5, 1.5) },
- { type: "sine", frequency: randrange(0.75, 2.25) },
- { type: "sine", frequency: randrange(1, 3) },
- { type: "sine", frequency: randrange(2, 4) },
+ { type: "triangle", frequency: randrange(0.5, 1.5) },
+ { type: "triangle", frequency: randrange(0.75, 2.25) },
+ { type: "triangle", frequency: randrange(1, 3) },
+ { type: "triangle", frequency: randrange(2, 4) },
];
this.bounds = bounds;
- this.previousValue = 0;
+ this.previousValue = null;
this.canvas = new RelabiCanvas({ relabi: this, parent });
}
@@ -51,8 +51,8 @@ export default class Relabi {
this.stop();
this.clock = new Tone.Clock((time) => {
const values = this.generate(time);
- this.canvas.append(time, values);
- this.play(values);
+ const notes = this.play(values);
+ this.canvas.append(time, values, notes);
}, this.updateTime);
this.clock.start();
}
@@ -77,8 +77,11 @@ export default class Relabi {
let value;
let values = [];
+ // Overshoot the line slightly each time
+ let stepCount = this.steps + 20;
+
// Generate several events per second
- for (step = 0; step < this.steps; step += 1) {
+ for (step = 0; step < stepCount; step += 1) {
// Time offset for this event
const timeOffset = time + (step * this.updateTime) / this.steps;
@@ -109,6 +112,7 @@ export default class Relabi {
let index;
let step;
let noteCount = 0;
+ const notes = [];
for (step = 0; step < this.steps; step += 1) {
// Get the next value
@@ -117,13 +121,23 @@ export default class Relabi {
// 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) {
+ if (
+ value < bound.level &&
+ bound.level < previousValue &&
+ previousValue !== null
+ ) {
// Going down
this.trigger(time, bound.sounds[0]);
+ notes.push([time, bound.level, true]);
noteCount += 1;
- } else if (value > bound.level && bound.level > previousValue) {
+ } else if (
+ value > bound.level &&
+ bound.level > previousValue &&
+ previousValue !== null
+ ) {
// Going up
this.trigger(time, bound.sounds[1]);
+ notes.push([time, bound.level, false]);
noteCount += 1;
}
}
@@ -134,6 +148,9 @@ export default class Relabi {
// Store the latest value
this.previousValue = previousValue;
+
+ // Return the notes
+ return notes;
}
/**
diff --git a/src/ui/App.jsx b/src/ui/App.jsx
index 5ee21b7..eaca9a4 100644
--- a/src/ui/App.jsx
+++ b/src/ui/App.jsx
@@ -4,12 +4,13 @@
*/
import * as React from "react";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import Relabi from "../relabi";
import { Kalimba, Drums } from "../lib/instruments";
export default function App() {
const [relabi, setRelabi] = useState();
+ const relabiRef = useRef();
/**
* Instantiate the Relabi generator
@@ -26,7 +27,7 @@ export default function App() {
bounds: [
{
level: -0.75,
- color: "#f00",
+ color: "#f33",
sounds: [
{ instrument: Drums, index: 0 },
{ instrument: Drums, index: 1 },
@@ -34,7 +35,7 @@ export default function App() {
},
{
level: 0.75,
- color: "#f00",
+ color: "#f83",
sounds: [
{ instrument: Drums, index: 2 },
{ instrument: Drums, index: 3 },
@@ -42,7 +43,7 @@ export default function App() {
},
{
level: -0.25,
- color: "#00f",
+ color: "#3b8",
sounds: [
{ instrument: Kalimba, frequency: 440 },
{ instrument: Kalimba, frequency: (440 * 3) / 2 },
@@ -50,7 +51,7 @@ export default function App() {
},
{
level: 0.25,
- color: "#00f",
+ color: "#36f",
sounds: [
{ instrument: Kalimba, frequency: (440 * 6) / 5 },
{ instrument: Kalimba, frequency: (440 * 6) / 7 },
@@ -63,9 +64,28 @@ export default function App() {
}
}, [relabi]);
+ useEffect(() => {
+ if (relabi && relabiRef.current) {
+ relabi.canvas.appendCanvas(relabiRef.current);
+ }
+ }, [relabi, relabiRef.current]);
+
/**
* Render
*/
-
- return <div>Relabi generator</div>;
+ return (
+ <div
+ style={{
+ height: "100%",
+ width: "100%",
+ padding: 0,
+ margin: 0,
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ }}
+ >
+ <div ref={relabiRef} />
+ </div>
+ );
}