Based on this dribbble shot - https://dribbble.com/shots/2232385-Pull-Down-to-Refresh by Hoang Nguyen.
No animation libs. Line animation not working in IE.
A Pen by Nikolay Talanov on CodePen.
Based on this dribbble shot - https://dribbble.com/shots/2232385-Pull-Down-to-Refresh by Hoang Nguyen.
No animation libs. Line animation not working in IE.
A Pen by Nikolay Talanov on CodePen.
<div class="phone"> | |
<div class="phone__screen"> | |
<div class="demo__top"> | |
<p class="demo__text">Pull down content part</p> | |
<p class="demo__text half">Activities</p> | |
<p class="demo__text half">Saved</p> | |
<svg class="demo__svg" viewBox="0 0 375 37"> | |
<path class="demo__svg__line" stroke-width="3" fill="none" d="M0,1 187,1 a18,18 0 0,1 0,36 a18,18 0 0,1 -12.6,-30.6"/> | |
<path class="demo__svg__smile" stroke-width="3" fill="none" d="M176.61,25 Q187,14 197.39,25" /> | |
</svg> | |
</div> | |
<div class="demo__body"> | |
<svg class="demo__svg-bg" viewBox="0 0 375 667"> | |
<path class="demo__svg-bg__path" d="M0,0 C125,0 250,0 375,0 S375,0 375,0 L375,667 0,667z" /> | |
</svg> | |
<div class="demo__content"> | |
<div class="demo__el"></div> | |
<div class="demo__el"></div> | |
<div class="demo__el"></div> | |
</div> | |
</div> | |
</div> | |
</div> |
$(document).ready(function() { | |
var animating = false; | |
var $body = $(".demo__body"); | |
var $content = $(".demo__content"); | |
var $line = $(".demo__svg__line"); | |
var $smile = $(".demo__svg__smile"); | |
var $bgPath = $(".demo__svg-bg__path"); | |
var resetD = "M0,0 C125,0 250,0 375,0 S375,0 375,0 L375,667 0,667z"; | |
var smileInit = "M176.61,25 Q187,19 197.39,25"; | |
var smileMid = "M176.61,25 Q187,25 197.39,25"; | |
var smileEnd = "M176.61,25 Q187,34 197.39,25"; | |
var totalLen = $line[0].getTotalLength(); | |
var upperLen = 187; | |
var circleLen = totalLen - upperLen; | |
var minLineLen = 60; | |
var lineDragBoundries = upperLen - minLineLen; | |
var pullDeltaY = 0; | |
var pullSlowCoef = 1.2; | |
var maxPullY = 150; | |
var minReleaseY = 120; | |
var contentYCoef = 0.4; | |
var maxContentY = maxPullY*contentYCoef; | |
var bodyOffsetX = 0; | |
var bodyW = $body.outerWidth(); | |
var resetAT = 500; | |
var releaseStep1AT = 800; | |
var releaseWaitTime = 3000; | |
var easings = { | |
elastic: function(t,b,c,d) { | |
var ts = (t/=d)*t; | |
var tc = ts*t; | |
return b+c*(33*tc*ts + -106*ts*ts + 126*tc + -67*ts + 15*t); | |
}, | |
inCubic: function(t,b,c,d) { | |
var tc = (t/=d)*t*t; | |
return b+c*(tc); | |
} | |
}; | |
function SvgAddClass(el, cl) { | |
var regEx = new RegExp('(?:^|\\s)'+cl+'(?!\\S)', "gi"); | |
if (regEx.test(el.attr("class"))) return; | |
el.attr("class", el.attr("class") + " " + cl); | |
}; | |
function SvgRemoveClass(el, cl) { | |
var regEx = new RegExp('(?:^|\\s)'+cl+'(?!\\S)', "gi"); | |
el.attr("class", el.attr("class").replace(regEx, "")); | |
}; | |
function SvgPathTween(from, to, time, easing) { | |
var regex = /\d+(\.\d{1,2})?/g; | |
var fromD = from.attr("d"); | |
var fm = fromD.match(regex); | |
var tm = to.match(regex); | |
var diff = []; | |
for (var i = 0; i < fm.length; i++) { | |
diff.push(+fm[i] - (+tm[i])); | |
} | |
var time = time || 600; | |
var curFrame = 0; | |
var frames = time / 1000 * 60; | |
var easing = easing || "elastic"; | |
function animate() { | |
if (curFrame > frames) return; | |
var i = 0; | |
var newD = fromD.replace(regex, function(m) { | |
if (+m === 0 || // if nothing changed - skip | |
i % 2 === 0) { // in this demo I want to animate only y values | |
i++; | |
return m; | |
} | |
return +easings[easing](curFrame, +fm[i], 0 - diff[i++], frames).toFixed(2); | |
}); | |
from.attr("d", newD); | |
curFrame++; | |
requestAnimationFrame(animate); | |
}; | |
animate(); | |
}; | |
function pullChange(y, x) { | |
var pullY = (y <= maxPullY) ? y : maxPullY; | |
if (pullY < 0) pullY = 0; | |
var pullCoef = pullY / maxPullY; | |
var sdo = circleLen + pullCoef * lineDragBoundries; | |
var pullYCont = +(pullY*contentYCoef).toFixed(2); | |
var bodyX = parseInt(x - bodyOffsetX, 10); | |
if (bodyX < 0) bodyX = 0; | |
if (bodyX > bodyW) bodyX = bodyW; | |
var leftX = bodyX - 50; | |
if (leftX < 0) leftX = 0; | |
var rightX = bodyX + 50; | |
if (rightX > bodyW) rightX = bodyW; | |
$body.css("transform", "translate3d(0,"+ pullY +"px,0)"); | |
$content.css("transform", "translate3d(0,"+ pullYCont +"px,0)"); | |
$line.css("stroke-dashoffset", sdo); | |
$bgPath.attr("d", "M0,0 C"+(leftX-25)+",0 "+(leftX)+","+pullYCont+" "+bodyX+","+pullYCont+" S"+(rightX+25)+",0 375,0 L375,667 0,667z"); | |
}; | |
function reset() { | |
$body.addClass("reset"); | |
$content.addClass("reset"); | |
SvgAddClass($line, "reset"); | |
SvgPathTween($bgPath, resetD); | |
setTimeout(function() { | |
$body.removeClass("reset").attr("style", ""); | |
$content.removeClass("reset").attr("style", ""); | |
SvgRemoveClass($line, "reset"); | |
$line.attr("style", ""); | |
animating = false; | |
}, resetAT); | |
}; | |
function performMagic() { | |
$body.addClass("waiting"); | |
SvgAddClass($line, "show-circle"); | |
SvgPathTween($bgPath, resetD); | |
$content.addClass("reset"); | |
setTimeout(function() { | |
SvgAddClass($line, "rotating"); | |
}, releaseStep1AT*0.32); | |
setTimeout(function() { | |
$content.removeClass("reset").attr("style", ""); | |
SvgAddClass($smile, "active"); | |
setTimeout(function() { | |
SvgPathTween($smile, smileMid, 300, "inCubic"); | |
setTimeout(function() { | |
SvgPathTween($smile, smileEnd, 300, "inCubic"); | |
SvgAddClass($smile, "minified"); | |
}, 500); | |
}, 700); | |
}, resetAT); | |
setTimeout(function() { | |
SvgRemoveClass($line, "rotating"); | |
SvgRemoveClass($line, "show-circle"); | |
SvgAddClass($line, "reset"); | |
$body.removeClass("waiting").addClass("reset"); | |
setTimeout(function() { | |
$body.removeClass("reset").attr("style", ""); | |
SvgRemoveClass($line, "reset"); | |
$line.attr("style", ""); | |
SvgRemoveClass($smile, "minified"); | |
SvgRemoveClass($smile, "active"); | |
SvgPathTween($smile, smileInit, 50, "inCubic"); | |
animating = false; | |
}, resetAT); | |
}, releaseWaitTime); | |
}; | |
function release() { | |
animating = true; | |
if (pullDeltaY < minReleaseY) { | |
reset(); | |
} else { | |
performMagic(); | |
} | |
}; | |
$(document).on("mousedown touchstart", ".demo__body", function(e) { | |
if (animating) return; | |
var startY = e.pageY || e.originalEvent.touches[0].pageY; | |
bodyOffsetX = $body.offset().left; | |
$(document).on("mousemove touchmove", function(e) { | |
var y = e.pageY || e.originalEvent.touches[0].pageY; | |
var x = e.pageX || e.originalEvent.touches[0].pageX; | |
pullDeltaY = (y - startY) / pullSlowCoef; | |
if (!pullDeltaY) return; | |
pullChange(pullDeltaY, x); | |
}); | |
$(document).on("mouseup touchend", function() { | |
$(document).off("mousemove touchmove mouseup touchend"); | |
if (!pullDeltaY) return; | |
release(); | |
}); | |
}); | |
}); |
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> |
*, *:before, *:after { | |
box-sizing: border-box; | |
margin: 0; | |
padding: 0; | |
} | |
html, body { | |
font-size: 62.5%; | |
} | |
body { | |
background: #393A56; | |
} | |
$screenW: 37.5rem; | |
$screenH: 66.7rem; | |
$padSide: 2.4rem; | |
$padVert: 10.6rem; | |
$phoneW: $screenW + $padSide*2; | |
$phoneH: $screenH + $padVert*2; | |
$phoneBorderW: 0.4rem; | |
$topH: 9rem; | |
$lineColor: #4EDDC8; | |
$totalLen: 285.53px; | |
$lineLen: 187px; | |
$circleLen: $totalLen - $lineLen; | |
$resetAT: 0.5s; | |
$releaseStep1AT: 0.5s; | |
$releaseStep2AT: 0.3s; | |
.phone { | |
position: relative; | |
width: $phoneW; | |
height: $phoneH; | |
padding: $padVert - $phoneBorderW $padSide - $phoneBorderW; | |
margin: 3rem auto 3rem; | |
background: #000000; | |
border-radius: 5.6rem; | |
border: $phoneBorderW solid #B5B5B5; | |
&__screen { | |
overflow: hidden; | |
position: relative; | |
width: $screenW; | |
height: $screenH; | |
background: #39425A; | |
} | |
} | |
.demo { | |
position: relative; | |
height: 100%; | |
&__top { | |
position: relative; | |
height: $topH; | |
padding-top: 3rem; | |
text-align: center; | |
font-size: 0; | |
color: #CECED6; | |
text-transform: uppercase; | |
} | |
&__text { | |
font-size: 1.2rem; | |
margin-bottom: 1.6rem; | |
&.half { | |
display: inline-block; | |
vertical-align: top; | |
width: 50%; | |
} | |
} | |
&__body { | |
position: relative; | |
min-height: $screenH; | |
padding: 1.5rem; | |
transform: translate3d(0,0,0); | |
user-select: none; | |
&.reset { | |
transition: transform $resetAT; | |
transform: translate3d(0,0,0) !important; | |
} | |
&.waiting { | |
transition: transform $releaseStep2AT; | |
transform: translate3d(0,5rem,0) !important; | |
} | |
} | |
&__svg-bg { | |
overflow: visible; | |
position: absolute; | |
left: 0; | |
top: 0; | |
width: 100%; | |
height: 100%; | |
&__path { | |
fill: #FAFAFB; | |
stroke: transparent; | |
} | |
} | |
&__content { | |
&.reset { | |
transition: transform $resetAT; | |
transform: translate3d(0,0,0) !important; | |
} | |
} | |
&__el { | |
position: relative; | |
height: 16rem; | |
margin-bottom: 2rem; | |
background: #ffffff; | |
&:before { | |
content: ""; | |
position: absolute; | |
left: 1.5rem; | |
top: 1.5rem; | |
width: 9rem; | |
height: 9rem; | |
border-radius: 1.5rem; | |
background: #5A534F; | |
} | |
&:after { | |
content: ""; | |
position: absolute; | |
left: 12rem; | |
top: 1.5rem; | |
width: $screenW - 16.5rem; | |
height: 1.5rem; | |
border-radius: 0.7rem; | |
background: #5A534F; | |
color: #5A534F; | |
box-shadow: 0 3rem, 0 6rem; | |
} | |
} | |
&__svg { | |
overflow: visible; | |
position: absolute; | |
left: 0; | |
top: $topH - 0.3rem; | |
width: 100%; | |
height: 3.7rem; | |
&__line { | |
stroke: $lineColor; | |
stroke-dashoffset: $circleLen; | |
stroke-dasharray: $totalLen, $totalLen; | |
transform-origin: 187px 19px; | |
&.show-circle { | |
transition: stroke-dashoffset $releaseStep1AT*0.65; | |
stroke-dashoffset: -$lineLen !important; | |
} | |
&.rotating { | |
animation: rotating 0.5s infinite linear; | |
} | |
&.reset { | |
transition: stroke-dashoffset $resetAT; | |
stroke-dashoffset: $circleLen !important; | |
transform: rotate(0); | |
} | |
} | |
&__smile { | |
stroke: $lineColor; | |
stroke-dashoffset: 30px; | |
stroke-dasharray: 30px, 30px; | |
transform-origin: 187px 19px; | |
opacity: 0; | |
transition: opacity 0.1s, stroke-dashoffset 0.5s; | |
transition-delay: 0.2s; | |
transform: rotate(0); | |
&.active { | |
opacity: 1; | |
stroke-dashoffset: 0; | |
} | |
&.minified { | |
transition: stroke-dashoffset 0.5s 0.85s, opacity 0.1s 1.25s; | |
stroke-dashoffset: 25px; | |
opacity: 0; | |
animation: rotating 0.5s 0.35s infinite linear; | |
} | |
} | |
} | |
} | |
@keyframes rotating { | |
to { | |
transform: rotate(360deg); | |
} | |
} |