2020-04-07 21:44:46 +02:00
|
|
|
class VTUtils {
|
|
|
|
static random(min, max) {
|
|
|
|
let rand = Math.random();
|
|
|
|
if (typeof min === 'undefined') {
|
|
|
|
return rand;
|
|
|
|
} else if (typeof max === 'undefined') {
|
|
|
|
if (min instanceof Array) {
|
|
|
|
return min[Math.floor(rand * min.length)];
|
|
|
|
} else {
|
|
|
|
return rand * min;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (min > max) {
|
|
|
|
let tmp = min;
|
|
|
|
min = max;
|
|
|
|
max = tmp;
|
|
|
|
}
|
|
|
|
return rand * (max - min) + min;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
static randomInt(min, max) {
|
|
|
|
return Math.floor(VTUtils.random(min, max));
|
|
|
|
}
|
|
|
|
|
|
|
|
static normalize(val, max, min) {
|
|
|
|
return (val - min) / (max - min);
|
|
|
|
};
|
|
|
|
|
|
|
|
static distance(x, y, x2, y2) {
|
|
|
|
let a = x - x2;
|
|
|
|
let b = y - y2;
|
|
|
|
|
|
|
|
return Math.sqrt(a * a + b * b);
|
|
|
|
}
|
|
|
|
|
|
|
|
static map(n, start1, stop1, start2, stop2, withinBounds) {
|
|
|
|
let newVal = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
|
|
|
|
if (!withinBounds) {
|
|
|
|
return newVal;
|
|
|
|
}
|
|
|
|
if (start2 < stop2) {
|
|
|
|
return this.constrain(newVal, start2, stop2);
|
|
|
|
} else {
|
|
|
|
return this.constrain(newVal, stop2, start2);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
static constrain(n, low, high) {
|
|
|
|
return Math.max(Math.min(n, high), low);
|
|
|
|
}
|
|
|
|
|
|
|
|
static hsvToRgb(h, s, v) {
|
|
|
|
var r, g, b;
|
|
|
|
|
|
|
|
var i = Math.floor(h * 6);
|
|
|
|
var f = h * 6 - i;
|
|
|
|
var p = v * (1 - s);
|
|
|
|
var q = v * (1 - f * s);
|
|
|
|
var t = v * (1 - (1 - f) * s);
|
|
|
|
|
|
|
|
switch (i % 6) {
|
|
|
|
case 0:
|
|
|
|
r = v, g = t, b = p;
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
r = q, g = v, b = p;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
r = p, g = v, b = t;
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
r = p, g = q, b = v;
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
r = t, g = p, b = v;
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
r = v, g = p, b = q;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {r: r, g: g, b: b};
|
|
|
|
}
|
|
|
|
|
|
|
|
static peakRGB(peak) {
|
|
|
|
return {
|
|
|
|
r: peak,
|
|
|
|
g: 1 - peak,
|
|
|
|
b: 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class VTVector {
|
|
|
|
constructor(x, y, z) {
|
|
|
|
this.x = x || 0;
|
|
|
|
this.y = y || 0;
|
|
|
|
this.z = z || 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
//helper
|
|
|
|
static createRandom(x, y, z) {
|
|
|
|
x = x || 1;
|
|
|
|
y = y || 1;
|
|
|
|
z = z || 0;
|
|
|
|
return new VTVector(VTUtils.random(-x, x), VTUtils.random(-y, y), VTUtils.random(-z, z));
|
|
|
|
}
|
|
|
|
|
|
|
|
mult(times) {
|
|
|
|
this.x *= times;
|
|
|
|
this.y *= times;
|
|
|
|
this.z *= times;
|
|
|
|
}
|
|
|
|
|
|
|
|
set(vector) {
|
|
|
|
this.x = vector.x;
|
|
|
|
this.y = vector.y;
|
|
|
|
this.z = vector.z;
|
|
|
|
}
|
|
|
|
|
|
|
|
add(vector) {
|
|
|
|
this.x = this.x + vector.x;
|
|
|
|
this.y = this.y + vector.y;
|
|
|
|
this.z = this.z + vector.z;
|
|
|
|
}
|
|
|
|
|
|
|
|
addXYZ(x, y, z) {
|
|
|
|
this.x += x;
|
|
|
|
this.y += y;
|
|
|
|
this.z += z;
|
|
|
|
}
|
|
|
|
|
|
|
|
setXYZ(x, y, z) {
|
|
|
|
this.x = x || 0;
|
|
|
|
this.y = y || 0;
|
|
|
|
this.z = z || 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
clone() {
|
|
|
|
return new VTVector(this.x, this.y, this.z);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class TDUtils {
|
|
|
|
static multiply(a, b) {
|
|
|
|
let b00 = b[0 * 4 + 0];
|
|
|
|
let b01 = b[0 * 4 + 1];
|
|
|
|
let b02 = b[0 * 4 + 2];
|
|
|
|
let b03 = b[0 * 4 + 3];
|
|
|
|
let b10 = b[1 * 4 + 0];
|
|
|
|
let b11 = b[1 * 4 + 1];
|
|
|
|
let b12 = b[1 * 4 + 2];
|
|
|
|
let b13 = b[1 * 4 + 3];
|
|
|
|
let b20 = b[2 * 4 + 0];
|
|
|
|
let b21 = b[2 * 4 + 1];
|
|
|
|
let b22 = b[2 * 4 + 2];
|
|
|
|
let b23 = b[2 * 4 + 3];
|
|
|
|
let b30 = b[3 * 4 + 0];
|
|
|
|
let b31 = b[3 * 4 + 1];
|
|
|
|
let b32 = b[3 * 4 + 2];
|
|
|
|
let b33 = b[3 * 4 + 3];
|
|
|
|
let a00 = a[0 * 4 + 0];
|
|
|
|
let a01 = a[0 * 4 + 1];
|
|
|
|
let a02 = a[0 * 4 + 2];
|
|
|
|
let a03 = a[0 * 4 + 3];
|
|
|
|
let a10 = a[1 * 4 + 0];
|
|
|
|
let a11 = a[1 * 4 + 1];
|
|
|
|
let a12 = a[1 * 4 + 2];
|
|
|
|
let a13 = a[1 * 4 + 3];
|
|
|
|
let a20 = a[2 * 4 + 0];
|
|
|
|
let a21 = a[2 * 4 + 1];
|
|
|
|
let a22 = a[2 * 4 + 2];
|
|
|
|
let a23 = a[2 * 4 + 3];
|
|
|
|
let a30 = a[3 * 4 + 0];
|
|
|
|
let a31 = a[3 * 4 + 1];
|
|
|
|
let a32 = a[3 * 4 + 2];
|
|
|
|
let a33 = a[3 * 4 + 3];
|
|
|
|
let dst = [];
|
|
|
|
dst[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30;
|
|
|
|
dst[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31;
|
|
|
|
dst[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32;
|
|
|
|
dst[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33;
|
|
|
|
dst[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30;
|
|
|
|
dst[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31;
|
|
|
|
dst[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32;
|
|
|
|
dst[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33;
|
|
|
|
dst[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30;
|
|
|
|
dst[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31;
|
|
|
|
dst[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32;
|
|
|
|
dst[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33;
|
|
|
|
dst[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30;
|
|
|
|
dst[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31;
|
|
|
|
dst[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32;
|
|
|
|
dst[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33;
|
|
|
|
return dst;
|
|
|
|
}
|
|
|
|
|
|
|
|
static xRotation(angleInRadians) {
|
|
|
|
let c = Math.cos(angleInRadians);
|
|
|
|
let s = Math.sin(angleInRadians);
|
|
|
|
|
|
|
|
return [
|
|
|
|
1, 0, 0, 0,
|
|
|
|
0, c, s, 0,
|
|
|
|
0, -s, c, 0,
|
|
|
|
0, 0, 0, 1,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
static yRotation(angleInRadians) {
|
|
|
|
let c = Math.cos(angleInRadians);
|
|
|
|
let s = Math.sin(angleInRadians);
|
|
|
|
|
|
|
|
return [
|
|
|
|
c, 0, -s, 0,
|
|
|
|
0, 1, 0, 0,
|
|
|
|
s, 0, c, 0,
|
|
|
|
0, 0, 0, 1,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
static zRotation(angleInRadians) {
|
|
|
|
let c = Math.cos(angleInRadians);
|
|
|
|
let s = Math.sin(angleInRadians);
|
|
|
|
|
|
|
|
return [
|
|
|
|
c, s, 0, 0,
|
|
|
|
-s, c, 0, 0,
|
|
|
|
0, 0, 1, 0,
|
|
|
|
0, 0, 0, 1,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
static degToRad(d) {
|
|
|
|
return d * Math.PI / 180;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function $(sel, s) {
|
|
|
|
return $$(sel, s)[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
function $$(sel, s) {
|
|
|
|
s = s || document;
|
|
|
|
return s.querySelectorAll(sel);
|
|
|
|
}
|
|
|
|
|
|
|
|
Node.prototype.addDelegatedEventListener = function (type, aim, cb) {
|
|
|
|
this.addEventListener(type, (event) => {
|
|
|
|
let target = event.target;
|
|
|
|
if (target.matches(aim)) {
|
|
|
|
cb(event, target);
|
|
|
|
} else {
|
|
|
|
let parent = target.closest(aim);
|
|
|
|
if (parent) {
|
|
|
|
cb(event, parent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
};
|
|
|
|
|
2020-08-01 21:51:54 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-04-07 21:44:46 +02:00
|
|
|
function create(name, content) {
|
|
|
|
let d = document.createElement(name);
|
|
|
|
if (content) {
|
|
|
|
d.innerHTML = content;
|
|
|
|
}
|
|
|
|
return d;
|
|
|
|
}
|
|
|
|
|
|
|
|
function append(to, array) {
|
|
|
|
for (let item in array) {
|
|
|
|
to.appendChild(array[item]);
|
|
|
|
}
|
|
|
|
}
|
2020-08-01 21:51:54 +02:00
|
|
|
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/"
|
2020-04-07 21:44:46 +02:00
|
|
|
class ShaderHandler {
|
|
|
|
constructor(gl) {
|
|
|
|
this.gl = gl;
|
|
|
|
this.shaderNames = [];
|
|
|
|
this.shaders = {};
|
|
|
|
this.programs = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
setGL(gl) {
|
|
|
|
this.gl = gl;
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadShader(name, path) {
|
|
|
|
this.shaderNames.push(name);
|
|
|
|
await this.load(name, path + name + ".vert", this.gl.VERTEX_SHADER);
|
|
|
|
await this.load(name, path + name + ".frag", this.gl.FRAGMENT_SHADER);
|
|
|
|
}
|
|
|
|
|
|
|
|
async load(name, url, type) {
|
|
|
|
let realName = name + "_" + type;
|
|
|
|
if (!this.shaders[realName]) {
|
|
|
|
let data = await fetch(url);
|
|
|
|
let shader = this.createShader(await data.text(), type);
|
|
|
|
if (shader) {
|
|
|
|
this.shaders[realName] = shader;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return !!this.shaders[realName];
|
|
|
|
}
|
|
|
|
|
|
|
|
getShader(name, type) {
|
|
|
|
let realName = name + "_" + type;
|
|
|
|
return this.shaders[realName];
|
|
|
|
}
|
|
|
|
|
|
|
|
getAllShaders() {
|
|
|
|
return this.shaderNames;
|
|
|
|
}
|
|
|
|
|
|
|
|
async createProgramForEach(arr) {
|
|
|
|
arr = arr || this.shaderNames;
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
let shader = arr[i];
|
|
|
|
let v = await shaderHandler.createProgram(shader, [shader])
|
|
|
|
if (!v) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
createShader(source, type) {
|
|
|
|
let gl = this.gl;
|
|
|
|
let shader = gl.createShader(type);
|
|
|
|
gl.shaderSource(shader, source);
|
|
|
|
gl.compileShader(shader);
|
|
|
|
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
|
|
return shader;
|
|
|
|
}
|
|
|
|
console.error(gl.getShaderInfoLog(shader));
|
|
|
|
gl.deleteShader(shader);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
createProgram(name, shaders) {
|
|
|
|
let gl = this.gl;
|
|
|
|
let pro = gl.createProgram();
|
|
|
|
for (let i = 0; i < shaders.length; i++) {
|
|
|
|
gl.attachShader(pro, this.getShader(shaders[i], gl.VERTEX_SHADER));
|
|
|
|
gl.attachShader(pro, this.getShader(shaders[i], gl.FRAGMENT_SHADER));
|
|
|
|
}
|
|
|
|
gl.linkProgram(pro);
|
|
|
|
var success = gl.getProgramParameter(pro, gl.LINK_STATUS);
|
|
|
|
if (success) {
|
|
|
|
this.programs[name] = pro;
|
|
|
|
return pro;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(gl.getProgramInfoLog(pro));
|
|
|
|
gl.deleteProgram(pro);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getProgram(name) {
|
|
|
|
return this.programs[name];
|
|
|
|
}
|
|
|
|
|
2020-08-01 21:51:54 +02:00
|
|
|
use(name) {
|
|
|
|
let pro = this.programs[name];
|
|
|
|
this.gl.useProgram(pro);
|
|
|
|
return pro;
|
|
|
|
}
|
|
|
|
|
2020-04-07 21:44:46 +02:00
|
|
|
async loadArray(list, path) {
|
|
|
|
let self = this;
|
|
|
|
for (const e of list) {
|
|
|
|
await self.loadShader(e, path)
|
|
|
|
}
|
|
|
|
await self.createProgramForEach(list)
|
|
|
|
}
|
|
|
|
}
|
2020-08-01 21:51:54 +02:00
|
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
|
|
|
2020-04-07 21:44:46 +02:00
|
|
|
class AudioHandler {
|
|
|
|
async init() {
|
2020-08-01 21:51:54 +02:00
|
|
|
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;
|
2020-04-07 21:44:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
class Player {
|
|
|
|
async init() {
|
2020-08-01 21:51:54 +02:00
|
|
|
this.playlist = new Playlist();
|
|
|
|
}
|
|
|
|
|
|
|
|
nextSong() {
|
|
|
|
let next = this.playlist.getNext();
|
|
|
|
audioHandler.loadSong(next.file);
|
|
|
|
}
|
2020-04-07 21:44:46 +02:00
|
|
|
|
2020-08-01 21:51:54 +02:00
|
|
|
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 = "<h1>No Songs uploaded!</h1>";
|
|
|
|
}
|
|
|
|
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!");
|
|
|
|
}
|
2020-04-07 21:44:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
class GUI {
|
2020-08-01 21:51:54 +02:00
|
|
|
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;
|
|
|
|
}
|
2020-04-07 21:44:46 +02:00
|
|
|
|
2020-08-01 21:51:54 +02:00
|
|
|
showModal() {
|
|
|
|
this.parent.removeClass("hide")
|
2020-04-07 21:44:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
class Visual {
|
|
|
|
constructor() {
|
|
|
|
this.data = []; //for drawing
|
|
|
|
this.dataArray = [];
|
|
|
|
}
|
2020-08-01 21:51:54 +02:00
|
|
|
|
|
|
|
updateData() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-04-07 21:44:46 +02:00
|
|
|
draw() {
|
|
|
|
}
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class VisualDrawer {
|
2020-08-01 21:51:54 +02:00
|
|
|
constructor() {
|
|
|
|
this.visuals = {
|
|
|
|
"sphere": new Sphere(),
|
|
|
|
"wave": new Wave(),
|
|
|
|
"water": new Water()
|
|
|
|
}
|
|
|
|
this.c = "wave";
|
|
|
|
}
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this.visuals[this.c].setup();
|
|
|
|
this.updateLoop();
|
|
|
|
}
|
2020-04-07 21:44:46 +02:00
|
|
|
|
2020-08-01 21:51:54 +02:00
|
|
|
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;
|
|
|
|
}
|
2020-04-07 21:44:46 +02:00
|
|
|
}
|
|
|
|
class Sphere extends Visual {
|
|
|
|
draw() {
|
|
|
|
}
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
}
|
|
|
|
}
|
2020-08-01 21:51:54 +02:00
|
|
|
// 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');
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2020-04-07 21:44:46 +02:00
|
|
|
const shaderHandler = new ShaderHandler(null),
|
|
|
|
audioHandler = new AudioHandler(),
|
|
|
|
gui = new GUI(),
|
2020-08-01 21:51:54 +02:00
|
|
|
visual = new VisualDrawer(),
|
|
|
|
template = new Template(),
|
|
|
|
player = new Player(),
|
|
|
|
config = new Config();
|
|
|
|
|
|
|
|
let c = null,
|
|
|
|
gl = null;
|
2020-04-07 21:44:46 +02:00
|
|
|
|
|
|
|
async function startUP() {
|
2020-08-01 21:51:54 +02:00
|
|
|
c = document.body.querySelector('#c'),
|
2020-04-07 21:44:46 +02:00
|
|
|
gl = c.getContext("webgl2");
|
|
|
|
if (!gl) {
|
|
|
|
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
shaderHandler.setGL(gl)
|
2020-08-01 21:51:54 +02:00
|
|
|
await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/');
|
2020-04-07 21:44:46 +02:00
|
|
|
await audioHandler.init();
|
|
|
|
await player.init();
|
2020-08-01 21:51:54 +02:00
|
|
|
await visual.init();
|
|
|
|
await gui.init();
|
|
|
|
await initHandler();
|
2020-04-07 21:44:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
startUP().then(r => {
|
|
|
|
setTimeout(e => {
|
|
|
|
$('.loading-screen').remove();
|
|
|
|
}, 100)
|
|
|
|
});
|