This commit is contained in:
Maurice Grönwoldt 2020-08-01 21:51:54 +02:00
parent d1ae2059f7
commit 300b6c4106
30 changed files with 1399 additions and 29 deletions

View File

@ -7,19 +7,22 @@ const basePath = __dirname + '/../../raw/javascript/';
const visualPath = basePath + 'visuals/';
const visuals = [
visualPath + 'sphere.js',
//visualPath + 'wave.js',
//visualPath + 'water.js',
visualPath + 'wave.js',
visualPath + 'water.js',
//visualPath + 'experimental.js',
]
const config = {
src: [
basePath + 'utils.js',
basePath + 'template.js',
basePath + 'handler.js',
basePath + 'audio.js',
basePath + 'player.js',
basePath + 'gui.js',
basePath + 'visual.js',
basePath + 'config.js',
...visuals,
basePath + 'eventHandler.js',
basePath + 'app.js'
],
dest: __dirname + '/../../out/js'

View File

@ -42,6 +42,7 @@ function buildIconSprites() {
fal.faCogs,
fal.faFolderUpload,
fal.faListMusic,
fal.faFileAudio,
],
vt: []
};

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WEBGL Test</title>
<title>VS3D-Vis</title>
<link rel="stylesheet" href="out/theme/style.css">
</head>
<body>
@ -19,11 +19,19 @@
</div>
<div class="upload menu-icon">
<label for="upload">
<svg role="img" class="icon">
<use href="out/icon-sprite.svg#fal-fa-file-audio"></use>
</svg>
</label>
<input type="file" multiple accept="audio/*" id="upload">
</div>
<div class="upload menu-icon">
<label for="upload-dir">
<svg role="img" class="icon">
<use href="out/icon-sprite.svg#fal-fa-folder-upload"></use>
</svg>
</label>
<input type="file" accept="audio/*" id="upload">
<input type="file" multiple directory webkitdirectory accept="audio/*" id="upload-dir">
</div>
<div class="playlist menu-icon">
<svg role="img" class="icon">
@ -38,10 +46,10 @@
</svg>
</button>
<button id="play">
<svg role="img" class="icon hide">
<svg role="img" data-name="pause" class="pause icon hide">
<use href="out/icon-sprite.svg#fal-fa-pause"></use>
</svg>
<svg role="img" class="icon">
<svg role="img" data-name="play" class="icon">
<use href="out/icon-sprite.svg#fal-fa-play"></use>
</svg>
</button>
@ -52,6 +60,25 @@
</button>
</div>
<canvas id="c"></canvas>
<div class="grey-screen hide">
<div id="modal">
<header>
<span class="headline">Playlist</span>
<span class="close">X</span>
</header>
<modal-content>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</modal-content>
<modal-footer>
</modal-footer>
</div>
</div>
<script src="out/js/scripts.min.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -260,6 +260,16 @@ Node.prototype.addDelegatedEventListener = function (type, aim, cb) {
})
};
Node.prototype.hasClass = function (className) {
return this.classList.contains(className);
}
Node.prototype.addClass = function (className) {
return this.classList.add(className);
}
Node.prototype.removeClass = function (className) {
return this.classList.remove(className);
}
function create(name, content) {
let d = document.createElement(name);
if (content) {
@ -273,6 +283,50 @@ function append(to, array) {
to.appendChild(array[item]);
}
}
class Template {
constructor() {
this.tpl = {};
}
async loadTemplate(name) {
let self = this;
if (!this.tpl[name]) {
await fetch(templateDir + name + ".tpl").then((r) => r.text()).then(c => {
self.tpl[name] = c;
})
}
}
async loadArray(names) {
for (let name of names) {
await this.loadTemplate(name);
}
}
parseTemplate(name, data) {
if (!this.tpl[name]) {
return ""
}
let m, d = this.tpl[name];
while ((m = templateEx.exec(d)) !== null) {
if (m.index === templateEx.lastIndex) {
templateEx.lastIndex++;
}
let key = m[0];
d = d.replace(key, data[m[1]] || "")
}
return d;
}
parseFromAPI(url, name, cb) {
fetch(url).then((r) => r.json()).then(d => {
cb(this.parseTemplate(name, d))
}).catch(console.error)
}
}
const templateEx = /\$(.*?)\$/gm;
const templateDir = "out/tpl/"
class ShaderHandler {
constructor(gl) {
this.gl = gl;
@ -360,6 +414,12 @@ class ShaderHandler {
return this.programs[name];
}
use(name) {
let pro = this.programs[name];
this.gl.useProgram(pro);
return pro;
}
async loadArray(list, path) {
let self = this;
for (const e of list) {
@ -368,19 +428,355 @@ class ShaderHandler {
await self.createProgramForEach(list)
}
}
const AudioContext = window.AudioContext || window.webkitAudioContext;
class AudioHandler {
async init() {
this.audioFile = new Audio();
let self = this;
self.isStarted = false;
self.audioFile = new Audio();
self.actx = new AudioContext()
self.analyser = self.actx.createAnalyser()
self.analyser.fftSize = 4096;
self.lastSong = null;
await self.connectAll();
}
async connectAll() {
let self = this;
self.source = self.actx.createMediaElementSource(self.audioFile);
self.source.connect(self.analyser);
self.analyser.connect(self.actx.destination);
self.audioFile.addEventListener('ended', player.nextSong.bind(player));
}
async start() {
if (this.audioFile.src === '') {
return;
}
if (!this.isStarted) {
this.isStarted = true;
await this.actx.resume();
}
}
async stop() {
if (this.isStarted) {
this.isStarted = false;
await this.actx.suspend();
}
}
fftSize(size) {
this.analyser.fftSize = size;
}
smoothing(float) {
this.analyser.smoothingTimeConstant = float;
}
loadSong(src) {
let self = this;
if (self.lastSong) {
URL.revokeObjectURL(self.lastSong);
}
self.lastSong = this.audioFile.src = URL.createObjectURL(src);
if (!this.isStarted) {
this.start();
}
this.audioFile.play();
}
getIntArray(steps) {
let dataArray = new Uint8Array(steps);
this.analyser.getByteFrequencyData(dataArray);
return dataArray;
}
getFloatArray() {
let dataArray = new Float32Array(this.analyser.frequencyBinCount);
this.analyser.getFloatTimeDomainData(dataArray);
return dataArray;
}
}
class Player {
async init() {
this.playlist = new Playlist();
}
nextSong() {
let next = this.playlist.getNext();
audioHandler.loadSong(next.file);
}
prevSong() {
let next = this.playlist.getPrevious();
audioHandler.loadSong(next.file);
}
playStop() {
if (!audioHandler.lastSong) {
let next = this.playlist.getCurrent();
audioHandler.loadSong(next.file);
}
let audioFile = audioHandler.audioFile;
if (audioFile.paused) {
audioFile.play();
} else {
audioFile.pause();
}
}
playByID(number) {
let next = this.playlist.getFile(number);
audioHandler.loadSong(next.file);
}
}
const PAGINATIONLIMIT = 50;
class Playlist {
constructor() {
this.list = [];
this.shuffled = [];
this.index = 0;
this.page = 0;
this.isShuffle = false;
$('body').addDelegatedEventListener('change', 'input[type="file"]', this.changeFiles.bind(this));
$('body').addDelegatedEventListener('click', '.pagination .item', this.handlePagination.bind(this));
}
shuffle() {
// only shuffle if more then 2 elements are in
let len = this.list.length;
if (len < 3) {
this.shuffled = this.list;
}
// the current-list need to be shuffled...
for (let i = 0; i < len; i++) {
let random = VTUtils.randomInt(0, len - 1);
this.swap(i, random);
}
}
swap(a, b) {
this.shuffled[a] = this.list[b];
this.shuffled[b] = this.list[a];
}
getNext() {
let items = this.isShuffle ? this.shuffled : this.list,
len = items.length - 1,
next = this.index + 1;
if (next > len) {
next = 0;
}
this.index = next;
return items[next];
}
getPrevious() {
let items = this.isShuffle ? this.shuffled : this.list,
len = items.length - 1,
next = this.index - 1;
if (next < 0) {
next = len;
}
this.index = next;
return items[next];
}
getCurrent() {
let items = this.isShuffle ? this.shuffled : this.list;
return items[this.index];
}
getFile(index) {
let items = this.isShuffle ? this.shuffled : this.list;
return items[index];
}
// on new upload... this has to be an array!
setPlaylist(files) {
this.index = 0;
this.list = files;
this.shuffle();
}
handlePagination(event, el) {
if (el.hasClass('inactive')) {
return;
}
if (el.hasClass('next-site')) {
this.renderPagination(this.page + 1);
} else {
this.renderPagination(this.page - 1);
}
}
renderPagination(page) {
let length = this.list.length,
maxSite = Math.ceil(length / PAGINATIONLIMIT) - 1;
if (page < 0) {
page = 0;
}
if (page > maxSite) {
page = maxSite;
}
let s = page * PAGINATIONLIMIT,
e = s + PAGINATIONLIMIT,
data = "";
this.page = page;
if (e >= length) {
e = length;
}
if (length > 0) {
let items = this.isShuffle ? this.shuffled : this.list;
for (let i = s; i < e; i++) {
let obj = {
index: i,
nr: i + 1,
title: items[i].name
}
data += template.parseTemplate("playlist-item", obj);
}
} else {
data = "<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!");
}
}
}
class GUI {
init() {
async init() {
this.data = {};
this.modal = new Modal();
// load first vis window!
await this.loadForVis();
await template.loadArray([
'playlist-item',
'playlist',
'playlist-footer'
]);
this.initDropZone();
}
async loadForVis() {
let c = visual.c,
d = this.data[c];
if (d == null) {
this.data[c] = await fetch("out/gui/" + c + ".json").then((r) => r.json());
}
}
renderModal(content, title) {
let modal = $('#modal'),
p = modal.parentNode,
h = $('header .headline', modal),
con = $('modal-content', modal);
h.innerHTML = title;
con.innerHTML = content;
p.classList.remove('hide');
}
initDropZone() {
let items = 'drag dragstart dragend dragover dragenter dragleave drop'.split(' ');
items.forEach(el => {
window.addEventListener(el, async e => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'drop') {
if (e.dataTransfer.files.length > 0) {
player.playlist.changeFiles(e, e.dataTransfer);
} else {
alert("Sorry you need to upload files!");
}
}
});
});
};
}
class Modal {
constructor() {
let self = this;
self.currentModal = '';
self.modal = $('#modal');
self.parent = self.modal.parentNode;
self.modal.addDelegatedEventListener('click', 'header .close', this.closeModal.bind(this));
}
resetModal() {
this.renderModal('', '', '');
}
renderModal(title, content, footer) {
this.currentModal = title;
this.renderHeader(title);
this.renderContent(content);
this.renderFooter(footer);
this.showModal();
}
renderHeader(header) {
let h = $('header .headline', this.modal);
h.innerHTML = header;
}
renderContent(content) {
let con = $('modal-content', this.modal);
con.innerHTML = content;
}
renderFooter(footer) {
let con = $('modal-footer', this.modal);
con.innerHTML = footer;
}
closeModal() {
this.parent.addClass("hide")
}
isCurrent(title) {
return title === this.currentModal;
}
showModal() {
this.parent.removeClass("hide")
}
}
class Visual {
@ -388,6 +784,11 @@ class Visual {
this.data = []; //for drawing
this.dataArray = [];
}
updateData() {
}
draw() {
}
@ -396,7 +797,70 @@ class Visual {
}
class VisualDrawer {
constructor() {
this.visuals = {
"sphere": new Sphere(),
"wave": new Wave(),
"water": new Water()
}
this.c = "wave";
}
init() {
this.visuals[this.c].setup();
this.updateLoop();
}
switch(visual) {
if (this.visuals[visual] != null) {
this.c = visual;
this.visuals[this.c].setup();
}
}
updateLoop() {
let self = this;
let pro = shaderHandler.use(self.c);
let vis = self.visuals[self.c];
vis.updateData();
vis.draw(pro);
requestAnimationFrame(self.updateLoop.bind(self))
}
}
class Config {
constructor() {
this.config = {};
this.name = ''
}
loadConfigByName(name) {
this.saveConfig();
this.name = 'config-' + name;
this.config = JSON.parse(this.name);
}
saveConfig() {
if (this.name !== '') {
localStorage.setItem(this.name, JSON.stringify(this.config));
}
}
addItem(name, value) {
this.config[name] = value;
}
removeItem(name) {
delete this.config[name];
}
getItem(name, def) {
let value = this.config[name];
if (value === undefined || value === null) {
this.config[name] = def;
value = def;
}
return value;
}
}
class Sphere extends Visual {
draw() {
@ -405,23 +869,140 @@ class Sphere extends Visual {
setup() {
}
}
// 3D Audio-Waves -> maybe also 2D?
class Wave extends Visual {
updateData() {
this.data = [];
let data = audioHandler.getFloatArray();
let add = 2 / data.length,
x = -1;
for (let i = 0; i < data.length; i++) {
this.data.push(x, data[i], data[i]);
x += add;
}
}
draw(program) {
c.width = window.innerWidth;
c.height = window.innerHeight;
this.prepare(program);
let position = this.position,
color = this.color,
positionBuffer = gl.createBuffer();
this.rotate(program);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.data), gl.DYNAMIC_DRAW);
let vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 3, gl.FLOAT, true, 0, 0);
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clearDepth(2.0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.drawArrays(gl.LINE_STRIP || gl.POINTS, 0, this.data.length / 3);
}
rotate(program) {
let aspect = c.height / c.width,
matrix = [
1 / aspect, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
matrix = TDUtils.multiply(matrix, TDUtils.xRotation(config.getItem("xRotate", 0)));
matrix = TDUtils.multiply(matrix, TDUtils.yRotation(config.getItem("yRotate", 0)));
matrix = TDUtils.multiply(matrix, TDUtils.zRotation(config.getItem("zRotate", 0)));
let rotate = gl.getUniformLocation(program, "u_matrix");
gl.uniformMatrix4fv(rotate, false, matrix);
}
setup() {
audioHandler.fftSize(16384)
}
prepare(program) {
this.position = gl.getAttribLocation(program, "a_position");
this.color = gl.getUniformLocation(program, "u_color");
}
}
//animate Water the way like the Audio is Coming... 256FFT-Size max!
class Water extends Visual {
draw() {
}
setup() {
audioHandler.fftSize(256)
}
}
async function initHandler() {
let body = $('body');
$('.playlist.menu-icon').addEventListener('click', e => {
player.playlist.renderPagination(player.playlist.page);
});
body.addDelegatedEventListener('click', '.playlist-item', (e, el) => {
let number = el.dataset.index;
player.playByID(parseInt(number));
togglePlayButton('pause');
});
body.addDelegatedEventListener('click', '.controls button', (e, el) => {
switch (el.id) {
case 'previous':
player.prevSong();
break;
case 'next':
player.nextSong()
break;
case 'play':
player.playStop();
break;
}
togglePlayButton(audioHandler.audioFile.paused ? 'play' : 'pause');
});
}
function togglePlayButton(status) {
let icons = $$('#play .icon');
icons.forEach(el => {
if(el.dataset.name === status) {
el.removeClass('hide');
} else {
el.addClass('hide');
}
})
}
const shaderHandler = new ShaderHandler(null),
audioHandler = new AudioHandler(),
gui = new GUI(),
player = new Player();
visual = new VisualDrawer(),
template = new Template(),
player = new Player(),
config = new Config();
let c = null,
gl = null;
async function startUP() {
let c = document.body.querySelector('#c'),
c = document.body.querySelector('#c'),
gl = c.getContext("webgl2");
if (!gl) {
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
return false;
}
shaderHandler.setGL(gl)
await shaderHandler.loadArray(["wave", "sphere"], 'shaders/');
await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/');
await audioHandler.init();
await player.init();
gui.init();
await visual.init();
await gui.init();
await initHandler();
}
startUP().then(r => {

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
*{box-sizing:border-box}:focus{outline:0}body,html{padding:0;margin:0;overflow:hidden;font-size:16px;font-family:sans-serif;background-color:#000}div{position:fixed;color:#fff;padding:1em}.hide{display:none!important}.icon{width:1em;height:1em;vertical-align:middle;font-size:1em;shape-rendering:geometricPrecision;transition:transform .5s cubic-bezier(.22,.61,.36,1);stroke-width:5px;text-align:center;display:block}#c{width:100%;height:100%}group{display:block;padding-bottom:10px;user-select:none}group-label{display:block;border-bottom:1px solid #fff;font-size:21px;font-weight:500;user-select:none}group-input{display:flex;align-items:center;margin-top:5px;user-select:none}group-input label{padding-right:10px;user-select:none;width:150px}group-input input{flex-grow:1;user-select:none;max-width:150px}group-input button{border:1px solid #dcdcdc;background-color:transparent;color:#fff;margin-left:5px}.closed{transform:translateX(-350px);transition:all .5s}.top-menu-left{display:flex}.top-menu-left div{position:relative}.loading-screen{z-index:100;background-color:#000;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column}.loading-screen span{font-family:monospace;font-size:4vw;z-index:2}.loading-screen loader{position:absolute;top:calc(6vw + 10px);left:0;right:0;bottom:0;margin:auto;display:block;width:30vw;height:6px;transform:scaleX(0);transform-origin:left;background-color:#006ea8;animation:loadingBar 2s infinite}.loading-screen loader.delay{background-color:rgba(0,110,168,.24);filter:blur(1px);animation-delay:.05s}@keyframes loadingBar{0%,100%{transform:scaleX(0) scaleY(0)}50%{transform:scaleX(1) scaleY(1);transform-origin:left}100%,51%{transform-origin:right}}input[type=range]{width:100%}input[type=range]:focus{outline:0}input[type=range]:focus::-webkit-slider-runnable-track{background:#545454}input[type=range]:focus::-ms-fill-lower{background:#424242}input[type=range]:focus::-ms-fill-upper{background:#545454}input[type=range]::-webkit-slider-runnable-track{width:100%;height:25.6px;cursor:pointer;box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;background:#424242;border-radius:0;border:0 solid #010101}input[type=range]::-webkit-slider-thumb{height:25px;width:15px;border-radius:0;cursor:pointer;margin-top:.3px}switch input{position:absolute;appearance:none;opacity:0}switch input:checked+label:after{transform:translateX(20px)}switch label{display:block;border-radius:10px;width:40px;height:20px;background-color:#dcdcdc;position:relative;cursor:pointer;padding:0}switch label:after{content:'';background-color:#ff3232;position:absolute;top:2px;left:2px;height:16px;width:16px;border-radius:10px;transition:.5s}input[type=file]{position:fixed;left:-100000vw;height:1px;width:1px}.controls{right:0;display:flex}.controls button,.menu-icon{background-color:rgba(33,33,33,.6);border:none;font-size:1.4em;border-top:4px solid #089bec;padding:1.5rem;cursor:pointer;color:#fff;transition:.5s}.controls button.active,.menu-icon.active{border-color:#ff066a}.controls button:hover,.menu-icon:hover{background-color:rgba(21,21,21,.7);border-color:#aaef22}
*{box-sizing:border-box}:focus{outline:0}body,html{padding:0;margin:0;overflow:hidden;font-size:16px;font-family:sans-serif;background-color:#000}div{position:fixed;color:#fff;padding:1em}.hide{display:none!important}.icon{width:1em;height:1em;vertical-align:middle;font-size:1em;shape-rendering:geometricPrecision;transition:transform .5s cubic-bezier(.22,.61,.36,1);stroke-width:5px;text-align:center;display:block}#c{width:100%;height:100%}group{display:block;padding-bottom:10px;user-select:none}group-label{display:block;border-bottom:1px solid #fff;font-size:21px;font-weight:500;user-select:none}group-input{display:flex;align-items:center;margin-top:5px;user-select:none}group-input label{padding-right:10px;user-select:none;width:150px}group-input input{flex-grow:1;user-select:none;max-width:150px}group-input button{border:1px solid #dcdcdc;background-color:transparent;color:#fff;margin-left:5px}.closed{transform:translateX(-350px);transition:all .5s}.top-menu-left{display:flex}.top-menu-left div{position:relative}.loading-screen{z-index:100;background-color:#000;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column}.loading-screen span{font-family:monospace;font-size:4vw;z-index:2}.loading-screen loader{position:absolute;top:calc(6vw + 10px);left:0;right:0;bottom:0;margin:auto;display:block;width:30vw;height:6px;transform:scaleX(0);transform-origin:left;background-color:#006ea8;animation:loadingBar 2s infinite}.loading-screen loader.delay{background-color:rgba(0,110,168,.24);filter:blur(1px);animation-delay:.05s}@keyframes loadingBar{0%,100%{transform:scaleX(0) scaleY(0)}50%{transform:scaleX(1) scaleY(1);transform-origin:left}100%,51%{transform-origin:right}}.grey-screen{position:fixed;top:0;left:0;background-color:rgba(0,0,0,.5);width:100vw;height:100vh;display:flex;justify-content:center;align-items:center}.grey-screen.hide{display:none!important}#modal{max-width:860px;width:90%;min-height:200px;background-color:#333;padding:unset;box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}#modal header{height:50px;font-size:30px;line-height:50px;padding-left:10px;overflow:hidden;background-color:#212121;display:flex}#modal header .headline{flex-grow:1}#modal header .close{margin-right:10px;font-size:24px;cursor:pointer}#modal header .close:hover{color:#ff3232}#modal modal-content{display:block;max-height:calc(100vh - 200px);overflow:auto}#modal modal-footer{display:block}input[type=range]{width:100%}input[type=range]:focus{outline:0}input[type=range]:focus::-webkit-slider-runnable-track{background:#545454}input[type=range]:focus::-ms-fill-lower{background:#424242}input[type=range]:focus::-ms-fill-upper{background:#545454}input[type=range]::-webkit-slider-runnable-track{width:100%;height:25.6px;cursor:pointer;box-shadow:1px 1px 1px #000,0 0 1px #0d0d0d;background:#424242;border-radius:0;border:0 solid #010101}input[type=range]::-webkit-slider-thumb{height:25px;width:15px;border-radius:0;cursor:pointer;margin-top:.3px}switch input{position:absolute;appearance:none;opacity:0}switch input:checked+label:after{transform:translateX(20px)}switch label{display:block;border-radius:10px;width:40px;height:20px;background-color:#dcdcdc;position:relative;cursor:pointer;padding:0}switch label:after{content:'';background-color:#ff3232;position:absolute;top:2px;left:2px;height:16px;width:16px;border-radius:10px;transition:.5s}input[type=file]{position:fixed;left:-100000vw;height:1px;width:1px}.controls{right:0;display:flex}.controls button,.menu-icon{background-color:rgba(33,33,33,.6);border:none;font-size:1.4em;border-top:4px solid #089bec;padding:1.5rem;cursor:pointer;color:#fff;transition:.5s}.controls button.active,.menu-icon.active{border-color:#ff066a}.controls button:hover,.menu-icon:hover{background-color:rgba(21,21,21,.7);border-color:#aaef22}playlist{display:flex;flex-direction:column}playlist div{padding:unset;position:unset}playlist .pagination{display:flex;justify-content:flex-end;font-size:1.5em;padding:5px}playlist .pagination .item{cursor:pointer;user-select:none;border-radius:5px;margin:0 3px}playlist .pagination .item.inactive{color:#aaa;pointer-events:none;cursor:not-allowed}playlist .pagination .item:hover{color:#006ea8}playlist .playlist-item{display:flex;padding:5px;border-bottom:1px solid #dcdcdc;cursor:pointer}playlist .playlist-item-title{margin-left:10px;padding:5px;display:flex;align-items:center}playlist .playlist-item-number{padding:5px 10px 5px 5px;border-right:1px solid #ff3232;width:50px}playlist .playlist-item:hover{background-color:rgba(0,0,0,.4)}

View File

@ -0,0 +1,17 @@
<playlist>
<div class="pagination">
<div class="item prev-site $prevActive$">
<svg role="img" class="icon">
<use href="out/icon-sprite.svg#fal-fa-caret-left"></use>
</svg>
</div>
<div class="item current inactive">
$page$
</div>
<div class="item next-site $nextActive$">
<svg role="img" class="icon">
<use href="out/icon-sprite.svg#fal-fa-caret-right"></use>
</svg>
</div>
</div>
</playlist>

View File

@ -0,0 +1,4 @@
<playlist-item class="playlist-item" data-index="$index$">
<div class="playlist-item-number">$nr$</div>
<div class="playlist-item-title">$title$</div>
</playlist-item>

5
out/tpl/playlist.tpl Normal file
View File

@ -0,0 +1,5 @@
<playlist>
<div class="playlist-content">
$content$
</div>
</playlist>

1
raw/gui/wave.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -1,20 +1,28 @@
const shaderHandler = new ShaderHandler(null),
audioHandler = new AudioHandler(),
gui = new GUI(),
player = new Player();
visual = new VisualDrawer(),
template = new Template(),
player = new Player(),
config = new Config();
let c = null,
gl = null;
async function startUP() {
let c = document.body.querySelector('#c'),
c = document.body.querySelector('#c'),
gl = c.getContext("webgl2");
if (!gl) {
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
return false;
}
shaderHandler.setGL(gl)
await shaderHandler.loadArray(["wave", "sphere"], 'shaders/');
await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/');
await audioHandler.init();
await player.init();
gui.init();
await visual.init();
await gui.init();
await initHandler();
}
startUP().then(r => {

View File

@ -1,5 +1,71 @@
const AudioContext = window.AudioContext || window.webkitAudioContext;
class AudioHandler {
async init() {
this.audioFile = new Audio();
let self = this;
self.isStarted = false;
self.audioFile = new Audio();
self.actx = new AudioContext()
self.analyser = self.actx.createAnalyser()
self.analyser.fftSize = 4096;
self.lastSong = null;
await self.connectAll();
}
async connectAll() {
let self = this;
self.source = self.actx.createMediaElementSource(self.audioFile);
self.source.connect(self.analyser);
self.analyser.connect(self.actx.destination);
self.audioFile.addEventListener('ended', player.nextSong.bind(player));
}
async start() {
if (this.audioFile.src === '') {
return;
}
if (!this.isStarted) {
this.isStarted = true;
await this.actx.resume();
}
}
async stop() {
if (this.isStarted) {
this.isStarted = false;
await this.actx.suspend();
}
}
fftSize(size) {
this.analyser.fftSize = size;
}
smoothing(float) {
this.analyser.smoothingTimeConstant = float;
}
loadSong(src) {
let self = this;
if (self.lastSong) {
URL.revokeObjectURL(self.lastSong);
}
self.lastSong = this.audioFile.src = URL.createObjectURL(src);
if (!this.isStarted) {
this.start();
}
this.audioFile.play();
}
getIntArray(steps) {
let dataArray = new Uint8Array(steps);
this.analyser.getByteFrequencyData(dataArray);
return dataArray;
}
getFloatArray() {
let dataArray = new Float32Array(this.analyser.frequencyBinCount);
this.analyser.getFloatTimeDomainData(dataArray);
return dataArray;
}
}

35
raw/javascript/config.js Normal file
View File

@ -0,0 +1,35 @@
class Config {
constructor() {
this.config = {};
this.name = ''
}
loadConfigByName(name) {
this.saveConfig();
this.name = 'config-' + name;
this.config = JSON.parse(this.name);
}
saveConfig() {
if (this.name !== '') {
localStorage.setItem(this.name, JSON.stringify(this.config));
}
}
addItem(name, value) {
this.config[name] = value;
}
removeItem(name) {
delete this.config[name];
}
getItem(name, def) {
let value = this.config[name];
if (value === undefined || value === null) {
this.config[name] = def;
value = def;
}
return value;
}
}

View File

@ -0,0 +1,39 @@
async function initHandler() {
let body = $('body');
$('.playlist.menu-icon').addEventListener('click', e => {
player.playlist.renderPagination(player.playlist.page);
});
body.addDelegatedEventListener('click', '.playlist-item', (e, el) => {
let number = el.dataset.index;
player.playByID(parseInt(number));
togglePlayButton('pause');
});
body.addDelegatedEventListener('click', '.controls button', (e, el) => {
switch (el.id) {
case 'previous':
player.prevSong();
break;
case 'next':
player.nextSong()
break;
case 'play':
player.playStop();
break;
}
togglePlayButton(audioHandler.audioFile.paused ? 'play' : 'pause');
});
}
function togglePlayButton(status) {
let icons = $$('#play .icon');
icons.forEach(el => {
if(el.dataset.name === status) {
el.removeClass('hide');
} else {
el.addClass('hide');
}
})
}

View File

@ -1,5 +1,98 @@
class GUI {
init() {
async init() {
this.data = {};
this.modal = new Modal();
// load first vis window!
await this.loadForVis();
await template.loadArray([
'playlist-item',
'playlist',
'playlist-footer'
]);
this.initDropZone();
}
async loadForVis() {
let c = visual.c,
d = this.data[c];
if (d == null) {
this.data[c] = await fetch("out/gui/" + c + ".json").then((r) => r.json());
}
}
renderModal(content, title) {
let modal = $('#modal'),
p = modal.parentNode,
h = $('header .headline', modal),
con = $('modal-content', modal);
h.innerHTML = title;
con.innerHTML = content;
p.classList.remove('hide');
}
initDropZone() {
let items = 'drag dragstart dragend dragover dragenter dragleave drop'.split(' ');
items.forEach(el => {
window.addEventListener(el, async e => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'drop') {
if (e.dataTransfer.files.length > 0) {
player.playlist.changeFiles(e, e.dataTransfer);
} else {
alert("Sorry you need to upload files!");
}
}
});
});
};
}
class Modal {
constructor() {
let self = this;
self.currentModal = '';
self.modal = $('#modal');
self.parent = self.modal.parentNode;
self.modal.addDelegatedEventListener('click', 'header .close', this.closeModal.bind(this));
}
resetModal() {
this.renderModal('', '', '');
}
renderModal(title, content, footer) {
this.currentModal = title;
this.renderHeader(title);
this.renderContent(content);
this.renderFooter(footer);
this.showModal();
}
renderHeader(header) {
let h = $('header .headline', this.modal);
h.innerHTML = header;
}
renderContent(content) {
let con = $('modal-content', this.modal);
con.innerHTML = content;
}
renderFooter(footer) {
let con = $('modal-footer', this.modal);
con.innerHTML = footer;
}
closeModal() {
this.parent.addClass("hide")
}
isCurrent(title) {
return title === this.currentModal;
}
showModal() {
this.parent.removeClass("hide")
}
}

View File

@ -85,6 +85,12 @@ class ShaderHandler {
return this.programs[name];
}
use(name) {
let pro = this.programs[name];
this.gl.useProgram(pro);
return pro;
}
async loadArray(list, path) {
let self = this;
for (const e of list) {

View File

@ -1,5 +1,182 @@
class Player {
async init() {
this.playlist = new Playlist();
}
nextSong() {
let next = this.playlist.getNext();
audioHandler.loadSong(next.file);
}
prevSong() {
let next = this.playlist.getPrevious();
audioHandler.loadSong(next.file);
}
playStop() {
if (!audioHandler.lastSong) {
let next = this.playlist.getCurrent();
audioHandler.loadSong(next.file);
}
let audioFile = audioHandler.audioFile;
if (audioFile.paused) {
audioFile.play();
} else {
audioFile.pause();
}
}
playByID(number) {
let next = this.playlist.getFile(number);
audioHandler.loadSong(next.file);
}
}
const PAGINATIONLIMIT = 50;
class Playlist {
constructor() {
this.list = [];
this.shuffled = [];
this.index = 0;
this.page = 0;
this.isShuffle = false;
$('body').addDelegatedEventListener('change', 'input[type="file"]', this.changeFiles.bind(this));
$('body').addDelegatedEventListener('click', '.pagination .item', this.handlePagination.bind(this));
}
shuffle() {
// only shuffle if more then 2 elements are in
let len = this.list.length;
if (len < 3) {
this.shuffled = this.list;
}
// the current-list need to be shuffled...
for (let i = 0; i < len; i++) {
let random = VTUtils.randomInt(0, len - 1);
this.swap(i, random);
}
}
swap(a, b) {
this.shuffled[a] = this.list[b];
this.shuffled[b] = this.list[a];
}
getNext() {
let items = this.isShuffle ? this.shuffled : this.list,
len = items.length - 1,
next = this.index + 1;
if (next > len) {
next = 0;
}
this.index = next;
return items[next];
}
getPrevious() {
let items = this.isShuffle ? this.shuffled : this.list,
len = items.length - 1,
next = this.index - 1;
if (next < 0) {
next = len;
}
this.index = next;
return items[next];
}
getCurrent() {
let items = this.isShuffle ? this.shuffled : this.list;
return items[this.index];
}
getFile(index) {
let items = this.isShuffle ? this.shuffled : this.list;
return items[index];
}
// on new upload... this has to be an array!
setPlaylist(files) {
this.index = 0;
this.list = files;
this.shuffle();
}
handlePagination(event, el) {
if (el.hasClass('inactive')) {
return;
}
if (el.hasClass('next-site')) {
this.renderPagination(this.page + 1);
} else {
this.renderPagination(this.page - 1);
}
}
renderPagination(page) {
let length = this.list.length,
maxSite = Math.ceil(length / PAGINATIONLIMIT) - 1;
if (page < 0) {
page = 0;
}
if (page > maxSite) {
page = maxSite;
}
let s = page * PAGINATIONLIMIT,
e = s + PAGINATIONLIMIT,
data = "";
this.page = page;
if (e >= length) {
e = length;
}
if (length > 0) {
let items = this.isShuffle ? this.shuffled : this.list;
for (let i = s; i < e; i++) {
let obj = {
index: i,
nr: i + 1,
title: items[i].name
}
data += template.parseTemplate("playlist-item", obj);
}
} else {
data = "<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!");
}
}
}

View File

@ -0,0 +1,44 @@
class Template {
constructor() {
this.tpl = {};
}
async loadTemplate(name) {
let self = this;
if (!this.tpl[name]) {
await fetch(templateDir + name + ".tpl").then((r) => r.text()).then(c => {
self.tpl[name] = c;
})
}
}
async loadArray(names) {
for (let name of names) {
await this.loadTemplate(name);
}
}
parseTemplate(name, data) {
if (!this.tpl[name]) {
return ""
}
let m, d = this.tpl[name];
while ((m = templateEx.exec(d)) !== null) {
if (m.index === templateEx.lastIndex) {
templateEx.lastIndex++;
}
let key = m[0];
d = d.replace(key, data[m[1]] || "")
}
return d;
}
parseFromAPI(url, name, cb) {
fetch(url).then((r) => r.json()).then(d => {
cb(this.parseTemplate(name, d))
}).catch(console.error)
}
}
const templateEx = /\$(.*?)\$/gm;
const templateDir = "out/tpl/"

View File

@ -260,6 +260,16 @@ Node.prototype.addDelegatedEventListener = function (type, aim, cb) {
})
};
Node.prototype.hasClass = function (className) {
return this.classList.contains(className);
}
Node.prototype.addClass = function (className) {
return this.classList.add(className);
}
Node.prototype.removeClass = function (className) {
return this.classList.remove(className);
}
function create(name, content) {
let d = document.createElement(name);
if (content) {

View File

@ -3,6 +3,11 @@ class Visual {
this.data = []; //for drawing
this.dataArray = [];
}
updateData() {
}
draw() {
}
@ -11,5 +16,33 @@ class Visual {
}
class VisualDrawer {
constructor() {
this.visuals = {
"sphere": new Sphere(),
"wave": new Wave(),
"water": new Water()
}
this.c = "wave";
}
init() {
this.visuals[this.c].setup();
this.updateLoop();
}
switch(visual) {
if (this.visuals[visual] != null) {
this.c = visual;
this.visuals[this.c].setup();
}
}
updateLoop() {
let self = this;
let pro = shaderHandler.use(self.c);
let vis = self.visuals[self.c];
vis.updateData();
vis.draw(pro);
requestAnimationFrame(self.updateLoop.bind(self))
}
}

View File

@ -0,0 +1,10 @@
//animate Water the way like the Audio is Coming... 256FFT-Size max!
class Water extends Visual {
draw() {
}
setup() {
audioHandler.fftSize(256)
}
}

View File

@ -0,0 +1,60 @@
// 3D Audio-Waves -> maybe also 2D?
class Wave extends Visual {
updateData() {
this.data = [];
let data = audioHandler.getFloatArray();
let add = 2 / data.length,
x = -1;
for (let i = 0; i < data.length; i++) {
this.data.push(x, data[i], data[i]);
x += add;
}
}
draw(program) {
c.width = window.innerWidth;
c.height = window.innerHeight;
this.prepare(program);
let position = this.position,
color = this.color,
positionBuffer = gl.createBuffer();
this.rotate(program);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.data), gl.DYNAMIC_DRAW);
let vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 3, gl.FLOAT, true, 0, 0);
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clearDepth(2.0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.drawArrays(gl.LINE_STRIP || gl.POINTS, 0, this.data.length / 3);
}
rotate(program) {
let aspect = c.height / c.width,
matrix = [
1 / aspect, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
matrix = TDUtils.multiply(matrix, TDUtils.xRotation(config.getItem("xRotate", 0)));
matrix = TDUtils.multiply(matrix, TDUtils.yRotation(config.getItem("yRotate", 0)));
matrix = TDUtils.multiply(matrix, TDUtils.zRotation(config.getItem("zRotate", 0)));
let rotate = gl.getUniformLocation(program, "u_matrix");
gl.uniformMatrix4fv(rotate, false, matrix);
}
setup() {
audioHandler.fftSize(16384)
}
prepare(program) {
this.position = gl.getAttribLocation(program, "a_position");
this.color = gl.getUniformLocation(program, "u_color");
}
}

View File

@ -50,6 +50,7 @@ group-input {
.top-menu-left {
display: flex;
div {
position: relative;
}
@ -105,4 +106,63 @@ group-input {
51%, 100% {
transform-origin: right;
}
}
.grey-screen {
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, .5);
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
&.hide {
display: none !important;
}
}
#modal {
max-width: 860px;
width: 90%;
min-height: 200px;
background-color: #333;
padding: unset;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
header {
height: 50px;
font-size: 30px;
line-height: 50px;
padding-left: 10px;
overflow: hidden;
background-color: #212121;
display: flex;
.headline {
flex-grow: 1;
}
.close {
margin-right: 10px;
font-size: 24px;
cursor: pointer;
&:hover {
color: #ff3232;
}
}
}
modal-content {
display: block;
max-height: calc(100vh - 200px);
overflow: auto;
}
modal-footer {
display: block;
}
}

57
raw/scss/_playlist.scss Normal file
View File

@ -0,0 +1,57 @@
playlist {
display: flex;
flex-direction: column;
div {
padding: unset;
position: unset;
}
.pagination {
display: flex;
justify-content: flex-end;
font-size: 1.5em;
padding: 5px;
.item {
cursor: pointer;
user-select: none;
border-radius: 5px;
margin: 0 3px;
&.inactive {
color: #aaa;
pointer-events: none;
cursor: not-allowed;
}
&:hover {
color: #006ea8;
}
}
}
.playlist-item {
display: flex;
padding: 5px;
border-bottom: 1px solid #dcdcdc;
cursor: pointer;
&-title {
margin-left: 10px;
padding: 5px;
display: flex;
align-items: center;
}
&-number {
padding: 5px 10px 5px 5px;
border-right: 1px solid #ff3232;
width: 50px;
}
&:hover {
background-color: rgba(0, 0, 0, .4);
}
}
}

View File

@ -40,4 +40,5 @@ div {
@import "gui";
@import "input";
@import "controls";
@import "controls";
@import "playlist";

13
shaders/water.frag Normal file
View File

@ -0,0 +1,13 @@
#version 300 es
// fragment shaders don't have a default precision so we need
// to pick one. mediump is a good default. It means "medium precision"
precision mediump float;
uniform vec4 u_color;
out vec4 outColor;
void main() {
outColor = vec4(255,255,255,0);
}

11
shaders/water.vert Normal file
View File

@ -0,0 +1,11 @@
#version 300 es
in vec2 a_position;
void main() {
// convert the position from pixels to 0.0 to 1.0
vec2 scale = a_position / vec2(255.0, 255.0);
vec2 remap = scale * 2.0;
vec2 space = remap - 1.0;
space.y = space.y * 0.85;
gl_Position = vec4(space, 0,1);
}

View File

@ -4,10 +4,15 @@
// to pick one. mediump is a good default. It means "medium precision"
precision mediump float;
in vec3 pos;
uniform vec4 u_color;
out vec4 outColor;
void main() {
outColor = u_color;
vec3 color = pos.xyz;
color.z = color.z + 1.0;
color.z = color.z / 2.0;
color.z = color.z * 255.0;
outColor = vec4(color, 1.0);
}

View File

@ -1,11 +1,14 @@
#version 300 es
in vec2 a_position;
in vec3 a_position;
uniform mat4 u_matrix;
out vec3 pos;
void main() {
// convert the position from pixels to 0.0 to 1.0
vec2 scale = a_position / vec2(255.0, 255.0);
vec2 remap = scale * 2.0;
vec2 space = remap - 1.0;
space.y = space.y * 0.85;
gl_Position = vec4(space, 0,1);
vec4 scale = vec4(a_position, 1) * u_matrix;
scale.y = scale.y * 0.85;
gl_Position = scale;
pos = a_position;
}