SomaFM Music Player Code

<!DOCTYPE html>

<html lang="en" >

<head>

  <meta charset="UTF-8">

  <title>SomaFM Music Player</title>

  <link rel='stylesheet' href='https:////fonts.googleapis.com/css?family=Roboto+Condensed:700'>

<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'> 

<style>

*, *:before, *:after {

  margin: 0;

  padding: 0;

  border: 0;

  outline: none;

  background-color: transparent;

  text-transform: none;

  text-shadow: none;

  box-shadow: none;

  box-sizing: border-box;

  -webkit-appearance: none;

     -moz-appearance: none;

          appearance: none;

  -webkit-overflow-scrolling: touch;

  -webkit-font-smoothing: antialiased;

  -moz-osx-font-smoothing: grayscale;

  transform-style: flat;

  transition: border-color 400ms cubic-bezier(0.215, 0.61, 0.355, 1), background-color 400ms cubic-bezier(0.215, 0.61, 0.355, 1), opacity 400ms cubic-bezier(0.215, 0.61, 0.355, 1), transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

}


article, aside, details, figcaption, figure, footer, header, hgroup,

menu, nav, section, main, summary, div, h1, h2, h3, h4, h5, h6, hr,

p, ol, ul, form, img {

  display: block;

}


input, textarea, select, optgroup, option, button {

  font-family: inherit;

  font-size: inherit;

  font-weight: normal;

  line-height: inherit;

  color: inherit;

}


select, button {

  cursor: pointer;

}


a {

  color: cornflowerblue;

}

a:hover {

  color: #92b4f2;

}


hr {

  display: block;

  overflow: hidden;

  margin: 1em 0;

  height: 0;

  border: 0;

  border-bottom: 2px solid rgba(0, 0, 0, 0.08);

}


html, body {

  display: block;

  position: relative;

  max-width: 100vw;

  min-height: 100vh;

}


html {

  overflow: hidden;

  overflow-y: auto;

}


body {

  font-family: "Roboto Condensed", sans-serif;

  font-weight: 700;

  font-size: calc( 20px - 6px );

  line-height: 1.2em;

  color: #787ba2;

  background-size: cover;

  background-color: #8086a0;

  background-image: linear-gradient(217deg, rgba(220, 20, 60, 0.8), rgba(220, 20, 60, 0) 70.71%), linear-gradient(127deg, #8086a0, rgba(128, 134, 160, 0) 70.71%), linear-gradient(336deg, rgba(100, 149, 237, 0.8), rgba(100, 149, 237, 0) 70.71%);

}

@media only screen and (min-width : 420px) {

  body {

    font-size: calc( 20px - 4px );

  }

}

@media only screen and (min-width : 720px) {

  body {

    font-size: calc( 20px - 2px );

  }

}

@media only screen and (min-width : 1200px) {

  body {

    font-size: 20px;

  }

}


.if-small {

  display: none;

}

@media only screen and (min-width : 420px) {

  .if-small {

    display: initial;

  }

}


.if-medium {

  display: none;

}

@media only screen and (min-width : 720px) {

  .if-medium {

    display: initial;

  }

}


.if-large {

  display: none;

}

@media only screen and (min-width : 1200px) {

  .if-large {

    display: initial;

  }

}


.hidden, [hidden], [v-cloak] {

  display: none;

}


.disabled, [disabled] {

  pointer-events: none;

  opacity: 0.5;

}


.clickable {

  cursor: pointer;

}


.card {

  padding: 1em;

  background-color: rgba(0, 0, 0, 0.08);

  border-radius: 6px;

}


.push-top {

  margin-top: 1em;

}


.push-right {

  margin-right: 1em;

}


.push-bottom {

  margin-bottom: 1em;

}


.push-left {

  margin-left: 1em;

}


.push-all {

  margin: 1em;

}


.pad-top {

  padding-top: 1em;

}


.pad-right {

  padding-right: 1em;

}


.pad-bottom {

  padding-bottom: 1em;

}


.pad-left {

  padding-left: 1em;

}


.pad-all {

  padding: 1em;

}


.border-top {

  border-top: 2px solid rgba(0, 0, 0, 0.08);

}


.border-right {

  border-right: 2px solid rgba(0, 0, 0, 0.08);

}


.border-bottom {

  border-bottom: 2px solid rgba(0, 0, 0, 0.08);

}


.border-left {

  border-left: 2px solid rgba(0, 0, 0, 0.08);

}


.shadow-box {

  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

}


.shadow-text {

  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

}


.fx {

  position: relative;

  -webkit-animation-direction: normal;

          animation-direction: normal;

  -webkit-animation-duration: 400ms;

          animation-duration: 400ms;

  -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);

          animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);

  -webkit-animation-iteration-count: 1;

          animation-iteration-count: 1;

  -webkit-animation-fill-mode: forwards;

          animation-fill-mode: forwards;

}


.fx-notx {

  transition: none !important;

}


.fx-ibk {

  display: inline-block !important;

}


.fx-delay-1 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 1 );

          animation-delay: calc( calc( 400ms / 3 ) * 1 );

}


.fx-delay-2 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 2 );

          animation-delay: calc( calc( 400ms / 3 ) * 2 );

}


.fx-delay-3 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 3 );

          animation-delay: calc( calc( 400ms / 3 ) * 3 );

}


.fx-delay-4 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 4 );

          animation-delay: calc( calc( 400ms / 3 ) * 4 );

}


.fx-delay-5 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 5 );

          animation-delay: calc( calc( 400ms / 3 ) * 5 );

}


.fx-delay-6 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 6 );

          animation-delay: calc( calc( 400ms / 3 ) * 6 );

}


.fx-delay-7 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 7 );

          animation-delay: calc( calc( 400ms / 3 ) * 7 );

}


.fx-delay-8 {

  -webkit-animation-delay: calc( calc( 400ms / 3 ) * 8 );

          animation-delay: calc( calc( 400ms / 3 ) * 8 );

}


@-webkit-keyframes spinRight {

  0% {

    transform: rotate(0deg);

  }

  100% {

    transform: rotate(359deg);

  }

}


@keyframes spinRight {

  0% {

    transform: rotate(0deg);

  }

  100% {

    transform: rotate(359deg);

  }

}

.fx-spin-right {

  -webkit-animation-name: spinRight;

          animation-name: spinRight;

  -webkit-animation-duration: 1s;

          animation-duration: 1s;

  -webkit-animation-timing-function: linear;

          animation-timing-function: linear;

  -webkit-animation-iteration-count: infinite;

          animation-iteration-count: infinite;

}


@-webkit-keyframes spinLeft {

  0% {

    transform: rotate(359deg);

  }

  100% {

    transform: rotate(0deg);

  }

}


@keyframes spinLeft {

  0% {

    transform: rotate(359deg);

  }

  100% {

    transform: rotate(0deg);

  }

}

.fx-spin-left {

  -webkit-animation-name: spinLeft;

          animation-name: spinLeft;

  -webkit-animation-duration: 1s;

          animation-duration: 1s;

  -webkit-animation-timing-function: linear;

          animation-timing-function: linear;

  -webkit-animation-iteration-count: infinite;

          animation-iteration-count: infinite;

}


@-webkit-keyframes fadeIn {

  0% {

    opacity: 0;

  }

  100% {

    opacity: 1;

  }

}


@keyframes fadeIn {

  0% {

    opacity: 0;

  }

  100% {

    opacity: 1;

  }

}

.fx-fade-in {

  opacity: 0;

  -webkit-animation-name: fadeIn;

          animation-name: fadeIn;

}


@-webkit-keyframes fadeOut {

  0% {

    opacity: 1;

  }

  100% {

    opacity: 0;

  }

}


@keyframes fadeOut {

  0% {

    opacity: 1;

  }

  100% {

    opacity: 0;

  }

}

.fx-fade-out {

  opacity: 1;

  -webkit-animation-name: fadeOut;

          animation-name: fadeOut;

}


@-webkit-keyframes dropIn {

  0% {

    opacity: 0;

    transform: scale(1.4);

  }

  100% {

    opacity: 1;

    transform: scale(1);

  }

}


@keyframes dropIn {

  0% {

    opacity: 0;

    transform: scale(1.4);

  }

  100% {

    opacity: 1;

    transform: scale(1);

  }

}

.fx-drop-in {

  opacity: 0;

  transform: scale(1.4);

  -webkit-animation-name: dropIn;

          animation-name: dropIn;

}


@-webkit-keyframes zoomIn {

  0% {

    opacity: 0;

    transform: scale(0.4);

  }

  100% {

    opacity: 1;

    transform: scale(1);

  }

}


@keyframes zoomIn {

  0% {

    opacity: 0;

    transform: scale(0.4);

  }

  100% {

    opacity: 1;

    transform: scale(1);

  }

}

.fx-zoom-in {

  opacity: 0;

  transform: scale(0.4);

  -webkit-animation-name: zoomIn;

          animation-name: zoomIn;

}


@-webkit-keyframes zoomOut {

  0% {

    opacity: 1;

    transform: scale(1);

  }

  100% {

    opacity: 0;

    transform: scale(0.4);

  }

}


@keyframes zoomOut {

  0% {

    opacity: 1;

    transform: scale(1);

  }

  100% {

    opacity: 0;

    transform: scale(0.4);

  }

}

.fx-zoom-out {

  opacity: 1;

  transform: scale(1);

  -webkit-animation-name: zoomOut;

          animation-name: zoomOut;

}


@-webkit-keyframes slideLeft {

  0% {

    opacity: 0;

    transform: translateX(80px);

  }

  100% {

    opacity: 1;

    transform: translateX(0);

  }

}


@keyframes slideLeft {

  0% {

    opacity: 0;

    transform: translateX(80px);

  }

  100% {

    opacity: 1;

    transform: translateX(0);

  }

}

.fx-slide-left {

  opacity: 0;

  transform: translateX(80px);

  -webkit-animation-name: slideLeft;

          animation-name: slideLeft;

}


@-webkit-keyframes slideRight {

  0% {

    opacity: 0;

    transform: translateX(calc( 0 - 80px ));

  }

  100% {

    opacity: 1;

    transform: translateX(0);

  }

}


@keyframes slideRight {

  0% {

    opacity: 0;

    transform: translateX(calc( 0 - 80px ));

  }

  100% {

    opacity: 1;

    transform: translateX(0);

  }

}

.fx-slide-right {

  opacity: 0;

  transform: translateX(calc( 0 - 80px ));

  -webkit-animation-name: slideRight;

          animation-name: slideRight;

}


@-webkit-keyframes slideUp {

  0% {

    opacity: 0;

    transform: translateY(80px);

  }

  100% {

    opacity: 1;

    transform: translateY(0);

  }

}


@keyframes slideUp {

  0% {

    opacity: 0;

    transform: translateY(80px);

  }

  100% {

    opacity: 1;

    transform: translateY(0);

  }

}

.fx-slide-up {

  opacity: 0;

  transform: translateY(80px);

  -webkit-animation-name: slideUp;

          animation-name: slideUp;

}


@-webkit-keyframes slideDown {

  0% {

    opacity: 0;

    transform: translateY(calc( 0 - 80px ));

  }

  100% {

    opacity: 1;

    transform: translateY(0);

  }

}


@keyframes slideDown {

  0% {

    opacity: 0;

    transform: translateY(calc( 0 - 80px ));

  }

  100% {

    opacity: 1;

    transform: translateY(0);

  }

}

.fx-slide-down {

  opacity: 0;

  transform: translateY(calc( 0 - 80px ));

  -webkit-animation-name: slideDown;

          animation-name: slideDown;

}


@-webkit-keyframes pulseFade {

  0% {

    opacity: 0.7;

  }

  50% {

    opacity: 1;

  }

  100% {

    opacity: 0.7;

  }

}


@keyframes pulseFade {

  0% {

    opacity: 0.7;

  }

  50% {

    opacity: 1;

  }

  100% {

    opacity: 0.7;

  }

}

.fx-pulse {

  opacity: 0.7;

  -webkit-animation-name: pulseFade;

          animation-name: pulseFade;

  -webkit-animation-duration: 1s;

          animation-duration: 1s;

  -webkit-animation-timing-function: linear;

          animation-timing-function: linear;

  -webkit-animation-iteration-count: infinite;

          animation-iteration-count: infinite;

}


.flex-row {

  display: flex;

  flex-direction: row;

  flex-wrap: nowrap;

}


.flex-wrap {

  flex-wrap: wrap;

}


.flex-left {

  justify-content: flex-start;

}


.flex-center {

  justify-content: center;

}


.flex-right {

  justify-content: flex-end;

}


.flex-space {

  justify-content: space-between;

}


.flex-around {

  justify-content: space-around;

}


.flex-stretch {

  justify-content: stretch;

}


.flex-top {

  align-items: flex-start;

}


.flex-middle {

  align-items: center;

}


.flex-bottom {

  align-items: flex-end;

}


.flex-half {

  flex: 0.5;

}


.flex-1 {

  flex: 1;

}


.flex-2 {

  flex: 2;

}


.flex-3 {

  flex: 3;

}


.flex-4 {

  flex: 4;

}


.flex-5 {

  flex: 5;

}


.flex-autorow {

  display: flex;

  flex-direction: column;

  flex-wrap: nowrap;

}

.flex-autorow > .flex-item {

  flex: 1;

  width: 100%;

  margin: 0 0 1em 0;

}

.flex-autorow > .flex-item:last-of-type {

  margin: 0;

}

@media only screen and (min-width : 720px) {

  .flex-autorow {

    flex-direction: row;

  }

  .flex-autorow > .flex-item {

    margin: 0 1em 0 0;

  }

  .flex-autorow > .flex-item:last-of-type {

    margin: 0;

  }

}


.img-round {

  overflow: hidden;

  text-indent: -1000px;

  border-radius: 1000px;

  border: 2px solid whitesmoke;

  background-color: #32334f;

  background-image: linear-gradient(45deg, #32334f, #4f527e);

  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

}


.img-center {

  display: block;

  margin: 0 auto;

}


.common-btn {

  display: inline-block;

  text-align: center;

  font-size: 180%;

  font-weight: normal;

  line-height: 1em;

  width: 1em;

  color: whitesmoke;

}

.common-btn:hover {

  color: #c2c2c2;

}


.cta-btn {

  display: inline-block;

  text-decoration: none;

  padding: 0.5em 1em;

  color: #fac3ce;

  background-color: #a41935;

  border-radius: 100px;

  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

  line-height: 1.1em;

}

.cta-btn:hover {

  color: #fcdae1;

  background-color: #c51236;

}


.form-input {

  display: flex;

  flex: 1;

  flex-direction: row;

  align-items: center;

  justify-content: stretch;

  color: whitesmoke;

}

.form-input > input {

  flex: 1;

  line-height: 1.5em;

  padding: 0 0.5em;

}


.form-slider {

  display: flex;

  position: relative;

  flex-direction: row;

  align-items: center;

  justify-content: stretch;

  width: 100%;

  max-width: 6em;

  line-height: 1em;

}

.form-slider > input {

  -webkit-appearance: none;

  -moz-appearance: none;

       appearance: none;

  width: 100%;

  margin: 0 0.5em;

}

.form-slider > input::-webkit-slider-runnable-track {

  width: 100%;

  height: 3px;

  background-color: #32334f;

  color: transparent !important;

  border-color: transparent !important;

  border-radius: 6px !important;

  border: 0 !important;

}

.form-slider > input::-moz-range-track {

  width: 100%;

  height: 3px;

  background-color: #32334f;

  color: transparent !important;

  border-color: transparent !important;

  border-radius: 6px !important;

  border: 0 !important;

}

.form-slider > input::-ms-track {

  width: 100%;

  height: 3px;

  background-color: #32334f;

  color: transparent !important;

  border-color: transparent !important;

  border-radius: 6px !important;

  border: 0 !important;

}

.form-slider > input::-webkit-slider-thumb {

  -webkit-appearance: none;

  width: 1em;

  height: 1em;

  margin: -0.4em 0 0 0;

  border-radius: 50%;

  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

  background-color: whitesmoke;

  -webkit-transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

  transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

  color: transparent !important;

  border-color: transparent !important;

  border: 0 !important;

  cursor: pointer;

}

.form-slider > input::-webkit-slider-thumb:hover {

  background-color: #c2c2c2;

}

.form-slider > input::-moz-range-thumb {

  width: 1em;

  height: 1em;

  margin: -0.4em 0 0 0;

  border-radius: 50%;

  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

  background-color: whitesmoke;

  -moz-transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

  transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

  color: transparent !important;

  border-color: transparent !important;

  border: 0 !important;

  cursor: pointer;

}

.form-slider > input::-moz-range-thumb:hover {

  background-color: #c2c2c2;

}

.form-slider > input::-ms-thumb {

  width: 1em;

  height: 1em;

  margin: -0.4em 0 0 0;

  border-radius: 50%;

  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

  background-color: whitesmoke;

  -ms-transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

  transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);

  color: transparent !important;

  border-color: transparent !important;

  border: 0 !important;

  cursor: pointer;

}

.form-slider > input::-ms-thumb:hover {

  background-color: #c2c2c2;

}


@-webkit-keyframes popoverShow {

  0% {

    transform: translateX(-50%) scale(0.8);

    opacity: 0;

  }

  35% {

    transform: translateX(-50%) scale(1.2);

    opacity: 0.8;

  }

  100% {

    transform: translateX(-50%) scale(1);

    opacity: 1;

  }

}


@keyframes popoverShow {

  0% {

    transform: translateX(-50%) scale(0.8);

    opacity: 0;

  }

  35% {

    transform: translateX(-50%) scale(1.2);

    opacity: 0.8;

  }

  100% {

    transform: translateX(-50%) scale(1);

    opacity: 1;

  }

}

.popover {

  position: relative;

}

.popover .popover-box {

  display: none;

  position: absolute;

  padding: 0.5em 0;

  max-width: 300px;

  min-height: 100px;

  left: 50%;

  bottom: 50%;

  transition: none;

  transform: translateX(-50%);

  background-color: #2e2f49;

  border-radius: 6px;

  box-shadow: 0 1px 20px rgba(0, 0, 0, 0.6);

  -webkit-animation: popoverShow 400ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;

          animation: popoverShow 400ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;

  z-index: 2000;

}

.popover .popover-box:before {

  content: "";

  display: none;

  position: absolute;

  transition: none;

  width: 0;

  height: 0;

  transform: translateX(-50%);

  left: 50%;

  z-index: 2001;

}

.popover .popover-box > button {

  display: block;

  width: 100%;

  text-align: left;

  padding: 0.5em 1em;

  line-height: 1.2em;

  white-space: nowrap;

  background-color: rgba(30, 31, 48, 0);

}

.popover .popover-box > button:hover {

  background-color: rgba(30, 31, 48, 0.2);

}

.popover .popover-box > button + button {

  border-top: 2px solid rgba(0, 0, 0, 0.08);

}

.popover .popover-box.popover-left {

  transform: none;

  left: auto;

  right: 0;

}

.popover .popover-box.popover-right {

  transform: none;

  left: 0;

  right: auto;

}

.popover .popover-box.popover-top {

  top: auto;

  bottom: 100%;

}

.popover .popover-box.popover-top:before {

  display: block;

  top: auto;

  bottom: -10px;

  border-left: 10px solid transparent;

  border-right: 10px solid transparent;

  border-top: 10px solid #2e2f49;

}

.popover .popover-box.popover-bottom {

  top: 100%;

  bottom: auto;

}

.popover .popover-box.popover-bottom:before {

  display: block;

  top: -10px;

  bottom: auto;

  border-left: 10px solid transparent;

  border-right: 10px solid transparent;

  border-bottom: 10px solid #2e2f49;

}

.popover:hover > .popover-box, .popover:active > .popover-box {

  display: block;

}


h1, h2, h3, h4, h5, h6 {

  display: block;

  font-weight: normal;

  line-height: 1.1em;

  color: whitesmoke;

}


h1 {

  font-size: 220%;

}


h2 {

  font-size: 200%;

}


h3 {

  font-size: 180%;

}


h4 {

  font-size: 160%;

}


h5 {

  font-size: 140%;

}


h6 {

  font-size: 120%;

}


.text-left {

  text-align: left;

}


.text-right {

  text-align: right;

}


.text-center {

  text-align: center;

}


.text-justify {

  text-align: justify;

}


.text-uppercase {

  text-transform: uppercase;

}


.text-lowercase {

  text-transform: lowercase;

}


.text-capitalize {

  text-transform: capitalize;

}


.text-underline {

  text-decoration: underline;

}


.text-striked {

  text-decoration: line-through;

}


.text-italic {

  font-style: italic;

}


.text-bold {

  font-weight: bold;

}


.text-nowrap {

  white-space: nowrap;

}


.text-clip {

  overflow: hidden;

  white-space: nowrap;

  text-overflow: ellipsis;

}


.text-primary {

  color: crimson;

}


.text-secondary {

  color: cornflowerblue;

}


.text-grey {

  color: slategray;

}


.text-bright {

  color: whitesmoke;

}


.text-faded {

  opacity: 0.5;

}


.text-big {

  font-size: 120%;

}


.text-bigger {

  font-size: 180%;

}


.text-huge {

  font-size: 240%;

}


.text-small {

  font-size: 90%;

}


.text-condense {

  letter-spacing: -1px;

}


.app-wrap {

  display: flex;

  flex-direction: row;

  align-items: center;

  justify-content: center;

  flex-wrap: nowrap;

  min-height: 100vh;

  width: 100%;

}


.player-wrap {

  display: block;

  overflow: hidden;

  position: relative;

  flex: 1;

  width: 100%;

  height: 100vh;

  background-color: #1e1f30;

}

.player-wrap > .player-bg, .player-wrap > .player-canvas {

  display: block;

  position: absolute;

  left: 0;

  top: 0;

  width: 100%;

  height: 100%;

  z-index: 0;

}

.player-wrap > .player-bg {

  background-image: url("https://raw.githubusercontent.com/rainner/soma-fm-player/master/public/img/bg.jpg");

  background-position: bottom right;

  background-repeat: no-repeat;

  background-size: cover;

  opacity: 0.4;

}

@media only screen and (min-width : 720px) {

  .player-wrap {

    margin: 0 2em;

    max-width: 1080px;

    height: calc( 100vh - ( 1em * 4 ) );

    max-height: 700px;

    border-radius: 6px;

    box-shadow: 0 1px 30px rgba(0, 0, 0, 0.8);

  }

}


.player-layout {

  display: flex;

  flex-direction: column;

  align-items: stretch;

  justify-content: stretch;

  height: 100%;

}

.player-layout .player-header,

.player-layout .player-content,

.player-layout .player-footer {

  position: relative;

}

.player-layout .player-header,

.player-layout .player-footer {

  padding: 0 1em;

  height: 3.5em;

  min-height: 3.5em;

  background-color: rgba(0, 0, 0, 0.08);

}

.player-layout .player-header > h2 {

  color: crimson;

}

.player-layout .player-header > h2 i {

  vertical-align: bottom;

}

.player-layout .player-content {

  flex: 1;

  height: 100%;

  overflow: hidden;

  overflow-y: auto;

  padding: 1em;

}

.player-layout .player-content > section {

  margin: auto 0;

}

@media only screen and (min-width : 720px) {

  .player-layout .player-content {

    padding: 1em 2em;

  }

}


.player-greet {

  flex: 1;

}

@media only screen and (min-width : 720px) {

  .player-greet {

    flex: 0.5;

  }

}


.player-tracklist {

  display: block;

  position: relative;

  list-style: none;

}

.player-tracklist > li + li {

  margin-top: 0.5em;

}


.player-controls {

  position: relative;

}


.player-stations {

  position: absolute;

  left: 0;

  top: 0;

  width: 100%;

  height: 100%;

  background-color: rgba(0, 0, 0, 0);

  pointer-events: none;

  z-index: 1;

}

.player-stations .player-stations-sidebar {

  display: flex;

  flex-direction: column;

  flex-wrap: nowrap;

  justify-content: stretch;

  position: absolute;

  top: 0;

  right: -320px;

  width: 320px;

  min-height: 100%;

  max-height: 100%;

  background-color: #222336;

}

@media only screen and (min-width : 420px) {

  .player-stations .player-stations-sidebar {

    right: -420px;

    width: 420px;

  }

}

.player-stations .player-stations-sidebar .player-stations-header,

.player-stations .player-stations-sidebar .player-stations-footer {

  padding: 0 1em;

  min-height: 3.5em;

  box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);

}

.player-stations .player-stations-sidebar .player-stations-list {

  display: block;

  list-style: none;

  overflow: hidden;

  overflow-y: auto;

  margin-left: -10px;

  padding-left: 10px;

  flex: 1;

}

.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item {

  position: relative;

  padding: 1em;

  background-color: rgba(0, 0, 0, 0.1);

  cursor: pointer;

}

.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item:nth-child(odd) {

  background-color: rgba(0, 0, 0, 0.18);

}

.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item:hover {

  background-color: rgba(0, 0, 0, 0);

}

.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item.active {

  background-color: #1a1b2a;

}

.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item.active h6 {

  color: crimson;

}

.player-stations.visible {

  background-color: rgba(0, 0, 0, 0.4);

  pointer-events: auto;

  z-index: 1000;

}

.player-stations.visible .player-stations-sidebar {

  transform: translateX(-320px);

  box-shadow: 0 1px 20px rgba(0, 0, 0, 0.6);

}

@media only screen and (min-width : 420px) {

  .player-stations.visible .player-stations-sidebar {

    transform: translateX(-420px);

  }

}

.player-stations.visible .player-stations-list-item.active:before {

  content: "";

  display: block;

  position: absolute;

  transition: none;

  transform: translateY(-50%);

  top: 50%;

  left: -10px;

  border-top: 10px solid transparent;

  border-bottom: 10px solid transparent;

  border-right: 10px solid #1a1b2a;

}

</style>


  <script>

  window.console = window.console || function(t) {};

</script>


  

  

</head>


<body translate="no">

  <!-- app root container -->

<div class="app-wrap" id="app" v-cloak>


  <!-- app player container -->

  <main class="player-wrap fx fx-fade-in" ref="playerWrap" style="opacity: 0">


    <!-- bg absolute elements -->

    <figure class="player-bg" ref="playerBg"></figure>

    <canvas class="player-canvas" ref="playerCanvas"></canvas>


    <!-- main player layout -->

    <section class="player-layout">


      <!-- player top header -->

      <header class="player-header flex-row flex-middle flex-stretch">

        <h2 class="text-clip flex-1"><i class="fa fa-headphones"></i> <span>Soma FM Player</span></h2>

        <button class="text-nowrap common-btn" @click="toggleSidebar( true )"><i class="fa fa-bars"></i></button>

      </header>


      <!-- player middle content area -->

      <main class="player-content flex-row">


        <!-- default greet message -->

        <section class="player-greet" v-if="!hasChannel && !hasErrors">

          <div class="fx fx-slide-left push-bottom"><h1>Pick a Station</h1></div>

          <div class="fx fx-slide-left fx-delay-1 push-bottom">This is a music streaming player for the channels provided by SomaFM.com. Just pick a station from the sidebar to the right to start listening.</div>

          <div class="fx fx-slide-up fx-delay-2 pad-top"><button class="cta-btn" @click="toggleSidebar( true )"><i class="fa fa-headphones">&nbsp;</i> View Stations</button></div>

        </section>


        <!-- show selected channel info if possible -->

        <section class="player-channel flex-1" v-if="hasChannel && !hasErrors" :key="channel.id">

          <div class="flex-autorow flex-middle flex-stretch">


            <!-- station details -->

            <div class="flex-item flex-1">

              <!-- station -->

              <div class="push-bottom pad-bottom border-bottom">

                <div class="flex-row flex-middle">

                  <img class="img-round fx fx-drop-in fx-delay-1" :src="channel.largeimage" width="80" height="80" :alt="channel.title" />

                  <div class="pad-left fx fx-slide-left fx-delay-2">

                    <div class="text-clip text-uppercase">{{ channel.genre | toSpaces }}</div>

                    <h2 class="text-clip">{{ channel.title }}</h2>

                  </div>

                </div>

              </div>

              <!-- description -->

              <div class="push-bottom pad-bottom border-bottom fx fx-slide-up fx-delay-3">

                {{ channel.description }}

              </div>

              <!-- current track -->

              <div class="push-bottom pad-bottom border-bottom fx fx-slide-up fx-delay-4" :key="track.date">

                <div><span class="text-faded">DJ:</span> <span class="text-default">{{ channel.dj | toText( 'N/A' ) }}</span></div>

                <div><span class="text-faded">Playing:</span> <span class="text-secondary">{{ track.title | toText( 'N/A' ) }}</span></div>

                <div><span class="text-faded">From:</span> <span class="text-bright">{{ track.album | toText( 'N/A' ) }}</span></div>

                <div><span class="text-faded">By:</span> <span class="text-default">{{ track.artist | toText( 'N/A' ) }}</span></div>

              </div>

              <!-- buttons -->

              <div class="push-bottom">

                <a class="cta-btn text-nowrap fx fx-slide-up fx-delay-5" :href="channel.twitter" title="Open link" target="_blank">

                  <i class="fa fa-twitter"></i> Twitter

                </a> &nbsp;

                <a class="cta-btn text-nowrap fx fx-slide-up fx-delay-6" :href="channel.infourl" title="Channel page" target="_blank">

                  <span class="fx fx-notx fx-ibk fx-drop-in fx-delay-1" :key="channel.listeners"><i class="fa fa-headphones"></i> {{ channel.listeners | toCommas( 0 ) }}</span>

                </a> &nbsp;

                <a class="cta-btn text-nowrap fx fx-slide-up fx-delay-7" :href="channel.plsfile" title="Download PLS" target="_blank">

                  <i class="fa fa-download"></i>

                </a> &nbsp;

              </div>

            </div>


            <!-- songs list -->

            <div class="flex-item flex-1">

              <div class="push-bottom">

                <h5 class="fx fx-slide-left fx-delay-1">Recent Tracks</h5>

              </div>

              <div class="card push-bottom" v-if="!hasSongs">

                There are no songs loaded yet for this station.

              </div>

              <ul class="player-tracklist push-bottom" v-if="hasSongs">

                <li v-for="( s, i ) of songsList" :key="s.date" class="card fx" :class="'fx-slide-left fx-delay-' + ( i + 2 )">

                  <div><span class="text-secondary">{{ s.title | toText( 'N/A' ) }}</span></div>

                  <div><span class="text-faded">From:</span> <span class="text-bright">{{ s.album | toText( 'N/A' ) }}</span></div>

                  <div><span class="text-faded">By:</span> <span class="text-default">{{ s.artist | toText( 'N/A' ) }}</span></div>

                </li>

              </ul>

            </div>


          </div>

        </section>


        <!-- show tracks for selected channel if possible -->

        <section class="player-errors flex-1 text-center" v-if="hasErrors" key="errors">

          <div class="push-bottom fx fx-drop-in fx-delay-1">

            <i class="fa fa-plug text-huge text-faded"></i>

          </div>

          <div class="push-bottom fx fx-slide-up fx-delay-2">

            <h3>Oops, there's a problem!</h3>

          </div>

          <hr />

          <div class="text-primary push-bottom fx fx-slide-up fx-delay-3" v-if="errors.init" v-text="errors.init"></div>

          <div class="text-primary push-bottom fx fx-slide-up fx-delay-4" v-if="errors.stream" v-text="errors.stream"></div>

          <hr />

          <button class="cta-btn text-nowrap fx fx-slide-up fx-delay-5" @click="tryAgain">

            <i class="fa fa-refresh"></i> Try again

          </button>

        </section>


      </main>


      <!-- player footer with controls -->

      <footer class="player-footer flex-row flex-middle flex-space">

        <!-- player controls -->

        <section class="player-controls flex-row flex-middle push-right" :class="{ 'disabled': !canPlay }">

          <button class="common-btn" @click="togglePlay()">

            <i v-if="playing" class="fa fa-stop fx fx-drop-in" key="stop"></i>

            <i v-else class="fa fa-play fx fx-drop-in" key="play"></i>

          </button>

          <div class="form-slider push-left">

            <i class="fa fa-volume-down"></i>

            <input class="common-slider" type="range" min="0.0" max="1.0" step="0.1" value="0.5" v-model="volume" />

            <i class="fa fa-volume-up"></i>

          </div>

          <div class="text-clip push-left">

            <span>{{ timeDisplay }}</span>

            <span class="fx fx-fade-in fx-delay-1" v-if="hasChannel" :key="channel.id">&nbsp;|&nbsp;{{ channel.title }}</span>

          </div>

        </section>

        <!-- player links -->

        <section class="player-links text-nowrap">

          <a class="common-btn text-faded" href="" title="View on Github" target="_blank">

            <i class="fa fa-github"></i>

          </a> &nbsp;

          <a class="common-btn text-faded" href="" title="Codepen Projects" target="_blank">

            <i class="fa fa-codepen"></i>

          </a>

        </section>

      </footer>


    </section> <!-- layout wrapper -->


    <!-- player stations overlay + sidebar -->

    <section class="player-stations" :class="{ 'visible': sidebar }" @click="toggleSidebar( false )">

      <aside class="player-stations-sidebar" @click.stop>

        <!-- sidebar search -->

        <header class="player-stations-header flex-row flex-middle flex-stretch">

          <div class="form-input push-right">

            <i class="fa fa-search"></i>

            <input type="text" placeholder="Search station..." v-model="searchText" />

          </div>

          <button class="common-btn" @click="toggleSidebar( false )"><i class="fa fa-times-circle"></i></button>

        </header>

        <!-- sidebar stations list -->

        <ul class="player-stations-list">

          <li class="player-stations-list-item flex-row flex-top flex-stretch" v-for="c of channelsList" :key="c.id" @click="selectChannel( c )" :class="{ 'active': c.active }">

            <figure class="push-right if-small">

              <img class="img-round" width="70" height="70" :src="c.largeimage" :alt="c.title" />

            </figure>

            <aside class="flex-1">

              <div class="flex-row flex-middle flex-space">

                <h6 class="text-bright text-clip">{{ c.title }}</h6>

                <div class="text-secondary"><i class="fa fa-headphones"></i> {{ c.listeners | toCommas( 0 ) }}</div>

              </div>

              <div class="text-small">

                <span class="text-faded text-uppercase text-small">{{ c.genre | toSpaces }}</span> <br />

                {{ c.description }}

              </div>

            </aside>

          </li>

        </ul>

        <!-- sidebar sort options -->

        <footer class="player-stations-footer flex-row flex-middle flex-stretch">

          <div class="flex-1 push-right">

            <span @click="toggleSortOrder()" class="fa clickable" :class="{ 'fa-sort-amount-desc': sortOrder === 'desc', 'fa-sort-amount-asc': sortOrder === 'asc' }">&nbsp;</span>

            <span class="text-faded">Sort: &nbsp;</span>

            <span class="text-secondary popover">

              <span class="clickable">{{ sortLabel }}</span>

              <span class="popover-box popover-top">

                <button @click="sortBy( 'title', 'asc' )">Station Name</button>

                <button @click="sortBy( 'listeners', 'desc' )">Listeners Count</button>

                <button @click="sortBy( 'genre', 'asc' )">Music Genre</button>

              </span>

            </span>

          </div>

          <div>&nbsp;</div>

        </footer>

      </aside>

    </section>


  </main> <!-- player -->


</div> <!-- wrapper -->

  <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/96/three.min.js'></script>

<script src='https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js'></script>

<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js'></script>

      <script id="rendered-js" >

/**

 * Soma FM Web Player

 * Author: 

 * Site: /

 */


//               .andAHHAbnn.

//            .aAHHHAAUUAAHHHAn.

//           dHP^~"        "~^THb.

//     .   .AHF                YHA.   .

//     |  .AHHb.              .dHHA.  |

//     |  HHAUAAHAbn      adAHAAUAHA  |

//     I  HF~"_____        ____ ]HHH  I

//    HHI HAPK""~^YUHb  dAHHHHHHHHHH IHH

//    HHI HHHD> .andHH  HHUUP^~YHHHH IHH

//    YUI ]HHP     "~Y  P~"     THH[ IUP

//     "  `HK                   ]HH'  "

//         THAn.  .d.aAAn.b.  .dHHP

//         ]HHHHAAUP" ~~ "YUAAHHHH[

//         `HHP^~"  .annn.  "~^YHH'

//          YHb    ~" "" "~    dHF

//           "YAb..abdHHbndbndAP"

//            THHAAb.  .adAHHF

//             "UHHHHHHHHHHU"

//               ]HHUUHHHHHH[

//             .adHHb "HHHHHbn.

//      ..andAAHHHHHHb.AHHHHHHHAAbnn..

// .ndAAHHHHHHUUHHHHHHHHHHUP^~"~^YUHHHAAbn.

//   "~^YUHHP"   "~^YUHHUP"        "^YUP^"

//        ""         "~~"


/**

 * Sphere object

 */

const Sphere = {

  group: null,

  shapes: [],

  move: new THREE.Vector3(0, 0, 0),

  ease: 8,


  create(box, scene) {

    this.group = new THREE.Object3D();

    let shape1 = new THREE.CircleGeometry(1, 10);

    let shape2 = new THREE.CircleGeometry(2, 20);

    let points = new THREE.SphereGeometry(100, 30, 14).vertices;

    let material = new THREE.MeshLambertMaterial({ color: 0xffffff, opacity: 0, side: THREE.DoubleSide });

    let center = new THREE.Vector3(0, 0, 0);

    let radius = 12;


    for (let i = 0; i < points.length; i++) {

      let { x, y, z } = points[i];

      let home = { x, y, z };

      let cycle = THREE.Math.randInt(0, 100);

      let pace = THREE.Math.randInt(10, 30);

      let shape = new THREE.Mesh(i % 2 ? shape1 : shape2, material);


      shape.position.set(x, y, z);

      shape.lookAt(center);

      shape.userData = { radius, cycle, pace, home };

      this.group.add(shape);

    }

    this.group.position.set(500, 0, 0);

    this.group.rotation.x = Math.PI / 2 + .6;

    scene.add(this.group);

  },


  update(box, mouse, freq) {

    let bass = Math.floor(freq[1] | 0) / 255;

    this.move.x = box.width * .06 + -(mouse.x * 0.02);

    this.group.position.x += (this.move.x - this.group.position.x) / this.ease;

    this.group.position.y += (this.move.y - this.group.position.y) / this.ease;

    this.group.position.z = 10 + bass * 80;

    this.group.rotation.y -= 0.003;


    for (let i = 0; i < this.group.children.length; i++) {

      let shape = this.group.children[i];

      let { radius, cycle, pace, home } = shape.userData;

      shape.position.set(home.x, home.y, home.z);

      shape.translateZ(bass * Math.sin(cycle / pace) * radius);

      shape.userData.cycle++;

    }

  } };



/**

 * Vue filters

 */

Vue.filter('toCommas', (num, decimals) => {

  let o = { style: 'decimal', minimumFractionDigits: decimals, maximumFractionDigits: decimals };

  return new Intl.NumberFormat('en-US', o).format(num);

});

Vue.filter('toSpaces', str => {

  return String(str || '').trim().replace(/[^\w\`\'\-]+/g, ' ').trim();

});

Vue.filter('toText', (str, def) => {

  str = String(str || '').replace(/[^\w\`\'\-\.\!\?]+/g, ' ').trim();

  return str || String(def || '');

});


/**

 * Vue app

 */

new Vue({

  el: '#app',

  data: {

    // toggles

    init: false,

    playing: false,

    loading: false,

    sidebar: false,

    // channels stuff

    channels: [], // all channels

    channel: {}, // selected channel

    songs: [], // recent tracks

    track: {}, // current track

    errors: {}, // error messages

    // animation stuff

    fxBox: null,

    fxRenderer: null,

    fxScene: null,

    fxColor: null,

    fxLight: null,

    fxCamera: null,

    fxMouse: { x: 0, y: 0 },

    fxObjects: [],

    // audio stuff

    audio: new Audio(),

    context: new AudioContext(),

    freqData: new Uint8Array(),

    audioSrc: null,

    audioGain: null,

    analyser: null,

    volume: 0.5,

    // timer stuff

    timeStart: 0,

    timeDisplay: '00:00:00',

    timeItv: null,

    // sorting stuff

    searchText: '',

    sortParam: 'listeners',

    sortOrder: 'desc',

    // timer stuff

    anf: null,

    sto: null,

    itv: null },



  // watch methods

  watch: {


    // when app is ready

    init() {

      setTimeout(this.setupCanvas, 100);

      setTimeout(this.initSidebar, 500);

    },


    // watch playing status

    playing() {

      if (this.playing) {this.startClock();} else

      {this.stopClock();}

    },


    // update player volume

    volume() {

      this.setVolume(this.volume);

    } },



  // computed methods

  computed: {


    // filter channels list

    channelsList() {

      let list = this.channels.slice();

      let search = this.searchText.replace(/[^\w\s\-]+/g, '').replace(/[\r\s\t\n]+/g, ' ').trim();


      if (search && search.length > 1) {

        let reg = new RegExp('^(' + search + ')', 'i');

        list = list.filter(i => reg.test(i.title + ' ' + i.description));

      }

      if (this.sortParam) {

        list = this.sortList(list, this.sortParam, this.sortOrder);

      }

      if (this.channel.id) {

        list = list.map(i => {

          i.active = this.channel.id === i.id ? true : false;

          return i;

        });

      }

      return list;

    },


    // filter songs list

    songsList() {

      let list = this.songs.slice();

      return list;

    },


    // sort-by label for buttons, etc

    sortLabel() {

      switch (this.sortParam) {

        case 'title':return 'Station Name';

        case 'listeners':return 'Listeners Count';

        case 'genre':return 'Music Genre';}


    },


    // check if audio can be played

    canPlay() {

      return this.channel.id && !this.loading ? true : false;

    },


    // check if a channel is selected

    hasChannel() {

      return this.channel.id ? true : false;

    },


    // check if there are tracks loaded

    hasSongs() {

      return this.songs.length ? true : false;

    },


    // check if there are errors to show

    hasErrors() {

      return this.checkError('init') || this.checkError('stream') ? true : false;

    } },



  // custom methods

  methods: {


    // set an erro message

    setError(key, err) {

      let errors = Object.assign({}, this.errors);

      errors[key] = String(err || '').trim();

      if (err) console.warn('ERROR(' + key + '):', err);

      this.errors = errors;

      this.init = true;

    },


    // check if an error has been set for a key

    checkError(key) {

      return key && this.errors.hasOwnProperty(key) && this.errors[key];

    },


    // clear all error messages

    clearErrors() {

      Object.keys(this.errors).forEach(key => {

        this.errors[key] = '';

      });

    },


    // reset selected channel

    resetPlayer() {

      this.channel = {};

      this.songs = [];

      this.clearErrors();

      this.getChannels(true);

    },


    // try resuming stream problem if possible

    tryAgain() {

      if (this.checkError('init')) return this.resetPlayer();

      if (this.channel.id) return this.playChannel(this.channel);

    },


    // show/hide the sidebar

    toggleSidebar(toggle) {

      this.sidebar = typeof toggle === 'boolean' ? toggle : false;

    },


    // show sidebar at startup if there are no errors

    initSidebar() {

      if (this.checkError('init')) return;

      this.toggleSidebar(true);

    },


    // toggle stream playback for current selected channel

    togglePlay() {

      if (this.loading) return;

      if (this.playing) return this.closeAudio();

      if (this.channel.id) return this.playChannel(this.channel);

    },


    // toggle sort order

    toggleSortOrder() {

      this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';

    },


    // apply sorting and toggle order

    sortBy(param, order) {

      if (this.sortParam === param) {this.toggleSortOrder();} else

      {this.sortOrder = order || 'asc';}

      this.sortParam = param;

    },


    // sort an array by key and order

    sortList(list, param, order) {

      return list.sort((a, b) => {

        if (a.hasOwnProperty(param) && b.hasOwnProperty(param)) {

          let _a = a[param];

          let _b = b[param];


          _a = typeof _a === 'string' ? _a.toUpperCase() : _a;

          _b = typeof _b === 'string' ? _b.toUpperCase() : _b;


          if (order === 'asc') {

            if (_a < _b) return -1;

            if (_a > _b) return 1;

          }

          if (order === 'desc') {

            if (_a > _b) return -1;

            if (_a < _b) return 1;

          }

        }

        return 0;

      });

    },


    // get channels data from api

    getChannels(sidebar) {

      let endpoint = 'https://somafm.com/channels.json';

      let emsg = ['There was a problem trying to load the list of available channels from SomaFM.'];


      axios.get(endpoint).then(res => {

        if (!res || !res.data || !res.data.channels) {

          emsg.push('The API response did not have any channels data available at this time.');

          emsg.push('Status: Channels API Error.');

          return this.setError('channels', emsg.join(' '));

        }

        for (let c of res.data.channels) {

          if (!Array.isArray(c.playlists)) continue;

          // filter and sanitize list of channels

          c.twitter = c.twitter ? 'https://twitter.com/@' + c.twitter : ''; // full twitter url

          c.plsfile = c.playlists.filter(p => p.format === 'mp3' && /^(highest|high)$/.test(p.quality)).shift().url || '';

          c.mp3file = 'http://ice1.somafm.com/' + c.id + '-128-mp3'; // assumed stream url

          c.songsurl = 'https://somafm.com/songs/' + c.id + '.json'; // songs data url

          c.infourl = 'https://somafm.com/' + c.id + '/'; // channel page url

          c.listeners = c.listeners | 0; // force numeric

          c.updated = c.updated | 0; // force numeric

          c.active = false; // select state

          // update selected channel

          if (this.isCurrentChannel(c)) {

            c.active = true;

            this.channel = Object.assign(this.channel, c);

          }

        }

        this.channels = res.data.channels.slice();

        if (sidebar) this.toggleSidebar(true);

        this.setError('init', '');

        this.setError('channels', '');

      }).

      catch(e => {

        emsg.push('Try again, or check your internet connection.');

        emsg.push('Status: ' + String(e.message || 'Channels API Error') + '.');

        let errstr = emsg.join(' ');

        if (!this.channels.length) this.setError('init', errstr);

        this.setError('channels', errstr);

      });

    },


    // fetch songs for a channel

    fetchSongs(channel, cb) {

      if (!channel || !channel.id || !channel.songsurl) return;

      if (!this.isCurrentChannel(channel)) {this.songs = [];this.track = {};}

      let emsg = ['There was a problem trying to load the list of songs for channel ' + channel.title + ' from SomaFM.'];


      axios.get(channel.songsurl).then(res => {

        if (!res || !res.data || !res.data.songs) {

          emsg.push('The API response did not have any songs data available at this time.');

          emsg.push('Status: Songs API Error.');

          return this.setError('songs', emsg.join(' '));

        }

        let songs = res.data.songs.slice();

        this.track = songs.shift();

        this.songs = songs.slice(0, 3);

        this.setError('songs', '');

        if (typeof cb === 'function') cb(songs);

      }).

      catch(e => {

        emsg.push('Try again, or check your internet connection.');

        emsg.push('Status: ' + String(e.message || 'Songs API Error') + '.');

        this.setError('songs', emsg.join(' '));

      });

    },


    // run maintenance tasks on a timer

    setupMaintenance() {

      this.itv = setInterval(() => {

        this.getChannels(); // update channels

        this.fetchSongs(this.channel); // update channel tracks

        // ...

      }, 1000 * 30);

    },


    // setup animation canvas

    setupCanvas() {

      if (!this.$refs.playerWrap) return;

      if (!this.$refs.playerCanvas) return;

      // default canvas and player dimensions

      const player = this.$refs.playerWrap;

      const canvas = this.$refs.playerCanvas;

      // setup THREE renderer and replace default canvas

      this.fxBox = player.getBoundingClientRect();

      this.fxScene = new THREE.Scene();

      this.fxRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, precision: 'highp' });

      this.fxRenderer.setClearColor(0x000000, 0);

      this.fxRenderer.setPixelRatio(window.devicePixelRatio);

      this.fxRenderer.domElement.className = canvas.className;

      // setup camera

      this.fxCamera = new THREE.PerspectiveCamera(60, this.fxBox.width / this.fxBox.height, 0.1, 20000);

      this.fxCamera.lookAt(this.fxScene.position);

      this.fxCamera.position.set(0, 0, 300);

      this.fxCamera.rotation.set(0, 0, 0);

      // light color

      this.fxColor = new THREE.Color();

      this.fxColor.setHSL(this.fxHue, 1, .5);

      // setup light source

      this.fxLight = new THREE.PointLight(0xffffff, 4, 400);

      this.fxLight.position.set(0, 0, 420);

      this.fxLight.castShadow = false;

      this.fxLight.target = this.fxScene;

      this.fxLight.color = this.fxColor;

      this.fxScene.add(this.fxLight);

      // setup canvas and events

      canvas.parentNode.replaceChild(this.fxRenderer.domElement, canvas);

      window.addEventListener('mousemove', this.updateMousePosition);

      window.addEventListener('resize', this.updateStageSize);

      // add objects

      this.fxObjects.push(Sphere);

      // setup objects and start animation

      for (let o of this.fxObjects) o.create(this.fxBox, this.fxScene);

      this.updateStageSize();

      this.updateAnimations();

    },


    // update mouse position from center of canvas

    updateMousePosition(e) {

      if (!this.fxBox || !e) return;

      this.fxMouse.x = Math.max(0, e.pageX || e.clientX || 0) - (this.fxBox.left + this.fxBox.width / 2);

      this.fxMouse.y = Math.max(0, e.pageY || e.clientY || 0) - (this.fxBox.top + this.fxBox.height / 2);

    },


    // update canvas size

    updateStageSize() {

      if (!this.$refs.playerWrap || !this.fxRenderer) return;

      this.fxBox = this.$refs.playerWrap.getBoundingClientRect();

      this.fxCamera.aspect = this.fxBox.width / this.fxBox.height;

      this.fxCamera.updateProjectionMatrix();

      this.fxRenderer.setSize(this.fxBox.width, this.fxBox.height);

    },


    // update light color based on audio freq

    updateStageLight() {

      let dist = Math.floor(this.freqData[1] | 0) / 255;

      let color = Math.floor(this.freqData[16] | 0) / 255;

      this.fxLight.distance = 360 + 140 * dist;

      this.fxColor.setHSL(color, .5, .5);

    },


    // update custom objects in 3d scene

    updateSceneObjects() {

      for (let o of this.fxObjects) {

        o.update(this.fxBox, this.fxMouse, this.freqData);

      }

    },


    // audio visualizer animation loop

    updateAnimations() {

      this.anf = requestAnimationFrame(this.updateAnimations);

      if (!this.fxRenderer || !this.fxCamera || !this.analyser || !this.freqData) return;

      this.analyser.getByteFrequencyData(this.freqData);

      this.updateSceneObjects();

      this.updateStageLight();

      this.fxRenderer.render(this.fxScene, this.fxCamera);

    },


    // setup audio routing and stream events

    setupAudio() {

      // setup audio sources

      this.audioSrc = this.context.createMediaElementSource(this.audio);

      this.audioGain = this.context.createGain();

      this.analyser = this.context.createAnalyser();

      // connect sources

      this.audioSrc.connect(this.audioGain);

      this.audioSrc.connect(this.analyser);

      this.audioGain.connect(this.context.destination);

      this.setVolume(this.volume);

      // check when stream can start playing

      this.audio.addEventListener('canplay', e => {

        this.audio.play();

        this.freqData = new Uint8Array(this.analyser.frequencyBinCount);

      });

      // check if stream is buffering

      this.audio.addEventListener('waiting', e => {

        this.playing = false;

        this.loading = true;

      });

      // check if stream is done buffering

      this.audio.addEventListener('playing', e => {

        this.setError('stream', '');

        this.playing = true;

        this.loading = false;

      });

      // check if stream has ended

      this.audio.addEventListener('ended', e => {

        this.playing = false;

        this.loading = false;

      });

      // check for steam error

      this.audio.addEventListener('error', e => {

        let emsg = [];

        emsg.push('The selected audio stream could not load, or has stopped loading.');

        emsg.push('Try again, or check your internet connection.');

        emsg.push('Status: ' + String(e.message || 'Stream URL Error') + '.');

        this.setError('stream', emsg.join(' '));

        this.playing = false;

        this.loading = false;

      });

    },

    // set audio volume

    setVolume(volume) {

      if (!this.audioGain) return;

      volume = parseFloat(volume) || 0;

      volume = volume < 0 ? 0 : volume;

      volume = volume > 1 ? 1 : volume;

      this.audioGain.gain.value = volume;

    },

    // checks is a channel is currently selected

    isCurrentChannel(channel) {

      if (!channel || !channel.id || !this.channel.id) return false;

      if (this.channel.id !== channel.id) return false;

      return true;

    },

    // play audio stream for a channel

    playChannel(channel) {

      if (this.playing) return;

      this.clearErrors();

      this.audio.src = channel.mp3file + '/?x=' + Date.now();

      this.audio.crossOrigin = 'anonymous';

      this.audio.load();

    },

    // select a channel to play

    selectChannel(channel) {

      if (!channel || !channel.id) return;

      if (this.isCurrentChannel(channel)) return;

      this.closeAudio();

      this.toggleSidebar(false);

      this.playChannel(channel);

      this.fetchSongs(channel);

      this.channel = channel;

    },

    // close active audio

    closeAudio() {

      this.setError('stream', '');

      try {this.audio.pause();} catch (e) {}

      try {this.audio.stop();} catch (e) {}

      try {this.audio.close();} catch (e) {}

      this.playing = false;

    },

    // start tracking playback time

    startClock() {

      this.stopClock();

      this.timeStart = Date.now();

      this.timeItv = setInterval(this.updateClock, 1000);

      this.updateClock();

    },

    // update tracking playback time

    updateClock() {

      let p = n => n < 10 ? '0' + n : '' + n;

      let elapsed = (Date.now() - this.timeStart) / 1000;

      let seconds = Math.floor(elapsed % 60);

      let minutes = Math.floor(elapsed / 60 % 60);

      let hours = Math.floor(elapsed / 3600);

      this.timeDisplay = p(hours) + ':' + p(minutes) + ':' + p(seconds);

    },

    // stop tracking playback time

    stopClock() {

      if (this.timeItv) clearInterval(this.timeItv);

      this.timeItv = null;

    },

    // clear timer refs

    clearTimers() {

      if (this.sto) clearTimeout(this.sto);

      if (this.itv) clearInterval(this.itv);

      if (this.anf) cancelAnimationFrame(this.anf);

    } },

  // on app mounted

  mounted() {

    this.getChannels();

    this.setupAudio();

    this.setupMaintenance();

  },

  // on app destroyed

  destroyed() {

    this.closeAudio();

    this.clearTimers();

  } });

    </script>  

</body>

</html>


Comments