Created
July 15, 2018 16:41
-
-
Save thquinn/99b1f2055145583b771fc1cbefda3668 to your computer and use it in GitHub Desktop.
Waveform visualizer for the Math Square exhibit at MoMath
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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