// spacing and padding
$padSpace: 1em;
$padSmall: $padSpace / 2;
$headerHeight: 3.5em;
$bgImg: 'https://res.cloudinary.com/tercuman-b-l-m-merkez/image/upload/v1561460680/ServQuick.jpg_g0rquc.png';
// document colors
$colorDocument: #8086a0;
$colorDocumentDark: #1e1f30;
$colorDocumentDarker: darken( $colorDocumentDark, 10% );
$colorDocumentLight: #a0a6b0;
$colorDocumentText: desaturate( lighten( $colorDocumentDark, 40% ), 5% );
// common colors
$colorPrimary: crimson;
$colorPrimaryText: lighten( $colorPrimary, 40% );
$colorSecondary: cornflowerblue;
$colorSecondaryText: darken( $colorSecondary, 40% );
$colorDefault: lightslategray;
$colorDefaultText: darken( $colorDefault, 40% );
$colorGrey: slategray;
$colorGreyText: darken( $colorGrey, 40% );
$colorBright: whitesmoke;
$colorBrightText: darken( $colorBright, 40% );
$colorOverlay: rgba( black, 0.4 );
$colorCard: rgba( black, 0.08 );
// borders and lines
$lineWidth: 2px;
$lineStyle: solid;
$lineColor: rgba( black, 0.08 );
$lineJoin: 6px;
// base font options
$fontFamily: 'Roboto Condensed', sans-serif;
$fontSize: 20px;
$fontSpace: 1.2em;
$fontWeight: 700;
// shadow styles
$shadowContainer: 0 1px 30px rgba( black, 0.8 );
$shadowOverlay: 0 1px 20px rgba( black, 0.6 );
$shadowPaper: 0 1px 3px rgba( black, 0.5 );
// transition props
$fxSpeed: 400ms;
$fxEase: cubic-bezier( 0.215, 0.610, 0.355, 1.000 );
$fxSpeedOffset: calc( #{$fxSpeed} / 3 );
$fxSlideDist: 80px;
$fxShrinkScale: .4;
$fxGrowScale: 1.4;
$fxRotateAmount: 8deg;
// screen sizes
$sizeSmall: 420px;
$sizeMedium: 720px;
$sizeLarge: 1200px;
// screen breakpoints
$screenSmall: "only screen and (min-width : #{$sizeSmall})";
$screenMedium: "only screen and (min-width : #{$sizeMedium})";
$screenLarge: "only screen and (min-width : #{$sizeLarge})";
// page reset
*, *: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;
appearance: none;
-webkit-overflow-scrolling: touch;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform-style: flat;
transition:
border-color $fxSpeed $fxEase,
background-color $fxSpeed $fxEase,
opacity $fxSpeed $fxEase,
transform $fxSpeed $fxEase;
}
// block types
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;
}
// form elements
input, textarea, select, optgroup, option, button {
font-family: inherit;
font-size: inherit;
font-weight: normal;
line-height: inherit;
color: inherit;
}
select, button {
cursor: pointer;
}
// links
a {
color: $colorSecondary;
&:hover {
color: lighten( $colorSecondary, 10% );
}
}
// horizontal lines
hr {
display: block;
overflow: hidden;
margin: $padSpace 0;
height: 0;
border: 0;
border-bottom: $lineWidth $lineStyle $lineColor;
}
// document setup
html, body {
display: block;
position: relative;
max-width: 100vw;
min-height: 100vh;
}
html {
overflow: hidden;
overflow-y: auto;
}
body {
font-family: $fontFamily;
font-weight: $fontWeight;
font-size: calc( #{$fontSize} - 6px );
line-height: $fontSpace;
color: $colorDocumentText;
background-size: cover;
background-color: $colorDocument;
background-image:
linear-gradient( 217deg, rgba( $colorPrimary, .8 ), rgba( $colorPrimary, 0 ) 70.71% ),
linear-gradient( 127deg, rgba( $colorDocument, 1 ), rgba( $colorDocument, 0 ) 70.71% ),
linear-gradient( 336deg, rgba( $colorSecondary, .8 ), rgba( $colorSecondary, 0 ) 70.71% );
@media #{$screenSmall} {
font-size: calc( #{$fontSize} - 4px );
}
@media #{$screenMedium} {
font-size: calc( #{$fontSize} - 2px );
}
@media #{$screenLarge} {
font-size: $fontSize;
}
}
// media query helpers
.if-small {
display: none;
@media #{$screenSmall} {
display: initial;
}
}
.if-medium {
display: none;
@media #{$screenMedium} {
display: initial;
}
}
.if-large {
display: none;
@media #{$screenLarge} {
display: initial;
}
}
// not rendered
.hidden, [hidden], [v-cloak] {
display: none;
}
// visible but not usable
.disabled, [disabled] {
pointer-events: none;
opacity: 0.5;
}
// clickable elms
.clickable {
cursor: pointer;
}
// common card style
.card {
padding: $padSpace;
background-color: $colorCard;
border-radius: $lineJoin;
}
// margin helpers
.push-top { margin-top: $padSpace; }
.push-right { margin-right: $padSpace; }
.push-bottom { margin-bottom: $padSpace; }
.push-left { margin-left: $padSpace; }
.push-all { margin: $padSpace; }
// padding helpers
.pad-top { padding-top: $padSpace; }
.pad-right { padding-right: $padSpace; }
.pad-bottom { padding-bottom: $padSpace; }
.pad-left { padding-left: $padSpace; }
.pad-all { padding: $padSpace; }
// border helpers
.border-top { border-top: $lineWidth $lineStyle $lineColor; }
.border-right { border-right: $lineWidth $lineStyle $lineColor; }
.border-bottom { border-bottom: $lineWidth $lineStyle $lineColor; }
.border-left { border-left: $lineWidth $lineStyle $lineColor; }
// shadow helpers
.shadow-box { box-shadow: $shadowPaper; }
.shadow-text { text-shadow: $shadowPaper; }
// animations on
.fx {
position: relative;
animation-direction: normal;
animation-duration: $fxSpeed;
animation-timing-function: $fxEase;
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
// disable transitions on element
.fx-notx {
transition: none !important;
}
// convert inline elements into inline-block
.fx-ibk {
display: inline-block !important;
}
// effect delays
@for $i from 1 through 8 {
.fx-delay-#{$i} {
animation-delay: calc( #{$fxSpeedOffset} * #{$i} );
}
}
// spin right animation
@keyframes spinRight {
0% { transform: rotate( 0deg ); }
100% { transform: rotate( 359deg ); }
}
.fx-spin-right {
animation-name: spinRight;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
// spin right animation
@keyframes spinLeft {
0% { transform: rotate( 359deg ); }
100% { transform: rotate( 0deg ); }
}
.fx-spin-left {
animation-name: spinLeft;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
// fade-in animation
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
.fx-fade-in {
opacity: 0;
animation-name: fadeIn;
}
// fade-out animation
@keyframes fadeOut {
0% { opacity: 1; }
100% { opacity: 0; }
}
.fx-fade-out {
opacity: 1;
animation-name: fadeOut;
}
// drop-in animation (scale)
@keyframes dropIn {
0% { opacity: 0; transform: scale( $fxGrowScale ); }
100% { opacity: 1; transform: scale( 1 ); }
}
.fx-drop-in {
opacity: 0;
transform: scale( $fxGrowScale );
animation-name: dropIn;
}
// zoom-in animation (modal, alert, etc)
@keyframes zoomIn {
0% { opacity: 0; transform: scale( $fxShrinkScale ); }
100% { opacity: 1; transform: scale( 1 ); }
}
.fx-zoom-in {
opacity: 0;
transform: scale( $fxShrinkScale );
animation-name: zoomIn;
}
// zoom-out animation (modal, alert, etc)
@keyframes zoomOut {
0% { opacity: 1; transform: scale( 1 ); }
100% { opacity: 0; transform: scale( $fxShrinkScale ); }
}
.fx-zoom-out {
opacity: 1;
transform: scale( 1 );
animation-name: zoomOut;
}
// slide in to the left
@keyframes slideLeft {
0% { opacity: 0; transform: translateX( $fxSlideDist ); }
100% { opacity: 1; transform: translateX( 0 ); }
}
.fx-slide-left {
opacity: 0;
transform: translateX( $fxSlideDist );
animation-name: slideLeft;
}
// slide in to the right
@keyframes slideRight {
0% { opacity: 0; transform: translateX( calc( 0 - #{$fxSlideDist} ) ); }
100% { opacity: 1; transform: translateX( 0 ); }
}
.fx-slide-right {
opacity: 0;
transform: translateX( calc( 0 - #{$fxSlideDist} ) );
animation-name: slideRight;
}
// slide in to the top
@keyframes slideUp {
0% { opacity: 0; transform: translateY( $fxSlideDist ); }
100% { opacity: 1; transform: translateY( 0 ); }
}
.fx-slide-up {
opacity: 0;
transform: translateY( $fxSlideDist );
animation-name: slideUp;
}
// slide in to the bottom
@keyframes slideDown {
0% { opacity: 0; transform: translateY( calc( 0 - #{$fxSlideDist} ) ); }
100% { opacity: 1; transform: translateY( 0 ); }
}
.fx-slide-down {
opacity: 0;
transform: translateY( calc( 0 - #{$fxSlideDist} ) );
animation-name: slideDown;
}
// pulse opacity
@keyframes pulseFade {
0% { opacity: 0.7; }
50% { opacity: 1.0; }
100% { opacity: 0.7; }
}
.fx-pulse {
opacity: 0.7;
animation-name: pulseFade;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
// flex helpers
.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: .5; }
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.flex-3 { flex: 3; }
.flex-4 { flex: 4; }
.flex-5 { flex: 5; }
// auto switch between column and row
.flex-autorow {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
& > .flex-item {
flex: 1;
width: 100%;
margin: 0 0 $padSpace 0; // push bottom
&:last-of-type { margin: 0; }
}
@media #{$screenMedium} {
flex-direction: row;
& > .flex-item {
margin: 0 $padSpace 0 0; // push right
&:last-of-type { margin: 0; }
}
}
}
// rouded image
.img-round {
overflow: hidden;
text-indent: -1000px;
border-radius: 1000px;
border: $lineWidth solid $colorBright;
background-color: lighten( $colorDocumentDark, 10% );
background-image: linear-gradient( 45deg, lighten( $colorDocumentDark, 10% ), lighten( $colorDocumentDark, 25% ) );
box-shadow: $shadowPaper;
}
// centered image
.img-center {
display: block;
margin: 0 auto;
}
// common large bright text buttons
.common-btn {
display: inline-block;
text-align: center;
font-size: 180%;
font-weight: normal;
line-height: 1em;
width: 1em;
color: $colorBright;
&:hover {
color: darken( $colorBright, 20% );
}
}
// common cta button/link
.cta-btn {
display: inline-block;
text-decoration: none;
padding: ( $padSpace / 2 ) $padSpace;
color: $colorPrimaryText;
background-color: darken( desaturate( $colorPrimary, 10% ), 10% );
border-radius: 100px;
box-shadow: $shadowPaper;
line-height: 1.1em;
&:hover {
color: lighten( $colorPrimaryText, 5% );
background-color: darken( $colorPrimary, 5% );
}
}
// common form input wrapper
.form-input {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: stretch;
color: $colorBright;
& > input {
flex: 1;
line-height: 1.5em;
padding: 0 ( $padSpace / 2 );
}
}
// common form slider container
@mixin sliderTrack {
width: 100%;
height: 3px;
background-color: lighten( $colorDocumentDark, 10% );
color: transparent !important;
border-color: transparent !important;
border-radius: $lineJoin !important;
border: 0 !important;
}
@mixin sliderThumb {
width: 1em;
height: 1em;
margin: -.4em 0 0 0;
border-radius: 50%;
box-shadow: $shadowPaper;
background-color: $colorBright;
transition: background $fxSpeed $fxEase;
color: transparent !important;
border-color: transparent !important;
border: 0 !important;
cursor: pointer;
&:hover {
background-color: darken( $colorBright, 20% );
}
}
.form-slider {
display: flex;
position: relative;
flex-direction: row;
align-items: center;
justify-content: stretch;
width: 100%;
max-width: 6em;
line-height: 1em;
& > input {
-webkit-appearance: none;
appearance: none;
width: 100%;
margin: 0 .5em;
// track
&::-webkit-slider-runnable-track { @include sliderTrack; }
&::-moz-range-track { @include sliderTrack; }
&::-ms-track { @include sliderTrack; }
// thumb
&::-webkit-slider-thumb { -webkit-appearance: none; @include sliderThumb; }
&::-moz-range-thumb { @include sliderThumb; }
&::-ms-thumb { @include sliderThumb; }
}
}
// common absolute popover
@keyframes popoverShow {
0% { transform: translateX( -50% ) scale( .8 ); opacity: 0; }
35% { transform: translateX( -50% ) scale( 1.2 ); opacity: .8; }
100% { transform: translateX( -50% ) scale( 1 ); opacity: 1; }
}
.popover {
position: relative;
.popover-box {
display: none;
position: absolute;
padding: ( $padSpace / 2 ) 0;
max-width: 300px;
min-height: 100px;
left: 50%;
bottom: 50%;
transition: none;
transform: translateX( -50% );
background-color: lighten( $colorDocumentDark, 8% );
border-radius: $lineJoin;
box-shadow: $shadowOverlay;
animation: popoverShow $fxSpeed $fxEase forwards;
z-index: 2000;
&:before {
content: '';
display: none;
position: absolute;
transition: none;
width: 0;
height: 0;
transform: translateX( -50% );
left: 50%;
z-index: 2001;
}
& > button {
display: block;
width: 100%;
text-align: left;
padding: ( $padSpace / 2 ) $padSpace;
line-height: 1.2em;
white-space: nowrap;
background-color: rgba( $colorDocumentDark, 0 );
&:hover {
background-color: rgba( $colorDocumentDark, .2 );
}
& + button {
border-top: $lineWidth $lineStyle $lineColor;
}
}
&.popover-left {
transform: none;
left: auto;
right: 0;
}
&.popover-right {
transform: none;
left: 0;
right: auto;
}
&.popover-top {
top: auto;
bottom: 100%;
&:before {
display: block;
top: auto;
bottom: -10px;;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid lighten( $colorDocumentDark, 8% );
}
}
&.popover-bottom {
top: 100%;
bottom: auto;
&:before {
display: block;
top: -10px;
bottom: auto;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid lighten( $colorDocumentDark, 8% );
}
}
}
&:hover > .popover-box,
&:active > .popover-box {
display: block;
}
}
// headings
h1, h2, h3, h4, h5, h6 {
display: block;
font-weight: normal;
line-height: 1.1em;
color: $colorBright;
}
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 helpers
.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: $colorPrimary; }
.text-secondary { color: $colorSecondary; }
.text-grey { color: $colorGrey; }
.text-bright { color: $colorBright; }
.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 root
.app-wrap {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
min-height: 100vh;
width: 100%;
}
// player container
.player-wrap {
display: block;
overflow: hidden;
position: relative;
flex: 1;
width: 100%;
height: 100vh;
background-color: $colorDocumentDark;
& > .player-bg,
& > .player-canvas {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
& > .player-bg {
background-image: url( $bgImg );
background-position: bottom right;
background-repeat: no-repeat;
background-size: cover;
opacity: .4;
}
@media #{$screenMedium} {
margin: 0 ( $padSpace * 2 );
max-width: 1080px;
height: calc( 100vh - ( #{$padSpace} * 4 ) );
max-height: 700px;
border-radius: $lineJoin;
box-shadow: $shadowContainer;
}
}
// player layout container
.player-layout {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
height: 100%;
.player-header,
.player-content,
.player-footer {
position: relative;
}
.player-header,
.player-footer {
padding: 0 $padSpace;
height: $headerHeight;
min-height: $headerHeight;
background-color: $colorCard;
}
.player-header {
& > h2 {
color: $colorPrimary;
i { vertical-align: bottom; }
}
}
.player-content {
flex: 1;
height: 100%;
overflow: hidden;
overflow-y: auto;
padding: $padSpace;
& > section {
margin: auto 0; // prevent vertical aligned flex item from overflowing
}
@media #{$screenMedium} {
padding: $padSpace ( $padSpace * 2 );
}
}
}
// player greeting message
.player-greet {
flex: 1;
@media #{$screenMedium} { flex: .5; }
}
// player tracklist
.player-tracklist {
display: block;
position: relative;
list-style: none;
& > li + li {
margin-top: ( $padSpace / 2 );
}
}
// player footer controls
.player-controls {
position: relative;
}
// player stations sidebar
.player-stations {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba( $colorOverlay, 0 );
pointer-events: none;
z-index: 1;
.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: lighten( $colorDocumentDark, 2% );
@media #{$screenSmall} {
right: -420px;
width: 420px;
}
.player-stations-header,
.player-stations-footer {
padding: 0 $padSpace;
min-height: $headerHeight;
box-shadow: 0 0 3px rgba( black, 0.3 );
}
.player-stations-list {
display: block;
list-style: none;
overflow: hidden;
overflow-y: auto;
margin-left: -10px;
padding-left: 10px;
flex: 1;
.player-stations-list-item {
position: relative;
padding: $padSpace;
background-color: rgba( black, 0.1 );
cursor: pointer;
&:nth-child( odd ) {
background-color: rgba( black, 0.18 );
}
&:hover {
background-color: rgba( black, 0 );
}
&.active {
background-color: darken( $colorDocumentDark, 2% );
h6 { color: $colorPrimary; }
}
}
}
}
// slide out
&.visible {
background-color: $colorOverlay;
pointer-events: auto;
z-index: 1000;
.player-stations-sidebar {
transform: translateX( -320px );
box-shadow: $shadowOverlay;
@media #{$screenSmall} { transform: translateX( -420px ); }
}
.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 darken( $colorDocumentDark, 2% );
}
}
}
View Compiled
/**
* TEBİMTEBİTAGEM GAZETESİ RADYO TELEVİZYONU Web Player
* Author: Rainner Lins (2018)
* Site: http://harunpehlivan.fm.tc/
/**
* 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 = 'http://harunpehlivan.fm.tc/channels.json';
let emsg = [ 'There was a problem trying to load the list of available channels from TEBİMTEBİTAGEM GAZETESİ RADYO TELEVİZYONU.' ];
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.harunpehlivan.fm.tc/'+ c.id +'-128-mp3'; // assumed stream url
c.songsurl = 'http://harunpehlivan.fm.tc/songs/'+ c.id +'.json'; // songs data url
c.infourl = 'http://harunpehlivan.fm.tc'+ 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();
}
});