diff options
Diffstat (limited to 'client')
| -rw-r--r-- | client/draw.js | 48 | ||||
| -rw-r--r-- | client/index.js | 83 | ||||
| -rw-r--r-- | client/lib/color.js | 33 | ||||
| -rw-r--r-- | client/lib/kalimba.js | 56 | ||||
| -rw-r--r-- | client/lib/keys.js | 39 | ||||
| -rw-r--r-- | client/lib/mouse.js | 57 | ||||
| -rw-r--r-- | client/lib/sampler.js | 50 | ||||
| -rw-r--r-- | client/lib/startAudioContext.js | 181 | ||||
| -rw-r--r-- | client/lib/util.js | 69 |
9 files changed, 616 insertions, 0 deletions
diff --git a/client/draw.js b/client/draw.js new file mode 100644 index 0000000..3ff6620 --- /dev/null +++ b/client/draw.js @@ -0,0 +1,48 @@ +import { + browser, requestAudioContext, + randint, randrange, clamp, +} from './lib/util' + +import mouse from './lib/mouse' +import color from './lib/color' + +const canvas = document.createElement('canvas') +const ctx = canvas.getContext('2d') +document.body.appendChild(canvas) + +let w, h +function resize(){ + w = canvas.width = window.innerWidth + h = canvas.height = window.innerHeight +} +document.body.addEventListener('resize', resize) +resize() + + +function clear(n){ + ctx.fillStyle = 'rgba(255,255,255,' + (n || 0.5) + ')' + ctx.fillRect(0,0,w,h) +} +function triangle(px, py, r) { + ctx.save() + ctx.globalCompositeOperation = 'difference' + ctx.fillStyle = color((px+py)/(w+h), 0, 1, 1) + function p(){ + let theta = randrange(0, Math.PI*2) + let x = px + Math.cos(theta) * r + let y = py + Math.sin(theta) * r + return { x, y } + } + ctx.beginPath() + const p0 = p(), p1 = p(), p2 = p() + ctx.moveTo(p0.x, p0.y) + ctx.lineTo(p1.x, p1.y) + ctx.lineTo(p2.x, p2.y) + ctx.lineTo(p0.x, p0.y) + ctx.fill() + ctx.restore() +} + +export default { + triangle, clear +}
\ No newline at end of file diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..024c2d4 --- /dev/null +++ b/client/index.js @@ -0,0 +1,83 @@ +import Tone from 'tone' + +import Sampler from './lib/sampler' + +import draw from './draw' +import keys from './lib/keys' +import color from './lib/color' +import mouse from './lib/mouse' + +import { + browser, requestAudioContext, + randint, randrange, clamp, +} from './lib/util' + +const root = 440 +const s = 50 +const w = window.innerWidth +const h = window.innerHeight +const ws = w/s, hs = h/s + +let samplers = {} + +requestAudioContext( () => { + samplers['smash'] = new Sampler('samples/smash/g{}.mp3', 12) + samplers['glass'] = new Sampler('samples/glass/0{}Particle.mp3', 90) +}) + +let last_index = 0 +keys.listen(index => { + index = Math.abs(index+10) + const freq = 100 * Math.abs(index + 10) + const now = Tone.now() + const count = randrange(2, 6) + if (last_index !== index) { + samplers['smash'].play(randrange(90, 150) + index, 0) + last_index = index + } + else if (Math.random() < 0.09) { + last_index = -1 + } + for (var i = 0; i < count; i++) { + // kalimba.play(freq * (i+1)/4, now + Math.random()/(i+1)) + samplers['glass'].play( + 100 + index*(Math.random() * 10), + now + (Math.random()/2000 + i/10) + ) + } +}) + +mouse.register({ + down: (x, y) => { + samplers['smash'].play(randrange(90, 150) + 50 * (x/window.innerWidth + y/window.innerHeight), 0) + draw.clear() + draw.triangle(x, y, 400) + }, + move: (x, y, dx, dy) => { + let count = Math.abs(dx + dy) / 40 + if (count < 1) return + count = clamp(count, 1, 5) + if (Math.abs(dx) + Math.abs(dy) > 100) { + samplers['smash'].play(randrange(50, 300) + 100 * (x/window.innerWidth + y/window.innerHeight), 0) + draw.clear() + draw.triangle(x, y, 500) + } + let now = Tone.now() + let when, i + for (i = 0; i < count; i++) { + when = Math.random()/2000 + (i+ Math.random()/10)/randrange(2,5) + samplers['glass'].play( + 100 * randrange(2,5) / randrange(2,5), + now + when + ) + } + setTimeout( () => { + draw.triangle(x, y, Math.abs(dx) + Math.abs(dy)) + }, when * 1000) + }, + up: (x, y) => { + }, +}) + + + diff --git a/client/lib/color.js b/client/lib/color.js new file mode 100644 index 0000000..5f873b3 --- /dev/null +++ b/client/lib/color.js @@ -0,0 +1,33 @@ + +const palettes = [ + [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.00, 0.33, 0.67]], + [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.00, 0.10, 0.20]], + [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.30, 0.20, 0.20]], + [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 0.5], [0.80, 0.90, 0.30]], + [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 0.7, 0.4], [0.00, 0.15, 0.20]], + [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [2.0, 1.0, 0.0], [0.50, 0.20, 0.25]], + [[0.8, 0.5, 0.4], [0.2, 0.4, 0.2], [2.0, 1.0, 1.0], [0.00, 0.25, 0.25]], +] + +let palette = palettes[0] + +function channel (t, a, b, c, d, add, mul) { + return a + b * Math.cos(2 * Math.PI * (c * t + d)) * mul + add +} + +function color (t, add, mul, alpha) { + add = add || 0 + mul = mul || 1 + let a, b, c, d + const rgb = [] + for (var i = 0; i < 3; i++) { + a = palette[0][i] + b = palette[1][i] + c = palette[2][i] + d = palette[3][i] + rgb[i] = Math.round(channel(t, a, b, c, d, add, mul) * 255) + } + return 'rgba(' + rgb + ',' + alpha + ')' +} + +export default color diff --git a/client/lib/kalimba.js b/client/lib/kalimba.js new file mode 100644 index 0000000..bc048ff --- /dev/null +++ b/client/lib/kalimba.js @@ -0,0 +1,56 @@ +import Tone from 'tone' +import { choice } from './util' + +const player_count = 1 + +const compressor = new Tone.Compressor(-30, 3).toMaster() + +const samples = [ + { root: 226, fn: 'samples/kalimba/380737__cabled-mess__sansula-01-a-raw.wav', }, + { root: 267, fn: 'samples/kalimba/380736__cabled-mess__sansula-02-c-raw.wav', }, + { root: 340, fn: 'samples/kalimba/380735__cabled-mess__sansula-03-e-raw.wav', }, + { root: 452, fn: 'samples/kalimba/380733__cabled-mess__sansula-06-a-02-raw.wav', }, +// { root: 507, fn: 'samples/380734__cabled-mess__sansula-07-b-h-raw.wav', }, +// { root: 535, fn: 'samples/380731__cabled-mess__sansula-08-c-raw.wav', }, +// { root: 671, fn: 'samples/380732__cabled-mess__sansula-09-e-raw.wav', }, +] + +samples.forEach((sample) => { + sample.players = [] + sample.index = -1 + for (let i = 0; i < player_count; i++) { + let fn = sample.fn + if (window.location.href.match(/asdf.us/)) { + fn = 'http://asdf.us/kalimba/' + fn.replace('wav','mp3') + } + let player = new Tone.Player({ + url: fn, + retrigger: true, + playbackRate: 1, + }) + player.connect(compressor) + sample.players.push(player) + } +}) + +function play (freq, time) { +/* + while (freq < 440) { + freq *= 2 + } + while (freq > 880) { + freq /= 2 + } + freq /= 2 +*/ + time = time || Tone.now() + const best = { sample: choice(samples) } + best.sample.index = (best.sample.index + 1) % player_count + + const player = best.sample.players[ best.sample.index ] + player.playbackRate = freq / best.sample.root + player.start(time) +} + +export default { play } + diff --git a/client/lib/keys.js b/client/lib/keys.js new file mode 100644 index 0000000..c9e51ac --- /dev/null +++ b/client/lib/keys.js @@ -0,0 +1,39 @@ +const keys = {} +const key_numbers = {} +const letters = "zxcvbnmasdfghjklqwertyuiop" +const numbers = "1234567890" + +let callback = function(){} + +letters.toUpperCase().split("").map(function(k,i){ + keys[k.charCodeAt(0)] = i +}) + +numbers.split("").map(function(k,i){ + keys[k.charCodeAt(0)] = i+letters.length + key_numbers[k.charCodeAt(0)] = true +}) + +window.addEventListener("keydown", keydown, true) +function keydown (e) { + if (e.altKey || e.ctrlKey || e.metaKey) { + e.stopPropagation() + return + } + if (document.activeElement instanceof HTMLInputElement && + (e.keyCode in key_numbers)) { + e.stopPropagation() + return + } + if (! (e.keyCode in keys)) return + var index = keys[e.keyCode] + if (e.shiftKey) index += letters.length + index -= 7 + callback(index) +} + +function listen (fn) { + callback = fn +} + +export default { listen }
\ No newline at end of file diff --git a/client/lib/mouse.js b/client/lib/mouse.js new file mode 100644 index 0000000..b4fa961 --- /dev/null +++ b/client/lib/mouse.js @@ -0,0 +1,57 @@ +/* + +mouse.register({ + down: (x, y) => { + }, + move: (x, y, dx, dy) => { + }, + up: (x, y) => { + }, +}) + +*/ + + +let fns = { + down: [], + move: [], + up: [], +} + +export default { + register: (callbacks) => { + callbacks.down && fns.down.push(callbacks.down) + callbacks.move && fns.move.push(callbacks.move) + callbacks.up && fns.up.push(callbacks.up) + } +} + +let x, y, dragging = false + +function down(e){ + x = e.pageX + y = e.pageY + dragging = true + fns.down.map(f => f(x, y)) +} +function move(e){ + if (!dragging) return + let dx = e.pageX - x + let dy = e.pageY - y + x = e.pageX + y = e.pageY + dragging = true + fns.move.map(f => f(x, y, dx, dy)) +} +function up(e){ + dragging = false + fns.up.map(f => f(x, y)) +} + +function touch(f){ return (e) => f(e.touches[0]) } +document.body.addEventListener("mousedown", down) +document.body.addEventListener("mousemove", move) +document.body.addEventListener("mouseup", up) +document.body.addEventListener("touchstart", touch(down)) +document.body.addEventListener("touchmove", touch(move)) +document.body.addEventListener("touchup", touch(up)) diff --git a/client/lib/sampler.js b/client/lib/sampler.js new file mode 100644 index 0000000..cbbf281 --- /dev/null +++ b/client/lib/sampler.js @@ -0,0 +1,50 @@ +import Tone from 'tone' +import { choice } from './util' + +const player_count = 2 + +const compressor = new Tone.Compressor(-30, 3).toMaster() + +export default class Sampler { + constructor(path, count){ + this.samples = (()=>{ + let s = '', a = [] + for (let i = 1; i < count; i++) { + const s = i < 10 ? '0' + i : i; + a.push({ root: 100, fn: path.replace(/{}/, s) }) + } + return a + })() + + this.samples.forEach((sample) => { + sample.players = [] + sample.index = -1 + for (let i = 0; i < player_count; i++) { + let fn = sample.fn + if (window.location.href.match(/asdf.us/)) { + fn = 'http://asdf.us/glass/' + fn.replace('wav','mp3') + } + let player = new Tone.Player({ + url: fn, + retrigger: true, + playbackRate: 1, + }) + player.connect(compressor) + sample.players.push(player) + } + }) + + } + play(freq, time) { + const best = { sample: choice(this.samples) } + best.sample.index = (best.sample.index + 1) % player_count + + const player = best.sample.players[ best.sample.index ] + + freq = freq || best.sample.root + time = time || Tone.now() + + player.playbackRate = freq / best.sample.root + player.start(time) + } +} diff --git a/client/lib/startAudioContext.js b/client/lib/startAudioContext.js new file mode 100644 index 0000000..f3a9793 --- /dev/null +++ b/client/lib/startAudioContext.js @@ -0,0 +1,181 @@ +/** + * StartAudioContext.js + * @author Yotam Mann + * @license http://opensource.org/licenses/MIT MIT License + * @copyright 2016 Yotam Mann + */ +(function (root, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.StartAudioContext = factory(); + } +}(this, function () { + + /** + * The StartAudioContext object + */ + var StartAudioContext = { + /** + * The audio context passed in by the user + * @type {AudioContext} + */ + context : null, + /** + * The TapListeners bound to the elements + * @type {Array} + * @private + */ + _tapListeners : [], + /** + * Callbacks to invoke when the audio context is started + * @type {Array} + * @private + */ + _onStarted : [], + }; + + + /** + * Set the context + * @param {AudioContext} ctx + * @returns {StartAudioContext} + */ + StartAudioContext.setContext = function(ctx){ + StartAudioContext.context = ctx; + return StartAudioContext; + }; + + /** + * Add a tap listener to the audio context + * @param {Array|Element|String|jQuery} element + * @returns {StartAudioContext} + */ + StartAudioContext.on = function(element){ + if (Array.isArray(element) || (NodeList && element instanceof NodeList)){ + for (var i = 0; i < element.length; i++){ + StartAudioContext.on(element[i]); + } + } else if (typeof element === "string"){ + StartAudioContext.on(document.querySelectorAll(element)); + } else if (element.jquery && typeof element.toArray === "function"){ + StartAudioContext.on(element.toArray()); + } else if (Element && element instanceof Element){ + //if it's an element, create a TapListener + var tap = new TapListener(element, onTap); + StartAudioContext._tapListeners.push(tap); + } + return StartAudioContext; + }; + + /** + * Bind a callback to when the audio context is started. + * @param {Function} cb + * @return {StartAudioContext} + */ + StartAudioContext.onStarted = function(cb){ + //if it's already started, invoke the callback + if (StartAudioContext.isStarted()){ + cb(); + } else { + StartAudioContext._onStarted.push(cb); + } + return StartAudioContext; + }; + + /** + * returns true if the context is started + * @return {Boolean} + */ + StartAudioContext.isStarted = function(){ + return (StartAudioContext.context !== null && StartAudioContext.context.state === "running"); + }; + + /** + * @class Listens for non-dragging tap ends on the given element + * @param {Element} element + * @internal + */ + var TapListener = function(element){ + + this._dragged = false; + + this._element = element; + + this._bindedMove = this._moved.bind(this); + this._bindedEnd = this._ended.bind(this); + + element.addEventListener("touchmove", this._bindedMove); + element.addEventListener("touchend", this._bindedEnd); + element.addEventListener("mouseup", this._bindedEnd); + }; + + /** + * drag move event + */ + TapListener.prototype._moved = function(e){ + this._dragged = true; + }; + + /** + * tap ended listener + */ + TapListener.prototype._ended = function(e){ + if (!this._dragged){ + onTap(); + } + this._dragged = false; + }; + + /** + * remove all the bound events + */ + TapListener.prototype.dispose = function(){ + this._element.removeEventListener("touchmove", this._bindedMove); + this._element.removeEventListener("touchend", this._bindedEnd); + this._element.removeEventListener("mouseup", this._bindedEnd); + this._bindedMove = null; + this._bindedEnd = null; + this._element = null; + }; + + /** + * Invoked the first time of the elements is tapped. + * Creates a silent oscillator when a non-dragging touchend + * event has been triggered. + */ + function onTap(){ + //start the audio context with a silent oscillator + if (StartAudioContext.context && !StartAudioContext.isStarted()){ + var osc = StartAudioContext.context.createOscillator(); + var silent = StartAudioContext.context.createGain(); + silent.gain.value = 0; + osc.connect(silent); + silent.connect(StartAudioContext.context.destination); + var now = StartAudioContext.context.currentTime; + osc.start(now); + osc.stop(now+0.5); + } + + //dispose all the tap listeners + if (StartAudioContext._tapListeners){ + for (var i = 0; i < StartAudioContext._tapListeners.length; i++){ + StartAudioContext._tapListeners[i].dispose(); + } + StartAudioContext._tapListeners = null; + } + //the onstarted callbacks + if (StartAudioContext._onStarted){ + for (var j = 0; j < StartAudioContext._onStarted.length; j++){ + StartAudioContext._onStarted[j](); + } + StartAudioContext._onStarted = null; + } + } + + return StartAudioContext; +})); + + diff --git a/client/lib/util.js b/client/lib/util.js new file mode 100644 index 0000000..6d8577a --- /dev/null +++ b/client/lib/util.js @@ -0,0 +1,69 @@ +import Tone from 'tone' +import StartAudioContext from './startAudioContext' + +const isIphone = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)) +const isIpad = (navigator.userAgent.match(/iPad/i)) +const isAndroid = (navigator.userAgent.match(/Android/i)) +const isMobile = isIphone || isIpad || isAndroid +const isDesktop = ! isMobile + +document.body.classList.add(isMobile ? 'mobile' : 'desktop') + +const browser = { isIphone, isIpad, isMobile, isDesktop } + +function clamp(n,a,b){ return n<a?a:n<b?n:b } +function choice (a){ return a[ Math.floor(Math.random() * a.length) ] } +function mod(n,m){ return n-(m * Math.floor(n/m)) } +function randint(n){ return Math.random(n)|0 } +function randrange(a,b){ return a + Math.random() * (b-a) } +function randsign(){ return Math.random() >= 0.5 ? -1 : 1 } + +function requestAudioContext (fn) { + if (isMobile) { + const container = document.createElement('div') + const button = document.createElement('div') + button.innerHTML = 'Tap to start - please unmute your phone' + Object.assign(container.style, { + display: 'block', + position: 'absolute', + width: '100%', + height: '100%', + zIndex: '10000', + top: '0px', + left: '0px', + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }) + Object.assign(button.style, { + display: 'block', + position: 'absolute', + left: '50%', + top: '50%', + padding: '20px', + backgroundColor: '#7F33ED', + color: 'white', + fontFamily: 'monospace', + borderRadius: '3px', + transform: 'translate3D(-50%,-50%,0)', + textAlign: 'center', + lineHeight: '1.5', + width: '150px', + }) + container.appendChild(button) + document.body.appendChild(container) + StartAudioContext.setContext(Tone.context) + StartAudioContext.on(button) + StartAudioContext.onStarted(_ => { + container.remove() + fn() + }) + } else { + fn() + } +} + +export { + choice, mod, clamp, + randint, randrange, randsign, + browser, requestAudioContext, +} + |
