<div class="phone">
<svg width="360" height="480" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="goo" filterUnits="userSpaceOnUse" x="130" y="0" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="11" 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 19 -7" result="contrast" />
<feComposite in="SourceGraphic" in2="contrast" operator="atop"/>
</filter>
</defs>
<path id="card" d="M0,0 H360 V480 H0"/>
<g filter="url(#goo)">
<use xlink:href="#card"/>
<circle id="circle" cx="180" cy="50" r="20"/>
</g>
<g transform="translate(180, 50) scale(1, 1) rotate(90)">
<path id="progress"/>
</g>
</svg>
<div class="content">
<ul>
<li>
<span class="image"></span>
<div class="text">
<span class="headline"></span>
<span class="paragraph"></span>
<span class="paragraph"></span>
</div>
</li>
<li>
<span class="image"></span>
<div class="text">
<span class="headline"></span>
<span class="paragraph"></span>
<span class="paragraph"></span>
</div>
</li>
<li>
<span class="image"></span>
<div class="text">
<span class="headline"></span>
<span class="paragraph"></span>
<span class="paragraph"></span>
</div>
</li>
<li>
<span class="image"></span>
<div class="text">
<span class="headline"></span>
<span class="paragraph"></span>
<span class="paragraph"></span>
</div>
</li>
<li>
<span class="image"></span>
<div class="text">
<span class="headline"></span>
<span class="paragraph"></span>
<span class="paragraph"></span>
</div>
</li>
</ul>
</div>
</div>
html,
body {
width: 100%;
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.phone {
position: relative;
overflow: hidden;
width: 360px;
height: 480px;
background: darken(lightblue, 10%);
}
#card,
#circle {
fill: lighten(lightblue, 10%);
}
#progress {
fill: none;
stroke: lighten(lightblue, 10%);
stroke-width: 3px;
transform: scale(1, 1);
opacity: 1;
transition: none;
&.animated {
transform: scale(1.5, 1.5);
opacity: 0;
stroke-width: 0px;
transition: all .35s ease-in;
}
}
#circle {
transform: translate(0, 100px);
transition: none;
&.animated {
transform: translate(0, 0);
transition: all .25s .05s ease-out;
}
}
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
opacity: 1;
}
ul {
margin: 0;
padding: 0;
}
li {
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgba(255, 255, 255, .25);
margin-bottom: -1px;
&:last-child {
border-bottom: 0;
}
}
.image {
width: 4em;
height: 4em;
border-radius: 3px;
margin: 1em;
background: rgba(255, 255, 255, .5);
}
.text {
flex: 1;
padding-right: 1em;
}
.headline {
display: block;
margin: .5em 0;
height: 1em;
width: 35%;
border-radius: 3px;
background: rgba(255, 255, 255, .5);
}
.paragraph {
display: block;
margin: .5em 0;
height: 1em;
border-radius: 3px;
background: rgba(255, 255, 255, .35);
&:nth-child(2n-1) {
width: 90%;
}
}
View Compiled
const easing = {
easeInCubic: t => t ** 3,
easeOutCubic: t => (--t) * t ** 2 + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t ** 3 : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};
let flag;
const $phone = document.querySelector('.phone');
const $content = document.querySelector('.content');
const $card = document.querySelector('#card');
const $circle = document.querySelector('#circle');
const $progress = document.querySelector('#progress');
let trigger;
let release;
const getClientY = e => {
return parseInt(e.clientY || e.changedTouches[0].clientY, 10);
};
const dragStartListener = e => {
const y = getClientY(e);
const edge = $phone.offsetTop;
if (y > edge && y < edge + 44) {
resetAnimation();
flag = true;
trigger = y + 100;
release = y + 200;
}
};
const dragListener = e => {
if (flag) {
const y = getClientY(e);
const size = 100 - (trigger - y);
setCardPath(size, size);
setContentStyle(size, 1 - (size / 100));
if (y >= trigger) {
setCardPath(Math.min(size, 100), size);
if (y >= release) {
flag = false;
removeEventListeners();
startBounceAnimation();
$circle.classList.add('animated');
setTimeout(() => startProgressAnimation(), 500);
}
}
}
};
const dragEndListener = e => {
if (flag) {
flag = false;
startCloseAnimation(100 - (trigger - getClientY(e)));
}
};
const addEventListeners = () => {
document.addEventListener('touchstart', dragStartListener);
document.addEventListener('mousedown', dragStartListener);
document.addEventListener('touchmove', dragListener);
document.addEventListener('mousemove', dragListener);
document.addEventListener('touchend', dragEndListener);
document.addEventListener('mouseup', dragEndListener);
};
const removeEventListeners = () => {
document.removeEventListener('touchstart', dragStartListener);
document.removeEventListener('mousedown', dragStartListener);
document.removeEventListener('touchmove', dragListener);
document.removeEventListener('mousemove', dragListener);
document.removeEventListener('touchend', dragEndListener);
document.removeEventListener('mouseup', dragEndListener);
};
const resetAnimation = () => {
setCardPath(0, 0);
setContentStyle(0, 1);
setProgressPath(0);
$circle.classList.remove('animated');
$progress.classList.remove('animated');
};
const setContentStyle = (y, opacity) => {
$content.style.top = `${y}px`;
$content.style.opacity = opacity;
}
const setCardPath = (y1, y2) => {
var d = "M360,480 H0 V" + y1 + " Q180," + y2 + " 360," + y1;
$card.setAttribute('d', d);
};
const setProgressPath = percent => {
const x = 25 * Math.cos(percent * 6.283185);
const y = 25 * Math.sin(percent * 6.283185);
const largeArcFlag = percent <= 0.5 ? 0 : 1;
const d = "M25,0 A25,25 0 " + largeArcFlag + " 1 " + x + "," + y;
$progress.setAttribute('d', d);
};
const startBounceAnimation = () => {
let start;
const duration = 1250;
const animation = timestamp => {
if (!start) {
start = timestamp;
}
const progress = timestamp - start;
const amplitude = 100 - easing.easeOutCubic(progress / duration) * 100;
const time = 3 * (progress / duration);
const y = amplitude * Math.cos(6.283185 * time);
setCardPath(100, 100 + y);
if (progress < duration) {
requestAnimationFrame(animation);
}
};
requestAnimationFrame(animation);
}
const startProgressAnimation = () => {
let start;
const duration = 1000;
const animation = timestamp => {
if (!start) {
start = timestamp;
}
const progress = timestamp - start;
const percent = easing.easeInOutCubic(progress / duration);
setProgressPath(percent);
if (progress < duration) {
requestAnimationFrame(animation);
} else {
$card.classList.add('animated');
$progress.classList.add('animated');
window.setTimeout(() => startCloseAnimation(), 250);
}
};
requestAnimationFrame(animation);
}
const startCloseAnimation = (position = 100) => {
let start;
const duration = 1000;
const animation = timestamp => {
if (!start) {
start = timestamp;
}
const progress = timestamp - start;
const y = position - easing.easeInOutCubic(progress / duration) * position;
setCardPath(y, y);
setContentStyle(y, 1 - (y / 100));
if (progress < duration) {
requestAnimationFrame(animation);
} else {
addEventListeners();
}
};
requestAnimationFrame(animation);
}
addEventListeners();
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.