Skip to content

Instantly share code, notes, and snippets.

@e1blue
Created June 20, 2018 02:16
Show Gist options
  • Select an option

  • Save e1blue/3528b85b41e2117eec206632bcd4eb38 to your computer and use it in GitHub Desktop.

Select an option

Save e1blue/3528b85b41e2117eec206632bcd4eb38 to your computer and use it in GitHub Desktop.
Music Paint
<svg id="symbols">
<symbol id="square" viewBox="0 0 128 128">
<path style="fill:none;stroke:#aad400;stroke-width:4px;stroke-linecap:square;" d="M35,43 l0,40 l30,0 l0,-40 l30,0 l0,40" />
</symbol>
<symbol id="sawtooth" viewBox="0 0 128 128">
<path style="fill:none;stroke:#c83737;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;" d="M30,80 l30,-40 l0,40 l30,-40 l0,40"/>
</symbol>
<symbol id="triangle" viewBox="0 0 128 128">
<path style="fill:none;stroke:#2a7fff;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;" d="M30,80 l17,-40 l17,40 l17,-40 l17,40"/>
</symbol>
<symbol id="sine" viewBox="0 0 128 128">
<path style="fill:none;stroke:#9944ff;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;" d="M30,64 q17,-40 32,0 q17,40 32,0"/>
</symbol>
<symbol id="play" viewBox="0 0 128 128">
<path style="stroke:#aad400;fill:none;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel" d="M33,30 l0,68 l68,-34z"/>
</symbol>
<symbol id="pause" viewBox="0 0 128 128">
<rect style="stroke:#c83737;fill:none;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel" x="30" y="30" width="30" height="68"/>
<rect style="stroke:#c83737;fill:none;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;" x="68" y="30" width="30" height="68"/>
</symbol>
<symbol id="record" viewBox="0 0 128 128">
<circle style="fill:#c83737" cx="64" cy="64" r="32"/>
</symbol>
<symbol id="rewind" viewBox="0 0 128 128">
<path style="stroke:#4466ff;fill:none;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;" d="M30,64 l34,-34 l0,34 l34,-34 l0,68 l-34,-34 l0,34Z" />
</symbol>
<symbol id="clrscr" viewBox="0 0 128 128">
<path style="stroke:#c83737;stroke-width:4px;stroke-linecap:square;fill:none;" d="M30,30 l68,68 M30,98 l68,-68"/>
</symbol>
<symbol id="move" viewBox="0 0 128 128">
<path style="stroke:#4488ff;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;fill:none;" d="M30,30 l20,0 l-20,20 l0,-20 l68,68 l0,-20 l-20,20 l 20,0 M30,98 l0,-20 l20,20 l-20,0 l68,-68 l0,20 l-20,-20 l20,0"/>
</symbol>
<symbol id="cloud" viewBox="0 0 128 128">
<path style="stroke:#44ccff;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;fill:none;" d="M30,64 q12,0 12,-8 q0,-16 16,-16 q16,0 16,16 q24,0 24,16 q0,16 -16,16 l-42,0 q-16,0 -16,-18 Z"/>
</symbol>
<symbol id="disk" viewBox="0 0 128,128">
<path style="stroke:#44ccff;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;fill:none;" d="M30,30 l52,0 l16,16 l0,52 l-68,0 l 0,-68 M40,30 l0,24 l36,0 l0,-24 M48,30 l0,16 l6,0 l0,-16 M40,98 l0,-30 l48,0 l0,30"/>
</symbol>
<symbol id="open" viewBox="0 0 128,128">
<path style="stroke:#ffcc33;stroke-width:4px;stroke-linecap:square;stroke-linejoin:bevel;fill:none;" d="M30,98 l15,-30 l60,0 l-15,30 l-60,0 l0,-45 l8,-8 l8,0 l8,8 l36,0 l0,15"/>
</symbol>
<symbol id="noise" viewBox="0 0 128 128">
<path id="p" d="M30,92.5L32,59.5L34,42L36,32.5L38,35.5L40,41.5L42,84.5L44,60.5L46,52.5L48,77.5L50,88L52,53.5L54,79L56,57.5L58,59L60,37.5L62,45.5L64,88L66,53L68,72L70,90L72,91.5L74,95.5L76,51.5L78,95.5L80,50.5L82,44.5L84,93L86,52L88,61" style="fill:none;stroke:#fff;stroke-width:4px;">
</path>
</symbol>
</svg>
<nav>
<ul>
<li><a id="rewBtn" title="rewind" href="#"><svg><use xlink:href="#rewind"/></svg></a></li>
<li><a id="playBtn" title="play" href="#"><svg><use xlink:href="#play"/></svg></a></li>
<li class="hidden"><a id="pauseBtn" href="#"><svg><use xlink:href="#pause"/></svg></a></li>
<li><a role="type" href="#sine" title="sine"><svg><use xlink:href="#sine"/></svg></a></li>
<li class="hidden"><a role="type" href="#sawtooth" title="sawtooth"><svg><use xlink:href="#sawtooth"/></svg></a></li>
<li class="hidden"><a role="type" href="#triangle" title="triangle"><svg><use xlink:href="#triangle"/></svg></a></li>
<li class="hidden"><a role="type" href="#square" title="square"><svg><use xlink:href="#square"/></svg></a></li>
<li class="hidden"><a role="type" href="#noise" title="noise"><svg><use xlink:href="#noise"/></svg></a></li>
<li><a id="moveBtn" href="#move" title="move"><svg><use xlink:href="#move"/></svg></a></li>
<li><a id="clrBtn" href="#clear" title="clear screen"><svg><use xlink:href="#clrscr"/></svg></a></li>
<li><a id="dlBtn" href="#disk" title="save"><svg><use xlink:href="#disk"/></svg></a></li>
<li><a id="ulBtn" href="#disk" title="open"><svg><use xlink:href="#open"/></svg>
<input id="inputFile" type="file" class="hidden" accept="image/svg+xml"/>
</a></li>
</ul>
</nav>
<main>
<svg id="main">
<path id="grid" />
<path id="bounds"/>
<path id="cursor"/>
</svg>
</main>
<div class="overlay">
<div class="overlay__tap2start">
tap to start
</div>
</div>

Music Paint

This is a little web audio experiment where you can paint music.

Every stroke makes some noise. Either sine, square, triangle, sawtooth or white noise.

It has some cool features:

  • Unlimited canvas, (use the right mouse button to explore the canvas, or switch to move mode to move around)
  • Multi-touch is supported
  • Shake your phone to start over
  • Thanks to Web MIDI API, you can connect a MIDI device and make some noise :)
  • Or you can just use the keyboard
  • Save your work as an SVG file and reopen it for later use ☺

Limitations:

  • every tone is like a function (x) => y (only one y per x).

PS: Special thanks to my testers: My nieces (ages 4 and 7) had a lot of fun playing with it ☺

UPDATE: Unfortunately, Chrome only. Limited support in Firefox and Edge.

A Pen by Lea Rosema on CodePen.

License.

const isDebug = /debug/.test(window.location.href)
const w3 = "http://www.w3.org/"
const svgNS = w3 + "2000/svg"
const xlinkNS = w3 + "1999/xlink"
const notes = "C C# D D# E F F# G G# A A# B".split(" ")
let w, h
let scrollX = 0, scrollY = 0, cursorX = 0
const borders = { l: 0, r: 250 }
const borderExtend = 250
let moveMode = null
let playing = false
const d = document
const $ = document.querySelector.bind(d)
const $$ = (sel, con) => Array.prototype.slice.call((con||d).querySelectorAll(sel))
const { sqrt, min, max } = Math
const distance = (a, b) => sqrt((b[0]-a[0])**2 + (b[1]-a[1])**2)
const freq = (y) => max(880 - y, 10)
const freqToY = (f) => 880 - f
const svg = $`#main`
let masterVolume
let AC = new AudioContext()
const defaultVolume = 0.5
const initAudioContext = () => {
if(AC.state === "suspended") {
$`.overlay`.style.display='block'
$`.overlay`.addEventListener('click', () => {
$`.overlay`.style.display='none'
AC.resume()
})
}
masterVolume = AC.createGain()
masterVolume.gain.value = defaultVolume
masterVolume.connect(AC.destination)
}
initAudioContext()
const music = []
let mouseNoise = null
let touchNoises = []
let midiNoises = {}
let currentType = "sine"
let clientRect = svg.getBoundingClientRect()
Array.prototype.avg = function() {
let r = 0, i = 0;
for(i = 0; i < this.length; i++)
r+=this[i]
return r/this.length
}
const typeColors = {
"sine": "#9944ff",
"square": "#aad400",
"sawtooth": "#c83737",
"triangle": "#2a7fff",
"noise": "#ffffff"
}
const noteToFreq = (note, octave) => {
const n = (typeof note === "string") ? notes.indexOf(note.replace(/_/,'')) : n
const f = (110*2**octave)*2**((n+3)/12)
return f
}
const hexColor = (c) => {
if (c.slice(0) === '#') return c
if (c.slice(0,4) === 'rgb(') {
return "#" + c.slice(4,-1).split(",").map(a => ((a|0)>>4).toString(16)+((a|0)%16).toString(16)).join("")
}
return c
}
const types = Object.keys(typeColors)
const attribs = (el, attrs, x) => {
for(x in attrs)
if(attrs.hasOwnProperty(x) && attrs[x] !== undefined)
el.setAttribute(x,attrs[x])
}
const draw = (name,attrs) => {
const el = document.createElementNS(svgNS, name)
if(attrs) attribs(el,attrs)
return el
}
const scrollToCursor = () => {
const middleX = (clientRect.right - clientRect.left) / 2
if (cursorX > scrollX + middleX || scrollX > cursorX) {
scrollX = cursorX - middleX
}
}
const relPos = (x,y) => {
// calculate from mouse/touch position
// to svg coordinate
const relativeX = scrollX + x - clientRect.left
const relativeY = scrollY + y - clientRect.top
return { relX: relativeX, relY: relativeY }
}
const userIsJamming = () => {
return (!!mouseNoise) || touchNoises.length > 0 || Object.keys(midiNoises).length > 0
}
const setCursor = (x) => {
if (userIsJamming() && x < cursorX) {
return
}
cursorX = x
scrollToCursor()
setViewBox()
}
const SoundMachine = {
// Helper factory that creates and reuses
// oscillators. Usage:
// SoundMachine.noise(frequency, type)
// returns a noise that starts playing
// immediately. It does not create a
// new oscillator each time. After a noise
// is muted, it can be reused, avoiding
// memory issues.
noises: [],
whiteNoiseBuffer: null,
createWhiteNoise: () => {
if (! SoundMachine.whiteNoiseBuffer) {
const len = AC.sampleRate * 2
const buf = AC.createBuffer(1, AC.sampleRate, len)
const data = buf.getChannelData(0)
for (let i = 0; i < len; i++) {
data[i] = Math.random() * 2 - 1
}
SoundMachine.whiteNoiseBuffer = buf
}
const bufSrc = AC.createBufferSource()
bufSrc.buffer = SoundMachine.whiteNoiseBuffer
bufSrc.loop = true
bufSrc.playbackRate.value = 1.0
return bufSrc
},
makeSomeNoise: (freq, type) => {
const noise = {
osc: type === "noise" ?
SoundMachine.createWhiteNoise() : AC.createOscillator(),
env: AC.createGain()
}
const { osc, env } = noise
if (type !== "noise") {
osc.type = type
osc.frequency.value = freq
osc.detune.value = 0
}
env.gain.value = 1.0
osc.start()
osc.connect(env)
env.connect(masterVolume)
SoundMachine.noises.push(noise)
return noise
},
noise: (freq, type) => {
// reuse a noise we muted before
const noise = SoundMachine.noises.find(n => {
return n.env.gain.value === 0.0 &&
n.osc.type === type
})
if (! noise) {
// create new noise
return SoundMachine.makeSomeNoise(freq, type)
}
const { osc, env } = noise
osc.frequency.setValueAtTime(freq, 0)
// chromium issue 645776
// osc.frequency.value = freq
env.gain.value = 1.0
return noise
}
}
class Noise {
constructor(x,y,type) {
this.element=draw("path",{
"d":"",
"style": `stroke:${typeColors[type]};stroke-width:4;fill:none;`
})
this.coords = []
this.type = type
if (x) {
if (typeof x === "object") {
this.add(x)
} else {
if (isFinite(x) && y && isFinite(y)) this.add(x, y)
}
}
svg.appendChild(this.element)
this.render()
}
add(x, y, quiet) {
this.lastX = x
this.lastY = y
const { relX, relY } = typeof x === "object" ? x : relPos(x, y)
if (this.coords.length > 0) {
const lastCoord = this.coords[this.coords.length - 1]
const firstCoord = this.coords[0]
if (distance([relX, relY], lastCoord) < 5) {
return
}
if (firstCoord[0] < relX) {
this.coords = this.coords.filter(c => c[0] < relX)
} else {
this.coords = this.coords.filter(c => c[0] > relX)
}
}
this.coords.push([relX, relY])
if (relX > borders.r) borders.r+=borderExtend
if (relX < borders.l) borders.l-=borderExtend
this.render()
if (quiet) {
return
}
if (! this.noise) {
this.noise = SoundMachine.noise(freq(relY), this.type)
}
if (this.type !== "noise") {
this.noise.osc.frequency.value = freq(relY)
} else {
this.noise.osc.playbackRate.value = freq(relY) / 1e4
}
}
playAtX(x) {
const { coords } = this
const l = coords.length
if (l < 2) {
return
}
const isMuted = coords[0][0] > x || coords[l - 1][0] < x
if (isMuted) {
if (this.playerNoise) {
this.playerNoise.env.gain.value=0.0
this.playerNoise = null
}
} else {
const nearPoints = coords.filter((p) => Math.abs(p[0] - x) <= 5)
const averageFreq = freq(nearPoints.map(p => p[1]).avg())
if(!isFinite(averageFreq)) {
return
}
if (!this.playerNoise) {
this.playerNoise = SoundMachine.noise(averageFreq, this.type)
} else {
if (this.type !== "noise") {
this.playerNoise.osc.frequency.setValueAtTime(averageFreq, 0)
// https://bugs.chromium.org/p/chromium/issues/detail?id=645776
// this.playerNoise.osc.frequency.value = averageFreq
} else {
this.playerNoise.osc.playbackRate.value = averageFreq / 1e4
}
}
}
}
mute() {
if (this.noise) {
this.noise.env.gain.value=0.0
this.noise = null
}
if (this.playerNoise) {
this.playerNoise.env.gain.value=0.0
this.playerNoise = null
}
}
dispose() {
this.mute()
svg.removeChild(this.element)
this.element = null
}
sortCoords() {
this.coords.sort((a,b) => {
if (a[0] > b[0]) return 1
if (a[0] < b[0]) return -1
if (a[0] == b[0]) return 0
})
this.render()
}
render() {
const { coords } = this
if (coords.length < 2) {
return
}
this.element.setAttribute("d", "M"+coords[0].join(",")+
"L"+coords.slice(1).map(c=> c.join(",")).join("L"))
}
static fromPath(el) {
const waveForms = Object.keys(typeColors).reduce((obj,key) => {
obj[typeColors[key]] = key
return obj
},{})
const color = hexColor(el.style.stroke)
const d = el.getAttribute("d")
if (waveForms[color] && /M\d+\,\d+(L\d+\,\d+)+/.test(d)) {
const n = new Noise(null, null, waveForms[color] || "sine")
const coords = d.slice(1).split("L").map(p => p.split(',').map(x => x|0))
coords.forEach(c => n.add({relX: c[0], relY: c[1]}, null, true))
return n
}
}
}
const moveStart = (e) => {
moveMode = {
moving: true,
x0: e.clientX,
y0: e.clientY,
scrollX0: scrollX,
scrollY0: scrollY
}
}
const moveDrag = (e) => {
if (!moveMode.moving) {
return
}
const { x0, y0, scrollX0, scrollY0 } = moveMode
const dx = x0 - e.clientX
const dy = y0 - e.clientY
scrollX = scrollX0 + dx
scrollY = scrollY0 + dy
setViewBox()
}
const moveEnd = (e) => {
moveMode = { moving: false }
}
const setMoveMode = (move, e) => {
const btn = $('#moveBtn')
if (move) {
moveMode = e? {
moving: true,
x0: e.clientX,
y0: e.clientY,
scrollX0: scrollX,
scrollY0: scrollY
} : { moving: false }
btn.classList.add("selected")
svg.classList.add("move")
} else {
moveMode = null
btn.classList.remove("selected")
svg.classList.remove("move")
}
}
$('#moveBtn').addEventListener("click", e => {
setMoveMode(!!!moveMode)
// "multiple exclamation marks", he went on,
// shaking his head, "are a sure sign of
// a diseased mind."
// (Terry Pratchet in 'Eric') ♥
})
svg.addEventListener("mousedown", e => {
if (touchNoises.length > 0) {
// long tap on mobile
// triggers contextmenu.
// Additionally: mousedown
// without a mouseup event.
// this would result in a never-ending
// beeeeeeeep on mobile
return;
}
if (e.button === 1) {
const { relX, relY } = relPos(e.clientX, e.clientY)
setCursor(relX)
e.preventDefault()
return
}
if (!isDebug && e.button === 2) {
setMoveMode(true, e)
return
}
if (mouseNoise) {
mouseNoise.dispose()
mouseNoise = null
}
if (moveMode) {
moveStart(e)
return
}
mouseNoise = new Noise(e.clientX, e.clientY, currentType)
})
svg.addEventListener("mousemove", e => {
if (moveMode) {
moveDrag(e)
return
}
if (mouseNoise) {
mouseNoise.add(e.clientX, e.clientY)
}
})
svg.addEventListener("mouseup", e => {
if (moveMode) {
moveEnd(e)
if (e.button === 2) {
setMoveMode(false)
}
return
}
if (mouseNoise) {
mouseNoise.mute()
mouseNoise.sortCoords()
music.push(mouseNoise)
mouseNoise = null
}
})
svg.addEventListener("touchstart", e => {
if (moveMode) {
setMoveMode(true, e.changedTouches[0])
return
}
Array.prototype.slice.call(e.changedTouches).map(t => {
touchNoises.push({
id: t.identifier,
noise: new Noise(t.clientX, t.clientY, currentType)
})
})
})
const touch = (id) => touchNoises.find(el => el.id === id)
svg.addEventListener("touchmove", e => {
e.preventDefault()
if (moveMode) {
moveDrag(e.changedTouches[0])
return
}
Array.prototype.slice.call(e.changedTouches).map(t => {
const touchObj = touch(t.identifier)
if (touchObj) {
touchObj.noise.add(t.clientX, t.clientY)
}
})
})
svg.addEventListener("touchend", e => {
if (moveMode) {
moveEnd(e)
return
}
Array.prototype.slice.call(e.changedTouches).map((t, idx) => {
const touchObj = touch(t.identifier)
if (touchObj) {
// touchObj.identifier = void 0
touchObj.noise.mute()
touchObj.noise.sortCoords()
music.push(touchObj.noise)
touchObj.noise = null
}
})
touchNoises = touchNoises.filter(n => n.noise !== null)
})
const setViewBox = () => {
clientRect = svg.getBoundingClientRect()
w = max(0, innerWidth - clientRect.left)
h = max(0, innerHeight - clientRect.top)
const bounds = $`#bounds`
const x0 = scrollX - scrollX % (borderExtend/8)
const y0 = scrollY - scrollY % (borderExtend/8)
const nX = 2 + ((8*w / borderExtend)|0)
const nY = 2 + ((8*h / borderExtend)|0)
const gridLines = Array(nX).fill(0).map((e,i) => {
return `M${x0+i*borderExtend/8},${scrollY}l0,${h}`
}).join('') + Array(nY).fill(0).map((e,i) => {
return `M${scrollX},${y0+i*borderExtend/8}l${w},0`
})
bounds.setAttribute("d", `M${borders.l},${scrollY}l0,${h}`
+`M${borders.r},${scrollY}l0,${h}`)
const cursor = $`#cursor`
cursor.setAttribute("d", `M${cursorX},${scrollY}l0,${h}`)
svg.setAttribute("viewBox", [scrollX, scrollY, w, h])
const grid = $`#grid`
grid.setAttribute("d", gridLines)
}
setViewBox()
addEventListener("resize", setViewBox)
addEventListener("contextmenu", e => {
// prevent long-tap on touch screens
// to trigger the context menu event.
// Also prevent the context menu when
// not in debug-mode because it is
// used for moving the svg
if (!isDebug || touchNoises.length > 0) {
e.preventDefault()
return false
}
})
const rewind = () => {
scrollX = borders.l
cursorX = borders.l
setViewBox()
}
const play = () => {
playing = true
scrollX = 0
$('#playBtn').parentNode.classList.add('hidden')
$('#pauseBtn').parentNode.classList.remove('hidden')
}
const pause = () => {
playing = false
music.forEach(beep => beep.mute())
$('#pauseBtn').parentNode.classList.add('hidden')
$('#playBtn').parentNode.classList.remove('hidden')
}
$`#rewBtn`.addEventListener("click", rewind)
$`#playBtn`.addEventListener("click", play)
$`#pauseBtn`.addEventListener("click", pause)
const waveBtns = $$('a[role=type]')
waveBtns.map(btn => btn.addEventListener("click", (e) => {
waveBtns.map(btn => btn.parentNode.classList.add("hidden"))
let a = e.target
while (a.nodeName.toLowerCase() !== 'a') {
a = a.parentNode
}
const idx = waveBtns.indexOf(a)
const nextA = waveBtns[(idx+1) % waveBtns.length]
currentType = nextA.getAttribute("href").slice(1)
nextA.parentNode.classList.remove("hidden")
setMoveMode(false)
}))
const shakeEvent = new Shake()
shakeEvent.start()
const clearScr = () => {
pause()
const tmp = music.splice(0, music.length)
tmp.forEach(beep => beep.dispose())
rewind()
scrollY = 0
borders.l = 0
borders.r = borderExtend
setViewBox()
}
window.addEventListener('shake', clearScr)
$('#clrBtn').addEventListener('click', clearScr)
window.addEventListener('orientationchange', () => {
touchNoises.splice(0,music.length).forEach(beep => beep.dispose())
})
window.addEventListener('blur', () => {
if (masterVolume) masterVolume.gain.value = 0.0
})
window.addEventListener('focus', () => {
if (masterVolume) masterVolume.gain.value = 0.5
})
const getSurroundingViewBox = () => {
let t = Infinity, l = borders.l
let b = -Infinity, r = borders.r
music.forEach(m => m.coords.forEach(p => {
t = min(t, p[1])
b = max(b, p[1])
}))
if (!isFinite(t)) t = 0
if (!isFinite(b)) b = innerHeight
return `${t} ${l} ${b-t+1} ${r-l+1}`
}
$`#dlBtn`.addEventListener('click', () => {
const anchor = document.createElement("a")
anchor.setAttribute("download", "awesome-music.svg")
const viewBox = getSurroundingViewBox()
const code = `<?xml version="1.0" encoding="utf-8" standalone="no"?>\n`+
`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" \n` +
` "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n` +
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="${viewBox}">\n${music.map(m => m.element.outerHTML).join("\n")}\n</svg>`
anchor.setAttribute("href", "data:application/octet-stream;base64,"+btoa(code))
anchor.click()
})
$`#ulBtn`.addEventListener('click',() => {
if (music.length > 0) {
if (!confirm('Discard your current music ?')) return
clearScr()
}
$`#inputFile`.click()
})
$`#inputFile`.addEventListener('change', (e) => {
const file = $`#inputFile`.files[0]
const reader = new FileReader()
reader.onload = () => {
let code = reader.result.replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/ig, '').trim()
if (!code.slice(4) === "<svg") {
return
}
const container = document.createElement("div")
container.innerHTML = code
const innerSVG = $$('svg', container)[0]
$$("path", innerSVG).forEach(p => {
const n = Noise.fromPath(p)
if (n) music.push(n)
})
$`#inputFile`.value=""
setViewBox()
}
reader.readAsText(file)
})
WebMidi.enable(function(err) {
if (err) {
console.log("No midi. Too bad :(")
}
WebMidi.inputs.forEach(input => {
input.addListener('noteon', 'all', e => {
const noteName = e.note.name + e.note.octave
const f = noteToFreq(e.note.name, e.note.octave)
const y = freqToY(f)
if (!midiNoises[noteName]) {
midiNoises[noteName] = new Noise({relX: cursorX, relY: y}, null, currentType)
midiNoises[noteName].add({relX: cursorX + 5, relY: y}, null)
scrollToCursor()
}
})
input.addListener('noteoff', 'all', e => {
const noteName = e.note.name + e.note.octave
const f = noteToFreq(e.note.name, e.note.octave)
const y = freqToY(f)
if (midiNoises[noteName]) {
midiNoises[noteName].coords[1][0] = cursorX
music.push(midiNoises[noteName])
midiNoises[noteName].mute()
delete midiNoises[noteName]
scrollToCursor()
}
})
})
})
const keyboardMappings = () => {
const lang = navigator.language.slice(0,2)
const mappings = {
"de": "ysxdcvgbhnjmq2w3er5t6z7u",
"en": "zsxdcvgbhnjmq2w3er5t6y7u",
"fr": "wsxdcvgbhnj,a2z3er5t6y7u",
}
return mappings[lang]||mappings.en
}
window.addEventListener("keydown", (e) => {
const mappings = keyboardMappings()
console.log(e.key)
const i = mappings.indexOf(e.key)
if (i > -1) {
const note = notes[i % 12]
const oct = 1 + (i / 12)|0
const noteName = "_" + note + oct
const f = noteToFreq(note, oct)
const y = freqToY(f)
if (!midiNoises[noteName]) {
midiNoises[noteName] = new Noise({relX: cursorX, relY: y}, null, currentType)
midiNoises[noteName].add({relX: cursorX + 5, relY: y}, null)
scrollToCursor()
}
}
})
window.addEventListener("keyup", (e) => {
const mappings = keyboardMappings()
const i = mappings.indexOf(e.key)
if (i > -1) {
const note = notes[i % 12]
const oct = 1 + (i / 12)|0
const noteName = "_" + note + oct
if (midiNoises[noteName]) {
midiNoises[noteName].coords[1][0] = cursorX
music.push(midiNoises[noteName])
midiNoises[noteName].mute()
delete midiNoises[noteName]
scrollToCursor()
}
}
})
~function loop() {
const midiKeys = Object.keys(midiNoises)
if (playing || midiKeys.length > 0) {
cursorX+=2
if (midiKeys.length > 0 && cursorX > borders.r) {
borders.r += borderExtend
}
midiKeys.forEach(k => {
let n = midiNoises[k]
if (n.coords.length == 2) {
n.coords[1][0] = cursorX
n.render()
}
})
scrollToCursor()
setViewBox()
}
if (playing) {
if (cursorX > borders.r && (!userIsJamming())) setCursor(borders.l)
music.forEach(beep => beep.playAtX(cursorX))
}
if (touchNoises.length > 0 || mouseNoise) {
if (!playing) scrollX+=3
if (mouseNoise) {
mouseNoise.add(mouseNoise.lastX, mouseNoise.lastY)
}
touchNoises.forEach(n => n.noise.add(n.noise.lastX,n.noise.lastY))
setViewBox()
}
requestAnimationFrame(loop)
}(0)
<script src="https://unpkg.com/shake.js@1.2.2/shake.js"></script>
<script src="https://unpkg.com/webmidi@2.0.0-rc.2"></script>
* {
box-sizing: border-box;
}
body {
background: #222;
margin: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
nav {
background: #000;
position: absolute;
width: 64px;
bottom: 0;
top: 0;
}
nav svg {
width: 64px;
height: 64px;
}
main svg {
width: 100%;
height: 100%;
}
main {
position: absolute;
left: 64px;
top: 0;
cursor: crosshair;
bottom: 0;
right: 0;
}
nav ul {
z-index: 2;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
align-content: center;
justify-content: center;
list-style: none;
}
nav ul li {
display: block;
}
nav a.selected > svg,
nav a.selected > svg {
background: #333;
}
svg.move {
cursor: move;
}
#bounds {
stroke: mediumseagreen;
stroke-width: 1px
}
#cursor {
stroke: white;
stroke-width: 1px
}
#grid {
stroke: #333;
stroke-width: 1px;
fill: none;
}
.hidden {
display:none;
}
@media only screen and (orientation: landscape) {
nav {
height: 64px;
width: 100vw;
bottom: auto;
}
main {
left: 0;
top: 64px;
right: 0;
bottom: 0;
}
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
background: rgba(0,0,0,0.5);
}
.overlay__tap2start {
height: 100vh;
font-family: sans-serif;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 10vw;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment