Visualizer

HTML
<!DOCTYPE HTML> <html> <head> <title>js.nation</title> <!-- CDN --> <!-- Font Awesome --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <!-- jQuery --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <!-- jQuery UI --> <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css"> <!-- Database Wrapper --> <script src="https://cdnjs.cloudflare.com/ajax/libs/dexie/1.5.1/dexie.min.js"></script> <!-- Semantic OwO --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.min.js"></script> <!-- three.js --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script> <!-- JsRender --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.84/jsrender.min.js"></script> <!-- analytics --> <script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-101944034-1', 'auto'); ga('send', 'pageview'); </script> <!-- templates --> <script id="table-row-template" type="text/html"> <tr class="song-row" data-songid="{{:id}}"> <td class="db-song-control"> <div><a onclick="Database.handlePlay({{:id}})" class="fa fa-play interactable"></a></div> </td> <td class="row-art"><img width="50px" src="{{>img || './img/art_ph.png'}}"></td> <td class="row-title"><div>{{:title}}</div></td> <td class="row-artist"><div>{{:artist}}</div></td> <td><div>{{:duration}}</div></td> <td class="db-song-control"> <div><a onclick="Database.handleRemove({{:id}})" class="fa fa-trash interactable"></a></div> </td> </tr> </script> <!-- Local --> <!-- Styles --> <link rel="stylesheet" type="text/css" href="./css/main.css"> <link rel="stylesheet" type="text/css" href="./css/gui.css"> <link rel="stylesheet" type="text/css" href="./css/background.css"> <!-- audio --> <script src="./js/audio/nodes.js"></script> <!-- math --> <script src="./js/math/math_constants.js"></script> <script src="./js/math/transform.js"></script> <!-- misc --> <script src="./js/misc/callbacks.js"></script> <script src="./js/misc/config.js"></script> <script src="./js/misc/util.js"></script> <script src="./js/misc/database.js"></script> <script src="./js/misc/id3-minimized.js"></script> <script src="./js/misc/io_handler.js"></script> <!-- model --> <script src="./js/model/particle_data.js"></script> <script src="./js/model/priority.js"></script> <script src="./js/model/song.js"></script> <!-- visual --> <script src="./js/visual/background.js"></script> <!-- visual/emblem --> <script src="./js/visual/drawn/canvas.js"></script> <script src="./js/visual/drawn/emblem.js"></script> <script src="./js/visual/drawn/spectrum.js"></script> <!-- visual/gl --> <script src="./js/visual/gl/lighting.js"></script> <script src="./js/visual/gl/particles.js"></script> <script src="./js/visual/gl/renderer.js"></script> <script src="./js/visual/gl/scene.js"></script> <script src="./js/visual/gl/shaders.js"></script> <!-- visual/gui --> <script src="./js/visual/gui/audio_wrap.js"></script> <script src="./js/visual/gui/gui_wrapper.js"></script> <!-- root --> <script src="./js/main.js"></script> </head> <body> <div id="gui-full" class="overlay-container"> <div class="vert-cen overlay-window" id="gui-full-holder"> <div class="absolute-center overlay-pane" id="controls"> <div id="db-prev" class="page-control interactable fa fa-chevron-circle-left" onClick="Database.prevPage()" title="Previous page"></div> <div id="db-next" class="page-control interactable fa fa-chevron-circle-right" onClick="Database.nextPage()" title="Next page"></div> <div onClick="GuiWrapper.closeGui();" class="boxclose fa fa-times-circle interactable"></div> <div class="ui fluid action input inverted"> <input type="text" readonly Value="Select Audio File"> <input accept="audio/*" type="file" id="fileSelector"> <div id="upload-button" class="ui icon button interactable"> <span class="fa fa-cloud-upload"></span> </div> </div> <table class="ui selectable inverted table" id="db-view" border="1"></table> <div id="db-page-info">Page ??/??</div> <div id="db-input"> <div class="ui input"> <input type="text" id="field-artist" placeholder="Artist" disabled> </div> <div class="ui input"> <input type="text" id="field-title" placeholder="Title" disabled> </div> </div> <div id="db-controls"> <button class="ui button interactable" id="add2DB" disabled>Add To Database</button> <button class="ui button interactable" id="viewDB">Refresh Database</button> <button class="ui button interactable" id="delDB">Delete Database</button> </div> </div> </div> </div> <div id="about-full" class="overlay-container"> <div class="vert-cen overlay-window" id="about-full-holder"> <div class="absolute-center overlay-pane" id="about-pane"> <div onClick="GuiWrapper.closeAbout();" class="boxclose fa fa-times-circle interactable"></div> <div id="about-header"> js.nation </div> <div id="about-content"> <div> made with <span class="fa fa-heart"></span> by <a href="//caseif.net" target="_blank" rel="noopener noreferrer">caseif</a> & <a href="http://incept.online" target="_blank" rel="noopener noreferrer">incept</a> </div> <div> source available on <a href="//github.com/caseif/js.nation" target="_blank" rel="noopener noreferrer">github</a> </div> <div> <a href="technologies.html" target="_blank" rel="noopener noreferrer">technologies used</a> </div> <div> inspired by <a href="//nations.io/">the nations</a> </div> <hr> <div> hotkeys </div> <div id="about-hotkeys"> f — flip background <br> g — toggle spectrum glow <br> space — play/pause <br> escape — close overlay </div> </div> </div> </div> </div> <div id="welcome-full" class="overlay-container"> <div class="vert-cen overlay-window" id="welcome-full-holder"> <div class="absolute-center overlay-pane" id="welcome-pane"> <div id="welcome-content"> welcome to js.nation! <br><br> to get started, click "view database" in the bottom-left corner and add a song. </div> </div> </div> </div> <div class="flex gui-part" id="gui-top"> <div id="song-info"> <div id="gui-artist"></div> <div id="gui-title"></div> </div> </div> <div class="flex gui-part" id="gui-bottom"> <!-- Left --> <button class="ui button inverted transparent interactable" onclick="GuiWrapper.openGui();" id="view-database">View Database</button> <!-- Center --> <div class="flex" id="audio-player"> <div class="flex main-controls"> <span id="previous" class="fa fa-step-backward interactable" onClick="Database.playPrevSong()"></span> <span id="play" class="fa fa-pause action interactable"></span> <span id="next" class="fa fa-step-forward action interactable" onClick="Database.playNextSong()"></span> <span id="shuffle" class="fa fa-random action interactable" onClick="Database.toggleShuffle()"></span> </div> <span id="time" class="time">00:00</span> <progress id="progressbar" class="flex-auto interactable" value="0" max="100"></progress> <span id="mute" class="fa fa-volume-up interactable"></span> <progress id="volume" class="flex-auto interactable" value="100" max="100"></progress> </div> <audio autoplay id="audio" onended="Database.playNextSong()"></audio> <!-- Right --> <div id="elm-about"> <a href="javascript:GuiWrapper.openAbout();">About</a> </div> </div> <div id="content"> <canvas id="canvas" style="display: block;"></canvas> </div> <div id="background" class="lazyaf"> <img src="" class="bgleft" id="limg1" onerror="this.style.display='none'"> <img src="" class="bgright" id="limg2" onerror="this.style.display='none'"> </div> <div id="background" class="realbg"> <img src="" class="bgleft" id="bgimg1" onload="Background.fadeFullRes(this)" onerror="this.style.display='none'"> <img src="" class="bgright" id="bgimg2" onload="Background.fadeFullRes(this)" onerror="this.style.display='none'"> </div> </body> </html>
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; } }
Expand for more options Login