Skip to content

Instantly share code, notes, and snippets.

@thquinn
Created July 15, 2018 16:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thquinn/99b1f2055145583b771fc1cbefda3668 to your computer and use it in GitHub Desktop.
Save thquinn/99b1f2055145583b771fc1cbefda3668 to your computer and use it in GitHub Desktop.
Waveform visualizer for the Math Square exhibit at MoMath
/* MoMath Math Square Behavior
*
* Title: Waveforms
* Description: stand on the knobs to combine simple waveforms into something
* more complex, stand on the wave to distort it
* Framework: P5
* Author: Tom Quinn <thquinn.github.io>
* Created: 2017-07
* Status: works
*/
Math.randFloat = function (min, max) {
return Math.random() * (max - min) + min;
};
function HSVtoRGB(h, s, v) {
var r, g, b, i, f, p, q, t;
if (arguments.length === 1) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
import P5Behavior from 'p5beh';
const pb = new P5Behavior();
const SIZE = 576;
const SCROLL_SPEED = 9;
const SAMPLE_COUNT = Math.ceil(SIZE / SCROLL_SPEED);
const AMPLITUDE = 120;
const PERIOD = 40;
const KNOB_UP_SPEED = .0166;
const KNOB_TIMER = 45;
const KNOB_DOWN_SPEED = .005;
const KNOB_RADIUS = 50;
const INDENT_RADIUS = 12;
const INDENT_OFFSET = 24;
const KNOB_GLOW_RADIUS = 50;
const KNOB_GLOW_LAYERS = 8;
const FAKE_SAMPLES_COUNT = 3;
const FAKE_SAMPLES_MULTIPLIER = .1;
const FAKE_SAMPLES_POWER_FALLOFF = 3;
const TOUCH_RADIUS = 250;
const TOUCH_RADIUS_SQUARED = TOUCH_RADIUS * TOUCH_RADIUS;
const TOUCH_MAX_OFFSET = 35;
var frame = 0;
var waveforms = [];
var samples = Array.apply(null, Array(SAMPLE_COUNT)).map(Number.prototype.valueOf,0);
var fakeSamplesCollection = [];
var fakeSamplesPeriods = [];
for (let i = 0; i < FAKE_SAMPLES_COUNT; i++) {
fakeSamplesCollection.push(Array.apply(null, Array(SAMPLE_COUNT)).map(Number.prototype.valueOf,0));
fakeSamplesPeriods.push(Math.randFloat(PERIOD * .66, PERIOD * 1.33));
}
class Waveform {
constructor(type, x, y, hue, theta, dTheta) {
this.type = type;
this.x = x;
this.y = y;
this.hue = hue;
this.theta = theta;
this.dTheta = dTheta;
this.power = 0;
this.knobTimer = 0;
this.knobRGB = HSVtoRGB(hue, .15, .85);
this.indentRGB = HSVtoRGB(hue, .15, 1);
this.rgb = HSVtoRGB(hue, 1, 1);
}
update(touched) {
if (touched) {
this.power = Math.min(this.power + KNOB_UP_SPEED, 1);
this.knobTimer = 0;
} else {
this.knobTimer++;
if (this.knobTimer > KNOB_TIMER) {
this.power = Math.max(0, this.power - KNOB_DOWN_SPEED);
}
}
}
calculate() {
let x = (frame / PERIOD) % 1.0;
let output = 0;
if (this.type == 'sine') {
output = Math.sin(x * 2 * Math.PI);
} else if (this.type == 'triangle') {
if (x < .5) {
output = x < .25 ? x * 4 : 1 - (x - .25) * 4;
} else {
x -= .5;
output = x < .25 ? x * -4 : -1 + (x - .25) * 4;
}
output *= -1;
} else if (this.type == 'square') {
output = x < .5 ? 1 : -1;
} else if (this.type == 'noise') {
output = Math.random() * 2 - 1;
} else if (this.type == 'sawtooth') {
output = x * 2 - 1;
} else if (this.type == 'pulse25') {
output = (x > .5 && x < .75) ? 1 : -1;
} else if (this.type == 'buzzer') {
output = frame % 2 == 0 ? .66 : -.66;
}
return output * this.power * this.power;
}
draw(p) {
p.noStroke();
for (let i = 0; i < KNOB_GLOW_LAYERS; i++) {
let radius = KNOB_RADIUS + (i + 1.5) / KNOB_GLOW_LAYERS * KNOB_GLOW_RADIUS;
let alpha = .033 + (this.power * .05);
p.fill('rgba(' + this.rgb.r + ', ' + this.rgb.g + ', ' + this.rgb.b + ', ' + alpha + ')');
p.ellipse(this.x, this.y, radius * 2);
}
let b = .5 + .5 * this.power;
let outlineRGB = HSVtoRGB(this.hue, 1, b);
p.strokeWeight(8);
p.stroke(outlineRGB.r, outlineRGB.g, outlineRGB.b);
p.fill(this.knobRGB.r, this.knobRGB.g, this.knobRGB.b);
p.ellipse(this.x, this.y, 2 * KNOB_RADIUS);
p.noStroke();
let theta = this.theta + this.power * this.dTheta;
p.fill(this.indentRGB.r, this.indentRGB.g, this.indentRGB.b);
let indentX = this.x + Math.cos(theta) * INDENT_OFFSET;
let indentY = this.y - Math.sin(theta) * INDENT_OFFSET;
p.ellipse(indentX, indentY, 2 * INDENT_RADIUS);
}
}
waveforms.push(new Waveform('sine', KNOB_RADIUS * 2.5, KNOB_RADIUS * 2, 0, 2 * Math.PI / 3, -5 * Math.PI / 3));
waveforms.push(new Waveform('triangle', KNOB_RADIUS * 1.5, SIZE / 2, 0.066, 5 * Math.PI / 6, -5 * Math.PI / 3));
waveforms.push(new Waveform('square', KNOB_RADIUS * 2.5, SIZE - KNOB_RADIUS * 2, 0.125, Math.PI, -5 * Math.PI / 3));
waveforms.push(new Waveform('noise', SIZE - KNOB_RADIUS * 2.5, SIZE - KNOB_RADIUS * 2, 0.3, 5 * Math.PI / 3, -5 * Math.PI / 3));
waveforms.push(new Waveform('sawtooth', SIZE - KNOB_RADIUS * 1.5, SIZE / 2, 0.66, 11 * Math.PI / 6, -5 * Math.PI / 3));
waveforms.push(new Waveform('buzzer', SIZE - KNOB_RADIUS * 2.5, KNOB_RADIUS * 2, 0.85, 0, -5 * Math.PI / 3));
pb.preload = function (p) {
}
pb.setup = function (p) {
p.strokeJoin(p.ROUND);
};
var touches;
var offsets = [];
pb.draw = function (floor, p) {
// Update.
frame++;
touches = [];
let touched = Array.apply(null, Array(waveforms.length)).map(Boolean.prototype.valueOf, false);
for (let user of floor.users) {
let onDial = false;
for (let i = 0; i < waveforms.length; i++) {
let waveform = waveforms[i];
if (Math.hypot(user.x - waveform.x, user.y - waveform.y) < KNOB_RADIUS) {
touched[i] = true;
onDial = true;
break;
}
}
if (!onDial) {
touches.push([user.x, user.y]);
}
}
for (let i = 0; i < waveforms.length; i++) {
waveforms[i].update(touched[i]);
}
// Calculate next sample.
let total = 0, totalPower = 0;
for (let waveform of waveforms) {
if (waveform.power == 0) {
continue;
}
total += waveform.calculate();
totalPower += waveform.power;
}
totalPower = Math.max(totalPower, .001);
samples.push(total / totalPower);
samples.shift();
for (let i = 0; i < SAMPLE_COUNT; i++) {
offsets[i] = [0, 0];
let sampleX = SIZE / 2 + samples[i] * AMPLITUDE;
let sampleY = i * SCROLL_SPEED;
let offX = 0, offY = 0;
for (let touch of touches) {
if (Math.abs(sampleX - touch[0]) > TOUCH_RADIUS) {
continue;
}
if (Math.abs(sampleY - touch[1]) > TOUCH_RADIUS) {
continue;
}
let dSquared = Math.pow(sampleX - touch[0], 2) + Math.pow(sampleY - touch[1], 2);
if (dSquared > TOUCH_RADIUS_SQUARED) {
continue;
}
let strength = 1 - dSquared / TOUCH_RADIUS_SQUARED;
strength *= TOUCH_MAX_OFFSET;
let theta = Math.atan2(sampleY - touch[1], sampleX - touch[0]);
offsets[i][0] += Math.cos(theta) * strength;
offsets[i][1] += Math.sin(theta) * strength;
}
let percent = Math.hypot(offsets[0], offsets[1]) / TOUCH_MAX_OFFSET;
if (percent > 1) {
offsets[i][0] /= percent;
offsets[i][1] /= percent;
}
}
// Create fake samples.
let fakePowerFalloff = Math.min(totalPower / FAKE_SAMPLES_POWER_FALLOFF, 1);
for (let i = 0; i < fakeSamplesCollection.length; i++) {
let fakeSample = Math.sin(frame / fakeSamplesPeriods[i] * 2 * Math.PI);
//fakeSample *= 1 - fakePowerFalloff;
fakeSamplesCollection[i].push(fakeSample);
fakeSamplesCollection[i].shift();
}
this.clear();
// Draw fake samples.
this.strokeWeight(3);
this.stroke(66, 66, 66);
this.noFill();
for (let fakeSamples of fakeSamplesCollection) {
this.beginShape();
for (let i = 0; i < fakeSamples.length; i++) {
this.vertex(SIZE / 2 + samples[i] * AMPLITUDE + fakeSamples[i] * AMPLITUDE * FAKE_SAMPLES_MULTIPLIER + offsets[i][0], i * SCROLL_SPEED + offsets[i][1]);
}
this.endShape();
}
// Draw UI.
for (let waveform of waveforms) {
waveform.draw(p);
}
// Draw samples.
this.strokeWeight(5);
this.stroke(255, 255, 255);
this.noFill();
this.beginShape();
for (let i = 0; i < samples.length; i++) {
this.vertex(SIZE / 2 + samples[i] * AMPLITUDE + offsets[i][0], i * SCROLL_SPEED + offsets[i][1]);
}
this.endShape();
};
export const behavior = {
title: "Waveforms",
init: pb.init.bind(pb),
frameRate: 'animate',
render: pb.render.bind(pb),
numGhosts: 0
};
export default behavior
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment