这部分时间比较忙,正是我所在行业中的旺季,也没有时间更新,这篇文章我会用SCSS+GSAP实现一个屏幕指纹的登录效果,让我们先来看看效果图
开始创作
第一步:将指纹绘制转为SVG,
我用 Adobe Illustrator 用钢笔工具追踪指纹,得到这组路径:
<svg width='180' height='320'>
<path d="M46.1,214.3c0,0-4.7-15.6,4.1-33.3"/>
<path d="M53.5,176.8c0,0,18.2-30.3,57.5-13.7"/>
<path d="M115.8,166.5c0,0,19.1,8.7,19.6,38.4"/>
<path d="M47.3,221c0,0,3.1-2.1,4.1-4.6s-5.7-20.2,7-36.7c8.5-11,22.2-19,37.9-15.3"/>
<path d="M102.2,165.4c10.2,2.7,19.5,10.4,23.5,20.2c6.2,15.2,4.9,27.1,4.1,39.4"/>
<path d="M51.1,226.5c3.3-2.7,5.1-6.3,5.7-10.5c0.5-4-0.3-7.7-0.3-11.7"/>
<path d="M129.3,200.1"/>
<path d="M56.3,197.9c3.1-16.8,17.6-29.9,35.1-28.8c17.7,1.1,30.9,14.9,32.8,32.2"/>
<path d="M124.2,207.9c0.5,9.3,0.5,18.7-2.1,27.7"/>
<path d="M54.2,231.1c2.1-2.6,4.6-5.1,6.3-8c4.2-6.8,0.9-14.8,1.5-22.3c0.5-7.1,3.4-16.3,10.4-19.7"/>
<path d="M77.9,178.2c9.3-5.1,22.9-4.7,30.5,3.3"/>
<path d="M113,186.5c0,0,13.6,18.9,1,54.8"/>
<path d="M57.3,235.2c0,0,5.7-3.8,9-12.3"/>
<path d="M111.7,231.5c0,0-4.1,11.5-5.7,13.6"/>
<path d="M61.8,239.4c9.3-8.4,12.7-19.7,11.8-31.9c-0.9-12.7,3.8-20.6,18.5-21.2"/>
<path d="M97.3,188.1c8.4,2.7,11,13,11.3,20.8c0.4,11.8-2.5,23.7-7.9,34.1c-0.1,0.1-0.1,0.2-0.2,0.3
c-0.4,0.8-0.8,1.5-1.2,2.3c-0.5,0.8-1,1.7-1.5,2.5"/>
<path d="M66.2,242.5c0,0,15.3-11.1,13.6-34.9"/>
<path d="M78.7,202.5c1.5-4.6,3.8-9.4,8.9-10.6c13.5-3.2,15.7,13.3,14.6,22.1"/>
<path d="M102.2,219.7c0,0-1.7,15.6-10.5,28.4"/>
<path d="M72,244.9c0,0,8.8-9.9,9.9-15.7"/>
<path d="M84.5,223c0.3-2.6,0.5-5.2,0.7-7.8c0.1-2.1,0.2-4.6-0.1-6.8c-0.3-2.2-1.1-4.3-0.9-6.5c0.5-4.4,7.2-6.9,10.1-3.1c1.7,2.2,1.7,5.3,1.9,7.9c0.4,3.8,0.3,7.6,0,11.4c-1,10.8-5.4,21-11.5,29.9"/>
<path d="M90,201.2c0,0,4.6,28.1-11.4,45.2"/>
<path d="M67.3,219C65,188.1,78,180.1,92.7,180.3c18.3,2,23.7,18.3,20,46.7"/>
</svg>
复制代码
效果:
第二步:实现动画
我将重点介绍这里的重要部分,你可以参考演示以获取完整代码。
填写指纹: 让我们创建手机屏幕和指纹的 HTML 结构。
<div class="demo">
<div class="demo__screen demo__screen--clickable">
<svg class="demo__fprint" viewBox="0 0 180 320">
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M46.1,214.3c0,0-4.7-15.6,4.1-33.3"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M53.5,176.8c0,0,18.2-30.3,57.5-13.7"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M115.8,166.5c0,0,19.1,8.7,19.6,38.4"/>
</svg>
到目前为止,样式非常简单。请注意,我在整个演示中都使用了 Sass——并有助于我们完成一些更重的工作。
$scale: 1.65;
$purplish-color: #8742cc;
$pinkish-color: #a94a8c;
$bg-color: #372546;
.demo {
background: linear-gradient(45deg, lighten($pinkish-color, 10%), lighten($purplish-color, 10%));
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 0;
user-select: none;
overflow: hidden;
position: relative;
&__screen {
position: relative;
background-color: $bg-color;
overflow: hidden;
flex-shrink: 0;
&--clickable {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
}
&__fprint-path {
stroke-width: 2.5px;
stroke-linecap: round;
fill: none;
stroke: white;
visibility: hidden;
transition: opacity 0.5s ease;
&--pinkish {
stroke: $pinkish-color;
}
&--purplish {
stroke: $purplish-color;
}
}
&__fprint {
width: 180px * $scale;
height: 320px * $scale;
position: relative;
top: 20px * $scale;
overflow: visible;
background-image: url('fprintBackground.svg');
background-size: cover;
&--no-bg {
background-image: none;
}
}
}
现在是困难的部分:使指纹具有交互性。这就是我将用来填充每个单独路径的方法。
创建一个描述路径元素的类,以便以后更容易操作路径。
class Path {
constructor(selector, index) {
this.index = index;
this.querySelection = document.querySelectorAll(selector)[index];
this.length = this.querySelection.getTotalLength();
this.$ = $(selector).eq(index);
this.setDasharray();
this.removesForwards = this.$.hasClass('demo__fprint-path--removes-forwards');
}
setDasharray() {
this.$.css('stroke-dasharray', `${this.length} ${this.length + 2}`);
return this;
}
offset(ratio) {
this.$.css('stroke-dashoffset', -this.length * ratio + 1);
return this;
}
makeVisible() {
this.$.css('visibility', 'visible');
return this;
}
}
复制代码
大体思路是这样的:为我们指纹中的每条路径创建一个此类的实例,并在每一帧中修改它们。路径将以-1(完全不可见)的偏移比率开始,然后将每帧以恒定值增加偏移比率(我们将在此处称为“偏移”),直到它们到达0(完全可见)。此时填充动画将结束。
还要处理用户停止点击或按下鼠标按钮的情况。在这种情况下,我们将在相反的方向上设置动画(从每帧的偏移量中减去一个常数值,直到它-1再次到达)。
创建一个函数来计算每一帧的偏移增量——后面我们会用到的。
function getPropertyIncrement(startValue, endValue, transitionDuration) {
//制作60 fps的动画
const TICK_TIME = 1000 / 60;
const ticksToComplete = transitionDuration / TICK_TIME;
return (endValue - startValue) / ticksToComplete;
}
复制代码
第三步:制作动画
我们将指纹路径保存在一个数组中:
let fprintPaths = [];
// 为每个现有路径创建一个 Path 实例。
// 首先把路径设为不可见,等JavaScript 运行完成之后,我们将它设置为在 CSS 中不可见。 这样我们就可以抵消它们,然后使它们可见。
for (let i = 0; i < $(fprintPathSelector).length; i++) {
fprintPaths.push(new Path(fprintPathSelector, i));
fprintPaths[i].offset(-1).makeVisible();
}
复制代码
我们将为动画中的每一帧遍历该数组,对路径进行动画处理:
let fprintTick = getPropertyIncrement(0, 1, TIME_TO_FILL_FPRINT);
function fprintFrame(timestamp) {
// 如果时间少于 1000 / 65 毫秒,我从最后一帧开始绘制(因为有刷新率更高的屏幕在那里,我们要做到所有设备看起来都一样)。 为什么这里使用的是65而不是60呢,因为在60 Hz 屏幕中可能会跳帧。
if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
lastRafCallTimestamp = timestamp;
curFprintPathsOffset += fprintTick * fprintProgressionDirection;
offsetAllFprintPaths(curFprintPathsOffset);
}
// 如果动画未结束,则安排下一帧
if (curFprintPathsOffset >= -1 && curFprintPathsOffset <= 0) {
isFprintAnimationInProgress = true;
window.requestAnimationFrame(fprintFrame);
}
// 动画结束之后。 我们可以安排下一个动画步骤
else if (curFprintPathsOffset > 0) {
curFprintPathsOffset = 0;
offsetAllFprintPaths(curFprintPathsOffset);
isFprintAnimationInProgress = false;
isFprintAnimationOver = true;
// 删除带有灰色路径的背景
$fprint.addClass('demo__fprint--no-bg');
// 安排下一个动画步骤 - 将其中一个路径转换为字符串
startElasticAnimation();
// 安排指纹清除
window.requestAnimationFrame(removeFprint);
}
// 当用户松开点击的那一刻指纹恢复原状
else if (curFprintPathsOffset < -1) {
curFprintPathsOffset = -1;
offsetAllFprintPaths(curFprintPathsOffset);
isFprintAnimationInProgress = false;
}
}
复制代码
我们将在演示中附加一些事件侦听器:
$screen.on('mousedown touchstart', function() {
fprintProgressionDirection = 1;
// 如果动画已经在进行中,我们不会安排下一帧,因为它已经在 `fprintFrame` 中安排了。 此外,如果动画已经结束,我们显然不会安排它。 这就是为什么我们对这些条件有两个单独的标志
if (!isFprintAnimationInProgress && !isFprintAnimationOver)
window.requestAnimationFrame(fprintFrame);
})
// 在 `mouseup` / `touchend` 上,我们翻转动画方向
$(document).on('mouseup touchend', function() {
fprintProgressionDirection = -1;
if (!isFprintAnimationInProgress && !isFprintAnimationOver)
window.requestAnimationFrame(fprintFrame);
})
复制代码
现在我们应该完成点击的部分了!演示:
第四步:去除指纹
这部分与制作部分非常相似,只是我们必须考虑这样一个事实,即一些路径在一个方向上移除,而其余路径在另一个方向上移除。这就是我们之前添加--removes-forwards修饰符的原因。
首先,我们将有两个额外的数组:一个用于向前删除的路径,另一个用于向后删除的路径:
const fprintPathsFirstHalf = [];
const fprintPathsSecondHalf = [];
for (let i = 0; i < $(fprintPathSelector).length; i++) {
// ...
if (fprintPaths[i].removesForwards)
fprintPathsSecondHalf.push(fprintPaths[i]);
else
fprintPathsFirstHalf.push(fprintPaths[i]);
}
复制代码
我们将编写一个函数,将它们向正确的方向偏移:
function offsetFprintPathsByHalves(ratio) {
fprintPathsFirstHalf.forEach(path => path.offset(ratio));
fprintPathsSecondHalf.forEach(path => path.offset(-ratio));
}
复制代码
我们还需要一个绘制框架的函数:
function removeFprintFrame(timestamp) {
// 如果我们的速度超过 65 fps,则可能丢帧
if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
curFprintPathsOffset += fprintTick * fprintProgressionDirection;
offsetFprintPathsByHalves(curFprintPathsOffset);
lastRafCallTimestamp = timestamp;
}
// 如果动画未结束,则安排下一帧
if (curFprintPathsOffset >= -1)
window.requestAnimationFrame(removeFprintFrame);
else {
// 由于浮点错误,最终偏移量可能略小于 -1,因此如果超过该值,我们将为其分配 -1 并再动画一帧
curFprintPathsOffset = -1;
offsetAllFprintPaths(curFprintPathsOffset);
}
}
function removeFprint() {
fprintProgressionDirection = -1;
window.requestAnimationFrame(removeFprintFrame);
}
复制代码
现在剩下的就是removeFprint
在我们完成指纹填充后调用:
function fprintFrame(timestamp) {
// ...
else if (curFprintPathsOffset > 0) {
// ...
window.requestAnimationFrame(removeFprint);
}
// ...
}
复制代码
得到的效果是这样的:
第五步:动画路径结束
可以看到,由于指纹几乎被移除,因此其某些路径比开始时更长。我将它们移动到不同的路径中,在适当的时候开始动画。我可以将它们合并到现有路径中,但这会困难得多,而且在 60fps 下几乎没有区别。
<path class="demo__ending-path demo__ending-path--pinkish" d="M48.4,220c-5.8,4.2-6.9,11.5-7.6,18.1c-0.8,6.7-0.9,14.9-9.9,12.4c-9.1-2.5-14.7-5.4-19.9-13.4c-3.4-5.2-0.4-12.3,2.3-17.2c3.2-5.9,6.8-13,14.5-11.6c3.5,0.6,7.7,3.4,4.5,7.1"/>
复制代码
基本样式:
&__ending-path {
fill: none;
stroke-width: 2.5px;
stroke-dasharray: 60 1000;
stroke-dashoffset: 61;
stroke-linecap: round;
will-change: stroke-dashoffset, stroke-dasharray, opacity;
transform: translateZ(0);
transition: stroke-dashoffset 1s ease, stroke-dasharray 0.5s linear, opacity 0.75s ease;
&--removed {
stroke-dashoffset: -130;
stroke-dasharray: 5 1000;
}
&--transparent {
opacity: 0;
}
&--pinkish {
stroke: $pinkish-color;
}
&--purplish {
stroke: $purplish-color;
}
}
复制代码
添加--removed
修饰符以在适当的时候流入这些路径:
function removeFprint() {
$endingPaths.addClass('demo__ending-path--removed');
setTimeout(() => {
$endingPaths.addClass('demo__ending-path--transparent');
}, TIME_TO_REMOVE_FPRINT * 0.9);
// ...
}
复制代码
指纹效果完成:
最后一步:指纹变形
这里我们需要使用 GSAP 的morphSVG 插件
创建一条路径和一条线,它们将成为我们字符串的关键帧:
<line id='demo__straight-path' x1="0" y1="151.3" x2="180" y2="151.3"/>
<path class="demo__hidden-path" id='demo__arc-to-top' d="M0,148.4c62.3-13.5,122.3-13.5,180,0"/>
复制代码
然后我们将使用 morphSVG 来转换关键帧之间的路径:
const $elasticPath = $('#demo__elastic-path');
const ELASTIC_TRANSITION_TIME_TO_STRAIGHT = 250;
const WOBBLE_TIME = 1000;
function startElasticAnimation() {
$elasticPath.css('stroke-dasharray', 'none');
const elasticAnimationTimeline = new TimelineLite();
elasticAnimationTimeline
.to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_STRAIGHT / 1000, {
delay: TIME_TO_REMOVE_FPRINT / 1000 * 0.7,
morphSVG: '#demo__arc-to-top'
})
.to('#demo__elastic-path', WOBBLE_TIME / 1000, {
morphSVG: '#demo__straight-path',
ease: Elastic.easeOut.config(1, 0.3)
})
}
复制代码
fprintFrame
一旦指纹被填充,我们将在内部调用这个函数:
function fprintFrame(timestamp) {
// ...
else if (curFprintPathsOffset > 0) {
// ...
startElasticAnimation();
// ...
}
// ...
}
复制代码
最终效果完成:
完整代码 HTML:
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>CodePen - css-t. part 4</title>
<meta name="viewport" content="width=device-width, minimum-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="demo">
<div class="demo__screen demo__screen--clickable">
<svg class="demo__fprint" viewBox="0 0 180 320">
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M46.1,214.3c0,0-4.7-15.6,4.1-33.3"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M53.5,176.8c0,0,18.2-30.3,57.5-13.7"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M115.8,166.5c0,0,19.1,8.7,19.6,38.4"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M47.3,221c0,0,3.1-2.1,4.1-4.6s-5.7-20.2,7-36.7c8.5-11,22.2-19,37.9-15.3"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M102.2,165.4c10.2,2.7,19.5,10.4,23.5,20.2c6.2,15.2,4.9,27.1,4.1,39.4"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M51.1,226.5c3.3-2.7,5.1-6.3,5.7-10.5c0.5-4-0.3-7.7-0.3-11.7"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M56.3,197.9c3.1-16.8,17.6-29.9,35.1-28.8c17.7,1.1,30.9,14.9,32.8,32.2"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--purplish" d="M124.2,207.9c0.5,9.3,0.5,18.7-2.1,27.7"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M54.2,231.1c2.1-2.6,4.6-5.1,6.3-8c4.2-6.8,0.9-14.8,1.5-22.3c0.5-7.1,3.4-16.3,10.4-19.7"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M77.9,178.2c9.3-5.1,22.9-4.7,30.5,3.3"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--purplish" d="M113,186.5c0,0,13.6,18.9,1,54.8"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M57.3,235.2c0,0,5.7-3.8,9-12.3"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M111.7,231.5c0,0-4.1,11.5-5.7,13.6"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M61.8,239.4c9.3-8.4,12.7-19.7,11.8-31.9c-0.9-12.7,3.8-20.6,18.5-21.2"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M97.3,188.1c8.4,2.7,11,13,11.3,20.8c0.4,11.8-2.5,23.7-7.9,34.1c-0.1,0.1-0.1,0.2-0.2,0.3
c-0.4,0.8-0.8,1.5-1.2,2.3c-0.5,0.8-1,1.7-1.5,2.5"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M66.2,242.5c0,0,15.3-11.1,13.6-34.9"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M78.7,202.5c1.5-4.6,3.8-9.4,8.9-10.6c13.5-3.2,15.7,13.3,14.6,22.1"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M102.2,219.7c0,0-1.7,15.6-10.5,28.4"/>
<path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M72,244.9c0,0,8.8-9.9,9.9-15.7"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M84.5,223c0.3-2.6,0.5-5.2,0.7-7.8c0.1-2.1,0.2-4.6-0.1-6.8c-0.3-2.2-1.1-4.3-0.9-6.5c0.5-4.4,7.2-6.9,10.1-3.1
c1.7,2.2,1.7,5.3,1.9,7.9c0.4,3.8,0.3,7.6,0,11.4c-1,10.8-5.4,21-11.5,29.9"/>
<path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--purplish" d="M90,201.2c0,0,4.6,28.1-11.4,45.2"/>
<path class="demo__fprint-path demo__fprint-path--pinkish" id='demo__elastic-path' d="M67.3,219C65,188.1,78,180.1,92.7,180.3c18.3,2,23.7,18.3,20,46.7"/>
<line id='demo__straight-path' x1="0" y1="151.3" x2="180" y2="151.3"/>
<path class="demo__hidden-path" id='demo__arc-to-top' d="M0,148.4c62.3-13.5,122.3-13.5,180,0"/>
<path class="demo__ending-path demo__ending-path--pinkish"d="M48.4,220c-5.8,4.2-6.9,11.5-7.6,18.1c-0.8,6.7-0.9,14.9-9.9,12.4c-9.1-2.5-14.7-5.4-19.9-13.4c-3.4-5.2-0.4-12.3,2.3-17.2c3.2-5.9,6.8-13,14.5-11.6c3.5,0.6,7.7,3.4,4.5,7.1"/>
<path class="demo__ending-path demo__ending-path--pinkish"d="M57.3,235.2c-14.4,9.4-10.3,19.4-17.8,21.1c-5.5,1.3-8.4-7.8-13.8-4.2c-2.6,1.7-5.7,7.7-4.6,10.9c0.7,2,4.1,2,5.8,2.4c3,0.7,8.4,3,7.6,7.2c-0.6,3-5,5.3-2.4,8.7c1.8,2.2,4.7,1.1,6.9,0.3c11.7-4.3,14.5,0.8,16.5,0.9"/>
<path class="demo__ending-path demo__ending-path--purplish"d="M79,246c-1.8,2.4-4.9,2.9-7.6,3.2c-2.7,0.3-5.8-0.8-7.7,1.6c-2.9,3.3,0.7,8.2-1.2,12c-1.5,2.8-4.5,2.4-6.9,1.3c-10.1-4.7-33.2-17.5-38.1-2.5c-1.1,3.4-1.9,7.5-1.3,11c0.6,4,5.6,7.9,7.7,2.3c0.8-2.1,3.1-8.6-1-8.9"/>
<path class="demo__ending-path demo__ending-path--pinkish"d="M91.8,248c0,0-3.9,6.4-6.2,9.2c-3.8,4.5-7.9,8.9-11.2,13.8c-1.9,2.8-4.4,6.4-3.7,10c0.9,5.2,4.7,12.5,9.7,14.7c5.2,2.2,15.9-4.7,13.1-10.8c-1.4-3-6.3-7.9-10-7.2c-1,0.2-1.8,1-2,2"/>
<path class="demo__ending-path demo__ending-path--purplish"d="M114.8,239.4c-2.7,6.1-8.3,12.8-7.8,19.8c0.3,4.6,3.8,7.4,7.8,9.1c8.9,3.8,19.7,0.4,28.6-1.3c8.8-1.7,19.7-3.2,23.7,6.7c2.8,6.8,6.1,14.7,4.4,22.2"/>
<path class="demo__ending-path demo__ending-path--pinkish"d="M129.9,224.2c-0.4,7.5-3.1,18,0.7,25c2.8,5.1,14.3,6.3,19.5,7.4c3.7,0.7,8.7,2.2,12-0.5c6.7-5.4,11.1-13.7,14.1-21.6c3.1-8-4.4-12.8-11.1-14.5c-5-1.3-19.1-0.7-21-6.7c-0.9-2.8,1.8-5.9,3.4-7.9"/>
</svg>
</div>
</div>
<!-- partial -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/MorphSVGPlugin.min.js?r=182'></script><script src="./script.js"></script>
</body>
</html>
复制代码
CSS:
*, *:before, *:after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.demo {
background: linear-gradient(45deg, #bd69a3, #a16ad7);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
overflow: hidden;
position: relative;
}
.demo__screen {
position: relative;
background-color: #372546;
overflow: hidden;
flex-shrink: 0;
}
.demo__screen--clickable {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.demo__fprint-path {
stroke-width: 2.5px;
stroke-linecap: round;
fill: none;
stroke: white;
visibility: hidden;
transition: opacity 0.5s ease;
will-change: stroke-dashoffset, stroke-dasharray;
transform: translateZ(0);
}
.demo__fprint-path--pinkish {
stroke: #a94a8c;
}
.demo__fprint-path--purplish {
stroke: #8742cc;
}
.demo__fprint-path#demo__elastic-path {
will-change: opacity;
opacity: 1;
}
.demo__hidden-path {
display: none;
}
.demo__fprint {
width: 297px;
height: 528px;
position: relative;
top: 33px;
overflow: visible;
background-image: url("fprintBackground.svg");
background-size: cover;
}
.demo__fprint--no-bg {
background-image: none;
}
.demo__ending-path {
fill: none;
stroke-width: 2.5px;
stroke-dasharray: 60 1000;
stroke-dashoffset: 61;
stroke-linecap: round;
will-change: stroke-dashoffset, stroke-dasharray, opacity;
transform: translateZ(0);
transition: stroke-dashoffset 1s ease, stroke-dasharray 0.5s linear, opacity 0.75s ease;
}
.demo__ending-path--removed {
stroke-dashoffset: -130;
stroke-dasharray: 5 1000;
}
.demo__ending-path--transparent {
opacity: 0;
}
.demo__ending-path--pinkish {
stroke: #a94a8c;
}
.demo__ending-path--purplish {
stroke: #8742cc;
}
复制代码
JS:
$(document).ready(function() {
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(cb) {
setTimeout(() => cb(new Date()), 1000 / 60);
}
}
const TIME_TO_FILL_FPRINT = 700; //ms
const TIME_TO_REMOVE_FPRINT = 250;
const ELASTIC_TRANSITION_TIME_TO_STRAIGHT = 250;
const WOBBLE_TIME = 1000;
const fprintPathSelector = '.demo__fprint-path';
const $fprintPaths = $('.demo__fprint-path');
const $fprint = $('.demo__fprint');
const $screen = $('.demo__screen');
const $endingPaths = $('.demo__ending-path');
const $elasticPath = $('#demo__elastic-path');
let isFprintAnimationInProgress = false;
let isFprintAnimationOver = false;
let curFprintPathsOffset = -1;
let fprintProgressionDirection = 1;
let lastRafCallTimestamp = 0;
let fprintTick = getPropertyIncrement(0, 1, TIME_TO_FILL_FPRINT);
let fprintPaths = [];
const fprintPathsFirstHalf = [];
const fprintPathsSecondHalf = [];
for (let i = 0; i < $(fprintPathSelector).length; i++) {
fprintPaths.push(new Path(fprintPathSelector, i));
fprintPaths[i].offset(-1).makeVisible();
if (fprintPaths[i].removesForwards)
fprintPathsSecondHalf.push(fprintPaths[i]);
else
fprintPathsFirstHalf.push(fprintPaths[i]);
}
function fprintFrame(timestamp) {
if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
lastRafCallTimestamp = timestamp;
curFprintPathsOffset += fprintTick * fprintProgressionDirection;
offsetAllFprintPaths(curFprintPathsOffset);
}
if (curFprintPathsOffset >= -1 && curFprintPathsOffset <= 0) {
isFprintAnimationInProgress = true;
window.requestAnimationFrame(fprintFrame);
}
else if (curFprintPathsOffset > 0) {
curFprintPathsOffset = 0;
offsetAllFprintPaths(curFprintPathsOffset);
isFprintAnimationInProgress = false;
isFprintAnimationOver = true;
$fprint.addClass('demo__fprint--no-bg');
startElasticAnimation();
fprintTick = getPropertyIncrement(0, 1, TIME_TO_REMOVE_FPRINT);
window.requestAnimationFrame(removeFprint);
}
else if (curFprintPathsOffset < -1) {
curFprintPathsOffset = -1;
offsetAllFprintPaths(curFprintPathsOffset);
isFprintAnimationInProgress = false;
}
}
function removeFprint() {
$endingPaths.addClass('demo__ending-path--removed');
setTimeout(() => {
$endingPaths.addClass('demo__ending-path--transparent');
}, TIME_TO_REMOVE_FPRINT * 0.9);
fprintProgressionDirection = -1;
window.requestAnimationFrame(removeFprintFrame);
}
function removeFprintFrame(timestamp) {
if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
curFprintPathsOffset += fprintTick * fprintProgressionDirection;
offsetFprintPathsByHalves(curFprintPathsOffset);
lastRafCallTimestamp = timestamp;
}
if (curFprintPathsOffset >= -1)
window.requestAnimationFrame(removeFprintFrame);
else {
curFprintPathsOffset = -1;
offsetAllFprintPaths(curFprintPathsOffset);
}
}
function startElasticAnimation() {
$elasticPath.css('stroke-dasharray', 'none');
const elasticAnimationTimeline = new TimelineLite();
elasticAnimationTimeline
.to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_STRAIGHT / 1000, {
delay: TIME_TO_REMOVE_FPRINT / 1000 * 0.7,
morphSVG: '#demo__arc-to-top'
})
.to('#demo__elastic-path', WOBBLE_TIME / 1000, {
morphSVG: '#demo__straight-path',
ease: Elastic.easeOut.config(1, 0.3)
})
}
function offsetAllFprintPaths(ratio) {
fprintPaths.forEach(path => path.offset(ratio));
}
function offsetFprintPathsByHalves(ratio) {
fprintPathsFirstHalf.forEach(path => path.offset(ratio));
fprintPathsSecondHalf.forEach(path => path.offset(-ratio));
}
$screen.on('mousedown touchstart', function() {
fprintProgressionDirection = 1;
if (!isFprintAnimationInProgress && !isFprintAnimationOver)
window.requestAnimationFrame(fprintFrame);
})
$(document).on('mouseup touchend', function() {
fprintProgressionDirection = -1;
if (!isFprintAnimationInProgress && !isFprintAnimationOver)
window.requestAnimationFrame(fprintFrame);
});
});
function getPropertyIncrement(startValue, endValue, transitionDuration) {
const TICK_TIME = 1000 / 60;
const ticksToComplete = transitionDuration / TICK_TIME;
return (endValue - startValue) / ticksToComplete;
}
class Path {
constructor(selector, index) {
this.index = index;
this.querySelection = document.querySelectorAll(selector)[index];
this.length = this.querySelection.getTotalLength();
this.$ = $(selector).eq(index);
this.setDasharray();
this.removesForwards = this.$.hasClass('demo__fprint-path--removes-forwards');
}
setDasharray() {
this.$.css('stroke-dasharray', `${this.length} ${this.length + 2}`);
return this;
}
offset(ratio) {
this.$.css('stroke-dashoffset', -this.length * ratio + 1);
return this;
}
makeVisible() {
this.$.css('visibility', 'visible');
return this;
}
}
复制代码