CSS
/* ----------------- */
/* Main Page Styling */
/* ----------------- */
@import url("https://fonts.googleapis.com/css?family=Roboto:300,700|Open+Sans:300,700");
body {
font-family: "Open Sans", Helvetica, Verdana, sans-serif !important;
font-weight: 300 !important;
margin: 0;
background-color: #222 !important;
}
#content {
z-index: 2;
position: fixed;
left: 0;
top: 0;
}
#canvas, #canvas-gl {
width: 100%;
height: 100%;
position: fixed;
}
#canvas {
z-index: 3;
}
#canvas-gl {
z-index: 2;
}
.vert-cen {
position: relative;
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
.absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%)
}
/* ----------------- */
/* Background Styles */
/* ----------------- */
#background {
overflow: hidden;
position: absolute;
transition: opacity 0.3s;
}
.bgleft {
margin-left: 50%;
}
.bgright {
margin-left: -50%;
}
#bgimg1, #limg1, #bgimg2, #limg2 {
position: fixed;
display: block;
width: 100vw;
height: 100vh;
object-fit: cover;
}
#bgimg1, #bgimg2 {
filter: blur(40px);
opacity: 0;
transition: opacity 0.3s linear;
transition: filter 1s linear;
}
#bgimg2, #limg2 {
transform: scale(-1, 1);
}
.realbg img {
z-index: 0;
}
.lazyaf img {
z-index: -1;
filter: blur(40px);
}
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
}
a {
color: #3498DB;
}
.dg.ac {
z-index: 999 !important;
}
/* ------------------------- */
/* Temporary Control Styling */
/* ------------------------- */
#controls a {
font-weight: bold;
}
#controls > .header {
font-size: 14pt;
text-align: center;
}
#img {
max-width: 300px;
}
/* ----------- */
/* GUI Styling */
/* ----------- */
#gui-top, #gui-bottom {
z-index: 6;
position: fixed;
width: 100%;
height: 100px;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
}
#gui-bottom {
bottom: 0;
}
/* Chrome Only, Edge has black/purple player though */
audio::-webkit-media-controls-panel {
background: rgba(0, 0, 0, 0);
}
.overlay-container {
position: fixed;
width: 100%;
height: 100%;
z-index: 4;
background: rgba(0, 0, 0, 0.75);
pointer-events: none;
display: none;
}
.overlay-window {
height: calc(100% - 300px);
padding: 0px 50px;
pointer-events: none;
}
#gui-full-holder {
height: calc(100% - 300px);
}
#controls {
line-height: 0.8em;
font-size: 10pt;
}
.overlay-pane {
z-index: 4;
position: fixed;
padding: 15px;
background-color: rgba(34, 34, 34, 0.65);
color: #DBDBDB;
max-width: 100%;
pointer-events: auto;
border-radius: 15px;
}
.page-control {
font-size: 24pt;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 99;
}
#db-prev {
left: -38px;
}
#db-next {
right: -38px;
}
#db-page-info {
font-size: 12pt;
text-align: center;
padding-bottom: 16px;
}
/* ---------- */
/* About pane */
/* ---------- */
#about-pane, #welcome-pane {
text-align: center;
font-family: "Open Sans", Helvetica, Verdana, sans-serif !important;
font-weight: 300;
}
#about-pane {
line-height: 3em;
}
#welcome-pane {
max-width: 400px;
}
#about-header {
font-size: 32pt;
margin-bottom: 24px;
}
#about-content, #welcome-content {
font-size: 16pt;
}
#about-hotkeys {
line-height: 1.3em;
}
#welcome-content {
line-height: 1.5em;
}
/* ------------ */
/* GUI Elements */
/* ------------ */
#audio {
display: none;
}
#song-info {
color: white;
text-transform: uppercase;
/*line-height: 90%;*/
text-align: center;
}
#song-info > div {
margin-top: 12px;
margin-bottom: 12px;
letter-spacing: -1px;
}
#gui-artist {
font-size: 24pt;
margin-bottom: 12px;
font-weight: 700 !important;
}
#gui-title {
font-size: 15pt;
margin-top: 12px;
}
#view-database {
min-width: 100px;
margin-left: 15px;
}
#elm-about {
min-width: 60px;
text-align: center;
margin-right: 10px;
font-size: 12pt;
}
input[type="file"] {
display: none;
}
#upload-button:hover {
color: rgba(0, 0, 0, 0.87) !important;
}
#db-input {
margin-bottom: 8px;
}
.db-edit-input {
color: black;
display: inline;
min-width: 90%;
}
#db-view td {
line-height: 1.5em;
padding: 0px 15px;
}
#db-view td > div {
max-height: 50px;
overflow: hidden;
}
.row-art {
padding: 0 0 !important;
line-height: 0em !important;
}
.row-title, .row-artist {
min-width: 200px;
}
.db-song-control {
white-space: nowrap;
font-size: 14pt;
}
.db-song-control a {
color: #E0E1E2 !important;
}
.boxclose {
float: right;
margin-top:-26px;
margin-right:-30px;
cursor:pointer;
color: #E0E1E2 !important;
font-size: 26px;
font-weight: bold;
display: inline-block;
line-height: 0px;
padding: 11px 3px;
}
/* ------------- */
/* Audio Wrapper */
/* ------------- */
.flex {
display: flex;
align-items: center;
}
.flex-auto {
flex: 1 1 auto;
}
#audio-player {
width: 100%;
height: 65px;
color: white;
}
.time {
font-size: 14px;
font-weight: 900;
color: white;
position: relative;
margin-left: 18px;
margin-right: 8px;
}
#progressbar, #volume {
height: 18px;
min-width: 35px;
display: inline-block;
border-radius: 0px;
border: none;
position: relative;
margin-left: 10px;
margin-right: 10px;
}
#play, #mute {
font-size: 20px;
width: 17px;
}
#mute {
margin-left: 10px;
margin-right: 10px;
}
#next, #previous {
font-size: 14px;
}
#play, #next, #previous, #shuffle {
padding: 5px;
width: auto;
}
#shuffle {
margin-left: 10px;
font-size: 12pt;
}
#previous {
margin-left: 15px;
}
#mute {
margin-left: 8px;
}
#volume {
max-width: 110px;
margin-right: 15px;
}
progress {
-webkit-appearance: none;
color: #3498DB;
}
progress::-ms-fill {
border: none;
}
progress::-moz-progress-bar {
background: #3498DB;
}
progress::-webkit-progress-value {
background: #3498DB;
}
progress::-webkit-progress-bar {
background-color: #FFFFFF;
}
.interactable {
cursor: pointer !important;
}
.interactable:hover {
color: #BABBBC !important;
transition: 0.1s;
}
.interactable.on {
color: #3498DB;
}
.interactable.on:hover, a.interactable:hover {
color: #2979AF !important;
}
button.interactable:hover {
color: rgba(0,0,0,.87) !important;
}
@media screen and (max-width: 550px) {
#volume {
display: none !important;
}
#mute {
margin-right: 25px;
}
}
@media screen and (max-width: 450px) {
#time {
display: none !important;
}
#next, #shuffle {
margin-right: 15px;
}
}
@media screen and (max-width: 350px) {
#previous, #next {
display: none !important;
}
#play {
margin-left: 25px;
}
}
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300");
html {
font-family: Open Sans;
font-weight: 300;
text-align: center;
}
a {
color: #77a7ff;
text-decoration: none;
}
header {
font-size: 28pt;
}
.subheader {
font-size: 20pt;
font-weight: bold;
}
.section {
font-size: 16pt;
margin-top: 44px;
}
.subheader {
margin-bottom: 12px;
}
.item {
margin-top: 5px;
margin-bottom: 5px;
}
JAVASCRIPT
let Main = new function() {
this.init = function() {
Callbacks.setUp();
Util.setUp();
IoHandler.setUp();
Nodes.setUp();
Database.setUp();
GuiWrapper.setUp();
Canvas.setUp();
Emblem.setUp();
Spectrum.setUp();
Scene.setUp();
Particles.setUp();
Lighting.setUp();
Renderer.setUp();
AudioWrap.setUp();
}
this.resizeCallback = function() {
Canvas.setStyling();
Particles.updateSizes();
Renderer.updateSize();
}
window.onload = this.init;
window.onresize = this.resizeCallback;
}
let Background = new function() {
const WHITELISTED_DOMAINS = ["i.imgur.com", "i.redd.it", "i.reddituploads.com"];
let staticUrls = [
"u9muu7r", "elUmrNS", "TcA4IsQ", "PaMnxZn", "P7hwlaN",
"I5O4QWi", "fT4bxpb", "U7Bx7FQ", "Qujelxk", "KAHqXM2",
"laGeYSO", "HdsWnkU", "xEanEAB", "NG3moRJ", "31E8sfB",
"XGiYXHs", "QBAbrBJ", "uclwgUc", "koPzyZ1", "8VfPY96"
];
let redditData;
this.loadBackground = function() {
if (!Config.drawBackground) {
return;
}
if (Config.forceImgurBackground) {
loadImgurBackground(false);
return;
}
if (Config.forceStaticBackground) {
loadStaticBackground();
return;
}
this.loadRedditBackground();
}
this.loadRedditBackground = function(allowFallback = true) {
$.ajax({
url: "https://www.reddit.com/r/" + Config.backgroundSubreddit + "/.json",
method: "GET",
success: handleRedditData,
error: allowFallback ? handleRedditFail : null
});
}
let handleRedditData = function(result) {
let posts = result.data.children;
Util.shuffle(posts);
let post;
let found = false;
for (let i = 0; i < posts.length; i++) {
post = posts[i];
if (WHITELISTED_DOMAINS.indexOf(post.data.domain) != -1
&& post.data.preview.images[0].source.width >= 2560) {
found = true;
break;
}
}
if (!found) {
console.error("Reddit has failed to offer an image satisfactory to the client; falling back to Imgur.");
loadImgurBackground();
}
let smol = post.data.preview.images[0].resolutions[1].url.replaceAll("&", "&");
let full = post.data.url.replaceAll("&", "&");
setBackground(full, smol);
}
let handleRedditFail = function() {
console.error("The client doesn't want to talk to the Reddit API today. Falling back to Imgur...");
loadImgurBackground();
}
let loadImgurBackground = function(allowFallback = true) {
$.ajax({
url: "https://api.imgur.com/3/gallery/r/" + Config.backgroundSubreddit.toLowerCase() + "/0",
method: "GET",
headers: {
Authorization: "Client-ID 0428dcb72fbc5da",
Accept: "application/json"
},
data: {
image: localStorage.dataBase64,
type: "base64"
},
success: handleImgurData,
error: allowFallback ? handleImgurFail : null
});
}
let handleImgurData = function(result) {
let posts = result.data;
Util.shuffle(posts);
let post;
let found = false;
for (let i = 0; i < posts.length; i++) {
post = posts[i];
if (post.width >= 2560) {
found = true;
break;
}
}
if (!found) {
console.error("Imgur has failed to offer an image satisfactory to the client; falling back to static "
+ "background.");
loadImgurBackground();
}
let id = result.data[Math.floor(Math.random() * result.data.length)].id;
setBackground("http://i.imgur.com/" + id + ".jpg", "http://i.imgur.com/" + id + "m.jpg");
}
let handleImgurFail = function() {
console.error("The client doesn't want to talk to the Imgur API; falling back to static background.");
loadStaticBackground();
}
let loadStaticBackground = function() {
let id = staticUrls[Math.floor(Math.random() * staticUrls.length)];
setBackground("http://i.imgur.com/" + id + ".jpg", "http://i.imgur.com/" + id + "m.jpg");
}
let setBackground = function(fullRes, lowRes) {
document.getElementById("bgimg1").style.display = "";
document.getElementById("bgimg2").style.display = "";
document.getElementById("limg1").style.display = "";
document.getElementById("limg2").style.display = "";
document.getElementById("bgimg1").src = fullRes;
document.getElementById("bgimg2").src = fullRes;
if (lowRes !== undefined) {
document.getElementById("limg1").src = lowRes;
document.getElementById("limg2").src = lowRes;
}
}
this.flipImage = function() {
$(".bgleft, .bgright").toggleClass("bgright bgleft");
}
this.fadeFullRes = function(element) {
$("#" + element.id).css({"opacity": 1, "filter": "none"});
}
this.resetBG = function() {
document.getElementById("bgimg1").src = "";
document.getElementById("bgimg2").src = "";
document.getElementById("limg1").src = "";
document.getElementById("limg2").src = "";
}
}
let Shaders = new function() {
this.vertShader =
"attribute float size; \
attribute float alpha; \
uniform vec3 color; \
varying float vAlpha; \
varying vec3 vColor; \
void main() { \
vColor = color; \
vAlpha = alpha; \
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); \
gl_PointSize = 100.0 * size / length(mvPosition.xyz); \
gl_Position = projectionMatrix * mvPosition; \
}";
this.fragShader =
"uniform sampler2D texture; \
varying float vAlpha; \
varying vec3 vColor; \
void main() { \
gl_FragColor = vec4(vColor, vAlpha); \
gl_FragColor = gl_FragColor * texture2D(texture, gl_PointCoord); \
}";
}
let Scene = new function() {
const FOV = 45;
let ASPECT;
const Z_NEAR = 0.1;
const Z_FAR = 10000;
this.glScene;
this.glCamera;
let frustum;
this.setUp = function() {
ASPECT = $(document).width() / $(document).height();
this.glScene = new THREE.Scene();
this.glCamera = new THREE.PerspectiveCamera(FOV, ASPECT, Z_NEAR, Z_FAR);
frustum = new THREE.Frustum();
this.glCamera.position.z = Config.cameraZPlane;
this.glCamera.updateMatrixWorld();
frustum.setFromMatrix(new THREE.Matrix4().multiplyMatrices(
this.glCamera.projectionMatrix, this.glCamera.matrixWorldInverse
));
}
}
let Renderer = new function() {
const TARGET_FPS = 60;
const MS_DELAY = 1000 / TARGET_FPS;
let renderer;
this.setUp = function() {
renderer = new THREE.WebGLRenderer({alpha: true})
renderer.setSize($(document).width(), $(document).height());
renderer.domElement.id = "canvas-gl";
$("#content").append(renderer.domElement);
this.updateSize();
requestAnimationFrame(render);
}
let render = function() {
if (!Config.drawParticles) {
return;
}
requestAnimationFrame(render);
renderer.render(Scene.glScene, Scene.glCamera);
}
this.updateSize = function() {
renderer.setSize($(document).width(), $(document).height());
}
}
let Particles = new function() {
const VERTEX_SIZE = 3;
this.particlesGeom;
let particleTexture;
let particleSystem;
let particleData = [];
let baseSizes = [];
this.setUp = function() {
this.particlesGeom = new THREE.BufferGeometry();
let texLoader = new THREE.TextureLoader();
particleTexture = texLoader.load("./img/particle.png");
particleTexture.minFilter = THREE.LinearFilter;
let uniforms = {
color: { type: "c", value: new THREE.Color(0xFFFFFF)},
texture: { type: "t", value: particleTexture }
};
let pMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: Shaders.vertShader,
fragmentShader: Shaders.fragShader,
blending: THREE.AdditiveBlending,
transparent: true
});
particleSystem = new THREE.Points(this.particlesGeom, pMaterial);
particleSystem.sortParticles = true;
particleSystem.geometry.dynamic = true;
initializeParticles();
Scene.glScene.add(particleSystem);
Callbacks.addCallback(this.updateParticles, Priority.NORMAL);
}
this.updateParticles = function(spectrum, multiplier) {
for (let i = 0; i < Config.maxParticleCount / 2; i++) {
updatePosition(i, multiplier);
}
particleSystem.geometry.attributes.position.needsUpdate = true;
}
let updatePosition = function(i, multiplier, ignoreSpeed) {
let data = particleData[i];
if (data === undefined) {
return; // no data set, so particle is "despawned"
}
let speed = ignoreSpeed ? 1 : data.getSpeed();
adjustedSpeed = Math.max(speed * multiplier, Config.particleBaseSpeed);
let ampMult = (Config.particlePhaseAmplitudeMultMax - Config.particlePhaseAmplitudeMultMin) * multiplier
+ Config.particlePhaseAmplitudeMultMin;
let phaseX = Math.sin(MathConstants.TWO_PI * data.getPhase().x) * data.getPhaseAmplitude().x * ampMult;
let phaseY = Math.sin(MathConstants.TWO_PI * data.getPhase().y) * data.getPhaseAmplitude().y * ampMult;
let baseIndex = VERTEX_SIZE * i;
let x = Particles.particlesGeom.attributes.position.array[baseIndex + 0]
+ data.getTrajectory().x * adjustedSpeed
+ phaseX;
let y = Particles.particlesGeom.attributes.position.array[baseIndex + 1]
+ data.getTrajectory().y * adjustedSpeed
+ phaseY;
let z = Particles.particlesGeom.attributes.position.array[baseIndex + 2] + adjustedSpeed;
if (z + Config.particleDespawnBuffer > Config.cameraZPlane) {
despawnParticle(i);
} else {
applyPosition(i, x, y, z);
}
let speedMult = (Config.particlePhaseSpeedMultMax - Config.particlePhaseSpeedMultMin) * multiplier
+ Config.particlePhaseSpeedMultMin;
data.augmentPhase(
data.getPhaseSpeed().x * speedMult,
data.getPhaseSpeed().y * speedMult
);
}
let initializeParticles = function() {
let posArr = new Float32Array(Config.maxParticleCount * VERTEX_SIZE);
let sizeArr = new Float32Array(Config.maxParticleCount);
let alphaArr = new Float32Array(Config.maxParticleCount);
particleSystem.geometry.addAttribute("position", new THREE.BufferAttribute(posArr, 3));
particleSystem.geometry.addAttribute("size", new THREE.BufferAttribute(sizeArr, 1));
for (let i = 0; i < Config.maxParticleCount / 2; i++) {
applyPosition(i, 0, 0, 0);
baseSizes[i] = Util.random(Config.particleSizeMin, Config.particleSizeMax);
applyMirroredValue(alphaArr, i, Math.random(Config.particleOpacityMin, Config.particleOpacityMax));
resetVelocity(i);
}
Particles.updateSizes();
particleSystem.geometry.addAttribute("alpha", new THREE.BufferAttribute(alphaArr, 1));
for (let i = 0; i < Config.maxParticleCount / 2; i++) {
updatePosition(i, Math.random() * Config.cameraZPlane, true);
}
}
let spawnParticle = function(i) {
resetVelocity(i); // attach a new speed to the particle, effectively "spawning" it
}
let despawnParticle = function(i) {
// we can't technically despawn a discrete particle since it's part of a
// particle system, so we just reset the position and pretend
resetPosition(i);
particleData[i] = undefined; // clear the data so other functions know this particle is "despawned"
resetVelocity(i);
}
let resetPosition = function(i) {
applyPosition(i, 0, 0, 0);
}
let resetVelocity = function(i) {
let r = Util.random(Config.particleRadiusMin, Config.particleRadiusMax);
let theta = Math.PI * Math.random() - Math.PI / 2;
let trajectory = new THREE.Vector2(
r * Math.cos(theta) / Config.cameraZPlane,
r * Math.sin(theta) / Config.cameraZPlane
);
let speed = Util.random(Config.particleSpeedMultMin, Config.particleSpeedMultMax);
let phaseAmp = new THREE.Vector2(
Util.random(Config.particlePhaseAmplitudeMin, Config.particlePhaseAmplitudeMax),
Util.random(Config.particlePhaseAmplitudeMin, Config.particlePhaseAmplitudeMax)
);
let phaseSpeed = new THREE.Vector2(
Util.random(Config.particlePhaseSpeedMin, Config.particlePhaseSpeedMax),
Util.random(Config.particlePhaseSpeedMin, Config.particlePhaseSpeedMax)
);
particleData[i] = new ParticleData(trajectory, speed, phaseAmp, phaseSpeed);
}
this.updateSizes = function() {
for (let i = 0; i < Config.maxParticleCount / 2; i++) {
applyMirroredValue(this.particlesGeom.attributes.size.array, i,
baseSizes[i] * Util.getResolutionMultiplier());
}
this.particlesGeom.attributes.size.needsUpdate = true;
}
let applyPosition = function(i, x, y, z) {
let baseIndex = VERTEX_SIZE * i;
let shiftedBaseIndex = baseIndex + Config.maxParticleCount / 2;
applyMirroredValue(Particles.particlesGeom.attributes.position.array, baseIndex + 0, x, VERTEX_SIZE);
applyMirroredValue(Particles.particlesGeom.attributes.position.array, baseIndex + 1, y, VERTEX_SIZE);
applyMirroredValue(Particles.particlesGeom.attributes.position.array, baseIndex + 2, z, VERTEX_SIZE);
Particles.particlesGeom.attributes.position.array[baseIndex + Config.maxParticleCount * (3 / 2)] *= -1;
}
let applyMirroredValue = function(array, i, value, step = 1) {
array[i] = value;
array[i + step * Config.maxParticleCount / 2] = value;
}
}
let Spectrum = new function() {
const maxBufferSize = Math.max.apply(null, Config.delays);
let spectrumCache = Array();
let jqWindow;
this.setUp = function() {
Callbacks.addCallback(drawCallback, Priority.EARLY);
jqWindow = $(window);
}
let drawCallback = function(spectrum, multiplier) {
if (!Config.drawSpectrum) {
return;
}
if (spectrumCache.length >= maxBufferSize) {
spectrumCache.shift();
}
spectrumCache.push(spectrum);
let curRad = Emblem.calcRadius(multiplier);
for (let s = Config.spectrumCount - 1; s >= 0; s--) {
let curSpectrum = smooth(spectrumCache[Math.max(spectrumCache.length - Config.delays[s] - 1, 0)],
Config.smoothMargins[s]);
let points = [];
Canvas.context.fillStyle = Config.colors[s];
Canvas.context.shadowColor = Config.colors[s];
let len = curSpectrum.length;
for (let i = 0; i < len; i++) {
t = Math.PI * (i / (len - 1)) - MathConstants.HALF_PI;
r = curRad + Math.pow(curSpectrum[i] * Config.spectrumHeightScalar * Util.getResolutionMultiplier(),
Config.exponents[s]);
x = r * Math.cos(t);
y = r * Math.sin(t);
points.push({x: x, y: y});
}
drawPoints(points);
}
}
let drawPoints = function(points) {
if (points.length == 0) {
return;
}
Canvas.context.beginPath();
let halfWidth = jqWindow.width() / 2;
let halfHeight = jqWindow.height() / 2;
for (let neg = 0; neg <= 1; neg++) {
let xMult = neg ? -1 : 1;
Canvas.context.moveTo(halfWidth, points[0].y + halfHeight);
let len = points.length;
for (let i = 1; i < len - 2; i++) {
let c = xMult * (points[i].x + points[i + 1].x) / 2 + halfWidth;
let d = (points[i].y + points[i + 1].y) / 2 + halfHeight;
Canvas.context.quadraticCurveTo(xMult * points[i].x + halfWidth, points[i].y + halfHeight, c, d);
}
Canvas.context.quadraticCurveTo(xMult * points[len - 2].x + halfWidth + neg * 2,
points[len - 2].y + halfHeight, xMult * points[len - 1].x + halfWidth,
points[len - 1].y + halfHeight);
}
Canvas.context.fill();
}
let smooth = function(points, margin) {
if (margin == 0) {
return points;
}
let newArr = Array();
for (let i = 0; i < points.length; i++) {
let sum = 0;
let denom = 0;
for (let j = 0; j <= margin; j++) {
if (i - j < 0 || i + j > points.length - 1) {
break;
}
sum += points[i - j] + points[i + j];
denom += (margin - j + 1) * 2;
}
newArr[i] = sum / denom;
}
return newArr;
}
}