<div id="ui-overlay">
<div class="hidden loading" id="loader">
<div class="load-bar"></div>
<div class="load-bar"></div>
<div class="load-bar"></div>
<div class="load-bar"></div>
<div class="load-bar"></div>
</div>
<input id="chk-menu-toggle" type="checkbox" name="chk-menu-toggle"/>
<label class="hidden" id="lbl-menu-toggle" for="chk-menu-toggle"><span class="bar line"></span><span class="bar cross"></span><span class="bar cross"></span><span class="bar line"></span></label>
<div id="ui-drawer">
<div class="ui-drawer__container">
<div class="drawer-folder">
<div class="drawer-folder__label">Visuals</div>
<div class="drawer-folder__option option--checkbox">
<label class="option__label" for="chk-particles">
<input id="chk-particles" type="checkbox" checked="checked"/><i class="fa fa-spinner"></i>Particles
</label>
</div>
<div class="drawer-folder__option option--checkbox">
<label class="option__label" for="chk-backlight">
<input id="chk-backlight" type="checkbox" checked="checked"/><i class="fa fa-lightbulb-o"></i>Backlight
</label>
</div>
<div class="drawer-folder__option option--checkbox">
<label class="option__label" for="chk-spectrum">
<input id="chk-spectrum" type="checkbox" checked="checked"/><i class="fa fa-signal"></i>Spectrum
</label>
</div>
<div class="drawer-folder__option option--checkbox">
<label class="option__label" for="chk-glow">
<input id="chk-glow" type="checkbox" checked="checked"/><i class="fa fa-sun-o"></i>Glow
</label>
</div>
</div>
<div class="drawer-folder">
<div class="drawer-folder__label">Audio</div>
<div class="drawer-folder__option">
<div class="option__label" id="track--label"> <i class="fa fa-music"></i>Tracks
<div class="btn" id="btn-add-track">
<input id="in-add-track" type="file" accept="audio/*"/>
<label id="lbl-add-track" for="in-add-track"><i class="fa fa-plus"></i></label>
</div>
</div>
<div class="option__sub-menu">
<ul id="track-list"></ul>
<form class="hidden" id="frm-track-title" name="frm-track-title">
<label id="lbl-track-title" for="txt-track-title">Enter a Track Title:</label>
<input id="txt-track-title" type="text" name="title" placeholder="Track Title" maxlength="50" required="required"/>
<button id="btn-submit-title">
Submit<i class="fa fa-caret-right"></i></button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="hidden disabled" id="audio-controls">
<div id="seek-bar">
<div id="progress-bar"></div>
</div>
<div id="controls__container">
<div id="button-controls">
<div class="btn"><i class="fa fa-step-backward" id="btn-prev" data-value="-1"></i></div>
<div class="btn"><i class="fa fa-pause" id="btn-play"></i></div>
<div class="btn"><i class="fa fa-step-forward" id="btn-next" data-value="1"></i></div>
<div class="btn"><i class="fa fa-volume-up" id="icon-volume"><span class="input-container">
<input id="rng-volume" type="range" value="0.8" min="0" max="1" step="0.1"/></span></i></div>
</div>
<div id="title"></div>
</div>
</div>
<div id="btn-initialize">Click to Start</div>
</div><a id="codepen-link" href="http://www.musictrot.com/" target="_blank"></a>
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}
article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}
body{line-height:1}
ol,ul{list-style:none}
blockquote,q{quotes:none}
blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}
table{border-collapse:collapse;border-spacing:0}
html, body {
background: #010101;
overflow: hidden;
}
canvas {
background: #010101;
}
#lbl-menu-toggle {
position: fixed;
top: 20px;
left: 30px;
z-index: 2;
display: block;
width: 24px;
height: 20px;
cursor: pointer;
-webkit-transform: translateZ(0);
transform: translateZ(0);
transition: opacity 0.5s, -webkit-transform 0.5s;
transition: opacity 0.5s, transform 0.5s;
transition: opacity 0.5s, transform 0.5s, -webkit-transform 0.5s;
}
#lbl-menu-toggle.hidden {
opacity: 0;
-webkit-transform: translateY(-10px) translateZ(0);
transform: translateY(-10px) translateZ(0);
}
#lbl-menu-toggle .bar {
position: absolute;
display: block;
height: 2px;
width: 100%;
background-color: #ffb300;
transition: -webkit-transform 0.2s;
transition: transform 0.2s;
transition: transform 0.2s, -webkit-transform 0.2s;
}
#lbl-menu-toggle .bar.line:first-of-type {
top: 2px;
-webkit-transform-origin: right center;
transform-origin: right center;
transition-delay: 0.1s;
}
#lbl-menu-toggle .bar.cross {
top: 8px;
-webkit-transform-origin: center center;
transform-origin: center center;
}
#lbl-menu-toggle .bar.line:last-of-type {
top: 14px;
-webkit-transform-origin: left center;
transform-origin: left center;
transition-delay: 0.1s;
}
#ui-drawer {
position: absolute;
top: 0;
left: 0;
height: calc(100vh - 65px);
width: 40vw;
max-width: 500px;
padding-top: 60px;
box-sizing: border-box;
font-family: "Josefin Sans", sans-serif;
background: #222;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
overflow: hidden;
opacity: 0;
-webkit-transform: translateX(-40.15vw) translateZ(0);
transform: translateX(-40.15vw) translateZ(0);
transition: opacity 0.4s, -webkit-transform 0.4s;
transition: opacity 0.4s, transform 0.4s;
transition: opacity 0.4s, transform 0.4s, -webkit-transform 0.4s;
}
#ui-drawer .drawer-folder__label {
position: relative;
z-index: 2;
padding: 12px 30px 10px 30px;
color: #ffb300;
background: #333;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
#ui-drawer .drawer-folder__option {
position: relative;
}
#ui-drawer .drawer-folder__option .option__sub-menu {
position: relative;
}
#ui-drawer .drawer-folder__option .option__label {
position: relative;
width: 100%;
display: block;
padding: 12px 30px 10px 30px;
font-size: 0.95em;
color: white;
box-sizing: border-box;
background-color: #292929;
transition: background-color 0.3s;
}
#ui-drawer .drawer-folder__option .option__label:not(#track--label) {
cursor: pointer;
}
#ui-drawer .drawer-folder__option .option__label:not(#track--label):hover {
background-color: #2f2f2f;
}
#ui-drawer .drawer-folder__option:not(:last-child) .option__label {
border-bottom: 1px solid #1c1c1c;
}
#ui-drawer .drawer-folder__option .fa {
width: 16px;
margin-right: 10px;
box-sizing: border-box;
color: #6f6f6f;
transition: color 0.3s;
}
#ui-drawer .drawer-folder__option .fa-lightbulb-o {
padding: 0 3px;
}
#ui-drawer .drawer-folder__option .fa-signal {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
#ui-drawer .drawer-folder__option .fa-music, #ui-drawer .drawer-folder__option .fa-plus {
color: #ffb300;
}
#ui-drawer .drawer-folder__option .fa-plus {
margin-right: 0;
}
#ui-drawer .drawer-folder__option #btn-add-track {
position: absolute;
top: 12px;
right: 30px;
}
#ui-drawer .drawer-folder__option #btn-add-track #in-add-track {
display: none;
}
#ui-drawer .drawer-folder__option #btn-add-track #lbl-add-track {
cursor: pointer;
}
#ui-drawer .drawer-folder__option #btn-add-track #lbl-add-track:hover:before {
opacity: 1;
-webkit-transform: translateX(-4px);
transform: translateX(-4px);
}
#ui-drawer .drawer-folder__option #btn-add-track #lbl-add-track:before {
position: absolute;
display: block;
content: "Add";
white-space: nowrap;
padding: 4px 8px 4px 4px;
top: -2px;
right: 14px;
opacity: 0;
transition: opacity 0.3s, -webkit-transform 0.3s;
transition: opacity 0.3s, transform 0.3s;
transition: opacity 0.3s, transform 0.3s, -webkit-transform 0.3s;
}
#ui-drawer .drawer-folder__option input[type="checkbox"] {
display: none;
}
#ui-drawer .drawer-folder__option input[type="checkbox"]:checked ~ .fa {
color: #ffb300;
}
#ui-drawer .drawer-folder__option {
height: 100%;
}
#ui-drawer #track-list {
position: relative;
overflow-y: scroll;
max-height: calc(100vh - 400px);
width: calc(100% + 18px);
}
#ui-drawer #track-list .track-option {
padding: 12px 30px 10px 30px;
font-size: 0.95em;
color: white;
background-color: #1a1a1a;
cursor: pointer;
border-bottom: 1px solid #121212;
transition: background-color 0.3s;
}
#ui-drawer #track-list .track-option:hover {
background-color: #1f1f1f;
}
#ui-drawer #frm-track-title {
position: absolute;
top: 20px;
right: 20px;
width: calc(60% - 60px);
padding: 20px;
background-color: #3f3f3f;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.6);
-webkit-transform: translateZ(0);
transform: translateZ(0);
transition: opacity 0.3s, -webkit-transform 0.3s;
transition: opacity 0.3s, transform 0.3s;
transition: opacity 0.3s, transform 0.3s, -webkit-transform 0.3s;
}
#ui-drawer #frm-track-title.hidden {
pointer-events: none;
opacity: 0;
-webkit-transform: translateY(-10px) translateZ(0);
transform: translateY(-10px) translateZ(0);
}
#ui-drawer #frm-track-title:before {
position: absolute;
right: 12px;
top: -7px;
display: block;
content: "";
height: 16px;
width: 16px;
background-color: #3f3f3f;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
#ui-drawer #frm-track-title #lbl-track-title {
display: block;
margin-bottom: 8px;
color: #ffb300;
}
#ui-drawer #frm-track-title #txt-track-title {
width: 100%;
box-sizing: border-box;
padding: 4px;
border-width: 0 0 1px 0;
border-color: #ffb300;
color: white;
font-family: "Josefin Sans", sans-serif;
background-color: transparent;
}
#ui-drawer #frm-track-title #txt-track-title:focus {
outline: none;
}
#ui-drawer #frm-track-title #txt-track-title::-webkit-input-placeholder {
color: #9f9f9f;
}
#ui-drawer #frm-track-title #txt-track-title:-ms-input-placeholder {
color: #9f9f9f;
}
#ui-drawer #frm-track-title #txt-track-title::-moz-placeholder {
color: #9f9f9f;
opacity: 1;
}
#ui-drawer #frm-track-title #txt-track-title:-moz-placeholder {
color: #9f9f9f;
opacity: 1;
}
#ui-drawer #frm-track-title #btn-submit-title {
position: relative;
left: 100%;
padding: 6px 12px;
margin-top: 10px;
-webkit-transform: translateX(-100%);
transform: translateX(-100%);
background: transparent;
border: 1px solid #ffb300;
cursor: pointer;
color: white;
font-family: "Josefin Sans", sans-serif;
transition: color 0.3s;
}
#ui-drawer #frm-track-title #btn-submit-title:hover, #ui-drawer #frm-track-title #btn-submit-title:hover .fa {
color: #ffb300;
}
#ui-drawer #frm-track-title #btn-submit-title:focus {
outline: none;
}
#ui-drawer #frm-track-title #btn-submit-title .fa {
width: auto;
margin-right: 0;
margin-left: 8px;
color: white;
}
#chk-menu-toggle {
display: none;
}
#chk-menu-toggle:checked + #lbl-menu-toggle .line {
-webkit-transform: scaleX(0);
transform: scaleX(0);
transition-delay: 0;
}
#chk-menu-toggle:checked + #lbl-menu-toggle .cross {
transition-delay: 0.2s;
}
#chk-menu-toggle:checked + #lbl-menu-toggle .cross:nth-of-type(2) {
-webkit-transform: rotate(45deg) scale(0.8);
transform: rotate(45deg) scale(0.8);
}
#chk-menu-toggle:checked + #lbl-menu-toggle .cross:nth-of-type(3) {
-webkit-transform: rotate(135deg) scale(0.8);
transform: rotate(135deg) scale(0.8);
}
#chk-menu-toggle:checked ~ #ui-drawer {
opacity: 1;
-webkit-transform: translateX(0) translateZ(0);
transform: translateX(0) translateZ(0);
}
#ui-overlay {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 2;
}
#btn-initialize {
position: absolute;
top: 50%;
left: 50%;
padding: 0.8em 1em 0.6em 1em;
border: 2px solid;
border-radius: 1.5em;
color: white;
font-family: "Josefin Sans", sans-serif;
font-size: 1.2em;
cursor: pointer;
-webkit-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
transition: color 0.3s, opacity 0.5s;
}
#btn-initialize:hover {
color: #ffb300;
}
#btn-initialize:active {
-webkit-transform: translateX(-50%) translateY(-50%) scale(0.95);
transform: translateX(-50%) translateY(-50%) scale(0.95);
}
#btn-initialize.disabled {
opacity: 0;
pointer-events: none;
}
#loader {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 50px;
height: 20px;
top: 50%;
left: 50%;
-webkit-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
transition: opacity 0.5s;
}
#loader.hidden {
opacity: 0;
}
#loader.loading .load-bar {
-webkit-animation: load-bar-animation 2s linear infinite;
animation: load-bar-animation 2s linear infinite;
}
#loader .load-bar {
flex: 1;
height: 100%;
background: #ffb300;
opacity: 0.5;
-webkit-transform: scaleY(1) translateZ(0);
transform: scaleY(1) translateZ(0);
}
#loader .load-bar:not(:last-child) {
margin-right: 2px;
}
#loader .load-bar:nth-child(1) {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s;
}
#loader .load-bar:nth-child(2) {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
#loader .load-bar:nth-child(3) {
-webkit-animation-delay: 0.6s;
animation-delay: 0.6s;
}
#loader .load-bar:nth-child(4) {
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
#loader .load-bar:nth-child(5) {
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
#audio-controls {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column;
left: 0;
bottom: 0;
width: 100%;
height: 65px;
color: #ffb300;
opacity: 1;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
transition: opacity 0.5s, -webkit-transform 0.5s;
transition: opacity 0.5s, transform 0.5s;
transition: opacity 0.5s, transform 0.5s, -webkit-transform 0.5s;
}
#audio-controls.hidden {
-webkit-transform: translateY(10px);
transform: translateY(10px);
opacity: 0;
}
#audio-controls.disabled {
pointer-events: none;
}
#audio-controls.disabled .fa {
opacity: 0.2;
}
#audio-controls #controls__container {
position: relative;
background-color: #0c0c0c;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 20px 30px;
box-sizing: border-box;
}
#audio-controls #button-controls {
display: flex;
align-items: center;
justify-content: flex-start;
margin-right: 16px;
}
#audio-controls #button-controls .btn {
display: flex;
align-items: center;
width: 16px;
}
#audio-controls #button-controls .btn:not(:last-child) {
justify-content: center;
margin-right: 12px;
}
#audio-controls #title {
color: #ffb300;
flex: 1;
padding-top: 4px;
font-family: "Josefin Sans", sans-serif;
font-size: 1.2em;
text-align: right;
transition: opacity 0.3s, -webkit-transform 0.3s;
transition: opacity 0.3s, transform 0.3s;
transition: opacity 0.3s, transform 0.3s, -webkit-transform 0.3s;
}
#audio-controls #seek-bar {
position: absolute;
top: 0;
z-index: 2;
height: 6px;
width: 100%;
background-color: #4f4f4f;
cursor: pointer;
transition: -webkit-transform 0.5s;
transition: transform 0.5s;
transition: transform 0.5s, -webkit-transform 0.5s;
}
#audio-controls #seek-bar #progress-bar {
height: 100%;
width: 100%;
background: #ffb300;
-webkit-transform: scaleX(0);
transform: scaleX(0);
-webkit-transform-origin: center left;
transform-origin: center left;
transition: -webkit-transform 0.5s;
transition: transform 0.5s;
transition: transform 0.5s, -webkit-transform 0.5s;
}
#audio-controls #btn-prev, #audio-controls #btn-next {
-webkit-transform: scaleX(1.5);
transform: scaleX(1.5);
}
#audio-controls #btn-prev:active, #audio-controls #btn-next:active {
-webkit-transform: scaleY(0.85) scaleX(1.35);
transform: scaleY(0.85) scaleX(1.35);
}
#audio-controls .fa {
cursor: pointer;
transition: opacity 0.3s;
}
#audio-controls .fa:active:not(#icon-volume):not(#btn-prev):not(#btn-next) {
-webkit-transform: scale(0.85);
transform: scale(0.85);
}
#audio-controls #icon-volume {
position: relative;
}
#audio-controls #icon-volume:hover .input-container {
opacity: 1;
}
#audio-controls #icon-volume .input-container {
position: absolute;
top: -8px;
left: 12px;
width: 60px;
padding: 8px;
opacity: 0;
transition: opacity 0.3s;
}
#audio-controls #icon-volume #rng-volume {
position: relative;
bottom: 4px;
width: 100%;
-webkit-appearance: none;
}
#audio-controls #icon-volume #rng-volume:focus {
outline: none;
}
#audio-controls #icon-volume #rng-volume::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #444;
}
#audio-controls #icon-volume #rng-volume::-webkit-slider-thumb {
height: 14px;
width: 14px;
border-radius: 50%;
background: #ffb300;
cursor: pointer;
-webkit-appearance: none;
margin-top: -6px;
}
#audio-controls #icon-volume #rng-volume::-webkit-range-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #444;
}
#audio-controls #icon-volume #rng-volume::-webkit-range-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
border: 2px solid #444;
background: transparent;
cursor: pointer;
-webkit-appearance: none;
margin-top: -7px;
}
#audio-controls #icon-volume #rng-volume::-moz-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #444;
}
#audio-controls #icon-volume #rng-volume::-moz-slider-thumb {
height: 14px;
width: 14px;
border-radius: 50%;
background: #ffb300;
cursor: pointer;
-webkit-appearance: none;
margin-top: -6px;
}
#audio-controls #icon-volume #rng-volume::-moz-range-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #444;
}
#audio-controls #icon-volume #rng-volume::-moz-range-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
border: 2px solid #444;
background: transparent;
cursor: pointer;
-webkit-appearance: none;
margin-top: -7px;
}
#codepen-link {
position: absolute;
top: 20px;
right: 30px;
height: 40px;
width: 40px;
z-index: 10;
border-radius: 50%;
box-sizing: border-box;
background-image: url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/544318/logo.jpg");
background-position: center center;
background-size: cover;
opacity: 0.4;
transition: all 0.25s;
}
#codepen-link:hover {
opacity: 0.8;
box-shadow: 0 0 6px #efefef;
}
@-webkit-keyframes load-bar-animation {
30% {
opacity: 1;
-webkit-transform: scaleY(1.5) translateZ(0);
transform: scaleY(1.5) translateZ(0);
}
60% {
opacity: 0.5;
-webkit-transform: scaleY(1) translateZ(0);
transform: scaleY(1) translateZ(0);
}
100% {
opacity: 0.5;
-webkit-transform: scaleY(1) translateZ(0);
transform: scaleY(1) translateZ(0);
}
}
@keyframes load-bar-animation {
30% {
opacity: 1;
-webkit-transform: scaleY(1.5) translateZ(0);
transform: scaleY(1.5) translateZ(0);
}
60% {
opacity: 0.5;
-webkit-transform: scaleY(1) translateZ(0);
transform: scaleY(1) translateZ(0);
}
100% {
opacity: 0.5;
-webkit-transform: scaleY(1) translateZ(0);
transform: scaleY(1) translateZ(0);
}
}
"use strict";

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Vector2 = function () {
  function Vector2(x, y) {
    _classCallCheck(this, Vector2);

    this.x = typeof x === 'number' ? x : 0;
    this.y = typeof y === 'number' ? y : 0;
    return this;
  }

  _createClass(Vector2, [{
    key: 'zero',
    value: function zero() {
      this.x = 0;
      this.y = 0;
      return this;
    }
  }, {
    key: 'clone',
    value: function clone() {
      return new Vector2(this.x, this.y);
    }
  }, {
    key: 'add',
    value: function add(vec) {
      this.x += vec.x || 0;
      this.y += vec.y || 0;
      return this;
    }
  }, {
    key: 'addX',
    value: function addX(vec) {
      this.x += vec.x || 0;
      return this;
    }
  }, {
    key: 'addY',
    value: function addY(vec) {
      this.y += vec.y || 0;
      return this;
    }
  }, {
    key: 'addScalar',
    value: function addScalar(scalar) {
      this.x += scalar || 0;
      this.y += scalar || 0;
      return this;
    }
  }, {
    key: 'addScalarX',
    value: function addScalarX(scalar) {
      this.x += scalar || 0;
      return this;
    }
  }, {
    key: 'addScalarY',
    value: function addScalarY(scalar) {
      this.y += scalar || 0;
      return this;
    }
  }, {
    key: 'sub',
    value: function sub(vec) {
      this.x -= vec.x || 0;
      this.y -= vec.y || 0;
      return this;
    }
  }, {
    key: 'subX',
    value: function subX(vec) {
      this.x -= vec.x || 0;
      return this;
    }
  }, {
    key: 'subY',
    value: function subY(vec) {
      this.y -= vec.y || 0;
      return this;
    }
  }, {
    key: 'subScalar',
    value: function subScalar(scalar) {
      this.x -= scalar || 0;
      this.y -= scalar || 0;
      return this;
    }
  }, {
    key: 'subX',
    value: function subX(scalar) {
      this.x -= scalar || 0;
      return this;
    }
  }, {
    key: 'subY',
    value: function subY(scalar) {
      this.y -= scalar || 0;
      return this;
    }
  }, {
    key: 'multiply',
    value: function multiply(vec) {
      this.x *= vec.x || 1;
      this.y *= vec.y || 1;
      return this;
    }
  }, {
    key: 'multiplyX',
    value: function multiplyX(vec) {
      this.x *= vec.x || 1;
      return this;
    }
  }, {
    key: 'multiplyY',
    value: function multiplyY(vec) {
      this.y *= vec.y || 1;
      return this;
    }
  }, {
    key: 'multiplyScalar',
    value: function multiplyScalar(scalar) {
      this.x *= scalar || 1;
      this.y *= scalar || 1;
      return this;
    }
  }, {
    key: 'multiplyScalarX',
    value: function multiplyScalarX(scalar) {
      this.x *= scalar || 1;
      return this;
    }
  }, {
    key: 'multiplyScalarY',
    value: function multiplyScalarY(scalar) {
      this.y *= scalar || 1;
      return this;
    }
  }, {
    key: 'divide',
    value: function divide(vec) {
      if (vec.x === 0 || vec.y === 0) {
        console.log('! Cannot divide by zero !');
        return;
      } else {
        this.x /= vec.x || 1;
        this.y /= vec.y || 1;
        return this;
      }
    }
  }, {
    key: 'divideX',
    value: function divideX(vec) {
      if (vec.x === 0) {
        console.log('! Cannot divide by zero !');
        return;
      } else {
        this.x /= vec.x || 1;
        return this;
      }
    }
  }, {
    key: 'divideY',
    value: function divideY(vec) {
      if (vec.y === 0) {
        console.log('! Cannot divide by zero !');
        return;
      } else {
        this.y /= vec.y || 1;
        return this;
      }
    }
  }, {
    key: 'divideScalar',
    value: function divideScalar(scalar) {
      if (scalar === 0) {
        console.log('! Cannot divide by zero !');
        return;
      } else {
        this.x /= scalar || 1;
        this.y /= scalar || 1;
        return this;
      }
    }
  }, {
    key: 'divideScalarX',
    value: function divideScalarX(scalar) {
      if (scalar === 0) {
        console.log('! Cannot divide by zero !');
        return;
      } else {
        this.x /= scalar || 1;
        return this;
      }
    }
  }, {
    key: 'divideScalarY',
    value: function divideScalarY(scalar) {
      if (scalar === 0) {
        console.log('! Cannot divide by zero !');
        return;
      } else {
        this.Y /= scalar || 1;
        return this;
      }
    }
  }, {
    key: 'getMagnitude',
    value: function getMagnitude() {
      return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
    }
  }, {
    key: 'normalize',
    value: function normalize() {
      this.divideScalar(this.getMagnitude());
    }
  }, {
    key: 'randomize',
    value: function randomize(bounds) {
      bounds = bounds || new Vector2(1, 1);
      this.x = Math.random() * bounds.x;
      this.y = Math.random() * bounds.y;
      return this;
    }
  }, {
    key: 'addRandom',
    value: function addRandom(limit) {
      limit = limit || 0;
      this.x += limit - Math.random() * (limit * 2);
      this.y += limit - Math.random() * (limit * 2);
    }
  }, {
    key: 'addRandomX',
    value: function addRandomX(limit) {
      limit = limit || 0;
      this.x += limit - Math.random() * (limit * 2);
    }
  }, {
    key: 'addRandomY',
    value: function addRandomY(limit) {
      limit = limit || 0;
      this.y += limit - Math.random() * (limit * 2);
    }
  }, {
    key: 'lerp',
    value: function lerp(vec, amount) {
      vec = vec || this;
      amount = amount || 0.05;
      this.x = (1 - amount) * this.x + amount * vec.x;
      this.y = (1 - amount) * this.y + amount * vec.y;
      return this;
    }
  }, {
    key: 'midpoint',
    value: function midpoint(vec) {
      var mp = new Vector2(this.x + vec.x, this.y + vec.y);
      mp.divideScalar(2);
      return mp;
    }
  }, {
    key: 'slope',
    value: function slope(vec) {
      return (vec.y - this.y) / (vec.x - this.x) * -1;
    }
  }, {
    key: 'intercept',
    value: function intercept(slope) {
      console.log(-slope * this.x + this.y);
      return -slope * this.x + this.y;
    }
  }, {
    key: 'distanceTo',
    value: function distanceTo(vec) {
      vec = vec || this;
      return Math.sqrt(Math.pow(vec.x - this.x, 2) + Math.pow(vec.y - this.y, 2));
    }
  }, {
    key: 'angleTo',
    value: function angleTo(vec, format) {
      vec = vec || this;
      format = format || 'rad';
      var angle = format === 'rad' ? Math.atan2(vec.y - this.y, vec.x - this.x) : format === 'deg' ? Math.atan2(vec.y - this.y, vec.x - this.x) * 180 / Math.PI : undefined;
      return angle;
    }
  }]);

  return Vector2;
}();
const { PI, cos, sin, abs, sqrt, pow, floor, round } = Math;
const HALF_PI = 0.5 * PI;
const TAU = 2 * PI;
const rand = n => n * Math.random();
const randRange = n => n - rand(2 * n);
const fadeInOut = (t, m) => {
let hm = 0.5 * m;
return abs((t + hm) % m - hm) / hm;
};

class VectorArrayObject {
constructor(count = 0, max = 0) {
this.count = count || max;
this.max = max || count;
this.values = new Float32Array(max * 2);
}
get(i) {
return {
x: this.values[i * 2],
y: this.values[i * 2 + 1] };

}
getX(i) {
return this.values[i * 2];
}
getY(i) {
return this.values[i * 2 + 1];
}
set(i, x, y) {
this.values[i * 2] = x;
this.values[i * 2 + 1] = y;
return this;
}
setX(i, x) {
this.values[i * 2] = x;
return this;
}
setY(i, y) {
this.values[i * 2 + 1] = y;
return this;
}}

class VectorArrayObjectController {
constructor(count = 0, max = 0) {
this.count = count || max;
this.max = max || count;
this.life = new VectorArrayObject(this.count, this.max);
this.vertices = new VectorArrayObject(this.count, this.max);
this.velocities = new VectorArrayObject(this.count, this.max);
}
getLife(i) {
return this.life.getX(i);
}
getTTL(i) {
return this.life.getY(i);
}
setLife(i, life) {
this.life.setX(i, life);
return this;
}
setTTL(i, ttl) {
this.life.setY(i, ttl);
return this;
}
getVertex(i) {
return this.vertices.get(i);
}
setVertex(i, x, y) {
this.vertices.set(i, x, y);
return this;
}
getVelocity(i) {
return this.velocities.get(i);
}
setVelocity(i, x, y) {
this.velocities.set(i, x, y);
return this;
}}

class RenderObject {
constructor(x = 0, y = 0) {
this.life = 0;
this.position = new Vector2(x, y);
this.lastPosition = this.position.clone();
this.velocity = new Vector2();
}
getPosition() {
return this.position.clone();
}
setPosition(x, y) {
this.position.x = x;
this.position.y = y;
return this;
}
setLastPosition() {
this.lastPosition.x = this.position.x;
this.lastPosition.y = this.position.y;
return this;
}
getVelocity() {
return this.velocity.clone();
}
setVelocity(x, y) {
this.velocity.x = x;
this.velocity.y = y;
return this;
}
getLife() {
return this.life;
}
setLife(n) {
this.life = n;
return this;
}
setTTL(n) {
this.ttl = n;
return this;
}}

class Particle extends RenderObject {
constructor(x, y, bounds, controller) {
super(x, y, bounds);
this.parent = controller;
this.reset = false;
this.bounds = bounds;
this.alpha = 0;
this.hue = 0;
this.frequency = 0;
this.size = 0;
}
update(vX, vY) {
this.life++;
this.setVelocity(vX, vY).
setLastPosition().
checkLife().
checkBounds();

this.setSize().
setHue().
setAlpha().
setColor();

this.velocity.multiplyScalar(0.015 * pow(this.normalizedFrequency + this.normalizedAvgFrequency, 3) + 1);
this.position.add(this.velocity);

return this;
}
setIndex(i) {
this.index = i;
return this;
}
checkLife() {
if (this.life >= this.ttl) this.reset = true;
return this;
}
checkBounds() {
if (
this.position.x > this.bounds.x + this.size ||
this.position.x < -this.size ||
this.position.y > this.bounds.y + this.size ||
this.position.y < -this.size)
{
this.reset = true;
}
return this;
}
setSize() {
this.size = pow(this.normalizedFrequency * 4, 3) + 2;
return this;
}
setFrequency(currentFrequency, avgFrequency) {
this.frequency = currentFrequency;
this.normalizedFrequency = currentFrequency / 256;
this.avgFrequency = avgFrequency;
this.normalizedAvgFrequency = avgFrequency / 128;
return this;
}
setHue() {
this.hue =
1.5 * (this.index / this.parent.count - this.frequency + this.parent.delta);
return this;
}
setAlpha() {
this.alpha = fadeInOut(this.life, this.ttl) * this.normalizedFrequency;
return this;
}
setColor() {
this.color = `hsla(${this.hue}, 75%, 50%, ${this.alpha})`;
return this;
}
draw(canvas) {
canvas.buffer.save();
canvas.arc(this.position.x, this.position.y, this.size, 0, TAU, this.color);
canvas.buffer.restore();
return this;
}}
class ParticleController extends VectorArrayObjectController {
constructor(count, max, canvas) {
super(count, count);
this.delta = 0;
this.canvas = canvas;
this.bounds = canvas.dimensions;
this.populate();
}
populate() {
this.renderTarget = new Particle(0, 0, this.bounds, this);
for (let i = 0; i < this.count; i++) {
this.initRenderTarget(i);
}}
initRenderTarget(i) {
let x, y, theta, rTheta, rDist, vX, vY, ttl;

rTheta = rand(PI);
rDist = rand(0.5 * this.bounds.y);

x = 0.5 * this.bounds.x + rDist * cos(rTheta);
y = this.bounds.y - rDist * sin(rTheta);

this.renderTarget.
setLife(0).
setPosition(x, y).
setLastPosition();

theta = this.canvas.origin.angleTo(this.renderTarget.position);
vX = cos(theta);
vY = sin(theta);
ttl = rand(100) + 100;

this.renderTarget.setVelocity(vX, vY);

this.setVertex(i, x, y).
setLife(i, 0).
setTTL(i, ttl).
setVelocity(i, vX, vY);

this.renderTarget.reset = false;

return this;
}
drawRenderTarget(i, currentFrequency, avgFrequency) {
this.renderTarget.
setIndex(i).
setLife(this.getLife(i)).
setTTL(this.getTTL(i)).
setPosition(this.vertices.getX(i), this.vertices.getY(i)).
setFrequency(currentFrequency, avgFrequency).
update(this.velocities.getX(i), this.velocities.getY(i)).
draw(this.canvas);

this.setVertex(i, this.renderTarget.position.x, this.renderTarget.position.y).
setVelocity(i, this.renderTarget.velocity.x, this.renderTarget.velocity.y).
setLife(i, this.renderTarget.getLife());

if (this.renderTarget.reset) {
this.initRenderTarget(i);
}
}}

class AudioController {
constructor() {
this.playing = false;
this.initAudio();
this.btnInitialize = document.querySelector("#btn-initialize");
this.btnInitialize.addEventListener("click", this.initialize.bind(this));
  // "ended" 이벤트 핸들러를 등록하여 마지막 곡 재생이 완료되면 다음 곡을 재생하거나 처음 곡으로 돌아감
  this.element.addEventListener("ended", () => {
    this.element.currentTime = 0;
    this.element.pause();
    this.currentTrack =
      this.currentTrack < this.fileNames.length - 1 ? this.currentTrack + 1 : 0; // 다음 곡 재생 또는 처음 곡으로 돌아감
    this.load();
  });
    this.initialize(); // 생성자에서 바로 initialize() 호출
}
initialize() {
/*this.element.addEventListener("timeupdate", () => {
this.progressBar.style = `transform: scaleX(${this.element.currentTime /
this.element.duration})`;
});
this.element.addEventListener("ended", () => {
this.element.currentTime = 0;
this.element.pause();
this.currentTrack =
this.currentTrack < this.fileNames.length - 1 ? this.currentTrack + 1 : 1;
this.load();
});
this.initUI();
this.ctx.resume();
this.load();*/
    // 스타트 버튼 클릭 없이 자동으로 음악을 시작하려면 다음 코드 추가
    this.initUI();
    this.ctx.resume();
    this.load();
}
initAudio() {
this.baseURL =
"https://res.cloudinary.com/dlszhsahq/video/upload/";
this.currentFile = {};
this.files = {};
this.fileNames = [
"v1696954343/best/%EC%86%8C%EB%82%98%EA%B8%B0_-_%EC%96%91%EC%A7%80%EC%9D%80_vfgcxq.mp3",
"v1696954340/best/18%EC%84%B8_%EC%88%9C%EC%9D%B4_-_%EC%96%91%EC%A7%80%EC%9D%80_dzlix5.mp3",
"v1696954340/best/%EA%B0%90%EC%88%98%EA%B4%91_-_%EC%96%91%EC%A7%80%EC%9D%80_c9rhd5.mp3",
"v1696954335/best/%EA%B7%B8_%EA%B0%95%EC%9D%84_%EA%B1%B4%EB%84%88%EC%A7%80%EB%A7%88%EC%98%A4_-_%EC%96%91%EC%A7%80%EC%9D%80_neyu8i.mp3"];
this.trackTitles = [
"소나기 - 양지은",
"18세 순이 - 양지은",
"감수광 - 양지은",
"그 강을 건너지마오 - 양지은"];
this.currentTrack = floor(rand(this.fileNames.length));
this.element = document.createElement("audio");
document.body.appendChild(this.element);
this.ctx = new AudioContext();
this.source = this.ctx.createMediaElementSource(this.element);
this.gainNode = this.ctx.createGain();
this.analyser = this.ctx.createAnalyser();
this.analyser.smoothingTimeConstant = 0.88;
this.analyser.minDecibels = -140;
this.analyser.maxDecibels = -10;
this.analyser.fftSize = 1024;
this.source.connect(this.gainNode);
this.gainNode.connect(this.analyser);
this.analyser.connect(this.ctx.destination);
this.gainNode.gain.value = 0.8;
this.freqData = new Uint8Array(this.analyser.frequencyBinCount);
}
initUI() {
this.btnInitialize.classList.add("disabled");
this.controls = {
menu: {
toggle: document.querySelector("#lbl-menu-toggle"),
checkbox: document.querySelector("#chk-menu-toggle") },

parent: document.querySelector("#audio-controls"),
play: document.querySelector("#btn-play"),
next: document.querySelector("#btn-next"),
prev: document.querySelector("#btn-prev"),
seekBar: document.querySelector("#seek-bar"),
volume: {
icon: document.querySelector("#icon-volume"),
element: document.querySelector("#rng-volume") },

trackList: {
parent: document.querySelector("#track-list"),
input: document.querySelector("#in-add-track"),
titleForm: document.querySelector("#frm-track-title"),
options: [] } };

this.progressBar = document.querySelector("#progress-bar");

this.titleLabel = document.querySelector("#title");
this.loader = document.querySelector("#loader");
this.controls.menu.toggle.classList.remove("hidden");
this.controls.parent.classList.remove("hidden");
this.controls.play.addEventListener("click", this.playPause.bind(this));
this.controls.next.addEventListener("click", this.changeTrack.bind(this));
this.controls.prev.addEventListener("click", this.changeTrack.bind(this));
this.controls.volume.element.addEventListener(
"input",
this.changeVolume.bind(this));

this.controls.seekBar.addEventListener("click", this.changeTime.bind(this));
this.controls.trackList.input.addEventListener("change", e => {
let { name } = e.target.files[0];
if (this.validFile(name)) {
this.controls.trackList.titleForm.classList.remove("hidden");
this.controls.trackList.titleForm.title.value = name;
} else {
alert("Audio files only, please! (?°□°)?? ┻━┻");
}
});
this.controls.trackList.titleForm.addEventListener("submit", e => {
e.preventDefault();
this.controls.trackList.titleForm.classList.add("hidden");
this.uploadFile(
String(e.target.title.value),
this.controls.trackList.input.files[0]);
});
for (let i = 0; i < this.trackTitles.length; i++) {
this.addTrackOption(i, this.trackTitles[i]);
}
}
changeTime(e) {
this.element.currentTime =
this.element.duration * (e.clientX / e.target.offsetWidth);
}
changeVolume(e) {
let { value } = e.target;
this.gainNode.gain.value = value;
this.controls.volume.icon.className = "fa";
this.controls.volume.icon.classList.add(
value > 0.5 ? "fa-volume-up" : value > 0 ? "fa-volume-down" : "fa-volume-off");
}
addTrackOption(i, title) {
let el = document.createElement("li");
el.className = "track-option";
el.setAttribute("data-track", i);
el.innerHTML = `${i + 1}. ${title}`;
el.addEventListener("click", e => {
this.currentTrack = parseInt(e.target.getAttribute("data-track"));
this.closeMenu();
this.load();
});
this.controls.trackList.parent.appendChild(el);
}
validFile(fileName) {
return /(\.mp3|\.mp4|\.m4a|\.wav|\.flac|\.ogg)/gi.test(fileName);
}
uploadFile(title, data) {
this.files[data.name] = this.currentFile = {
title,
data };

this.fileNames.push(data.name);
this.trackTitles.push(title);
this.currentTrack = this.fileNames.length - 1;
this.addTrackOption(this.fileNames.length - 1, title);
this.closeMenu();
this.play();
}
closeMenu() {
this.controls.menu.checkbox.checked = false;
}
changeTrack(e) {
let value = parseInt(e.target.getAttribute("data-value"));
this.currentTrack += value;
if (this.currentTrack < 0) this.currentTrack = this.fileNames.length - 1;
if (this.currentTrack > this.fileNames.length - 1) this.currentTrack = 0;
this.load();
}
playPause() {
if (this.playing) {
this.controls.play.classList.remove("fa-pause");
this.controls.play.classList.add("fa-play");
this.playing = false;
this.element.pause();
} else {
this.controls.play.classList.remove("fa-play");
this.controls.play.classList.add("fa-pause");
this.playing = true;
this.element.play();
}
}
load() {
this.controls.parent.classList.add("disabled");
this.loader.classList.remove("hidden");
this.loader.classList.add("loading");
if (this.files[this.fileNames[this.currentTrack]]) {
this.currentFile = this.files[this.fileNames[this.currentTrack]];
this.play();
} else {
let request = new XMLHttpRequest();

request.open("GET", this.baseURL + this.fileNames[this.currentTrack], true);
request.responseType = "blob";
request.onload = () => {
this.files[this.fileNames[this.currentTrack]] = this.currentFile = {
title: this.trackTitles[this.currentTrack],
data: request.response };
this.play();
};
request.send();
}
}
play() {
this.playing = true;
this.audioReady = true;
this.controls.parent.classList.remove("disabled");
this.controls.play.classList.remove("fa-play");
this.controls.play.classList.add("fa-pause");
this.titleLabel.innerHTML = this.currentFile.title;
this.loader.classList.remove("loading");
this.loader.classList.add("hidden");
this.element.src = window.URL.createObjectURL(this.currentFile.data);
this.element.play();
}
getFrequencyData() {
this.analyser.getByteFrequencyData(this.freqData);
return this.freqData;
}}

class Canvas {
constructor(selector) {
this.element =
document.querySelector(selector) ||
(() => {
let element = document.createElement("canvas");
element.style = `position: absolute; top: 0; left: 0; z-index: 0; width: 100vw; height: calc(100vh - 65px);`;
document.body.appendChild(element);
return element;
})();
this.ctx = this.element.getContext("2d");
this.frame = document.createElement("canvas");
this.buffer = this.frame.getContext("2d");
this.dimensions = new Vector2();
this.origin = new Vector2();
window.addEventListener("resize", this.resize.bind(this));
this.resize();
}
resize() {
this.dimensions.x = this.frame.width = this.element.width = window.innerWidth;
this.dimensions.y = this.frame.height = this.element.height =
window.innerHeight;
this.origin.x = 0.5 * this.dimensions.x;
this.origin.y = this.dimensions.y;
}
clear() {
this.ctx.clearRect(0, 0, this.dimensions.x, this.dimensions.y);
this.buffer.clearRect(0, 0, this.dimensions.x, this.dimensions.y);
}
line(x1, y1, x2, y2, w, c) {
this.buffer.beginPath();
this.buffer.strokeStyle = c;
this.buffer.lineWidth = w;
this.buffer.moveTo(x1, y1);
this.buffer.lineTo(x2, y2);
this.buffer.stroke();
this.buffer.closePath();
}
fill(c) {
this.buffer.fillStyle = c;
this.buffer.fillRect(0, 0, this.dimensions.x, this.dimensions.y);
}
rect(x, y, w, h, c) {
this.buffer.fillStyle = c;
this.buffer.fillRect(x, y, w, h);
}
arc(x, y, r, s, e, c) {
this.buffer.beginPath();
this.buffer.fillStyle = c;
this.buffer.arc(x, y, r, s, e);
this.buffer.fill();
this.buffer.closePath();
}
render() {
this.ctx.drawImage(this.frame, 0, 0);
}
drawImage(image, x = 0, y = 0) {
this.buffer.drawImage(image, x, y);
}
}

class MPApp {
constructor() {
this.canvas = new Canvas();
this.audio = new AudioController();
this.particles = new ParticleController(
this.audio.analyser.frequencyBinCount,
this.audio.analyser.frequencyBinCount,
this.canvas);

this.initUI();
this.update();
}
initUI() {
this.controls = {
particles: document.querySelector("#chk-particles"),
backlight: document.querySelector("#chk-backlight"),
spectrum: document.querySelector("#chk-spectrum"),
glow: document.querySelector("#chk-glow") };

this.drawParticles = this.controls.particles.checked;
this.controls.particles.addEventListener(
"click",
e => this.drawParticles = e.target.checked);

this.backlight = this.controls.backlight.checked;
this.controls.backlight.addEventListener(
"click",
e => this.backlight = e.target.checked);

this.spectrum = this.controls.spectrum.checked;
this.controls.spectrum.addEventListener(
"click",
e => this.spectrum = e.target.checked);

this.glow = this.controls.glow.checked;
this.controls.glow.addEventListener(
"click",
e => this.glow = e.target.checked);

}
draw(freqData) {
const spectrumWidth = 0.5 * freqData.length;
let x, y, width, norm, hue, scale, data;

this.particles.delta += 0.15;
this.avgFrequency = freqData.reduce((a, b) => a + b + 1) / freqData.length;
this.canvas.clear();
if (this.backlight) {
this.drawBacklight(freqData);
}
for (let i = 0; i < this.particles.count; i++) {
data = freqData[i];
if (this.drawParticles) {
this.particles.drawRenderTarget(i, data, this.avgFrequency);
}
if (this.spectrum && i < spectrumWidth) {
x = i / spectrumWidth * this.canvas.origin.x;
y = this.canvas.dimensions.y;
width = this.canvas.origin.x / spectrumWidth * 0.5;
norm = data / 256;
hue = 110 * (1 - norm);
scale = norm * (0.25 * this.canvas.dimensions.y);

this.canvas.line(
this.canvas.origin.x + x + width,
y,
this.canvas.origin.x + x + width,
y - scale,
width,
`hsla(${hue}, 75%, 50%, 1)`);

this.canvas.line(
this.canvas.origin.x - x - width,
y,
this.canvas.origin.x - x - width,
y - scale,
width,
`hsla(${hue}, 75%, 50%, 1)`);
}
}
if (this.glow) {
this.drawGlowLayer();
}
this.canvas.render();
}
drawBacklight(freqData) {
let avg, hue, gradient;
hue = (128 - this.avgFrequency) / 128 * 100;
gradient = this.canvas.buffer.createRadialGradient(
this.canvas.origin.x,
this.canvas.origin.y,
0,
this.canvas.origin.x,
this.canvas.origin.y,
0.65 * this.canvas.dimensions.y);

gradient.addColorStop(0, `hsla(${hue}, 75%, 70%, ${pow(this.avgFrequency / 128, 2)})`);
gradient.addColorStop(1, `hsla(${hue}, 75%, 40%, 0)`);
this.canvas.rect(
0,
0,
this.canvas.dimensions.x,
this.canvas.dimensions.y,
gradient);

}
drawGlowLayer() {
this.canvas.ctx.save();
this.canvas.ctx.filter = "blur(5px) saturate(200%)";
this.canvas.render();
this.canvas.ctx.restore();
}
update() {
this.draw(this.audio.getFrequencyData());
window.requestAnimationFrame(this.update.bind(this));
}}

window.addEventListener("load", () => {
let app = new MPApp();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.