A VueJS based Audio Player created using HTML5 Audio API.
- Vue
- Animate.css
- Daniel Simon (@soundbible.com)
- Unsplash
- LocalStorage to add music
- Preload Images
smooth image trasitionbetter minimal iconsnot viable- documentation
:)
<div id="app"> | |
<div class="heading"> | |
<h1>VueJS Audio Player V2</h1> | |
<p>(photos from <a href="https://unsplash.com/">unsplash</a> & audios from <a href="https://soundbible.com/">soundbible</a>)</p> | |
</div> | |
<div class="audioContainer"> | |
<a class="nav-icon" v-on:click="isPlaylistActive=!isPlaylistActive" :class="{'isActive': isPlaylistActive}" title="Music List"> | |
<span></span> | |
<span></span> | |
<span></span> | |
</a> | |
<div class="audioPlayerList" :class="{'isActive': isPlaylistActive}"> | |
<div class="item" v-for="(item,index) in musicPlaylist" v-bind:class="{ 'isActive':isCurrentSong(index) }" v-on:click="changeSong(index),isPlaylistActive=!isPlaylistActive"> | |
<p class="title">{{ item.title }}</p> | |
<p class="artist">{{ item.artist }}</p> | |
</div> | |
</div> | |
<div class="audioPlayerUI" :class="{'isDisabled': isPlaylistActive}"> | |
<div class="albumImage"> | |
<transition name="ballmove" enter-active-class="animated zoomIn" leave-active-class="animated fadeOutDown" mode="out-in"> | |
<!--<transition name="slide-fade" mode="out-in">--> | |
<img @load="onImageLoaded()" :src="musicPlaylist[currentSong].image" :key="currentSong" ondragstart="return false;" id="playerAlbumArt"> | |
</transition> | |
<div class="loader" :key="currentSong">Loading...</div> | |
</div> | |
<div class="albumDetails"> | |
<transition name="slide-fade" mode="out-in"> | |
<p class="title" :key="currentSong">{{ musicPlaylist[currentSong].title }}</p> | |
</transition> | |
<transition name="slide-fade" mode="out-in"> | |
<p class="artist" :key="currentSong">{{ musicPlaylist[currentSong].artist }}</p> | |
</div> | |
<div class="playerButtons"> | |
<a class="button" :class="{'isDisabled':(currentSong==0)}" v-on:click="prevSong()" title="Previous Song"><i class="zmdi zmdi-skip-previous"></i></a> | |
<a class="button play" v-on:click="playAudio()" title="Play/Pause Song"> | |
<transition name="slide-fade" mode="out-in"> | |
<i class="zmdi" :class="[currentlyStopped ? 'zmdi-stop' : (currentlyPlaying ? 'zmdi-pause-circle' : 'zmdi-play-circle')]" :key="1"></i> | |
</transition> | |
</a> | |
<a class="button" :class="{'isDisabled':(currentSong==musicPlaylist.length-1)}" v-on:click="nextSong()" title="Next Song"><i class="zmdi zmdi-skip-next"></i></a> | |
</div> | |
<div class="currentTimeContainer" style="text-align:center"> | |
<span class="currentTime">{{ currentTime | fancyTimeFormat }}</span> | |
<span class="totalTime"> {{ trackDuration | fancyTimeFormat }}</span> | |
<!--<span style="color:black">({{ currentSong+1 }}/{{ musicPlaylist.length }})</span>--> | |
</div> | |
<div class="currentProgressBar"> | |
<div class="currentProgress" :style="{ width: currentProgressBar + '%' }"></div> | |
</div> | |
</div> | |
</div> | |
</div> |
var app = new Vue({ | |
el: "#app", | |
data: { | |
audio: "", | |
imgLoaded: false, | |
currentlyPlaying: false, | |
currentlyStopped: false, | |
currentTime: 0, | |
checkingCurrentPositionInTrack: "", | |
trackDuration: 0, | |
currentProgressBar: 0, | |
isPlaylistActive: false, | |
currentSong: 0, | |
debug: false, | |
musicPlaylist: [ | |
{ | |
title: "Service Bell", | |
artist: "Daniel Simion", | |
url: "https://soundbible.com/mp3/service-bell_daniel_simion.mp3", | |
image: "https://source.unsplash.com/crs2vlkSe98" | |
}, | |
{ | |
title: "Meadowlark", | |
artist: "Daniel Simion", | |
url: "https://soundbible.com/mp3/meadowlark_daniel-simion.mp3", | |
image: "https://source.unsplash.com/35bE_njbG9E" | |
}, | |
{ | |
title: "Hyena Laughing", | |
artist: "Daniel Simion", | |
url: "https://soundbible.com/mp3/hyena-laugh_daniel-simion.mp3", | |
image: "https://source.unsplash.com/4_N5a-_5K4o" | |
}, | |
{ | |
title: "Creepy Background", | |
artist: "Daniel Simion", | |
url: "http://soundbible.com/mp3/creepy-background-daniel_simon.mp3", | |
image: "https://source.unsplash.com/j0g8taxHZa0" | |
} | |
], | |
audioFile: "" | |
}, | |
mounted: function() { | |
this.changeSong(); | |
this.audio.loop = false; | |
}, | |
filters: { | |
fancyTimeFormat: function(s) { | |
return (s - (s %= 60)) / 60 + (9 < s ? ":" : ":0") + s; | |
} | |
}, | |
methods: { | |
togglePlaylist: function() { | |
this.isPlaylistActive = !this.isPlaylistActive; | |
}, | |
nextSong: function() { | |
if (this.currentSong < this.musicPlaylist.length - 1) | |
this.changeSong(this.currentSong + 1); | |
}, | |
prevSong: function() { | |
if (this.currentSong > 0) this.changeSong(this.currentSong - 1); | |
}, | |
changeSong: function(index) { | |
var wasPlaying = this.currentlyPlaying; | |
this.imageLoaded = false; | |
if (index !== undefined) { | |
this.stopAudio(); | |
this.currentSong = index; | |
} | |
this.audioFile = this.musicPlaylist[this.currentSong].url; | |
this.audio = new Audio(this.audioFile); | |
var localThis = this; | |
this.audio.addEventListener("loadedmetadata", function() { | |
localThis.trackDuration = Math.round(this.duration); | |
}); | |
this.audio.addEventListener("ended", this.handleEnded); | |
if (wasPlaying) { | |
this.playAudio(); | |
} | |
}, | |
isCurrentSong: function(index) { | |
if (this.currentSong == index) { | |
return true; | |
} | |
return false; | |
}, | |
getCurrentSong: function(currentSong) { | |
return this.musicPlaylist[currentSong].url; | |
}, | |
playAudio: function() { | |
if ( | |
this.currentlyStopped == true && | |
this.currentSong + 1 == this.musicPlaylist.length | |
) { | |
this.currentSong = 0; | |
this.changeSong(); | |
} | |
if (!this.currentlyPlaying) { | |
this.getCurrentTimeEverySecond(true); | |
this.currentlyPlaying = true; | |
this.audio.play(); | |
} else { | |
this.stopAudio(); | |
} | |
this.currentlyStopped = false; | |
}, | |
stopAudio: function() { | |
this.audio.pause(); | |
this.currentlyPlaying = false; | |
this.pausedMusic(); | |
}, | |
handleEnded: function() { | |
if (this.currentSong + 1 == this.musicPlaylist.length) { | |
this.stopAudio(); | |
this.currentlyPlaying = false; | |
this.currentlyStopped = true; | |
} else { | |
this.currentlyPlaying = false; | |
this.currentSong++; | |
this.changeSong(); | |
this.playAudio(); | |
} | |
}, | |
onImageLoaded: function() { | |
this.imgLoaded = true; | |
}, | |
getCurrentTimeEverySecond: function(startStop) { | |
var localThis = this; | |
this.checkingCurrentPositionInTrack = setTimeout( | |
function() { | |
localThis.currentTime = localThis.audio.currentTime; | |
localThis.currentProgressBar = | |
localThis.audio.currentTime / localThis.trackDuration * 100; | |
localThis.getCurrentTimeEverySecond(true); | |
}.bind(this), | |
1000 | |
); | |
}, | |
pausedMusic: function() { | |
clearTimeout(this.checkingCurrentPositionInTrack); | |
} | |
}, | |
watch: { | |
currentTime: function() { | |
this.currentTime = Math.round(this.currentTime); | |
} | |
}, | |
beforeDestroy: function() { | |
this.audio.removeEventListener("ended", this.handleEnded); | |
this.audio.removeEventListener("loadedmetadata", this.handleEnded); | |
clearTimeout(this.checkingCurrentPositionInTrack); | |
} | |
}); |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/feather-icons/4.7.3/feather.min.js"></script> |
@import url("https://fonts.googleapis.com/css?family=Inconsolata:400,700"); | |
@import url("https://fonts.googleapis.com/css?family=Raleway:400,400i,700"); | |
$primary_color: rgba(0, 0, 0, 0.75); | |
$player_width: 20rem; | |
$player_padding: 1.5rem; | |
$button_size: 2rem; | |
$anim_duration: 0.5s; | |
* { | |
box-sizing: border-box; | |
} | |
.animated { | |
animation-duration: $anim_duration; | |
} | |
.audioContainer { | |
position: relative; | |
background-color: #eceff1; | |
min-height: 25rem; | |
width: $player_width; | |
overflow: hidden; | |
padding: $player_padding; | |
margin: 0 auto; | |
border-radius: 6px; | |
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22); | |
user-select: none; | |
.nav-icon { | |
width: 15px; | |
height: 12px; | |
position: absolute; | |
top: ($player_padding)-($player_padding/4); | |
left: $player_padding; | |
transform: rotate(0deg); | |
transition: 0.25s ease-in-out; | |
cursor: pointer; | |
span { | |
display: block; | |
position: absolute; | |
height: 2px; | |
width: 100%; | |
background: $primary_color; | |
border-radius: 6px; | |
opacity: 1; | |
left: 0; | |
transform: rotate(0deg); | |
transition: 0.5s ease-in-out; | |
} | |
span:nth-child(1) { | |
top: 0px; | |
} | |
span:nth-child(2) { | |
top: 5px; | |
} | |
span:nth-child(3) { | |
top: 10px; | |
} | |
&.isActive span:nth-child(1) { | |
top: 5px; | |
transform: rotate(135deg); | |
} | |
&.isActive span:nth-child(2) { | |
opacity: 0; | |
left: -60px; | |
} | |
&.isActive span:nth-child(3) { | |
top: 5px; | |
transform: rotate(-135deg); | |
} | |
} | |
.audioPlayerList { | |
color: $primary_color; | |
width: ($player_width)-(2*$player_padding); | |
transition: $anim_duration; | |
transform: translateX(-200%); | |
position: absolute; | |
margin-top: $player_padding; | |
overflow: auto; | |
z-index: 10; | |
will-change: transform; | |
&.isActive { | |
transform: translateX(0); | |
} | |
.item { | |
margin-bottom: 1.5rem; | |
border-left: 0.1rem solid transparent; | |
transition: 0.2s; | |
&:hover { | |
padding-left: 0.5rem; | |
cursor: pointer; | |
} | |
.title { | |
color: rgba(0, 0, 0, 1); | |
font-size: 1rem; | |
margin-bottom: 0.75rem; | |
} | |
.artist { | |
color: rgba(0, 0, 0, 0.5); | |
font-size: 0.8rem; | |
} | |
&.isActive { | |
border-left-color: black; | |
padding-left: 1rem; | |
} | |
} | |
} | |
.audioPlayerUI { | |
margin-top: $player_padding; | |
will-change: transform, filter; | |
transition: $anim_duration; | |
&.isDisabled { | |
transform: scale(0.75) translateX(100%); | |
filter: blur(5px) grayscale(100%); | |
} | |
.albumDetails { | |
text-align: center; | |
margin: 2rem 0; | |
p { | |
margin: 0px; | |
&.title { | |
font-size: 1rem; | |
color: rgba(0, 0, 0, 1); | |
} | |
&.artist { | |
margin-top: 1rem; | |
font-size: 0.75rem; | |
font-weight: none; | |
color: $primary_color; | |
transition-delay: 100ms; | |
} | |
} | |
} | |
.albumImage { | |
width: ($player_width)-($player_padding*2); | |
height: ($player_width)-($player_padding*2); | |
overflow: hidden; | |
margin: 0 auto; | |
img { | |
width: 100%; | |
height: 100%; | |
z-index: 10; | |
object-fit: cover; | |
object-position: 50% 50%; | |
} | |
} | |
.playerButtons { | |
position: relative; | |
margin: 0 auto; | |
margin-bottom: 1.5rem; | |
text-align: center; | |
&* { | |
position: absolute; | |
top: 50%; | |
transform: translateY(-50%); | |
} | |
.button { | |
font-size: $button_size; | |
display: inline-block; | |
vertical-align: middle; | |
padding: 0.5rem; | |
margin: 0 0.25rem; | |
color: rgba(0, 0, 0, 0.75); | |
border-radius: 50%; | |
outline: 0; | |
text-decoration: none; | |
cursor: pointer; | |
transition: $anim_duration; | |
&.play { | |
font-size: 2*$button_size; | |
margin: 0 1.5rem; | |
} | |
&:active { | |
opacity: 0.75; | |
transform: scale(0.75); | |
} | |
&.isDisabled { | |
color: rgba(0, 0, 0, 0.2); | |
cursor: initial; | |
} | |
&.isDisabled:active { | |
transform: none; | |
} | |
} | |
} | |
.currentTimeContainer { | |
width: 100%; | |
height: 1rem; | |
.currentTime, | |
.totalTime { | |
font-size: 0.5rem; | |
font-family: monospace; | |
color: $primary_color; | |
} | |
.currentTime { | |
float: left; | |
} | |
.totalTime { | |
float: right; | |
} | |
} | |
.currentProgressBar { | |
width: 100%; | |
background-color: rgba(0, 0, 0, 0.1); | |
margin-top: 1.5rem; | |
.currentProgress { | |
background-color: $primary_color; | |
width: 0px; | |
height: 1px; | |
transition: 100ms; | |
} | |
} | |
} | |
} | |
.loader, | |
.loader:after { | |
border-radius: 50%; | |
width: 10em; | |
height: 10em; | |
} | |
.loader { | |
margin: 60px auto; | |
font-size: 10px; | |
position: relative; | |
text-indent: -9999em; | |
border-top: 0.1em solid rgba(0, 0, 0, 0.2); | |
border-right: 0.1em solid rgba(0, 0, 0, 0.2); | |
border-bottom: 0.1em solid rgba(0, 0, 0, 0.2); | |
border-left: 0.1em solid $primary_color; | |
-webkit-transform: translateZ(0); | |
-ms-transform: translateZ(0); | |
transform: translateZ(0); | |
-webkit-animation: load8 1.1s infinite linear; | |
animation: load8 1.1s infinite linear; | |
} | |
@-webkit-keyframes load8 { | |
0% { | |
-webkit-transform: rotate(0deg); | |
transform: rotate(0deg); | |
} | |
100% { | |
-webkit-transform: rotate(360deg); | |
transform: rotate(360deg); | |
} | |
} | |
@keyframes load8 { | |
0% { | |
-webkit-transform: rotate(0deg); | |
transform: rotate(0deg); | |
} | |
100% { | |
-webkit-transform: rotate(360deg); | |
transform: rotate(360deg); | |
} | |
} | |
/* data change transitions */ | |
.slide-fade-enter-active { | |
transition: all 0.3s ease; | |
} | |
.slide-fade-leave-active { | |
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); | |
} | |
.slide-fade-enter, | |
.slide-fade-leave-to { | |
transform: translateY(10px); | |
opacity: 0; | |
} | |
.fade-enter-active, | |
.fade-leave-active { | |
transition: opacity $anim_duration; | |
} | |
.fade-enter, | |
.fade-leave-to { | |
opacity: 0; | |
} | |
/* Codepen Formatting */ | |
body { | |
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
background-repeat:no-repeat; | |
background-attachment:fixed; | |
color: rgba(255, 255, 255, 0.7); | |
font-family: Raleway, sans-serif; | |
padding: 3rem; | |
} | |
.heading { | |
text-align: center; | |
margin: 0; | |
margin: 2rem 0; | |
font-family: Inconsolata, monospace; | |
h1 { | |
margin: 0; | |
margin-bottom: 1rem; | |
font-size: 1.5rem; | |
} | |
p { | |
margin: 0; | |
font-size: 0.8rem; | |
} | |
a { | |
color: rgba(255, 255, 255, 0.8); | |
transition: 0.3s; | |
&:hover { | |
color: rgba(255, 255, 255, 1) !important; | |
} | |
&:visited { | |
color: rgba(255, 255, 255, 0.5); | |
} | |
} | |
} |
<link href="https://cdnjs.cloudflare.com/ajax/libs/material-design-iconic-font/2.2.0/css/material-design-iconic-font.min.css" rel="stylesheet" /> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" rel="stylesheet" /> |