summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2018-02-21 04:10:28 +0100
committerJules Laplace <julescarbon@gmail.com>2018-02-21 04:10:28 +0100
commit9f68262f9eb4720b4d6466e1cf5cf9c0edb9c286 (patch)
tree30dab1f29749031680d19c70a6b4a88a553fa9ba /client
smash
Diffstat (limited to 'client')
-rw-r--r--client/draw.js48
-rw-r--r--client/index.js83
-rw-r--r--client/lib/color.js33
-rw-r--r--client/lib/kalimba.js56
-rw-r--r--client/lib/keys.js39
-rw-r--r--client/lib/mouse.js57
-rw-r--r--client/lib/sampler.js50
-rw-r--r--client/lib/startAudioContext.js181
-rw-r--r--client/lib/util.js69
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,
+}
+