xxxxxxxxxx
210
/*
* Main Sketch
* Jack B. Du (github@jackbdu.com)
* https://instagram.com/jackbdu/
*/
const sketch = ( p ) => {
p.specs = {
fps: 60,
numLoopFrames: 720,
colorBackground: 0,
outputWidth: 'auto',
outputHeight: 'auto',
}
p.env = {
canvasShort: undefined,
canvasLong: undefined,
};
p.options = {
innerRadiusFactor: 0.15,
outterRadiusFactor: 0.3,
depthFactor: 2,
depthCycles: 16,
details: 1024,
mouseSensitivity: 4,
}
p.mouse = {
positions: [],
deviations: [],
maxLength: 32,
add: function(mx, my) {
this.positions.unshift(p.createVector(mx, my));
if (this.positions.length > this.maxLength) this.positions.pop();
this.deviations.unshift(this.getStandardDeviation(this.positions));
if (this.deviations.length > this.maxLength) this.deviations.pop();
},
getStandardDeviation: function(vectors) {
const sum = p.createVector(0,0);
for (let v of vectors) {
sum.add(v);
}
const mean = p5.Vector.div(sum, vectors.length);
let sumDeviations = 0;
for (let v of vectors) {
sumDeviations += p5.Vector.dist(v, mean);
}
const standardDeviation = sumDeviations/vectors.length;
return standardDeviation;
},
interactionStarted: false,
}
p.sound = {
waveform: new Tone.Waveform(p.options.details),
meter: new Tone.Meter({
smoothing: 0.9,
normalRange: true,
channels: 1,
}),
filePaths: [
'assets/water-paddle-soft-009.wav',
'assets/water-paddle-soft-023.wav',
'assets/water-paddle-soft-024.wav',
'assets/water-paddle-medsoft-008.wav',
'assets/water-paddle-med-000.wav',
'assets/water-paddle-med-003.wav',
'assets/water-paddle-med-019.wav',
'assets/water-paddle-heavy-026.wav',
'assets/water-paddle-heavy-028.wav',
'assets/water-paddle-heavy-029.wav',
],
players: [],
isLoaded: false,
index: undefined,
}
p.setupSound = () => {
for (let fp of p.sound.filePaths) {
const soundPlayer = new Tone.Player(fp);
soundPlayer.toDestination();
soundPlayer.connect(p.sound.waveform);
soundPlayer.connect(p.sound.meter);
p.sound.players.push(soundPlayer);
}
Tone.loaded().then(() => {p.sound.isLoaded = true});
}
p.setup = () => {
p.updateCanvas(p.specs.outputWidth, p.specs.outputHeight);
p.frameRate(p.specs.fps);
p.smooth();
p.setupSound();
};
p.draw = () => {
if (p.beforeDraw) p.beforeDraw();
p.background(p.specs.colorBackground);
p.checkMouse();
p.drawDonut(p.options, p.env.canvasShort, p.specs.numLoopFrames, p.sound);
if (p.afterDraw) p.afterDraw();
};
p.drawDonut = (options, canvasShort, numLoopFrames, sound) => {
const loopAnimOffset = p.TWO_PI*p.frameCount / numLoopFrames;
const innerRadius = options.innerRadiusFactor * canvasShort;
const outterRadius = options.outterRadiusFactor * canvasShort;
const depth = options.depthFactor * canvasShort;
p.rotateX(p.QUARTER_PI*p.sin(loopAnimOffset));
p.rotateY(p.QUARTER_PI*p.cos(loopAnimOffset));
p.rotateZ(loopAnimOffset);
const waveformValues = sound.waveform.getValue();
const soundAmp = sound.meter.getValue();
p.noStroke();
p.beginShape(p.TRIANGLE_STRIP);
for (let i = 0; i < options.details+2; i++) {
const isOutter = i % 2 ? true : false;
const radiusFactor = p.map(waveformValues[i%waveformValues.length], -1, 1, 0, 2);
const radius = (isOutter ? outterRadius : innerRadius) * radiusFactor;
// const percentage = i/options.details;
const percentage = p.floor(i/2)/(options.details/2);
const angle = percentage * p.TWO_PI;
// 0 to 1
const gradientIntensity = 0.1;
const colorAngle = p.map(p.sin(percentage*p.TWO_PI),-1,1,0,p.TWO_PI*gradientIntensity);
const rValueColorAngleCycles = 1;
const gValueColorAngleCycles = 2;
const bValueColorAngleCycles = 1;
const rValueAngleOffset = 4;
const gValueAngleOffset = 3;
const bValueAngleOffset = 6;
const rValue = p.map(p.sin(colorAngle*rValueColorAngleCycles+rValueAngleOffset+loopAnimOffset), -1, 1, 0, 255);
const gValue = p.map(p.sin(colorAngle*gValueColorAngleCycles+gValueAngleOffset+loopAnimOffset), -1, 1, 0, 255);
const bValue = p.map(p.sin(colorAngle*bValueColorAngleCycles+bValueAngleOffset+loopAnimOffset), -1, 1, 0, 255);
p.fill(rValue, gValue, bValue);
const x = radius*p.sin(angle);
const y = radius*p.cos(angle);
let z ;
if (p.mouse.interactionStarted) {
z = depth/2*p.sin(angle*options.depthCycles)*soundAmp;
} else {
// use integer for exporting seamless loop
const zAnimSpeedFactor = 8;
const zAnimFactor = p.sin(loopAnimOffset*zAnimSpeedFactor);
z = depth/32*p.sin(angle*options.depthCycles)*zAnimFactor;
// hacky way to ensure loading animation ends smoothly
if (sound.isLoaded && zAnimFactor < 0.001) {
p.mouse.interactionStarted = true;
}
}
p.vertex(x, y, z);
}
p.endShape();
}
p.checkMouse = () => {
p.mouse.add(p.mouseX, p.mouseY);
const segmentSize = p.floor(p.width/p.sound.players.length/p.options.mouseSensitivity);
const activeIndex = p.constrain(p.floor(p.mouse.deviations[0]/segmentSize),0,p.sound.players.length)-1;
if (p.sound.isLoaded && activeIndex >= 0 && p.mouseIsPressed) {
// console.log(p.mouse.deviations[0], activeIndex);
if (
(activeIndex !== p.sound.index && p.sound.players[activeIndex].state === "stopped") ||
(activeIndex === p.sound.index && p.sound.players[activeIndex].state === "stopped")
) {
p.sound.index = activeIndex;
p.sound.players[activeIndex].start();
}
}
}
p.beforeDraw = () => {
if (p.beginCapture) p.beginCapture();
}
p.afterDraw = () => {
if (p.endCapture) p.endCapture();
}
p.windowResized = () => {
p.updateCanvas(p.specs.outputWidth, p.specs.outputHeight);
}
p.updateCanvas = (outputWidth = 'auto', outputHeight = 'auto') => {
const pd = p.pixelDensity();
const canvasWidth = outputWidth && outputWidth !== 'auto' ? outputWidth/pd : p.windowWidth;
const canvasHeight = outputHeight && outputHeight !== 'auto' ? outputHeight/pd : p.windowHeight;
if (canvasWidth !== p.width || canvasHeight !== p.height) {
if (!p.hasCanvas) {
p.createCanvas(canvasWidth, canvasHeight, p.WEBGL);
p.hasCanvas = true;
} else {
p.resizeCanvas(canvasWidth, canvasHeight);
}
}
p.env.canvasShort = p.min(canvasWidth, canvasHeight);
p.env.canvasLong = p.max(canvasWidth, canvasHeight);
}
};
let p5sketch = new p5(sketch);