WIP
This commit is contained in:
parent
d1ae2059f7
commit
300b6c4106
30 changed files with 1399 additions and 29 deletions
|
@ -7,19 +7,22 @@ const basePath = __dirname + '/../../raw/javascript/';
|
||||||
const visualPath = basePath + 'visuals/';
|
const visualPath = basePath + 'visuals/';
|
||||||
const visuals = [
|
const visuals = [
|
||||||
visualPath + 'sphere.js',
|
visualPath + 'sphere.js',
|
||||||
//visualPath + 'wave.js',
|
visualPath + 'wave.js',
|
||||||
//visualPath + 'water.js',
|
visualPath + 'water.js',
|
||||||
//visualPath + 'experimental.js',
|
//visualPath + 'experimental.js',
|
||||||
]
|
]
|
||||||
const config = {
|
const config = {
|
||||||
src: [
|
src: [
|
||||||
basePath + 'utils.js',
|
basePath + 'utils.js',
|
||||||
|
basePath + 'template.js',
|
||||||
basePath + 'handler.js',
|
basePath + 'handler.js',
|
||||||
basePath + 'audio.js',
|
basePath + 'audio.js',
|
||||||
basePath + 'player.js',
|
basePath + 'player.js',
|
||||||
basePath + 'gui.js',
|
basePath + 'gui.js',
|
||||||
basePath + 'visual.js',
|
basePath + 'visual.js',
|
||||||
|
basePath + 'config.js',
|
||||||
...visuals,
|
...visuals,
|
||||||
|
basePath + 'eventHandler.js',
|
||||||
basePath + 'app.js'
|
basePath + 'app.js'
|
||||||
],
|
],
|
||||||
dest: __dirname + '/../../out/js'
|
dest: __dirname + '/../../out/js'
|
||||||
|
|
|
@ -42,6 +42,7 @@ function buildIconSprites() {
|
||||||
fal.faCogs,
|
fal.faCogs,
|
||||||
fal.faFolderUpload,
|
fal.faFolderUpload,
|
||||||
fal.faListMusic,
|
fal.faListMusic,
|
||||||
|
fal.faFileAudio,
|
||||||
],
|
],
|
||||||
vt: []
|
vt: []
|
||||||
};
|
};
|
||||||
|
|
35
index.html
35
index.html
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>WEBGL Test</title>
|
<title>VS3D-Vis</title>
|
||||||
<link rel="stylesheet" href="out/theme/style.css">
|
<link rel="stylesheet" href="out/theme/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -19,11 +19,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="upload menu-icon">
|
<div class="upload menu-icon">
|
||||||
<label for="upload">
|
<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">
|
<svg role="img" class="icon">
|
||||||
<use href="out/icon-sprite.svg#fal-fa-folder-upload"></use>
|
<use href="out/icon-sprite.svg#fal-fa-folder-upload"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="file" accept="audio/*" id="upload">
|
<input type="file" multiple directory webkitdirectory accept="audio/*" id="upload-dir">
|
||||||
</div>
|
</div>
|
||||||
<div class="playlist menu-icon">
|
<div class="playlist menu-icon">
|
||||||
<svg role="img" class="icon">
|
<svg role="img" class="icon">
|
||||||
|
@ -38,10 +46,10 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="play">
|
<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>
|
<use href="out/icon-sprite.svg#fal-fa-pause"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<svg role="img" class="icon">
|
<svg role="img" data-name="play" class="icon">
|
||||||
<use href="out/icon-sprite.svg#fal-fa-play"></use>
|
<use href="out/icon-sprite.svg#fal-fa-play"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
@ -52,6 +60,25 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="c"></canvas>
|
<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>
|
<script src="out/js/scripts.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 |
|
@ -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) {
|
function create(name, content) {
|
||||||
let d = document.createElement(name);
|
let d = document.createElement(name);
|
||||||
if (content) {
|
if (content) {
|
||||||
|
@ -273,6 +283,50 @@ function append(to, array) {
|
||||||
to.appendChild(array[item]);
|
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 {
|
class ShaderHandler {
|
||||||
constructor(gl) {
|
constructor(gl) {
|
||||||
this.gl = gl;
|
this.gl = gl;
|
||||||
|
@ -360,6 +414,12 @@ class ShaderHandler {
|
||||||
return this.programs[name];
|
return this.programs[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use(name) {
|
||||||
|
let pro = this.programs[name];
|
||||||
|
this.gl.useProgram(pro);
|
||||||
|
return pro;
|
||||||
|
}
|
||||||
|
|
||||||
async loadArray(list, path) {
|
async loadArray(list, path) {
|
||||||
let self = this;
|
let self = this;
|
||||||
for (const e of list) {
|
for (const e of list) {
|
||||||
|
@ -368,19 +428,355 @@ class ShaderHandler {
|
||||||
await self.createProgramForEach(list)
|
await self.createProgramForEach(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
|
|
||||||
class AudioHandler {
|
class AudioHandler {
|
||||||
async init() {
|
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 {
|
class Player {
|
||||||
async init() {
|
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 {
|
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 {
|
class Visual {
|
||||||
|
@ -388,6 +784,11 @@ class Visual {
|
||||||
this.data = []; //for drawing
|
this.data = []; //for drawing
|
||||||
this.dataArray = [];
|
this.dataArray = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateData() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +797,70 @@ class Visual {
|
||||||
}
|
}
|
||||||
|
|
||||||
class VisualDrawer {
|
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 {
|
class Sphere extends Visual {
|
||||||
draw() {
|
draw() {
|
||||||
|
@ -405,23 +869,140 @@ class Sphere extends Visual {
|
||||||
setup() {
|
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),
|
const shaderHandler = new ShaderHandler(null),
|
||||||
audioHandler = new AudioHandler(),
|
audioHandler = new AudioHandler(),
|
||||||
gui = new GUI(),
|
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() {
|
async function startUP() {
|
||||||
let c = document.body.querySelector('#c'),
|
c = document.body.querySelector('#c'),
|
||||||
gl = c.getContext("webgl2");
|
gl = c.getContext("webgl2");
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
|
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
shaderHandler.setGL(gl)
|
shaderHandler.setGL(gl)
|
||||||
await shaderHandler.loadArray(["wave", "sphere"], 'shaders/');
|
await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/');
|
||||||
await audioHandler.init();
|
await audioHandler.init();
|
||||||
await player.init();
|
await player.init();
|
||||||
gui.init();
|
await visual.init();
|
||||||
|
await gui.init();
|
||||||
|
await initHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
startUP().then(r => {
|
startUP().then(r => {
|
||||||
|
|
2
out/js/scripts.min.js
vendored
2
out/js/scripts.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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)}
|
17
out/tpl/playlist-footer.tpl
Normal file
17
out/tpl/playlist-footer.tpl
Normal 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>
|
4
out/tpl/playlist-item.tpl
Normal file
4
out/tpl/playlist-item.tpl
Normal 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
5
out/tpl/playlist.tpl
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<playlist>
|
||||||
|
<div class="playlist-content">
|
||||||
|
$content$
|
||||||
|
</div>
|
||||||
|
</playlist>
|
1
raw/gui/wave.json
Normal file
1
raw/gui/wave.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,20 +1,28 @@
|
||||||
const shaderHandler = new ShaderHandler(null),
|
const shaderHandler = new ShaderHandler(null),
|
||||||
audioHandler = new AudioHandler(),
|
audioHandler = new AudioHandler(),
|
||||||
gui = new GUI(),
|
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() {
|
async function startUP() {
|
||||||
let c = document.body.querySelector('#c'),
|
c = document.body.querySelector('#c'),
|
||||||
gl = c.getContext("webgl2");
|
gl = c.getContext("webgl2");
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
|
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
shaderHandler.setGL(gl)
|
shaderHandler.setGL(gl)
|
||||||
await shaderHandler.loadArray(["wave", "sphere"], 'shaders/');
|
await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/');
|
||||||
await audioHandler.init();
|
await audioHandler.init();
|
||||||
await player.init();
|
await player.init();
|
||||||
gui.init();
|
await visual.init();
|
||||||
|
await gui.init();
|
||||||
|
await initHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
startUP().then(r => {
|
startUP().then(r => {
|
||||||
|
|
|
@ -1,5 +1,71 @@
|
||||||
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
|
|
||||||
class AudioHandler {
|
class AudioHandler {
|
||||||
async init() {
|
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
35
raw/javascript/config.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
39
raw/javascript/eventHandler.js
Normal file
39
raw/javascript/eventHandler.js
Normal 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');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,5 +1,98 @@
|
||||||
class GUI {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -85,6 +85,12 @@ class ShaderHandler {
|
||||||
return this.programs[name];
|
return this.programs[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use(name) {
|
||||||
|
let pro = this.programs[name];
|
||||||
|
this.gl.useProgram(pro);
|
||||||
|
return pro;
|
||||||
|
}
|
||||||
|
|
||||||
async loadArray(list, path) {
|
async loadArray(list, path) {
|
||||||
let self = this;
|
let self = this;
|
||||||
for (const e of list) {
|
for (const e of list) {
|
||||||
|
|
|
@ -1,5 +1,182 @@
|
||||||
class Player {
|
class Player {
|
||||||
async init() {
|
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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
44
raw/javascript/template.js
Normal file
44
raw/javascript/template.js
Normal 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/"
|
|
@ -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) {
|
function create(name, content) {
|
||||||
let d = document.createElement(name);
|
let d = document.createElement(name);
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|
|
@ -3,6 +3,11 @@ class Visual {
|
||||||
this.data = []; //for drawing
|
this.data = []; //for drawing
|
||||||
this.dataArray = [];
|
this.dataArray = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateData() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,5 +16,33 @@ class Visual {
|
||||||
}
|
}
|
||||||
|
|
||||||
class VisualDrawer {
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
10
raw/javascript/visuals/water.js
Normal file
10
raw/javascript/visuals/water.js
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
raw/javascript/visuals/wave.js
Normal file
60
raw/javascript/visuals/wave.js
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ group-input {
|
||||||
|
|
||||||
.top-menu-left {
|
.top-menu-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -106,3 +107,62 @@ group-input {
|
||||||
transform-origin: right;
|
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
57
raw/scss/_playlist.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,3 +41,4 @@ div {
|
||||||
@import "gui";
|
@import "gui";
|
||||||
@import "input";
|
@import "input";
|
||||||
@import "controls";
|
@import "controls";
|
||||||
|
@import "playlist";
|
13
shaders/water.frag
Normal file
13
shaders/water.frag
Normal 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
11
shaders/water.vert
Normal 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);
|
||||||
|
}
|
|
@ -4,10 +4,15 @@
|
||||||
// to pick one. mediump is a good default. It means "medium precision"
|
// to pick one. mediump is a good default. It means "medium precision"
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec3 pos;
|
||||||
uniform vec4 u_color;
|
uniform vec4 u_color;
|
||||||
|
|
||||||
out vec4 outColor;
|
out vec4 outColor;
|
||||||
|
|
||||||
void main() {
|
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);
|
||||||
}
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
#version 300 es
|
#version 300 es
|
||||||
|
|
||||||
in vec2 a_position;
|
in vec3 a_position;
|
||||||
|
uniform mat4 u_matrix;
|
||||||
|
|
||||||
|
out vec3 pos;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// convert the position from pixels to 0.0 to 1.0
|
// convert the position from pixels to 0.0 to 1.0
|
||||||
vec2 scale = a_position / vec2(255.0, 255.0);
|
vec4 scale = vec4(a_position, 1) * u_matrix;
|
||||||
vec2 remap = scale * 2.0;
|
scale.y = scale.y * 0.85;
|
||||||
vec2 space = remap - 1.0;
|
gl_Position = scale;
|
||||||
space.y = space.y * 0.85;
|
pos = a_position;
|
||||||
gl_Position = vec4(space, 0,1);
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue