Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active September 29, 2023 20:08
Show Gist options
  • Star 33 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mattdesl/50fa00622294a86261edf3c7fbee2d3e to your computer and use it in GitHub Desktop.
Save mattdesl/50fa00622294a86261edf3c7fbee2d3e to your computer and use it in GitHub Desktop.
interactive audio sketch

ToneJS + canvas-sketch

A interactive & generative site with canvas-sketch and Tone.js.

Live Demo:

Running the sketch.js Code

To run, first install canvas-sketch CLI globally with npm:

npm install canvas-sketch-cli@latest -g

Then, make a new folder and go into it:

mkdir my-sketch
cd my-sketch

Now, copy the contents of sketch.js and run the following to start it up:

pbpaste | canvas-sketch sketch.js --new --open

Now you can edit the code and it will reload the page at localhost:9966.

When you're done, use Ctrl + C to quit the development server.

Building to a Website

Quit the development server and build the site to a standalone HTML page like so:

canvas-sketch sketch.js --build --dir=build --inline --name=index --title="audio sketch"
const canvasSketch = require('canvas-sketch');
const Tone = require('tone');
const Random = require('canvas-sketch-util/random');
const createTouchListener = require('touches');
const palettes = require('nice-color-palettes');
const settings = {
animate: true,
};
const sketch = async (app) => {
const { canvas } = app;
// Use a hand cursor
canvas.style.cursor = 'pointer';
// Grab a random palette and shuffle it
const palette = Random.shuffle(Random.pick(palettes));
// Take a background color
const background = palette.shift();
// Setup a reverb with ToneJS
const reverb = new Tone.Reverb({
decay: 4,
wet: 0.5,
preDelay: 0.2
}).toMaster();
// Load the reverb
await reverb.generate();
// Setup a synth with ToneJS
const synth = new Tone.Synth({
oscillator: {
type: 'sine'
},
envelope: {
attack: 0.001,
decay: 0.5,
sustain: 0.001,
release: 5
}
}).connect(reverb);
// The notes we will use
const notes = [ 'C5', 'A3', 'D4', 'G4', 'A4', 'F4' ];
// Let's use this handy utility to handle mouse/touch taps
const touches = createTouchListener(canvas)
.on('start', addNote);
// Avoid iOS window dragging
const preventDefault = ev => ev.preventDefault();
document.addEventListener('touchmove', preventDefault, {
passive: false
});
// Store a list of active circles
const points = [];
// Return the renderer object
return {
// The draw function
render ({ deltaTime, context, width, height }) {
// First update point time & remove any inactive points
for (let i = points.length - 1; i >= 0; i--) {
const point = points[i];
point.time += deltaTime;
if (point.time > point.duration) {
points.splice(i, 1);
}
}
// Draw background
context.fillStyle = background;
context.fillRect(0, 0, width, height);
// Draw points
points.forEach(point => {
const { position, color, radius, time, duration } = point;
// Un-normalize coordinates
const x = position[0] * width;
const y = position[1] * height;
// Get a 0...1 value
const tween = Math.sin(time / duration * Math.PI);
context.beginPath();
context.arc(x, y, width * radius * tween, 0, Math.PI * 2, false);
context.fillStyle = color;
context.fill();
});
},
unload () {
// Disable mouse listeners
touches.disable();
document.removeEventListener('touchmove', preventDefault);
// Clean up the ToneJS nodes
reverb.dispose();
synth.dispose();
}
};
// Create a sound
function addNote (ev, clientPosition) {
// Get current DOM CSS style width & height from the application
// This is after retina scaling, and is always updated in the 'app' instance
const { styleWidth, styleHeight } = app;
// Mouse/touch XY comes from 'touches' utility
const [ x, y ] = clientPosition;
// Random note
const noteIndex = Random.rangeFloor(notes.length);
// Random color from note index
const color = palette[noteIndex % palette.length];
// Normalize so rendering is not bound to screen size
const position = [ x / styleWidth, y / styleHeight ];
const radius = Math.abs(Random.gaussian(1, 0.25));
// Add to screen
points.push({
position,
color,
duration: Random.range(1, 2),
time: 0,
radius: radius * 0.1
});
// Trigger a sound
const velocity = 1;
synth.triggerAttackRelease(notes[noteIndex], '16n', synth.context.currentTime, velocity);
}
};
canvasSketch(sketch, settings);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment