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) { let r, g, b, i = Math.floor(h * 6), f = h * 6 - i, p = v * (1 - s), q = v * (1 - f * s), 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); } } function $(sel, s) { s = s || document; return s.querySelector(sel); } 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) { try { cb(event, parent); } catch (e) { NotificationHandler.createNotification("FATAL ERROR WITHIN HANDLER!", "error", 1000); //nothing! } } } }) }; Node.prototype.hasClass = function (className) { let items = className.split(','), has = null; for (let item of items) { if (has === false) { break; } has = this.classList.contains(item.trim()); } return has === true; } Node.prototype.addClass = function (className) { let items = className.split(','); for (let item of items) { this.classList.add(item.trim()); } return this; } Node.prototype.removeClass = function (className) { let items = className.split(','); for (let item of items) { this.classList.remove(item.trim()); } return this; } Node.prototype.toggleClass = function (className, force) { let items = className.split(','); for (let item of items) { this.classList.toggle(item.trim(), force); } } Node.prototype.switchClass = function (clOne, clTwo, twoOne) { let cl = this.classList; if (twoOne) { cl.remove(clOne); cl.add(clTwo) } else { cl.remove(clTwo) cl.add(clOne) } } Node.prototype.toggleCheck = function (className, force) { let cl = this.classList; let items = className.split(','); for (let item of items) { let clOne = item.trim(); if (force) { cl.add(clOne); } else { cl.remove(clOne) } } } String.prototype.firstUpper = function () { return this.charAt(0).toUpperCase() + this.slice(1); } File.prototype.toBase64 = function (cb) { const reader = new FileReader(); reader.onloadend = cb; reader.readAsDataURL(this); } function b64toBlob(b64Data, type) { const byteCharacters = atob(b64Data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); return new Blob([byteArray], {type: type}); } function create(name, content) { let d = document.createElement(name); if (content) { d.innerHTML = content; } return d; } function append(to, array) { for (let item of array) { to.appendChild(item); } } function hexToRgb(hex) { hex = hex.replace("#", ""); let bigint = parseInt(hex, 16), r = (bigint >> 16) & 255, g = (bigint >> 8) & 255, b = bigint & 255; return [r / 255, g / 255, b / 255]; } // most of the functions are from https://webglfundamentals.org/webgl/resources/m4.js! but i doesnt want to use them all and make some adjustment to them! class TDUtils { static lastMatrix = {m: null}; static multiply(a, b) { let b00 = b[0]; let b01 = b[1]; let b02 = b[2]; let b03 = b[3]; let b10 = b[4]; let b11 = b[5]; let b12 = b[6]; let b13 = b[7]; let b20 = b[8]; let b21 = b[9]; let b22 = b[10]; let b23 = b[11]; let b30 = b[12]; let b31 = b[13]; let b32 = b[14]; let b33 = b[15]; let a00 = a[0]; let a01 = a[1]; let a02 = a[2]; let a03 = a[3]; let a10 = a[4]; let a11 = a[5]; let a12 = a[6]; let a13 = a[7]; let a20 = a[8]; let a21 = a[9]; let a22 = a[10]; let a23 = a[11]; let a30 = a[12]; let a31 = a[13]; let a32 = a[14]; let a33 = a[15]; return [ b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30, b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31, b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32, b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33, b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30, b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31, b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32, b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33, b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30, b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31, b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32, b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33, b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30, b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31, b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32, b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33 ]; } static translate(m, tx, ty, tz, dst) { dst = dst || new Float32Array(16); let m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3], m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7], m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11], m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15]; dst[0] = m00; dst[1] = m01; dst[2] = m02; dst[3] = m03; dst[4] = m10; dst[5] = m11; dst[6] = m12; dst[7] = m13; dst[8] = m20; dst[9] = m21; dst[10] = m22; dst[11] = m23; dst[12] = m00 * tx + m10 * ty + m20 * tz + m30; dst[13] = m01 * tx + m11 * ty + m21 * tz + m31; dst[14] = m02 * tx + m12 * ty + m22 * tz + m32; dst[15] = m03 * tx + m13 * ty + m23 * tz + m33; return dst; } static xRotation(angle) { angle = TDUtils.degToRad(angle); let c = Math.cos(angle); let s = Math.sin(angle); return [ 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, ]; } static yRotation(angle) { angle = TDUtils.degToRad(angle); let c = Math.cos(angle); let s = Math.sin(angle); return [ c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1, ]; } static zRotation(angle) { angle = TDUtils.degToRad(angle); let c = Math.cos(angle); let s = Math.sin(angle); 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; } static scale(sx, sy, sz, dst) { dst = dst || new Float32Array(16); dst[0] = sx; dst[5] = sy; dst[10] = sz; return dst; } static lookAt(cameraPosition, target, up, dst) { dst = dst || new Float32Array(16); let zAxis = TDUtils.normalize( TDUtils.subtractVectors(cameraPosition, target)); let xAxis = TDUtils.normalize(TDUtils.cross(up, zAxis)); let yAxis = TDUtils.normalize(TDUtils.cross(zAxis, xAxis)); dst[0] = xAxis[0]; dst[1] = xAxis[1]; dst[2] = xAxis[2]; dst[4] = yAxis[0]; dst[5] = yAxis[1]; dst[6] = yAxis[2]; dst[8] = zAxis[0]; dst[9] = zAxis[1]; dst[10] = zAxis[2]; dst[12] = cameraPosition[0]; dst[13] = cameraPosition[1]; dst[14] = cameraPosition[2]; dst[15] = 1; return dst; } static cross(a, b, dst) { dst = dst || new Float32Array(3); dst[0] = a[1] * b[2] - a[2] * b[1]; dst[1] = a[2] * b[0] - a[0] * b[2]; dst[2] = a[0] * b[1] - a[1] * b[0]; return dst; } static normalize(v, dst) { dst = dst || new Float32Array(3); let length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); if (length > 0.00001) { dst[0] = v[0] / length; dst[1] = v[1] / length; dst[2] = v[2] / length; } return dst; } static subtractVectors(a, b, dst) { dst = dst || new Float32Array(3); dst[0] = a[0] - b[0]; dst[1] = a[1] - b[1]; dst[2] = a[2] - b[2]; return dst; } static perspective(fieldOfViewInRadians, aspect, near, far, dst) { dst = dst || new Float32Array(16); let f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians), rangeInv = 1.0 / (near - far); dst[0] = f / aspect; dst[5] = f; dst[10] = (near + far) * rangeInv; dst[11] = -1; dst[14] = near * far * rangeInv * 2; return dst; } static projection(width, height, depth) { return [ 2 / width, 0, 0, 0, 0, -2 / height, 0, 0, 0, 0, 2 / depth, 0, -1, 1, 0, 1, ]; } static inverse(m, dst) { dst = dst || new Float32Array(16); let m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3], m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7], m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11], m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15], tmp_0 = m22 * m33, tmp_1 = m32 * m23, tmp_2 = m12 * m33, tmp_3 = m32 * m13, tmp_4 = m12 * m23, tmp_5 = m22 * m13, tmp_6 = m02 * m33, tmp_7 = m32 * m03, tmp_8 = m02 * m23, tmp_9 = m22 * m03, tmp_10 = m02 * m13, tmp_11 = m12 * m03, tmp_12 = m20 * m31, tmp_13 = m30 * m21, tmp_14 = m10 * m31, tmp_15 = m30 * m11, tmp_16 = m10 * m21, tmp_17 = m20 * m11, tmp_18 = m00 * m31, tmp_19 = m30 * m01, tmp_20 = m00 * m21, tmp_21 = m20 * m01, tmp_22 = m00 * m11, tmp_23 = m10 * m01, t0 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) - (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31), t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) - (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31), t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) - (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31), t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) - (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21), d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3); dst[0] = d * t0; dst[1] = d * t1; dst[2] = d * t2; dst[3] = d * t3; dst[4] = d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) - (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)); dst[5] = d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) - (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)); dst[6] = d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) - (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)); dst[7] = d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) - (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20)); dst[8] = d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) - (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)); dst[9] = d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) - (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)); dst[10] = d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) - (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)); dst[11] = d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) - (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23)); dst[12] = d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) - (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)); dst[13] = d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) - (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)); dst[14] = d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) - (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)); dst[15] = d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) - (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02)); return dst; } static aspectView(aspect) { return [ 1 / aspect, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] } static updateRotate(rotation, def) { let value = vConf.get(rotation, def) + vConf.get(rotation + '-inc', 0) if (value > 360) { value -= 360; } else if (value < -360) { value += 360; } vConf.set(rotation, value); } static makeZToWMatrix(fudgeFactor) { return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, fudgeFactor, 0, 0, 0, 1, ]; } } class GLHelper { constructor(program) { this.matrix = new Float32Array(16); this.program = program; } static uniform4fv(program, name, data) { let uniform = gl.getUniformLocation(program, name); gl.uniform4fv(uniform, data); } static uniform3fv(program, name, data) { let uniform = gl.getUniformLocation(program, name); gl.uniform3fv(uniform, data); } static uniform1f(program, name, data) { let uniform = gl.getUniformLocation(program, name); gl.uniform1f(uniform, data); } rotateX(deg) { this.matrix = TDUtils.multiply(this.matrix, TDUtils.xRotation(deg)); } rotateY(deg) { this.matrix = TDUtils.multiply(this.matrix, TDUtils.yRotation(deg)); } rotateZ(deg) { this.matrix = TDUtils.multiply(this.matrix, TDUtils.zRotation(deg)); } scale(scaling) { this.matrix = TDUtils.multiply(this.matrix, TDUtils.scale(scaling[0], scaling[1], scaling[2])) } project(depth) { depth = depth || (c.width > c.height) ? c.width : c.height; this.matrix = TDUtils.projection(c.width, c.height, depth) } translate(t) { this.matrix = TDUtils.translate(this.matrix, t[0] || 0, t[1] || 0, t[2] || 0); } addFudgeFactor(fudgeFactor) { this.matrix = TDUtils.multiply(TDUtils.makeZToWMatrix(fudgeFactor), this.matrix); } applyMatrix() { let matrix = gl.getUniformLocation(this.program, "u_matrix"); gl.uniformMatrix4fv(matrix, false, this.matrix); } } class Camera { constructor() { this.mouse; this.rotation = { x: 0, y: 0 } this.lastMouse; this.mousePressed = false; this.translate = { x: 0, y: 0, z: 0 } } async init() { this.mouse = { x: 0, y: 0 } window.addEventListener('mousedown', this.mouseDown.bind(this)); window.addEventListener('mouseup', this.mouseUp.bind(this)); window.addEventListener('mousemove', this.mouseMove.bind(this), {passive: true}); eventHandler.addEvent('keys-ArrowUp, keys-ArrowDown, keys-ArrowLeft, keys-ArrowRight, keys-KeyQ, keys-KeyE', this.keyPressed.bind(this)); } mouseDown() { this.mousePressed = true; this.lastMouse = null; } mouseUp() { this.mousePressed = false; this.lastMouse = null; } mouseMove(event) { if (!this.mousePressed || gui.modal.open) { return; } if (this.lastMouse) { let mouse = this.mouse, rotate = this.rotation; mouse.x += (this.lastMouse.x - event.clientX) * 0.2; mouse.y += (this.lastMouse.y - event.clientY) * 0.2; rotate.x = VTUtils.map(mouse.x, -c.width, c.width, 180, -180, false); rotate.y = VTUtils.map(mouse.y, -c.height, c.height, 180, -180, false); } this.lastMouse = { x: event.clientX, y: event.clientY } } keyPressed(data) { switch (data) { case 'keys-ArrowUp': this.translate.z += 10; break; case 'keys-ArrowDown': this.translate.z -= 10; break; case 'keys-ArrowLeft': this.translate.x -= 10; break; case 'keys-ArrowRight': this.translate.x += 10; break; case 'keys-KeyQ': this.translate.y += 10; break; case 'keys-KeyE': this.translate.y -= 10; break; } } } class Template { constructor() { this.tpl = {}; } async loadTemplate(name) { let self = this; if (!this.tpl[name]) { self.tpl[name] = await FetchHandler.loadFile(templateDir + name + '.tpl', false) } } 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]; let value = data[m[1]]; if (value === undefined || value === null) { value = ""; } d = d.replace(key, value) } return d; } } 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 FetchHandler.loadFile(url, false); let shader = this.createShader(data, 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); if (gl.getProgramParameter(pro, gl.LINK_STATUS)) { 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(file) { if (!file) { NotificationHandler.createNotification("Sorry!
Currently no Song is uploaded!", "error", 2000); return false; } let self = this, src = file.file; if (self.lastSong) { URL.revokeObjectURL(self.lastSong); } self.lastSong = this.audioFile.src = URL.createObjectURL(src); if (!this.isStarted) { this.start().catch(alert); } this.audioFile.play().then(e => { if (pConf.get("showPlaying", "true")) { NotificationHandler.createNotification("Now Playing:" + file.getAudioName(), "info", pConf.get("showPlayingTime", 1000)); } window.dispatchEvent(new CustomEvent('playSong')); }).catch(e => { NotificationHandler.createNotification(e.message, "error", 1000); player.nextSong(); }); } getIntArray(steps) { let dataArray = new Uint8Array(steps); this.analyser.getByteFrequencyData(dataArray); return dataArray; } getFloatArray() { let dataArray = new Float32Array(this.analyser.fftSize); this.analyser.getFloatTimeDomainData(dataArray); return dataArray; } } // Handler around the Playlist file to keep track on the ID3 and more! class AudioPlayerFile { constructor(file, index) { this.file = file; this.name = this.getName(); this.id3 = null; this.index = index; } getName() { let name = this.file.name.split("."); name.pop(); name = name.join("."); return name; } getID3Tag(force) { if (!force && this.id3 !== null) { return this.id3; } eventHandler.sendData('getData', { file: this.file, name: this.name, index: this.index, force: force === true }); return { title: this.name, artist: 'VA', } } getAudioName() { let tag = this.getID3Tag(); return template.parseTemplate('audio-information', tag); } } class FetchHandler { static files = {}; static async loadFiles(array, isJSON) { let content = []; for (let i = 0; i < array; i++) { content.push(await FetchHandler.loadFile(array[i], isJSON)); } return content; } static async loadFile(filename, isJSON) { filename += '?v=' + version; let files = FetchHandler.files; if (files[filename]) { return files[filename]; } let data = await FetchHandler.tryFromCache(filename); if (isJSON) { data = JSON.parse(data); } files[filename] = data; return data; } static async tryFromCache(filename) { if (caches) { let cache = await caches.open('vis3d-pwa-1'); let data = await cache.match(filename); if (!data) { data = await fetch(filename); } return await data.text(); } } } class PlayerConfigHandler { async init() { await template.loadArray([ 'config/nav', 'config/content', 'config/visualitem' ]); this.last = 'base'; $('.settings-icon').addEventListener('click', this.open.bind(this)); $('modal-content').addDelegatedEventListener('click', '.config-nav .item', this.navHandler.bind(this)); } open() { if (this.content === undefined) { let content = template.parseTemplate('config/nav', {}); content += template.parseTemplate('config/content', {content: ""}); this.content = content; } gui.modal.renderModal('Settings', this.content, "by VersusTuneZ"); this.handleById(); gui.modal.showModal(); } navHandler(e, el) { this.last = el.dataset.id; this.handleById(); } handleById() { let id = this.last; new VisualConfig(id === 'visual', id === 'base'); let active = $('.config-nav .item.active'), current = $('.config-nav .item[data-id="' + id + '"]'); if (active) { active.removeClass('active'); } if (current) { current.addClass('active'); } } } class VisualConfig { static visualTemplates = {}; constructor(showVisual, renderBase) { this.content = $('modal-content .config-content'); if (showVisual) { this.renderVisualConfig(visual.c); } else { if (renderBase) { this.renderBase(); } else { this.renderVisuals(); } } } renderVisuals() { let keys = Object.keys(visual.visuals), content = '
'; for (let i = 0; i < keys.length; i++) { content += template.parseTemplate('config/visualitem', { title: visual.visuals[keys[i]].name, id: keys[i], active: keys[i] === visual.c ? 'active' : '' }) } content += ''; this.content.innerHTML = content; } async renderBase() { let data = await this.loadVisualConfig('base'), div = create('section'); div.addClass('base'); div.innerHTML = GUIHelper.fromJSON(data, pConf); this.content.innerHTML = div.outerHTML; } // the name loads the json and handle it! async renderVisualConfig(name) { let data = await this.loadVisualConfig(name, vConf), div = create('section'); div.addClass('visual'); div.innerHTML = GUIHelper.fromJSON(data, vConf); div.innerHTML += GUIHelper.createButton({ action: "resetVConf", name: "Reset Visual Config" }); div.innerHTML += GUIHelper.createButton({ action: "makeModalTransparent", name: "toggle Modal Opacity" }) this.content.innerHTML = div.outerHTML; } async loadVisualConfig(name) { let tem = VisualConfig.visualTemplates; if (!tem[name]) { //load config and save it tem[name] = await FetchHandler.loadFile('/out/gui/' + name + ".json", true); } return tem[name]; } } class Player { async init() { this.playlist = new Playlist(); } nextSong() { let next = this.playlist.getNext(); audioHandler.loadSong(next); } prevSong() { let next = this.playlist.getPrevious(); audioHandler.loadSong(next); } playStop() { if (!audioHandler.lastSong) { let next = this.playlist.getCurrent(); audioHandler.loadSong(next); return; } let audioFile = audioHandler.audioFile; if (audioFile.paused) { audioFile.play(); } else { audioFile.pause(); } window.dispatchEvent(new CustomEvent('playSong')); } stop() { if (!audioHandler.lastSong) { return; } let audioFile = audioHandler.audioFile; audioFile.pause(); audioFile.currentTime = 0; window.dispatchEvent(new CustomEvent('playSong')); } playByID(number) { this.playlist.index = number; let next = this.playlist.getCurrent(); audioHandler.loadSong(next); } } const PAGINATIONLIMIT = 50; class Playlist { constructor() { this.list = []; this.shuffled = []; this.index = 0; this.page = 0; this.isShuffle = pConf.get("shuffle", false); $('body').addDelegatedEventListener('change', 'input[type="file"]', this.changeFiles.bind(this)); $('body').addDelegatedEventListener('click', '.pagination .item', this.handlePagination.bind(this)); eventHandler.addEvent('id3-request', this.handle.bind(this)); eventHandler.addEvent('id3-request-force', this.forceID3.bind(this)); } shuffle() { // only shuffle if more then 2 elements are in let len = this.list.length; if (len < 3) { this.shuffled = [0, 1, 2]; return; } // 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] = b; this.shuffled[b] = a; } getNext() { let items = this.list, len = items.length - 1, next = this.index + 1; if (next > len) { next = 0; } this.index = next; return items[this.getRealIndex()]; } getPrevious() { let items = this.list, len = items.length - 1, next = this.index - 1; if (next < 0) { next = len; } this.index = next; return items[this.getRealIndex()]; } getCurrent() { return this.list[this.getRealIndex()]; } // on new upload... this has to be an array! setPlaylist(files) { this.index = 0; this.forceData = undefined; 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) { if (page === undefined) { page = this.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.list; for (let i = s; i < e; i++) { let obj = { index: i.toString(), nr: i + 1, title: items[this.getRealIndex(i)].getAudioName(), active: !audioHandler.audioFile.paused && i === this.index ? 'active' : '' } 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) { if (el.id !== 'upload-dir') { return; } let files = []; let i = 0; for (let file of el.files) { if (file && file.type.indexOf('audio') !== -1 && file.name.match(".m3u") === null) { let audioFile = new AudioPlayerFile(file, i++); files.push(audioFile); } } this.setPlaylist(files); if (files.length > 0) { NotificationHandler.createNotification("Songs added successfully!
Songs: " + files.length, "success", 3000); this.renderPagination(0); } else { NotificationHandler.createNotification("File Upload failed!", "error", 3000); } } getRealIndex(index) { if (index === undefined) { index = this.index; } if (this.isShuffle) { return this.shuffled[index]; } return index; } handle(data) { let index = data.index; if (data.status === "waiting") { return; } this.list[index].id3 = data; if (this.timeout) { window.clearTimeout(this.timeout); } this.timeout = setTimeout(this.renderPagination.bind(this), 100); } forceID3(data) { let self = this; if (!self.forceData) { self.forceData = {}; self.forceNotification = NotificationHandler.createNotification("TagReader -> 0 / " + self.list.length, "info", -1); } let index = data.index; if (data.status === "waiting") { return; } self.list[index].id3 = data; self.forceData[index] = true; let leng = Object.keys(self.forceData).length; this.forceNotification.updateMessageOnly("TagReader -> " + leng + " / " + self.list.length); if (leng === self.list.length) { self.forceNotification.remove() } } } class GUI { async init() { this.data = {}; this.modal = new Modal(); // load first vis window! await template.loadArray([ 'playlist-item', 'playlist', 'playlist-footer', 'audio-information', 'inputs/color', 'inputs/input', 'inputs/slider', 'inputs/switch', 'inputs/select', 'inputs/option', 'help', ]); this.initDropZone(); } openHelp() { gui.modal.renderModal("Help", template.parseTemplate('help', {})); gui.modal.showModal(); } 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) { e.dataTransfer.id = 'upload-dir'; player.playlist.changeFiles(e, e.dataTransfer); } else { alert("Sorry you need to upload files!"); } } }); }); }; } // create config Inputs from JSON //@todo add support for gui grouping! class GUIHelper { static fromJSON(json, conf) { let data = []; for (let item of json) { switch (item.type) { case 'slider': data.push(GUIHelper.createSliders(item, conf)); break; case 'color': data.push(GUIHelper.createColorPicker(item, conf)); break; case 'checkbox': data.push(GUIHelper.createCheckbox(item, conf)); break; case 'input': data.push(GUIHelper.createInputField(item, conf)); break; case 'select': data.push(GUIHelper.createSelect(item, conf)); break; case 'button': data.push(GUIHelper.createButton(item, conf)); break; default: console.error(`Unknown Type: ${item.type}`); } } return data.join(" "); } static createSliders(data, conf) { let content = ""; if (typeof data.name === "object") { for (let i = 0; i < data.name.length; i++) { let newData = {}; Object.assign(newData, data); newData.showName = GUIHelper.richShowName(data, data.name[i].firstUpper()); newData.name = GUIHelper.richName(data, data.props[i]); content += GUIHelper.createSlider(newData, conf); } } else { content = GUIHelper.createSlider(data, conf); } return content; } static createSlider(data, conf) { let newData = {}; Object.assign(newData, data); newData.value = conf.get(newData.name, newData.value); return template.parseTemplate('inputs/slider', newData) } static createColorPicker(data, conf) { let newData = {}; Object.assign(newData, data); newData.value = conf.get(newData.name, newData.value); return template.parseTemplate('inputs/color', newData) } static createCheckbox(data, conf) { let newData = {}; Object.assign(newData, data); newData.value = conf.get(newData.name, newData.value) ? 'checked' : ''; return template.parseTemplate('inputs/switch', newData) } static createInputField(data, conf) { let newData = {}; Object.assign(newData, data); newData.value = conf.get(newData.name, newData.value); return template.parseTemplate('inputs/input', newData) } static createSelect(data, conf) { let newData = {}; Object.assign(newData, data); newData.value = conf.get(newData.name, newData.value); let options = ''; for (let i = 0; i < newData.options.length; i++) { options += template.parseTemplate('inputs/option', { value: newData.options[i] }) } newData.options = options; newData.event = 'visualConf'; newData.conf = conf.type; return template.parseTemplate('inputs/select', newData) } static richName(data, name) { if (data.group !== "") { return data.group + "-" + name } return name; } static richShowName(data, name) { if (data.group !== "") { return data.group.firstUpper() + ' ' + name } return name; } static createButton(item, conf) { return `
${item.name}
` } } class Modal { constructor() { let self = this; self.currentModal = ''; self.modal = $('#modal'); self.open = false; self.parent = self.modal.parentNode; self.modal.addDelegatedEventListener('click', 'header .close', this.closeModal.bind(this)); } resetModal() { this.renderModal('', '', ''); } renderModal(title, content, footer) { $('#modal').removeClass('lightMode') this.currentModal = title; this.renderHeader(title); this.renderContent(content); this.renderFooter(footer); } 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 .inner', this.modal); con.innerHTML = footer || "by VersusTuneZ"; } closeModal() { this.parent.addClass("hide") this.open = false; } isCurrent(title) { return title === this.currentModal; } showModal() { this.parent.removeClass("hide") this.open = true; } } class Visual { constructor() { this.data = []; //for drawing this.dataArray = []; this.name = "Default"; } updateData() { } updateFFT(fftSize) { } draw() { } setup() { } } class VisualDrawer { constructor() { this.visuals = { //"sphere": new Sphere(), "wave": new Wave(), "wave2d": new Wave2D(), //"water": new Water() } this.lastMainColor = { base: '#-1', color: [0, 0, 0] }; this.lastSecondColor = { base: '#-1', color: [0, 0, 0] } } init() { this.switch(pConf.get("visual", "wave2d")); this.updateLoop(); } switch(visual) { if (this.visuals[visual] != null) { this.c = visual; vConf.loadConfigByName(this.c); this.visuals[this.c].setup(); pConf.set("visual", this.c); pConf.save(); } } updateLoop() { let self = this; let vis = self.visuals[self.c]; let pro = shaderHandler.use(self.c); this.updateSeekbar(); this.prepare(pro); vis.updateData(); vis.draw(pro); requestAnimationFrame(self.updateLoop.bind(self)) } updateSeekbar() { cInfo.width = window.innerWidth; cInfo.height = window.innerHeight; let audioFile = audioHandler.audioFile; ctx.clearRect(0, 0, cInfo.width, cInfo.height); if (!audioFile.paused && pConf.get("showSeekbar", true)) { //show seekbar let dur = audioFile.duration, cur = audioFile.currentTime, percent = cur / dur * cInfo.width; ctx.fillStyle = pConf.get("seekColor", '#fff'); ctx.fillRect(0, c.height - 10, percent, c.height); } } prepare(pro) { c.width = window.innerWidth; c.height = window.innerHeight; gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.clearColor(0, 0, 0, parseFloat(pConf.get("alphaValue", 0))); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.CULL_FACE); // u_baseColor || u_maxColor this.setColor(pro); } setColor(program) { let baseColor = gl.getUniformLocation(program, "u_baseColor"), maxColor = gl.getUniformLocation(program, "u_maxColor"), self = this, mainColor = self.lastMainColor, secondColor = self.lastSecondColor; this.updateColor('lastMainColor', 'baseColor'); this.updateColor('lastSecondColor', 'gradientToColor'); gl.uniform3fv(baseColor, mainColor.color); gl.uniform3fv(maxColor, secondColor.color); } updateColor(index, col) { let color = this[index], value = vConf.get(col, '#ffffff') if(value !== color.base) { color.color = hexToRgb(value); color.base = value; } } } class ImageUploader { async init() { this.image = pConf.get("bgURL", ""); this.color = pConf.get("bgColour", "#000000"); this.alpha = pConf.get("alphaValue", 0.5); this.getRealImage(); this.applyValues(); $('#modal').addDelegatedEventListener('change', '#image-upload input:not([type="color"])', this.changeHandler.bind(this)); $('#modal').addDelegatedEventListener('input', '#image-upload input#color', this.changeHandler.bind(this)); } async renderModal() { await template.loadTemplate("image"); gui.modal.resetModal(); gui.modal.renderModal("Background-Image", template.parseTemplate("image", { value: this.image, bgValue: this.color, alphaValue: this.alpha }), ""); gui.modal.showModal(); } changeHandler(e, el) { if (el.id === 'color') { this.color = el.value; } else if (el.id === "alphaValue") { this.alpha = el.value; } else { pConf.set('bgMode', el.id); if (el.id === 'image') { el.files[0].toBase64((e, b) => { if (b) { alert("Error converting image!"); return; } pConf.set('bgURL', e.currentTarget.result); pConf.save(); }) this.image = URL.createObjectURL(el.files[0]); } else { this.image = el.value; pConf.set('bgURL', this.image); } } pConf.set('bgColour', this.color); pConf.set('alphaValue', this.alpha); this.applyValues(); pConf.save(); } applyValues() { let body = $('body'); body.style.backgroundImage = 'url(' + this.image + ')'; body.style.backgroundColor = this.color; } getRealImage() { let mode = pConf.get('bgMode'), value = pConf.get("bgURL", ""); if (mode === 'image') { if (value !== '' && value.startsWith('data:image')) { let split = value.split(";"), type = split.shift(), message = split.join(";").replace("base64,", ""); this.image = URL.createObjectURL(b64toBlob(message, type)); } } else { this.image = value; } } } const imageUploader = new ImageUploader(); class NotificationHandler { static instance = new NotificationHandler(); constructor() { this.outer = $('.notification'); this.notifications = []; } async init() { await template.loadTemplate('notification'); } static createNotification(message, type, time) { time = parseInt(time || "3000"); let handler = NotificationHandler.instance, not = new Notification(message, type, time); handler.notifications.push(not); not.show(); return not; } } class Notification { constructor(message, type, time) { this.outer = NotificationHandler.instance.outer; this.message = message; this.type = type; this.time = time; this.isRemoved = false; } async show() { let self = this, endless = self.time === -1; self.item = create('div'); self.item.addClass('notification-item, ' + self.type); if (endless) { self.type += ' endless'; } self.updateContent(self.message); this.outer.prepend(self.item); if (!endless) { setTimeout(this.remove.bind(this), self.time) } } async remove() { if (this.isRemoved) { return; } this.isRemoved = true; this.outer.removeChild(this.item); let not = NotificationHandler.instance.notifications, index = not.indexOf(this); not.splice(index, 1); delete this; } updateContent(message) { let self = this, isEndless = self.time === -1, data = { message: message, time: isEndless ? 1000 : self.time + 1, type: self.type, } this.item.innerHTML = template.parseTemplate('notification', data); } updateMessageOnly(message) { let item = $('.message', this.item); if (item) { item.innerHTML = message; } } } class Config { static allConfigs = {}; constructor(type) { this.config = {}; this.name = '' this.type = type; Config.allConfigs[type] = this; } loadConfigByName(name) { this.save(); this.name = 'config-' + name; let item = localStorage.getItem(this.name); if (item) { this.config = JSON.parse(item); } } save() { if (this.name !== '') { localStorage.setItem(this.name, JSON.stringify(this.config)); } } set(name, value) { this.config[name] = value; } remove(name) { delete this.config[name]; } get(name, def) { let value = this.config[name]; if (value === undefined || value === null) { this.config[name] = def; value = def; } return value; } reset() { NotificationHandler.createNotification(`CONFIG REQUEST SUCCESS FOR ${this.type}`, "success", 2000); this.config = {}; this.save(); } } class Sphere extends Visual { constructor() { super(); this.name = "Sphere"; } draw() { } setup() { } } // 3D Audio-Waves -> maybe also 2D? class Wave extends Visual { constructor() { super(); this.name = "3D Wave"; } updateData() { let data = audioHandler.getFloatArray(); let add = 2 / data.length, x = -1; let outerLoop = 0; for (let i = 0; i < data.length; i++) { //first this.data[outerLoop] = x; this.data[outerLoop + 1] = data[i]; this.data[outerLoop + 2] = 0; //second this.data[outerLoop + 3] = x; //third this.data[outerLoop + 6] = x; this.data[outerLoop + 8] = data[i + 1] || 0; outerLoop += 9; x += add; } } draw(program) { this.prepare(program); let position = this.position, 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.drawArrays(vConf.get("waveForm", gl.TRIANGLES), 0, this.data.length / 3); this.afterDraw(); } rotate(program) { let aspect = c.width / c.height, matrix = [ 1 / aspect, 0, 0, 0, 0, 0.6, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] matrix = TDUtils.multiply(matrix, TDUtils.xRotation(vConf.get("rotation-x", 10))); matrix = TDUtils.multiply(matrix, TDUtils.yRotation(vConf.get("rotation-y", 50))); matrix = TDUtils.multiply(matrix, TDUtils.zRotation(vConf.get("rotation-z", -30))); let rotate = gl.getUniformLocation(program, "u_matrix"); gl.uniformMatrix4fv(rotate, false, matrix); } setup() { this.updateFFT(vConf.get('fftSize', 4096)); vConf.get("rotation-z", -30); vConf.get("rotation-y", 50); vConf.get("rotation-x", 10); } updateFFT(fftSize) { audioHandler.fftSize(fftSize); this.data = new Float32Array(fftSize * 9); } prepare(program) { this.position = gl.getAttribLocation(program, "a_position"); this.color = gl.getUniformLocation(program, "u_color"); let lightPos = gl.getUniformLocation(program, "u_lightPos"), light = gl.getUniformLocation(program, "u_light"); gl.uniform3fv(lightPos, [ vConf.get("light-x", 0), vConf.get("light-y", 5), vConf.get("light-z", -56) ]); gl.uniform1f(light, parseFloat(vConf.get("light-strength", 0.3))); } afterDraw() { TDUtils.updateRotate('rotation-x', 10); TDUtils.updateRotate('rotation-y', 50); TDUtils.updateRotate('rotation-z', -30); vConf.save(); } } class Wave2D extends Visual { constructor() { super(); this.name = "2D Wave"; } updateData() { let data = audioHandler.getFloatArray(); let add = c.width / data.length, x = 0, y = c.height / 2, goTrough = y / 2; let outerLoop = 0; for (let i = 0; i < data.length; i++) { //first this.data[outerLoop] = x; this.data[outerLoop + 1] = y + (data[i] * goTrough); this.data[outerLoop + 2] = data[i]; outerLoop += 3; x += add; } } draw(program) { this.prepare(program); let position = this.position, 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.drawArrays(vConf.get("waveForm", gl.LINE_STRIP), 0, this.data.length / 3); this.afterDraw(); } rotate(program) { let glHelper = new GLHelper(program); glHelper.project(); glHelper.addFudgeFactor(vConf.get("fudgeFactor", 1)); glHelper.translate([ camera.translate.x, camera.translate.y, camera.translate.z ]); glHelper.rotateX(camera.mouse.x); glHelper.rotateY(camera.mouse.y); glHelper.rotateZ(vConf.get("rotation-z", 0)); glHelper.applyMatrix(); } setup() { this.updateFFT(vConf.get('fftSize', 16384)); } updateFFT(fftSize) { audioHandler.fftSize(fftSize); this.data = new Float32Array(fftSize * 3); } prepare(program) { this.position = gl.getAttribLocation(program, "a_position"); //GLHelper.uniform1f(program, "u_fudgeFactor", vConf.get("fudgeFactor", 1)); GLHelper.uniform3fv(program, "u_lightPos", vConf.get("light", [0, 5, -56])); } afterDraw() { TDUtils.updateRotate('rotation-x', 0); TDUtils.updateRotate('rotation-y', 0); TDUtils.updateRotate('rotation-z', 0); vConf.save(); } } //animate Water the way like the Audio is Coming... 256FFT-Size max! class Water extends Visual { constructor() { super(); this.name = "Water"; } draw() { } setup() { audioHandler.fftSize(256) } } class EventHandler { constructor() { this.events = {}; } addEvent(events, cb) { let names = events.split(","); for (let name of names) { this.events[name.trim()] = cb; } } sendData(name, data) { worker.postMessage({ cmd: name, data: data }); } handleEvent(event) { let data = event.data; if (!data.cmd) { return false; } if (this.events[data.cmd]) { try { this.events[data.cmd](data.data); } catch (e) { console.error('[EventHandler] > ' + e.message); } return true; } return false; } } async function initHandler() { let body = $('body'); $('.playlist.menu-icon').addEventListener('click', e => { player.playlist.renderPagination(player.playlist.page); gui.modal.showModal(); }); 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; case 'shuffle': toggleShuffle(); break; } }); window.addEventListener('playSong', setActiveOnPlaylist); window.addEventListener('playSong', e => { togglePlayButton(audioHandler.audioFile.paused ? 'play' : 'pause'); }); $('.upload-image').addEventListener('click', imageUploader.renderModal.bind(imageUploader)); body.addDelegatedEventListener('click', '.readAll', forceAllRead); body.addDelegatedEventListener('input', '.input-range input[type="range"]', (e, el) => { let current = $('.current', el.parentNode); current.innerText = el.value; }); body.addDelegatedEventListener('input', 'input[type="color"]', (e, el) => { let parent = el.parentNode; $('.colorBlob', parent).style.backgroundColor = el.value; }) body.addDelegatedEventListener('click', '.visual-item', (e, el) => { visual.switch(el.dataset.id || 'wave'); $('modal-content .visuals .active').removeClass('active'); el.addClass('active'); }) body.addDelegatedEventListener('input', 'section.base input', (e, el) => { if (el.type === 'checkbox') { pConf.set(el.name, el.checked); } else { setValue(el.name, el.value, pConf, el.dataset.type); } pConf.save(); }) body.addDelegatedEventListener('input', 'section.visual input', (e, el) => { if (el.type === 'checkbox') { vConf.set(el.name, el.checked); } else { setValue(el.name, el.value, vConf, el.dataset.type); } vConf.save(); }) body.addDelegatedEventListener('click', '.button[data-action]', (e, el) => { switch (el.dataset.action) { case 'resetVConf': vConf.reset(); setTimeout(e => { playerConf.handleById(); }, 30); break; case 'makeModalTransparent': $('#modal').toggleClass('lightMode') break; } }) $('.help.menu-icon').addEventListener('click', gui.openHelp); document.onfullscreenchange = e => { if (body.hasClass('fullscreen')) { body.removeClass('fullscreen') } else { body.addClass('fullscreen') } } } function forceAllRead() { let playlist = player.playlist.list; for (let i = 0; i < playlist.length; i++) { playlist[i].getID3Tag(true); } } function setValue(name, value, conf, type) { switch (type) { case 'float': value = parseFloat(value); break; case 'int': value = parseInt(value); break; } conf.set(name, value); } function setActiveOnPlaylist(e) { let item = $('.playlist-item[data-index="' + player.playlist.index + '"]'), active = $('.playlist-item.active'); if (active) { active.removeClass('active'); } if (item) { item.addClass('active'); } } function toggleShuffle(updateGUI) { let active = player.playlist.isShuffle; if (updateGUI !== false) { active = !active; let status = active ? 'enabled' : 'disabled'; NotificationHandler.createNotification("Shuffle: " + status, "info", 500); pConf.set("shuffle", active); pConf.save(); player.playlist.isShuffle = active; } $('#shuffle').toggleCheck('active', active); } function togglePlayButton(status) { let icons = $$('#play .icon'); icons.forEach(el => { if (el.dataset.name === status) { el.removeClass('hide'); } else { el.addClass('hide'); } }) } (function () { const body = $('body'); body.addDelegatedEventListener('click', 'custom-select .label', (e, el) => { let parent = el.parentNode; let options = $$('custom-option', parent), optionsDiv = $('custom-options', parent); if (parent.hasClass('open')) { optionsDiv.style.maxHeight = ''; parent.removeClass('open'); } else { let sum = 0; options.forEach(function (element) { sum += element.offsetHeight; }); optionsDiv.style.maxHeight = sum + 'px'; parent.addClass('open'); } }) body.addDelegatedEventListener('click', 'custom-select custom-option', (e, el) => { let select = el.closest('custom-select'), input = $('input', select); $$('custom-option.active').forEach(activeEl => { activeEl.removeClass('active'); }) el.addClass('active'); if (input) { input.value = el.dataset.value || el.innerText; $('.label', select).innerText = el.innerText; select.removeClass('open'); el.parentNode.style.maxHeight = ''; window.dispatchEvent(new CustomEvent('selectChanged', { detail: { select: select, event: select.dataset.event, value: input.value, name: input.name } })); } }) window.addEventListener('selectChanged', (e) => { if (e.detail.event === 'visualConf') { e.preventDefault(); e.stopPropagation(); handleVisualConf(e.detail); } }) function handleVisualConf(e) { try { let value = e.value, config = Config.allConfigs[e.select.dataset.conf]; if (e.name === 'fftSize') { value = parseInt(e.value); visual.visuals[visual.c].updateFFT(value); } config.set(e.name, value); config.save(); } catch (e) { console.error(e); } } })() class KeyHandler { async init() { await this.mediaKeys(); await this.addKeyHandler(); window.addEventListener('keydown', this.keyHandler.bind(this)); } async mediaKeys() { if ('mediaSession' in navigator) { let media = navigator.mediaSession; media.setActionHandler('play', player.playStop.bind(player)); media.setActionHandler('pause', player.playStop.bind(player)); media.setActionHandler('previoustrack', player.prevSong.bind(player)); media.setActionHandler('nexttrack', player.nextSong.bind(player)); media.setActionHandler('stop', player.stop.bind(player)); } } async addKeyHandler() { eventHandler.addEvent('keys-Space', player.playStop.bind(player)); eventHandler.addEvent('keys-KeyN', player.nextSong.bind(player)); eventHandler.addEvent('keys-KeyV', player.prevSong.bind(player)); eventHandler.addEvent('keys-KeyS', playerConf.open.bind(playerConf)); eventHandler.addEvent('keys-KeyS-shift', toggleShuffle); eventHandler.addEvent('keys-KeyB', imageUploader.renderModal.bind(imageUploader)); eventHandler.addEvent('keys-KeyF-shift', forceAllRead); eventHandler.addEvent('keys-KeyH', gui.openHelp); eventHandler.addEvent('keys-KeyP', e => { player.playlist.renderPagination(player.playlist.page); gui.modal.showModal(); }); eventHandler.addEvent('keys-Escape, keys-KeyC-shift', e => { gui.modal.resetModal(); gui.modal.closeModal(); }) eventHandler.addEvent('keys-F11', e => { if (document.fullscreenElement) { document.exitFullscreen().catch(console.error); } else { document.body.requestFullscreen().catch(console.error); } }); } async keyHandler(event) { let key = event.code, shift = event.shiftKey ? '-shift' : '', ctrl = event.ctrlKey ? '-ctrl' : '', name = 'keys-' + key + shift + ctrl; if (eventHandler.handleEvent({ data: { cmd: name, data: name } })) { event.preventDefault(); event.stopPropagation(); } } } class Startup { constructor() { this.modules = { 'startup': false, 'id3-ready': false }; } moduleLoaded(name) { this.modules[name] = true this.allModulesLoaded(); } allModulesLoaded() { for (let module in this.modules) { if (!this.modules[module]) { return false; } } window.dispatchEvent(new CustomEvent('startupFin')); return true; } } const shaderHandler = new ShaderHandler(null), audioHandler = new AudioHandler(), gui = new GUI(), visual = new VisualDrawer(), template = new Template(), player = new Player(), vConf = new Config("visual"), pConf = new Config("player"), worker = new Worker('/out/js/worker.min.js'), startup = new Startup(), eventHandler = new EventHandler(), playerConf = new PlayerConfigHandler(), keyHandler = new KeyHandler(), version = 1, camera = new Camera(); let c, gl, cInfo, ctx, sw; worker.addEventListener('message', e => { if (e.data.status === 'startup') { startup.moduleLoaded(e.data.cmd); return; } eventHandler.handleEvent(e); }); window.addEventListener('startupFin', e => { setTimeout(e => { $('.loading-screen').remove(); }, 100) }) async function startUP() { if ('serviceWorker' in navigator) { sw = await navigator.serviceWorker.register('/sw.js'); } pConf.loadConfigByName('default'); c = $('#c'), gl = c.getContext("webgl2"), cInfo = $('#cInfo'), ctx = cInfo.getContext('2d'); if (!gl) { alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2"); return false; } shaderHandler.setGL(gl) await shaderHandler.loadArray(["wave", "sphere", "water", "wave2d"], '/shaders/'); await NotificationHandler.instance.init(); await audioHandler.init(); await player.init(); await camera.init(); await visual.init(); await gui.init(); await imageUploader.init(); await playerConf.init(); await keyHandler.init(); await initHandler(); toggleShuffle(false); } startUP().then(r => { startup.moduleLoaded('startup'); });