练习动画最好的方式(二):屏幕指纹开锁动画效果

这部分时间比较忙,正是我所在行业中的旺季,也没有时间更新,这篇文章我会用SCSS+GSAP实现一个屏幕指纹的登录效果,让我们先来看看效果图

12345.gif

开始创作

第一步:将指纹绘制转为SVG,

我用 Adob​​e 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>
复制代码

效果:

image.png

第二步:实现动画

我将重点介绍这里的重要部分,你可以参考演示以获取完整代码。

填写指纹: 让我们创建手机屏幕和指纹的 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);
})
复制代码

现在我们应该完成点击的部分了!演示:

12.gif

第四步:去除指纹

这部分与制作部分非常相似,只是我们必须考虑这样一个事实,即一些路径在一个方向上移除,而其余路径在另一个方向上移除。这就是我们之前添加--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);
  }
  // ...
}
复制代码

得到的效果是这样的:

123.gif

第五步:动画路径结束

可以看到,由于指纹几乎被移除,因此其某些路径比开始时更长。我将它们移动到不同的路径中,在适当的时候开始动画。我可以将它们合并到现有路径中,但这会困难得多,而且在 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);
  // ...
}
复制代码

指纹效果完成:

1234.gif

最后一步:指纹变形

这里我们需要使用 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();
    // ...
  }
  // ...
}
复制代码

最终效果完成:

12345.gif

完整代码 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;
  }
}
复制代码

gan.png

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端仙人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值