2487 lines
70 KiB
JavaScript
2487 lines
70 KiB
JavaScript
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!<br> 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("<span class='now-playing'>Now Playing:</span>" + 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 = '<section class="visuals">';
|
|
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 += '</div>';
|
|
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 = "<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) {
|
|
|
|
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!<br> 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 `<div class='button spaced' data-action="${item.action}">${item.name}</div>`
|
|
}
|
|
}
|
|
|
|
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');
|
|
}); |