diff --git a/build/task/js.js b/build/task/js.js index 7b7741b..c612353 100644 --- a/build/task/js.js +++ b/build/task/js.js @@ -7,19 +7,22 @@ const basePath = __dirname + '/../../raw/javascript/'; const visualPath = basePath + 'visuals/'; const visuals = [ visualPath + 'sphere.js', - //visualPath + 'wave.js', - //visualPath + 'water.js', + visualPath + 'wave.js', + visualPath + 'water.js', //visualPath + 'experimental.js', ] const config = { src: [ basePath + 'utils.js', + basePath + 'template.js', basePath + 'handler.js', basePath + 'audio.js', basePath + 'player.js', basePath + 'gui.js', basePath + 'visual.js', + basePath + 'config.js', ...visuals, + basePath + 'eventHandler.js', basePath + 'app.js' ], dest: __dirname + '/../../out/js' diff --git a/build/task/spriteBuilder.js b/build/task/spriteBuilder.js index 29f5e33..d19382e 100644 --- a/build/task/spriteBuilder.js +++ b/build/task/spriteBuilder.js @@ -42,6 +42,7 @@ function buildIconSprites() { fal.faCogs, fal.faFolderUpload, fal.faListMusic, + fal.faFileAudio, ], vt: [] }; diff --git a/index.html b/index.html index eb50ef4..434111b 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - WEBGL Test + VS3D-Vis @@ -19,11 +19,19 @@ + +
+ +
diff --git a/out/icon-sprite.svg b/out/icon-sprite.svg index f13e5ca..6f4f495 100644 --- a/out/icon-sprite.svg +++ b/out/icon-sprite.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/out/js/scripts.js b/out/js/scripts.js index dac8955..a973d10 100644 --- a/out/js/scripts.js +++ b/out/js/scripts.js @@ -260,6 +260,16 @@ Node.prototype.addDelegatedEventListener = function (type, aim, cb) { }) }; +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) { @@ -273,6 +283,50 @@ function append(to, 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; @@ -360,6 +414,12 @@ class ShaderHandler { 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) { @@ -368,19 +428,355 @@ class ShaderHandler { await self.createProgramForEach(list) } } +const AudioContext = window.AudioContext || window.webkitAudioContext; + class AudioHandler { async init() { - this.audioFile = new Audio(); + 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 { - init() { + 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 { @@ -388,6 +784,11 @@ class Visual { this.data = []; //for drawing this.dataArray = []; } + + updateData() { + + } + draw() { } @@ -396,7 +797,70 @@ class Visual { } 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() { @@ -405,23 +869,140 @@ class Sphere extends Visual { 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(), - player = new Player(); + visual = new VisualDrawer(), + template = new Template(), + player = new Player(), + config = new Config(); + +let c = null, + gl = null; async function startUP() { - let c = document.body.querySelector('#c'), + 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"], 'shaders/'); + await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/'); await audioHandler.init(); await player.init(); - gui.init(); + await visual.init(); + await gui.init(); + await initHandler(); } startUP().then(r => { diff --git a/out/js/scripts.min.js b/out/js/scripts.min.js index ae2c780..d1f8c6c 100644 --- a/out/js/scripts.min.js +++ b/out/js/scripts.min.js @@ -1 +1 @@ -class VTUtils{static random(t,e){let a=Math.random();if(void 0===t)return a;if(void 0===e)return t instanceof Array?t[Math.floor(a*t.length)]:a*t;if(t>e){let a=t;t=e,e=a}return a*(e-t)+t}static randomInt(t,e){return Math.floor(VTUtils.random(t,e))}static normalize(t,e,a){return(t-a)/(e-a)}static distance(t,e,a,r){let s=t-a,i=e-r;return Math.sqrt(s*s+i*i)}static map(t,e,a,r,s,i){let n=(t-e)/(a-e)*(s-r)+r;return i?r{let r=t.target;if(r.matches(e))a(t,r);else{let s=r.closest(e);s&&a(t,s)}})};class ShaderHandler{constructor(t){this.gl=t,this.shaderNames=[],this.shaders={},this.programs={}}setGL(t){this.gl=t}async loadShader(t,e){this.shaderNames.push(t),await this.load(t,e+t+".vert",this.gl.VERTEX_SHADER),await this.load(t,e+t+".frag",this.gl.FRAGMENT_SHADER)}async load(t,e,a){let r=t+"_"+a;if(!this.shaders[r]){let t=await fetch(e),s=this.createShader(await t.text(),a);s&&(this.shaders[r]=s)}return!!this.shaders[r]}getShader(t,e){let a=t+"_"+e;return this.shaders[a]}getAllShaders(){return this.shaderNames}async createProgramForEach(t){t=t||this.shaderNames;for(let e=0;e{setTimeout(t=>{$(".loading-screen").remove()},100)}); \ No newline at end of file +class VTUtils{static random(t,e){let a=Math.random();if(void 0===t)return a;if(void 0===e)return t instanceof Array?t[Math.floor(a*t.length)]:a*t;if(t>e){let a=t;t=e,e=a}return a*(e-t)+t}static randomInt(t,e){return Math.floor(VTUtils.random(t,e))}static normalize(t,e,a){return(t-a)/(e-a)}static distance(t,e,a,i){let s=t-a,r=e-i;return Math.sqrt(s*s+r*r)}static map(t,e,a,i,s,r){let l=(t-e)/(a-e)*(s-i)+i;return r?i{let i=t.target;if(i.matches(e))a(t,i);else{let s=i.closest(e);s&&a(t,s)}})},Node.prototype.hasClass=function(t){return this.classList.contains(t)},Node.prototype.addClass=function(t){return this.classList.add(t)},Node.prototype.removeClass=function(t){return this.classList.remove(t)};class Template{constructor(){this.tpl={}}async loadTemplate(t){let e=this;this.tpl[t]||await fetch(templateDir+t+".tpl").then(t=>t.text()).then(a=>{e.tpl[t]=a})}async loadArray(t){for(let e of t)await this.loadTemplate(e)}parseTemplate(t,e){if(!this.tpl[t])return"";let a,i=this.tpl[t];for(;null!==(a=templateEx.exec(i));){a.index===templateEx.lastIndex&&templateEx.lastIndex++;let t=a[0];i=i.replace(t,e[a[1]]||"")}return i}parseFromAPI(t,e,a){fetch(t).then(t=>t.json()).then(t=>{a(this.parseTemplate(e,t))}).catch(console.error)}}const templateEx=/\$(.*?)\$/gm,templateDir="out/tpl/";class ShaderHandler{constructor(t){this.gl=t,this.shaderNames=[],this.shaders={},this.programs={}}setGL(t){this.gl=t}async loadShader(t,e){this.shaderNames.push(t),await this.load(t,e+t+".vert",this.gl.VERTEX_SHADER),await this.load(t,e+t+".frag",this.gl.FRAGMENT_SHADER)}async load(t,e,a){let i=t+"_"+a;if(!this.shaders[i]){let t=await fetch(e),s=this.createShader(await t.text(),a);s&&(this.shaders[i]=s)}return!!this.shaders[i]}getShader(t,e){let a=t+"_"+e;return this.shaders[a]}getAllShaders(){return this.shaderNames}async createProgramForEach(t){t=t||this.shaderNames;for(let e=0;ee&&(a=0),this.index=a,t[a]}getPrevious(){let t=this.isShuffle?this.shuffled:this.list,e=t.length-1,a=this.index-1;return a<0&&(a=e),this.index=a,t[a]}getCurrent(){return(this.isShuffle?this.shuffled:this.list)[this.index]}getFile(t){return(this.isShuffle?this.shuffled:this.list)[t]}setPlaylist(t){this.index=0,this.list=t,this.shuffle()}handlePagination(t,e){e.hasClass("inactive")||(e.hasClass("next-site")?this.renderPagination(this.page+1):this.renderPagination(this.page-1))}renderPagination(t){let e=this.list.length,a=Math.ceil(e/50)-1;t<0&&(t=0),t>a&&(t=a);let i=50*t,s=i+50,r="";if(this.page=t,s>=e&&(s=e),e>0){let t=this.isShuffle?this.shuffled:this.list;for(let e=i;e1&&t0?"active":"inactive",nextActive:l?"active":"inactive",page:t+1+" / "+parseInt(a+1)}))}changeFiles(t,e){let a=[],i=0;for(let t of e.files)if(t&&-1!==t.type.indexOf("audio")&&null===t.name.match(".m3u")){let e=t.name.split(".");e.pop(),e=e.join("."),a.push({file:t,name:e,index:i++})}this.setPlaylist(a),a.length>0?this.renderPagination(0):alert("No Valid AudioFiles found!")}}class GUI{async init(){this.data={},this.modal=new Modal,await this.loadForVis(),await template.loadArray(["playlist-item","playlist","playlist-footer"]),this.initDropZone()}async loadForVis(){let t=visual.c;null==this.data[t]&&(this.data[t]=await fetch("out/gui/"+t+".json").then(t=>t.json()))}renderModal(t,e){let a=$("#modal"),i=a.parentNode,s=$("header .headline",a),r=$("modal-content",a);s.innerHTML=e,r.innerHTML=t,i.classList.remove("hide")}initDropZone(){"drag dragstart dragend dragover dragenter dragleave drop".split(" ").forEach(t=>{window.addEventListener(t,async t=>{t.preventDefault(),t.stopPropagation(),"drop"===t.type&&(t.dataTransfer.files.length>0?player.playlist.changeFiles(t,t.dataTransfer):alert("Sorry you need to upload files!"))})})}}class Modal{constructor(){this.currentModal="",this.modal=$("#modal"),this.parent=this.modal.parentNode,this.modal.addDelegatedEventListener("click","header .close",this.closeModal.bind(this))}resetModal(){this.renderModal("","","")}renderModal(t,e,a){this.currentModal=t,this.renderHeader(t),this.renderContent(e),this.renderFooter(a),this.showModal()}renderHeader(t){$("header .headline",this.modal).innerHTML=t}renderContent(t){$("modal-content",this.modal).innerHTML=t}renderFooter(t){$("modal-footer",this.modal).innerHTML=t}closeModal(){this.parent.addClass("hide")}isCurrent(t){return t===this.currentModal}showModal(){this.parent.removeClass("hide")}}class Visual{constructor(){this.data=[],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(t){null!=this.visuals[t]&&(this.c=t,this.visuals[this.c].setup())}updateLoop(){let t=shaderHandler.use(this.c),e=this.visuals[this.c];e.updateData(),e.draw(t),requestAnimationFrame(this.updateLoop.bind(this))}}class Config{constructor(){this.config={},this.name=""}loadConfigByName(t){this.saveConfig(),this.name="config-"+t,this.config=JSON.parse(this.name)}saveConfig(){""!==this.name&&localStorage.setItem(this.name,JSON.stringify(this.config))}addItem(t,e){this.config[t]=e}removeItem(t){delete this.config[t]}getItem(t,e){let a=this.config[t];return null==a&&(this.config[t]=e,a=e),a}}class Sphere extends Visual{draw(){}setup(){}}class Wave extends Visual{updateData(){this.data=[];let t=audioHandler.getFloatArray(),e=2/t.length,a=-1;for(let i=0;i{player.playlist.renderPagination(player.playlist.page)}),t.addDelegatedEventListener("click",".playlist-item",(t,e)=>{let a=e.dataset.index;player.playByID(parseInt(a)),togglePlayButton("pause")}),t.addDelegatedEventListener("click",".controls button",(t,e)=>{switch(e.id){case"previous":player.prevSong();break;case"next":player.nextSong();break;case"play":player.playStop()}togglePlayButton(audioHandler.audioFile.paused?"play":"pause")})}function togglePlayButton(t){$$("#play .icon").forEach(e=>{e.dataset.name===t?e.removeClass("hide"):e.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(){if(c=document.body.querySelector("#c"),gl=c.getContext("webgl2"),!gl)return alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2"),!1;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(t=>{setTimeout(t=>{$(".loading-screen").remove()},100)}); \ No newline at end of file diff --git a/out/theme/style.css b/out/theme/style.css index 118e5e6..6071afb 100644 --- a/out/theme/style.css +++ b/out/theme/style.css @@ -1 +1 @@ -*{box-sizing:border-box}:focus{outline:0}body,html{padding:0;margin:0;overflow:hidden;font-size:16px;font-family:sans-serif;background-color:#000}div{position:fixed;color:#fff;padding:1em}.hide{display:none!important}.icon{width:1em;height:1em;vertical-align:middle;font-size:1em;shape-rendering:geometricPrecision;transition:transform .5s cubic-bezier(.22,.61,.36,1);stroke-width:5px;text-align:center;display:block}#c{width:100%;height:100%}group{display:block;padding-bottom:10px;user-select:none}group-label{display:block;border-bottom:1px solid #fff;font-size:21px;font-weight:500;user-select:none}group-input{display:flex;align-items:center;margin-top:5px;user-select:none}group-input label{padding-right:10px;user-select:none;width:150px}group-input input{flex-grow:1;user-select:none;max-width:150px}group-input button{border:1px solid #dcdcdc;background-color:transparent;color:#fff;margin-left:5px}.closed{transform:translateX(-350px);transition:all .5s}.top-menu-left{display:flex}.top-menu-left div{position:relative}.loading-screen{z-index:100;background-color:#000;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column}.loading-screen span{font-family:monospace;font-size:4vw;z-index:2}.loading-screen loader{position:absolute;top:calc(6vw + 10px);left:0;right:0;bottom:0;margin:auto;display:block;width:30vw;height:6px;transform:scaleX(0);transform-origin:left;background-color:#006ea8;animation:loadingBar 2s infinite}.loading-screen loader.delay{background-color:rgba(0,110,168,.24);filter:blur(1px);animation-delay:.05s}@keyframes loadingBar{0%,100%{transform:scaleX(0) scaleY(0)}50%{transform:scaleX(1) scaleY(1);transform-origin:left}100%,51%{transform-origin:right}}input[type=range]{width:100%}input[type=range]:focus{outline:0}input[type=range]:focus::-webkit-slider-runnable-track{background:#545454}input[type=range]:focus::-ms-fill-lower{background:#424242}input[type=range]:focus::-ms-fill-upper{background:#545454}input[type=range]::-webkit-slider-runnable-track{width:100%;height:25.6px;cursor:pointer;box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;background:#424242;border-radius:0;border:0 solid #010101}input[type=range]::-webkit-slider-thumb{height:25px;width:15px;border-radius:0;cursor:pointer;margin-top:.3px}switch input{position:absolute;appearance:none;opacity:0}switch input:checked+label:after{transform:translateX(20px)}switch label{display:block;border-radius:10px;width:40px;height:20px;background-color:#dcdcdc;position:relative;cursor:pointer;padding:0}switch label:after{content:'';background-color:#ff3232;position:absolute;top:2px;left:2px;height:16px;width:16px;border-radius:10px;transition:.5s}input[type=file]{position:fixed;left:-100000vw;height:1px;width:1px}.controls{right:0;display:flex}.controls button,.menu-icon{background-color:rgba(33,33,33,.6);border:none;font-size:1.4em;border-top:4px solid #089bec;padding:1.5rem;cursor:pointer;color:#fff;transition:.5s}.controls button.active,.menu-icon.active{border-color:#ff066a}.controls button:hover,.menu-icon:hover{background-color:rgba(21,21,21,.7);border-color:#aaef22} \ No newline at end of file +*{box-sizing:border-box}:focus{outline:0}body,html{padding:0;margin:0;overflow:hidden;font-size:16px;font-family:sans-serif;background-color:#000}div{position:fixed;color:#fff;padding:1em}.hide{display:none!important}.icon{width:1em;height:1em;vertical-align:middle;font-size:1em;shape-rendering:geometricPrecision;transition:transform .5s cubic-bezier(.22,.61,.36,1);stroke-width:5px;text-align:center;display:block}#c{width:100%;height:100%}group{display:block;padding-bottom:10px;user-select:none}group-label{display:block;border-bottom:1px solid #fff;font-size:21px;font-weight:500;user-select:none}group-input{display:flex;align-items:center;margin-top:5px;user-select:none}group-input label{padding-right:10px;user-select:none;width:150px}group-input input{flex-grow:1;user-select:none;max-width:150px}group-input button{border:1px solid #dcdcdc;background-color:transparent;color:#fff;margin-left:5px}.closed{transform:translateX(-350px);transition:all .5s}.top-menu-left{display:flex}.top-menu-left div{position:relative}.loading-screen{z-index:100;background-color:#000;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column}.loading-screen span{font-family:monospace;font-size:4vw;z-index:2}.loading-screen loader{position:absolute;top:calc(6vw + 10px);left:0;right:0;bottom:0;margin:auto;display:block;width:30vw;height:6px;transform:scaleX(0);transform-origin:left;background-color:#006ea8;animation:loadingBar 2s infinite}.loading-screen loader.delay{background-color:rgba(0,110,168,.24);filter:blur(1px);animation-delay:.05s}@keyframes loadingBar{0%,100%{transform:scaleX(0) scaleY(0)}50%{transform:scaleX(1) scaleY(1);transform-origin:left}100%,51%{transform-origin:right}}.grey-screen{position:fixed;top:0;left:0;background-color:rgba(0,0,0,.5);width:100vw;height:100vh;display:flex;justify-content:center;align-items:center}.grey-screen.hide{display:none!important}#modal{max-width:860px;width:90%;min-height:200px;background-color:#333;padding:unset;box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}#modal header{height:50px;font-size:30px;line-height:50px;padding-left:10px;overflow:hidden;background-color:#212121;display:flex}#modal header .headline{flex-grow:1}#modal header .close{margin-right:10px;font-size:24px;cursor:pointer}#modal header .close:hover{color:#ff3232}#modal modal-content{display:block;max-height:calc(100vh - 200px);overflow:auto}#modal modal-footer{display:block}input[type=range]{width:100%}input[type=range]:focus{outline:0}input[type=range]:focus::-webkit-slider-runnable-track{background:#545454}input[type=range]:focus::-ms-fill-lower{background:#424242}input[type=range]:focus::-ms-fill-upper{background:#545454}input[type=range]::-webkit-slider-runnable-track{width:100%;height:25.6px;cursor:pointer;box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;background:#424242;border-radius:0;border:0 solid #010101}input[type=range]::-webkit-slider-thumb{height:25px;width:15px;border-radius:0;cursor:pointer;margin-top:.3px}switch input{position:absolute;appearance:none;opacity:0}switch input:checked+label:after{transform:translateX(20px)}switch label{display:block;border-radius:10px;width:40px;height:20px;background-color:#dcdcdc;position:relative;cursor:pointer;padding:0}switch label:after{content:'';background-color:#ff3232;position:absolute;top:2px;left:2px;height:16px;width:16px;border-radius:10px;transition:.5s}input[type=file]{position:fixed;left:-100000vw;height:1px;width:1px}.controls{right:0;display:flex}.controls button,.menu-icon{background-color:rgba(33,33,33,.6);border:none;font-size:1.4em;border-top:4px solid #089bec;padding:1.5rem;cursor:pointer;color:#fff;transition:.5s}.controls button.active,.menu-icon.active{border-color:#ff066a}.controls button:hover,.menu-icon:hover{background-color:rgba(21,21,21,.7);border-color:#aaef22}playlist{display:flex;flex-direction:column}playlist div{padding:unset;position:unset}playlist .pagination{display:flex;justify-content:flex-end;font-size:1.5em;padding:5px}playlist .pagination .item{cursor:pointer;user-select:none;border-radius:5px;margin:0 3px}playlist .pagination .item.inactive{color:#aaa;pointer-events:none;cursor:not-allowed}playlist .pagination .item:hover{color:#006ea8}playlist .playlist-item{display:flex;padding:5px;border-bottom:1px solid #dcdcdc;cursor:pointer}playlist .playlist-item-title{margin-left:10px;padding:5px;display:flex;align-items:center}playlist .playlist-item-number{padding:5px 10px 5px 5px;border-right:1px solid #ff3232;width:50px}playlist .playlist-item:hover{background-color:rgba(0,0,0,.4)} \ No newline at end of file diff --git a/out/tpl/playlist-footer.tpl b/out/tpl/playlist-footer.tpl new file mode 100644 index 0000000..bf2dac5 --- /dev/null +++ b/out/tpl/playlist-footer.tpl @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/out/tpl/playlist-item.tpl b/out/tpl/playlist-item.tpl new file mode 100644 index 0000000..5e988d0 --- /dev/null +++ b/out/tpl/playlist-item.tpl @@ -0,0 +1,4 @@ + +
$nr$
+
$title$
+
\ No newline at end of file diff --git a/out/tpl/playlist.tpl b/out/tpl/playlist.tpl new file mode 100644 index 0000000..3ad6108 --- /dev/null +++ b/out/tpl/playlist.tpl @@ -0,0 +1,5 @@ + +
+ $content$ +
+
\ No newline at end of file diff --git a/raw/gui/wave.json b/raw/gui/wave.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/raw/gui/wave.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/raw/javascript/app.js b/raw/javascript/app.js index de5e54f..d1d3b6d 100644 --- a/raw/javascript/app.js +++ b/raw/javascript/app.js @@ -1,20 +1,28 @@ const shaderHandler = new ShaderHandler(null), audioHandler = new AudioHandler(), gui = new GUI(), - player = new Player(); + visual = new VisualDrawer(), + template = new Template(), + player = new Player(), + config = new Config(); + +let c = null, + gl = null; async function startUP() { - let c = document.body.querySelector('#c'), + 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"], 'shaders/'); + await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/'); await audioHandler.init(); await player.init(); - gui.init(); + await visual.init(); + await gui.init(); + await initHandler(); } startUP().then(r => { diff --git a/raw/javascript/audio.js b/raw/javascript/audio.js index f8aa9dc..0d34bf3 100644 --- a/raw/javascript/audio.js +++ b/raw/javascript/audio.js @@ -1,5 +1,71 @@ +const AudioContext = window.AudioContext || window.webkitAudioContext; + class AudioHandler { async init() { - this.audioFile = new Audio(); + 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; } } \ No newline at end of file diff --git a/raw/javascript/config.js b/raw/javascript/config.js new file mode 100644 index 0000000..07ec7ad --- /dev/null +++ b/raw/javascript/config.js @@ -0,0 +1,35 @@ +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; + } +} \ No newline at end of file diff --git a/raw/javascript/eventHandler.js b/raw/javascript/eventHandler.js new file mode 100644 index 0000000..41fdfd6 --- /dev/null +++ b/raw/javascript/eventHandler.js @@ -0,0 +1,39 @@ +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'); + } + }) +} \ No newline at end of file diff --git a/raw/javascript/gui.js b/raw/javascript/gui.js index 7228c33..b24922c 100644 --- a/raw/javascript/gui.js +++ b/raw/javascript/gui.js @@ -1,5 +1,98 @@ class GUI { - init() { + 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") } } \ No newline at end of file diff --git a/raw/javascript/handler.js b/raw/javascript/handler.js index a676610..5a6f1a6 100644 --- a/raw/javascript/handler.js +++ b/raw/javascript/handler.js @@ -85,6 +85,12 @@ class ShaderHandler { 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) { diff --git a/raw/javascript/player.js b/raw/javascript/player.js index ebe312f..6085afc 100644 --- a/raw/javascript/player.js +++ b/raw/javascript/player.js @@ -1,5 +1,182 @@ 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!"); + } } } \ No newline at end of file diff --git a/raw/javascript/template.js b/raw/javascript/template.js new file mode 100644 index 0000000..06ac580 --- /dev/null +++ b/raw/javascript/template.js @@ -0,0 +1,44 @@ +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/" \ No newline at end of file diff --git a/raw/javascript/utils.js b/raw/javascript/utils.js index 1b56266..e426e72 100644 --- a/raw/javascript/utils.js +++ b/raw/javascript/utils.js @@ -260,6 +260,16 @@ Node.prototype.addDelegatedEventListener = function (type, aim, cb) { }) }; +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) { diff --git a/raw/javascript/visual.js b/raw/javascript/visual.js index 5f8dbd7..1df87b0 100644 --- a/raw/javascript/visual.js +++ b/raw/javascript/visual.js @@ -3,6 +3,11 @@ class Visual { this.data = []; //for drawing this.dataArray = []; } + + updateData() { + + } + draw() { } @@ -11,5 +16,33 @@ class Visual { } 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)) + } } \ No newline at end of file diff --git a/raw/javascript/visuals/water.js b/raw/javascript/visuals/water.js new file mode 100644 index 0000000..848f19c --- /dev/null +++ b/raw/javascript/visuals/water.js @@ -0,0 +1,10 @@ +//animate Water the way like the Audio is Coming... 256FFT-Size max! +class Water extends Visual { + draw() { + } + + setup() { + audioHandler.fftSize(256) + } + +} \ No newline at end of file diff --git a/raw/javascript/visuals/wave.js b/raw/javascript/visuals/wave.js new file mode 100644 index 0000000..7b583b5 --- /dev/null +++ b/raw/javascript/visuals/wave.js @@ -0,0 +1,60 @@ +// 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"); + } +} \ No newline at end of file diff --git a/raw/scss/_gui.scss b/raw/scss/_gui.scss index 01338cf..f62aea7 100644 --- a/raw/scss/_gui.scss +++ b/raw/scss/_gui.scss @@ -50,6 +50,7 @@ group-input { .top-menu-left { display: flex; + div { position: relative; } @@ -105,4 +106,63 @@ group-input { 51%, 100% { transform-origin: right; } +} + +.grey-screen { + position: fixed; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, .5); + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + + &.hide { + display: none !important; + } +} + +#modal { + max-width: 860px; + width: 90%; + min-height: 200px; + background-color: #333; + padding: unset; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + + header { + height: 50px; + font-size: 30px; + line-height: 50px; + padding-left: 10px; + overflow: hidden; + background-color: #212121; + display: flex; + + .headline { + flex-grow: 1; + } + + .close { + margin-right: 10px; + font-size: 24px; + cursor: pointer; + + &:hover { + color: #ff3232; + } + } + } + + modal-content { + display: block; + max-height: calc(100vh - 200px); + overflow: auto; + } + + modal-footer { + display: block; + } } \ No newline at end of file diff --git a/raw/scss/_playlist.scss b/raw/scss/_playlist.scss new file mode 100644 index 0000000..215fb2a --- /dev/null +++ b/raw/scss/_playlist.scss @@ -0,0 +1,57 @@ +playlist { + display: flex; + flex-direction: column; + + div { + padding: unset; + position: unset; + } + + .pagination { + display: flex; + justify-content: flex-end; + font-size: 1.5em; + padding: 5px; + + .item { + cursor: pointer; + user-select: none; + border-radius: 5px; + margin: 0 3px; + + &.inactive { + color: #aaa; + pointer-events: none; + cursor: not-allowed; + } + + &:hover { + color: #006ea8; + } + } + } + + .playlist-item { + display: flex; + padding: 5px; + border-bottom: 1px solid #dcdcdc; + cursor: pointer; + + &-title { + margin-left: 10px; + padding: 5px; + display: flex; + align-items: center; + } + + &-number { + padding: 5px 10px 5px 5px; + border-right: 1px solid #ff3232; + width: 50px; + } + + &:hover { + background-color: rgba(0, 0, 0, .4); + } + } +} \ No newline at end of file diff --git a/raw/scss/style.scss b/raw/scss/style.scss index 5f04f9f..e1817a8 100644 --- a/raw/scss/style.scss +++ b/raw/scss/style.scss @@ -40,4 +40,5 @@ div { @import "gui"; @import "input"; -@import "controls"; \ No newline at end of file +@import "controls"; +@import "playlist"; \ No newline at end of file diff --git a/shaders/water.frag b/shaders/water.frag new file mode 100644 index 0000000..0bd75c4 --- /dev/null +++ b/shaders/water.frag @@ -0,0 +1,13 @@ +#version 300 es + +// fragment shaders don't have a default precision so we need +// to pick one. mediump is a good default. It means "medium precision" +precision mediump float; + +uniform vec4 u_color; + +out vec4 outColor; + +void main() { + outColor = vec4(255,255,255,0); +} \ No newline at end of file diff --git a/shaders/water.vert b/shaders/water.vert new file mode 100644 index 0000000..ddf3857 --- /dev/null +++ b/shaders/water.vert @@ -0,0 +1,11 @@ +#version 300 es + +in vec2 a_position; +void main() { + // convert the position from pixels to 0.0 to 1.0 + vec2 scale = a_position / vec2(255.0, 255.0); + vec2 remap = scale * 2.0; + vec2 space = remap - 1.0; + space.y = space.y * 0.85; + gl_Position = vec4(space, 0,1); +} \ No newline at end of file diff --git a/shaders/wave.frag b/shaders/wave.frag index 088b9b9..03b4508 100644 --- a/shaders/wave.frag +++ b/shaders/wave.frag @@ -4,10 +4,15 @@ // to pick one. mediump is a good default. It means "medium precision" precision mediump float; +in vec3 pos; uniform vec4 u_color; out vec4 outColor; void main() { - outColor = u_color; + vec3 color = pos.xyz; + color.z = color.z + 1.0; + color.z = color.z / 2.0; + color.z = color.z * 255.0; + outColor = vec4(color, 1.0); } \ No newline at end of file diff --git a/shaders/wave.vert b/shaders/wave.vert index ddf3857..863c1a3 100644 --- a/shaders/wave.vert +++ b/shaders/wave.vert @@ -1,11 +1,14 @@ #version 300 es -in vec2 a_position; +in vec3 a_position; +uniform mat4 u_matrix; + +out vec3 pos; + void main() { // convert the position from pixels to 0.0 to 1.0 - vec2 scale = a_position / vec2(255.0, 255.0); - vec2 remap = scale * 2.0; - vec2 space = remap - 1.0; - space.y = space.y * 0.85; - gl_Position = vec4(space, 0,1); + vec4 scale = vec4(a_position, 1) * u_matrix; + scale.y = scale.y * 0.85; + gl_Position = scale; + pos = a_position; } \ No newline at end of file