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); | |
| } | |
| } |