From 199f7c731886bd8e8ac0d28bd15ebec241255594 Mon Sep 17 00:00:00 2001 From: julian laplace Date: Tue, 9 May 2023 17:44:31 +0200 Subject: rendering dots --- src/index.jsx | 14 ++++++-- src/relabi/canvas.js | 90 +++++++++++++++++++++++++++++++++++++++++++++------- src/relabi/index.js | 37 +++++++++++++++------ src/ui/App.jsx | 34 ++++++++++++++++---- 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 = '
'; - 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(); }); 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 @@ -34,6 +34,16 @@ export default class RelabiCanvas { 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 */ @@ -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
Relabi generator
; + return ( +
+
+
+ ); } -- cgit v1.2.3-70-g09d2