<div class="o-background">
<div class="c-player">
<div class="c-player__picture">
<div class="c-player__picture__images">
<img src="https://images.unsplash.com/photo-1484589065579-248aad0d8b13?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&h=300&w=300">
<img src="https://images.unsplash.com/photo-1564463836192-7ca5d09c8e92?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&h=300&w=300">
</div>
</div>
<div class="c-player__details">
<strong>Sugar</strong>
<span>Rakata (Offical Single)</span>
</div>
<div class="c-player__ui">
<div class="c-player__ui__prev">
<svg class="icon" viewBox="0 0 232.153 232.153">
<use class="play" xlink:href="#play" x="0" y="0"/>
</svg>
<svg class="icon" viewBox="0 0 232.153 232.153">
<use class="play" xlink:href="#play" x="0" y="0"/>
</svg>
</div>
<div class="o-controls">
<div class="c-player__ui__play">
<svg class="icon" viewBox="0 0 232.153 232.153">
<use class="play" xlink:href="#play" x="0" y="0"/>
</svg>
</div>
<div class="c-player__ui__pause">
<svg class="icon" viewBox="0 0 14 36">
<use class="pause" xlink:href="#pause" x="0" y="0"/>
</svg>
<svg class="icon" viewBox="0 0 14 36">
<use class="pause" xlink:href="#pause" x="0" y="0"/>
</svg>
</div>
</div>
<div class="c-player__ui__next">
<svg class="icon" viewBox="0 0 232.153 232.153">
<use class="play" xlink:href="#play" x="0" y="0"/>
</svg>
<svg class="icon" viewBox="0 0 232.153 232.153">
<use class="play" xlink:href="#play" x="0" y="0"/>
</svg>
</div>
<div class="c-player__ui__seek">
<div class="c-player__ui__seek__seeker">
<div></div>
</div>
</div>
<div class="c-player__ui__dots">
<div class="c-player__ui__dots__dot"></div>
<div class="c-player__ui__dots__dot"></div>
<div class="c-player__ui__dots__dot"></div>
<div class="c-player__ui__dots__dot"></div>
</div>
</div>
</div>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur"/>
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9"
result="goo"/>
<feBlend in="SourceGraphic" in2="goo"/>
</filter>
</defs>
<symbol id="play">
<path style="fill-rule:evenodd;clip-rule:evenodd;"
d="M203.791,99.628L49.307,2.294c-4.567-2.719-10.238-2.266-14.521-2.266 c-17.132,0-17.056,13.227-17.056,16.578v198.94c0,2.833-0.075,16.579,17.056,16.579c4.283,0,9.955,0.451,14.521-2.267 l154.483-97.333c12.68-7.545,10.489-16.449,10.489-16.449S216.471,107.172,203.791,99.628z"/>
</symbol>
<symbol id="pause">
<rect x="0" width="13" height="36" rx="2"></rect>
</symbol>
</svg>
</div>
<div class="o-credits">Based on a Dribble by <a href="https://dribbble.com/TheGlyphStudio" target="_blank">The Glyph
Studio</a></div>
body {
font-family: "Quicksand";
min-height: 400px;
}
.o-background {
width: 100%;
height: 100%;
min-height: 480px;
position: absolute;
background-image: linear-gradient(to right top, #ffc4ee, #ead1fc, #dadcff, #d5e5fb, #dceaf3);
* {
box-sizing: border-box;
}
}
.o-credits {
position: absolute;
bottom: .5rem;
left: 0;
color: #fff;
width: 100%;
text-align: center;
a {
color: #fff;
}
}
.c-player {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 370px;
border-radius: 50%;
border-radius: 0 0 2rem 2rem;
padding: 2.5rem 1.8rem .4rem;
box-shadow: inset 0px -200px 180px -100px rgba(255, 255, 255, 0.3), 0px 12px 20px -20px rgba(0, 0, 0, 0.08);
* {
-webkit-tap-highlight-color: transparent;
}
&__picture {
width: 230px;
height: 230px;
border-radius: 50%;
overflow: hidden;
margin: 0 auto;
box-shadow: 0 8px 16px -6px rgba(16, 16, 16, 0.3);
//mix-blend-mode: exclusion;
&__images {
width: 230px;
height: 230px;
position: relative;
img {
position: absolute;
top: 0;
left: 0;
width: 230px;
height: 230px;
object-fit: cover;
max-width: 100%;
opacity: 1;
& + img {
opacity: 0;
}
}
}
}
&__details {
color: #fff;
text-align: center;
line-height: 1.3;
margin: 1.3rem 0 1.8rem;
font-size: 1.2rem;
strong {
display: block;
font-size: 1.4rem;
}
}
&__ui {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
width: 230px;
margin: 0 auto;
position: relative;
&__seek {
width: 100%;
border-radius: 15px;
overflow: hidden;
height: .4rem;
margin-top: 1.7rem;
&__seeker {
height: .5rem;
background: rgba(255, 255, 255, .3);
width: 0%;
margin: 0 auto;
border-radius: 15px;
div {
background: #fff;
width: 0;
height: .5rem;
}
}
}
.o-controls {
position: relative;
width: 40px;
height: 40px;
}
&__play {
width: 40px;
margin: 0;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
svg {
width: 40px;
fill: rgba(255, 255, 255, 1);
}
}
&__pause {
position: absolute;
visibility: hidden;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
width: 29px;
height: 29px;
cursor: pointer;
svg {
width: 12px;
fill: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.08);
}
svg + svg {
visibility: hidden;
}
}
&__next,
&__prev {
height: 25px;
cursor: pointer;
user-select: none;
svg {
fill: #fff;
width: 25px;
height: 25px;
opacity: .6;
& + svg {
margin-left: -15px
}
}
}
&__prev {
transform: rotate(180deg);
}
&__dots {
filter: url('#goo');
margin: 0 auto;
position: absolute;
left: 43%;
top: 29px;
&__dot {
height: .6rem;
width: .6rem;
background: #fff;
border-radius: 50%;
position: absolute;
top: 0;
visibility: hidden;
& + & + & {
height: .8rem;
top: -1px;
}
}
}
}
}
View Compiled
console.clear()
class PlayerWidget {
constructor(player, tracks) {
// State
this.current = 0
this.next = 0
this.currentImage = 0
this.tracks = tracks
this.player = player
this.isPaused = false
this.interval = null
// DOM
this.progressBar = this.player.querySelector('.c-player__ui__seek__seeker div')
this.progress = this.player.querySelector('.c-player__ui__seek__seeker')
this.playBtn = this.player.querySelector('.c-player__ui__play')
this.pauseBtn = this.player.querySelector('.c-player__ui__pause')
this.pauseSvgs = this.pauseBtn.querySelectorAll('svg')
this.prevBtn = this.player.querySelector('.c-player__ui__prev')
this.nextBtn = this.player.querySelector('.c-player__ui__next')
this.dots = this.player.querySelectorAll('.c-player__ui__dots__dot')
this.bindEvents()
}
bindEvents() {
this.nextBtn.addEventListener('click', e => this.nextTrack(e))
this.prevBtn.addEventListener('click', e => this.prevTrack(e))
this.playBtn.addEventListener('click', () => this.playTrack())
this.pauseBtn.addEventListener('click', () => this.pauseTrack())
}
playTrack() {
this.tiltX()
this.tl
.set(this.pauseBtn, {
transformPerspective: 1000,
rotationY: 45,
rotationX: -45,
scale: .8
})
.to(this.playBtn, .2, {
y: 8,
yoyo: true,
ease: Sine.easeInOut,
repeat: 1
}, 0)
// Dots
.set(this.dots, {autoAlpha: 1})
.to(this.dots[1], .5, {
y: 35,
scale: .4,
onComplete: () => {
this.tl.set(this.dots, {autoAlpha: 0})
this.play()
},
ease: Sine.easeIn
}, .65)
.to(this.dots[2], .9, {
y: 35,
scale: .7,
ease: Power4.easeIn
}, 0)
.to(this.dots[3], .9, {
y: 15,
scaleX: 0,
scaleY: 2,
ease: Power4.easeIn
}, 0)
}
pauseTrack() {
this.isPaused = true
this.tiltX()
this.tl
.set(this.playBtn, {rotation: -45})
.to(this.pauseSvgs[0], .25, {
y: 4,
yoyo: true,
repeat: 1,
ease: Sine.easeInOut
})
.to(this.playBtn, .3, {
rotation: 0,
scale: 1,
ease: Power4.easeIn,
autoAlpha: .9
}, .9)
.to(this.pauseBtn, .1, {autoAlpha: 0}, 1)
.to(this.progress, .7, {
width: '0%',
ease: Power4.easeOut
}, .8)
.to(this.pauseSvgs[1], .2, {autoAlpha: 0}, .9)
.to(this.progress, .6, {
alpha: 0,
ease: Power4.easeInOut
}, .8)
.set(this.dots, {autoAlpha: 1}, 1.1)
.to(this.dots[2], .6, {
y: '-=35',
scale: 1,
ease: Power4.ease
}, 1.1)
.to(this.dots[1], .6, {
y: '-=35',
scale: 1,
ease: Power4.ease,
onComplete: () => this.tl.set(this.dots, {autoAlpha: 0})
}, 1.1)
.set(this.dots[3], {
y: "-=15",
scaleX: 1,
scaleY: 1,
ease: Power4.ease
})
.to(this.player, .6, {
paddingBottom: '.4rem'
}, 1)
}
nextTrack(e) {
this.incrementNextTrack()
.animatePrevNext(e)
.tiltY('right')
.rewind()
.changeImage('+')
}
prevTrack(e) {
this.decrementNextTrack()
.animatePrevNext(e)
.tiltY('left')
.rewind()
.changeImage('-')
}
play() {
this.tl
.set(this.pauseSvgs[1], {scale: 0})
.set(this.progress, {alpha: 1})
.to(this.progress, .8, {
width: '100%',
ease: Power4.easeOut,
onComplete: () => {
this.isPaused = false
this.setInterval()
}
}, 0)
.to(this.playBtn, .3, {
rotation: -30,
scale: .8,
ease: Power4.easeIn,
autoAlpha: 0
}, .25)
.to(this.pauseBtn, .1, {
scale: 1,
rotationY: 0,
rotationX: 0,
autoAlpha: 1,
onComplete: () => {
this.tl
.set(this.pauseBtn, {
rotationY: 0,
rotationX: 0,
scale: 1
})
}
}, 0.35)
.to(this.pauseSvgs[1], .3, {
scale: 1,
autoAlpha: 1,
ease: Back.easeOut
}, .5)
.to(this.player, .6, {
paddingBottom: '2rem'
}, 0.2)
}
rewind() {
this.tl.to(this.progressBar, .5, {width: '-2%'})
return this
}
incrementNextTrack() {
if (this.current >= (this.tracks.length - 1)) {
this.next = -1
}
this.next++
return this
}
decrementNextTrack() {
if (this.current <= 0) {
this.next = this.tracks.length
}
this.next--
return this
}
animatePrevNext(e) {
const icons = e.currentTarget.querySelectorAll('svg')
this.tl
.set(icons, {scale: 1, alpha: .6})
.to(icons[1], .18, {
scale: .8,
repeat: 1,
alpha: 1,
yoyo: true
})
.to(icons[0], .18, {
scale: .8,
alpha: 1,
repeat: 1,
yoyo: true
}, '-=.22')
return this
}
tiltY(side) {
this.tl
.set(".c-player", {
transformPerspective: 1000,
transformStyle: "preserve-3d",
rotationY: 0
})
.to('.c-player', .25, {
rotationY: side === 'right' ? 8 : -8,
ease: Sine.easeInOut,
yoyo: true,
repeat: 1
}, 0)
return this
}
tiltX() {
this.tl
.set(this.player, {
transformPerspective: 1000,
rotationX: 0,
rotationY: 0
})
.to(this.player, .25, {
rotationX: -4,
ease: Sine.easeInOut,
yoyo: true,
repeat: 1
})
}
changeImage(direction) {
let images = this.player.querySelectorAll('img'),
current = this.currentImage % 2
images[1 - current].src = this.tracks[this.next].image
this.tl
.set('.c-player__picture__images', {rotation: 0})
.to('.c-player__picture__images', 1.8, {
rotation: direction + '=360',
ease: Back.easeOut
})
.to(images[current], 1.8, {alpha: 0, ease: Expo.easeOut}, 0)
.to(images[1 - current], 1.8, {
alpha: 1,
ease: Expo.easeOut
}, 0)
this.updateTrackName()
this.updateBackground()
// Set props for next transition
this.current = this.next
this.currentImage = ++this.currentImage % 2
}
updateTrackName() {
this.tl
.to('.c-player__details strong', .3, {
delay: .2,
alpha: 0,
scale: 0,
ease: Back.easeIn,
onComplete: timeline => {
timeline.target[0].innerText = this.tracks[this.next].artist
this.tl.to(timeline.target[0], .25, {
scale: 1,
delay: .1,
alpha: 1,
ease: Back.easeInOut
})
},
onCompleteParams: ['{self}']
})
.to('.c-player__details span', .25, {
alpha: 0,
onComplete: timeline => {
timeline.target[0].innerText = this.tracks[this.next].name
this.tl.to(timeline.target[0], .2, {alpha: 1})
},
onCompleteParams: ['{self}']
}, .5)
}
updateBackground() {
const gradient = this.tracks[this.next].gradient
this.tl
.to('.o-background', 1.5, {
'background-image': 'linear-gradient(to right top, ' + gradient + ')',
ease: Sine.easeOut
})
}
setInterval() {
if (this.interval !== null) {
return
}
this.interval = setInterval(() => {
if (this.isPaused === false) {
this.tl.to(this.progressBar, 0.2, {width: '+=1%'})
}
}, 1000)
}
get tl() {
return new TimelineLite()
}
}
const tracks = [
{
artist: 'Sugar',
name: 'Rakuta (Official Single)',
image: 'https://images.unsplash.com/photo-1484589065579-248aad0d8b13?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&h=300&w=300',
gradient: '#ffc4ee, #ead1fc, #dadcff, #d5e5fb, #dceaf3'
}, {
artist: 'Moon Boots',
name: 'Tied up feat. Steven Clavier',
image: 'https://images.unsplash.com/photo-1564463836192-7ca5d09c8e92?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE0NTg5fQ&h=300&w=300',
gradient: '#8bd0ff, #a4daff, #bbe3ff, #d2edff, #e8f6ff'
}, {
artist: 'The Glyph',
name: 'A great dribble shot',
image: 'https://images.unsplash.com/photo-1563386732972-99222d5cacb3?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=300&ixid=eyJhcHBfaWQiOjF9&ixlib=rb-1.2.1&q=80&w=300',
gradient: '#ffaff1, #ffb5cb, #ffc8ae, #ffe1a6, #f0f8b8'
}
]
const widget = new PlayerWidget(document.querySelector('.c-player'), tracks)