class VTUtils { static random(min, max) { let rand = Math.random(); if (typeof min === 'undefined') { return rand; } else if (typeof max === 'undefined') { if (min instanceof Array) { return min[Math.floor(rand * min.length)]; } else { return rand * min; } } else { if (min > max) { let tmp = min; min = max; max = tmp; } return rand * (max - min) + min; } }; static randomInt(min, max) { return Math.floor(VTUtils.random(min, max)); } static normalize(val, max, min) { return (val - min) / (max - min); }; static distance(x, y, x2, y2) { let a = x - x2; let b = y - y2; return Math.sqrt(a * a + b * b); } static map(n, start1, stop1, start2, stop2, withinBounds) { let newVal = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2; if (!withinBounds) { return newVal; } if (start2 < stop2) { return this.constrain(newVal, start2, stop2); } else { return this.constrain(newVal, stop2, start2); } }; static constrain(n, low, high) { return Math.max(Math.min(n, high), low); } static hsvToRgb(h, s, v) { var r, g, b; var i = Math.floor(h * 6); var f = h * 6 - i; var p = v * (1 - s); var q = v * (1 - f * s); var 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: r, g: g, b: b}; } static peakRGB(peak) { return { r: peak, g: 1 - peak, b: 0 }; } } class VTVector { constructor(x, y, z) { this.x = x || 0; this.y = y || 0; this.z = z || 0; } //helper static createRandom(x, y, z) { x = x || 1; y = y || 1; z = z || 0; return new VTVector(VTUtils.random(-x, x), VTUtils.random(-y, y), VTUtils.random(-z, z)); } mult(times) { this.x *= times; this.y *= times; this.z *= times; } set(vector) { this.x = vector.x; this.y = vector.y; this.z = vector.z; } add(vector) { this.x = this.x + vector.x; this.y = this.y + vector.y; this.z = this.z + vector.z; } addXYZ(x, y, z) { this.x += x; this.y += y; this.z += z; } setXYZ(x, y, z) { this.x = x || 0; this.y = y || 0; this.z = z || 0; } clone() { return new VTVector(this.x, this.y, this.z); } } class TDUtils { static multiply(a, b) { let b00 = b[0 * 4 + 0]; let b01 = b[0 * 4 + 1]; let b02 = b[0 * 4 + 2]; let b03 = b[0 * 4 + 3]; let b10 = b[1 * 4 + 0]; let b11 = b[1 * 4 + 1]; let b12 = b[1 * 4 + 2]; let b13 = b[1 * 4 + 3]; let b20 = b[2 * 4 + 0]; let b21 = b[2 * 4 + 1]; let b22 = b[2 * 4 + 2]; let b23 = b[2 * 4 + 3]; let b30 = b[3 * 4 + 0]; let b31 = b[3 * 4 + 1]; let b32 = b[3 * 4 + 2]; let b33 = b[3 * 4 + 3]; let a00 = a[0 * 4 + 0]; let a01 = a[0 * 4 + 1]; let a02 = a[0 * 4 + 2]; let a03 = a[0 * 4 + 3]; let a10 = a[1 * 4 + 0]; let a11 = a[1 * 4 + 1]; let a12 = a[1 * 4 + 2]; let a13 = a[1 * 4 + 3]; let a20 = a[2 * 4 + 0]; let a21 = a[2 * 4 + 1]; let a22 = a[2 * 4 + 2]; let a23 = a[2 * 4 + 3]; let a30 = a[3 * 4 + 0]; let a31 = a[3 * 4 + 1]; let a32 = a[3 * 4 + 2]; let a33 = a[3 * 4 + 3]; let dst = []; dst[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30; dst[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31; dst[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32; dst[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33; dst[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30; dst[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31; dst[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32; dst[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33; dst[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30; dst[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31; dst[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32; dst[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33; dst[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30; dst[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31; dst[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32; dst[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33; return dst; } static xRotation(angleInRadians) { let c = Math.cos(angleInRadians); let s = Math.sin(angleInRadians); return [ 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, ]; } static yRotation(angleInRadians) { let c = Math.cos(angleInRadians); let s = Math.sin(angleInRadians); return [ c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1, ]; } static zRotation(angleInRadians) { let c = Math.cos(angleInRadians); let s = Math.sin(angleInRadians); return [ c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, ]; } static degToRad(d) { return d * Math.PI / 180; } } function $(sel, s) { return $$(sel, s)[0]; } function $$(sel, s) { s = s || document; return s.querySelectorAll(sel); } Node.prototype.addDelegatedEventListener = function (type, aim, cb) { this.addEventListener(type, (event) => { let target = event.target; if (target.matches(aim)) { cb(event, target); } else { let parent = target.closest(aim); if (parent) { cb(event, parent); } } }) }; Node.prototype.hasClass = function (className) { return this.classList.contains(className); } Node.prototype.addClass = function (className) { return this.classList.add(className); } Node.prototype.removeClass = function (className) { return this.classList.remove(className); } function create(name, content) { let d = document.createElement(name); if (content) { d.innerHTML = content; } return d; } function append(to, array) { for (let item in array) { to.appendChild(array[item]); } } class Template { constructor() { this.tpl = {}; } async loadTemplate(name) { let self = this; if (!this.tpl[name]) { await fetch(templateDir + name + ".tpl").then((r) => r.text()).then(c => { self.tpl[name] = c; }) } } async loadArray(names) { for (let name of names) { await this.loadTemplate(name); } } parseTemplate(name, data) { if (!this.tpl[name]) { return "" } let m, d = this.tpl[name]; while ((m = templateEx.exec(d)) !== null) { if (m.index === templateEx.lastIndex) { templateEx.lastIndex++; } let key = m[0]; d = d.replace(key, data[m[1]] || "") } return d; } parseFromAPI(url, name, cb) { fetch(url).then((r) => r.json()).then(d => { cb(this.parseTemplate(name, d)) }).catch(console.error) } } const templateEx = /\$(.*?)\$/gm; const templateDir = "out/tpl/" class ShaderHandler { constructor(gl) { this.gl = gl; this.shaderNames = []; this.shaders = {}; this.programs = {}; } setGL(gl) { this.gl = gl; } async loadShader(name, path) { this.shaderNames.push(name); await this.load(name, path + name + ".vert", this.gl.VERTEX_SHADER); await this.load(name, path + name + ".frag", this.gl.FRAGMENT_SHADER); } async load(name, url, type) { let realName = name + "_" + type; if (!this.shaders[realName]) { let data = await fetch(url); let shader = this.createShader(await data.text(), type); if (shader) { this.shaders[realName] = shader; } } return !!this.shaders[realName]; } getShader(name, type) { let realName = name + "_" + type; return this.shaders[realName]; } getAllShaders() { return this.shaderNames; } async createProgramForEach(arr) { arr = arr || this.shaderNames; for (let i = 0; i < arr.length; i++) { let shader = arr[i]; let v = await shaderHandler.createProgram(shader, [shader]) if (!v) { return false; } } return true; } createShader(source, type) { let gl = this.gl; let shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { return shader; } console.error(gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } createProgram(name, shaders) { let gl = this.gl; let pro = gl.createProgram(); for (let i = 0; i < shaders.length; i++) { gl.attachShader(pro, this.getShader(shaders[i], gl.VERTEX_SHADER)); gl.attachShader(pro, this.getShader(shaders[i], gl.FRAGMENT_SHADER)); } gl.linkProgram(pro); var success = gl.getProgramParameter(pro, gl.LINK_STATUS); if (success) { this.programs[name] = pro; return pro; } console.log(gl.getProgramInfoLog(pro)); gl.deleteProgram(pro); return null; } getProgram(name) { return this.programs[name]; } use(name) { let pro = this.programs[name]; this.gl.useProgram(pro); return pro; } async loadArray(list, path) { let self = this; for (const e of list) { await self.loadShader(e, path) } await self.createProgramForEach(list) } } const AudioContext = window.AudioContext || window.webkitAudioContext; class AudioHandler { async init() { let self = this; self.isStarted = false; self.audioFile = new Audio(); self.actx = new AudioContext() self.analyser = self.actx.createAnalyser() self.analyser.fftSize = 4096; self.lastSong = null; await self.connectAll(); } async connectAll() { let self = this; self.source = self.actx.createMediaElementSource(self.audioFile); self.source.connect(self.analyser); self.analyser.connect(self.actx.destination); self.audioFile.addEventListener('ended', player.nextSong.bind(player)); } async start() { if (this.audioFile.src === '') { return; } if (!this.isStarted) { this.isStarted = true; await this.actx.resume(); } } async stop() { if (this.isStarted) { this.isStarted = false; await this.actx.suspend(); } } fftSize(size) { this.analyser.fftSize = size; } smoothing(float) { this.analyser.smoothingTimeConstant = float; } loadSong(src) { let self = this; if (self.lastSong) { URL.revokeObjectURL(self.lastSong); } self.lastSong = this.audioFile.src = URL.createObjectURL(src); if (!this.isStarted) { this.start(); } this.audioFile.play(); } getIntArray(steps) { let dataArray = new Uint8Array(steps); this.analyser.getByteFrequencyData(dataArray); return dataArray; } getFloatArray() { let dataArray = new Float32Array(this.analyser.frequencyBinCount); this.analyser.getFloatTimeDomainData(dataArray); return dataArray; } } class Player { async init() { this.playlist = new Playlist(); } nextSong() { let next = this.playlist.getNext(); audioHandler.loadSong(next.file); } prevSong() { let next = this.playlist.getPrevious(); audioHandler.loadSong(next.file); } playStop() { if (!audioHandler.lastSong) { let next = this.playlist.getCurrent(); audioHandler.loadSong(next.file); } let audioFile = audioHandler.audioFile; if (audioFile.paused) { audioFile.play(); } else { audioFile.pause(); } } playByID(number) { let next = this.playlist.getFile(number); audioHandler.loadSong(next.file); } } const PAGINATIONLIMIT = 50; class Playlist { constructor() { this.list = []; this.shuffled = []; this.index = 0; this.page = 0; this.isShuffle = false; $('body').addDelegatedEventListener('change', 'input[type="file"]', this.changeFiles.bind(this)); $('body').addDelegatedEventListener('click', '.pagination .item', this.handlePagination.bind(this)); } shuffle() { // only shuffle if more then 2 elements are in let len = this.list.length; if (len < 3) { this.shuffled = this.list; } // the current-list need to be shuffled... for (let i = 0; i < len; i++) { let random = VTUtils.randomInt(0, len - 1); this.swap(i, random); } } swap(a, b) { this.shuffled[a] = this.list[b]; this.shuffled[b] = this.list[a]; } getNext() { let items = this.isShuffle ? this.shuffled : this.list, len = items.length - 1, next = this.index + 1; if (next > len) { next = 0; } this.index = next; return items[next]; } getPrevious() { let items = this.isShuffle ? this.shuffled : this.list, len = items.length - 1, next = this.index - 1; if (next < 0) { next = len; } this.index = next; return items[next]; } getCurrent() { let items = this.isShuffle ? this.shuffled : this.list; return items[this.index]; } getFile(index) { let items = this.isShuffle ? this.shuffled : this.list; return items[index]; } // on new upload... this has to be an array! setPlaylist(files) { this.index = 0; this.list = files; this.shuffle(); } handlePagination(event, el) { if (el.hasClass('inactive')) { return; } if (el.hasClass('next-site')) { this.renderPagination(this.page + 1); } else { this.renderPagination(this.page - 1); } } renderPagination(page) { let length = this.list.length, maxSite = Math.ceil(length / PAGINATIONLIMIT) - 1; if (page < 0) { page = 0; } if (page > maxSite) { page = maxSite; } let s = page * PAGINATIONLIMIT, e = s + PAGINATIONLIMIT, data = ""; this.page = page; if (e >= length) { e = length; } if (length > 0) { let items = this.isShuffle ? this.shuffled : this.list; for (let i = s; i < e; i++) { let obj = { index: i, nr: i + 1, title: items[i].name } data += template.parseTemplate("playlist-item", obj); } } else { data = "

No Songs uploaded!

"; } let hasNext = maxSite > 1 && page < maxSite; gui.modal.renderModal( "Playlist", template.parseTemplate("playlist", { content: data, }), template.parseTemplate('playlist-footer', { prevActive: page > 0 ? 'active' : 'inactive', nextActive: hasNext ? 'active' : 'inactive', page: (page + 1) + ' / ' + parseInt(maxSite + 1), }) ); } //playlist handler for file input! changeFiles(e, el) { let files = []; let i = 0; for (let file of el.files) { if (file && file.type.indexOf('audio') !== -1 && file.name.match(".m3u") === null) { let name = file.name.split("."); name.pop(); name = name.join("."); files.push({ file: file, name: name, index: i++ }); } } this.setPlaylist(files); if (files.length > 0) { this.renderPagination(0); } else { alert("No Valid AudioFiles found!"); } } } class GUI { async init() { this.data = {}; this.modal = new Modal(); // load first vis window! await this.loadForVis(); await template.loadArray([ 'playlist-item', 'playlist', 'playlist-footer' ]); this.initDropZone(); } async loadForVis() { let c = visual.c, d = this.data[c]; if (d == null) { this.data[c] = await fetch("out/gui/" + c + ".json").then((r) => r.json()); } } renderModal(content, title) { let modal = $('#modal'), p = modal.parentNode, h = $('header .headline', modal), con = $('modal-content', modal); h.innerHTML = title; con.innerHTML = content; p.classList.remove('hide'); } initDropZone() { let items = 'drag dragstart dragend dragover dragenter dragleave drop'.split(' '); items.forEach(el => { window.addEventListener(el, async e => { e.preventDefault(); e.stopPropagation(); if (e.type === 'drop') { if (e.dataTransfer.files.length > 0) { player.playlist.changeFiles(e, e.dataTransfer); } else { alert("Sorry you need to upload files!"); } } }); }); }; } class Modal { constructor() { let self = this; self.currentModal = ''; self.modal = $('#modal'); self.parent = self.modal.parentNode; self.modal.addDelegatedEventListener('click', 'header .close', this.closeModal.bind(this)); } resetModal() { this.renderModal('', '', ''); } renderModal(title, content, footer) { this.currentModal = title; this.renderHeader(title); this.renderContent(content); this.renderFooter(footer); this.showModal(); } renderHeader(header) { let h = $('header .headline', this.modal); h.innerHTML = header; } renderContent(content) { let con = $('modal-content', this.modal); con.innerHTML = content; } renderFooter(footer) { let con = $('modal-footer', this.modal); con.innerHTML = footer; } closeModal() { this.parent.addClass("hide") } isCurrent(title) { return title === this.currentModal; } showModal() { this.parent.removeClass("hide") } } class Visual { constructor() { this.data = []; //for drawing this.dataArray = []; } updateData() { } draw() { } setup() { } } class VisualDrawer { constructor() { this.visuals = { "sphere": new Sphere(), "wave": new Wave(), "water": new Water() } this.c = "wave"; } init() { this.visuals[this.c].setup(); this.updateLoop(); } switch(visual) { if (this.visuals[visual] != null) { this.c = visual; this.visuals[this.c].setup(); } } updateLoop() { let self = this; let pro = shaderHandler.use(self.c); let vis = self.visuals[self.c]; vis.updateData(); vis.draw(pro); requestAnimationFrame(self.updateLoop.bind(self)) } } class Config { constructor() { this.config = {}; this.name = '' } loadConfigByName(name) { this.saveConfig(); this.name = 'config-' + name; this.config = JSON.parse(this.name); } saveConfig() { if (this.name !== '') { localStorage.setItem(this.name, JSON.stringify(this.config)); } } addItem(name, value) { this.config[name] = value; } removeItem(name) { delete this.config[name]; } getItem(name, def) { let value = this.config[name]; if (value === undefined || value === null) { this.config[name] = def; value = def; } return value; } } class Sphere extends Visual { draw() { } setup() { } } // 3D Audio-Waves -> maybe also 2D? class Wave extends Visual { updateData() { this.data = []; let data = audioHandler.getFloatArray(); let add = 2 / data.length, x = -1; for (let i = 0; i < data.length; i++) { this.data.push(x, data[i], data[i]); x += add; } } draw(program) { c.width = window.innerWidth; c.height = window.innerHeight; this.prepare(program); let position = this.position, color = this.color, positionBuffer = gl.createBuffer(); this.rotate(program); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.data), gl.DYNAMIC_DRAW); let vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 3, gl.FLOAT, true, 0, 0); gl.clearColor(0, 0, 0, 1); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clearDepth(2.0) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.drawArrays(gl.LINE_STRIP || gl.POINTS, 0, this.data.length / 3); } rotate(program) { let aspect = c.height / c.width, matrix = [ 1 / aspect, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] matrix = TDUtils.multiply(matrix, TDUtils.xRotation(config.getItem("xRotate", 0))); matrix = TDUtils.multiply(matrix, TDUtils.yRotation(config.getItem("yRotate", 0))); matrix = TDUtils.multiply(matrix, TDUtils.zRotation(config.getItem("zRotate", 0))); let rotate = gl.getUniformLocation(program, "u_matrix"); gl.uniformMatrix4fv(rotate, false, matrix); } setup() { audioHandler.fftSize(16384) } prepare(program) { this.position = gl.getAttribLocation(program, "a_position"); this.color = gl.getUniformLocation(program, "u_color"); } } //animate Water the way like the Audio is Coming... 256FFT-Size max! class Water extends Visual { draw() { } setup() { audioHandler.fftSize(256) } } async function initHandler() { let body = $('body'); $('.playlist.menu-icon').addEventListener('click', e => { player.playlist.renderPagination(player.playlist.page); }); body.addDelegatedEventListener('click', '.playlist-item', (e, el) => { let number = el.dataset.index; player.playByID(parseInt(number)); togglePlayButton('pause'); }); body.addDelegatedEventListener('click', '.controls button', (e, el) => { switch (el.id) { case 'previous': player.prevSong(); break; case 'next': player.nextSong() break; case 'play': player.playStop(); break; } togglePlayButton(audioHandler.audioFile.paused ? 'play' : 'pause'); }); } function togglePlayButton(status) { let icons = $$('#play .icon'); icons.forEach(el => { if(el.dataset.name === status) { el.removeClass('hide'); } else { el.addClass('hide'); } }) } const shaderHandler = new ShaderHandler(null), audioHandler = new AudioHandler(), gui = new GUI(), visual = new VisualDrawer(), template = new Template(), player = new Player(), config = new Config(); let c = null, gl = null; async function startUP() { c = document.body.querySelector('#c'), gl = c.getContext("webgl2"); if (!gl) { alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2"); return false; } shaderHandler.setGL(gl) await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/'); await audioHandler.init(); await player.init(); await visual.init(); await gui.init(); await initHandler(); } startUP().then(r => { setTimeout(e => { $('.loading-screen').remove(); }, 100) });