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 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'

View file

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

View file

@ -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

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) { 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 => {

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), 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 => {

View file

@ -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
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 { 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]; 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) {

View file

@ -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!");
}
} }
} }

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) { function create(name, content) {
let d = document.createElement(name); let d = document.createElement(name);
if (content) { if (content) {

View file

@ -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))
}
} }

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 { .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
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

@ -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
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" // 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);
} }

View file

@ -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);
} }