为 TouchID 创建动画登录表单

Avatar of Kirill Kiyutin
Kirill Kiyutin

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费信用额度!

我之前偶然发现了 Jakub Reis 的这个很棒的 Dribbble 作品。它引起了我的注意,我知道我必须尝试用代码重新创建它。当时,我不知道怎么做。我尝试了很多不同的方法,大约一年后,我终于成功地 制作了这个演示

我在这个过程中学到了一些东西,所以让我带您踏上一段小旅程,了解我做了什么来制作它,因为您也可能学到一两件事。

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 银行应用程序的启动屏幕

步骤 1:将工作分解成部分

我多次观看原始 GIF。我的目标是将动画分解成小的、易于理解的片段,我能够将其分解如下

我知道,这看起来很多,但我们可以做到!

步骤 2:逐帧拆解原始演示

我需要从原始 GIF 中提取尽可能多的信息,以便对动画有一个很好的理解,所以我将其分解成单个帧。实际上,有很多服务可以为我们做到这一点。我使用的是 ezgif.com 上的一个,但也可以很容易地使用其他服务。无论哪种方式,这都能让我们获取有关颜色、大小和比例等细节的信息,这些信息是我们需要创建的所有不同元素的信息。

哦,我们还需要将指纹转换为 SVG。同样,有很多应用程序可以帮助我们。我使用 Adobe Illustrator 用钢笔工具描绘指纹,以获得这组路径

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 css-t. 路径

我们将对动画结束时出现的折线图进行相同的处理,所以不妨保持矢量编辑器打开。🙂

步骤 3:实现动画

我将解释最终笔中的动画工作原理,但您也可以在文章末尾找到我在这个过程中的一些失败方法。

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

填充指纹

让我们创建手机屏幕和指纹的 HTML 结构。

<div class="demo">
  <div class="demo__screen demo__screen--clickable">
    <svg class="demo__fprint" viewBox="0 0 180 320">  
      <!-- removes-forwards and removes-backwards classes will be helpful later on -->
      <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"/>
      <!-- ... and about 20 more paths like this -->
    </svg>
  

到目前为止,样式非常简单。请注意,我在整个演示中都使用了 Sass,我发现它有助于保持工作整洁,并有助于完成一些我们必须完成的更繁重的工作。

// I use a $scale variable to quickly change the scaling of the whole pen, so I can focus on the animation and decide on the size later on.
$scale: 1.65;
$purplish-color: #8742cc;
$pinkish-color: #a94a8c;
$bg-color: #372546;

// The main container
.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;
  
  // The screen that holds the login component
  &__screen {
    position: relative;
    background-color: $bg-color;
    overflow: hidden;
    flex-shrink: 0;
    &--clickable {
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
    }
  }
  
  // Styles the fingerprint SVG paths
  &__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;
    }    
  }
  
  // Sizes positions the fingerprint SVG
  &__fprint {
    width: 180px * $scale;
    height: 320px * $scale;
    position: relative;
    top: 20px * $scale;
    overflow: visible;
    // This is going to serve as background to show "unfilled" paths. we're gonna remove it at the moment where the filling animation is over
    background-image: url('https://kiyutink.github.io/svg/fprintBackground.svg');
    background-size: cover;
    
    &--no-bg {
      background-image: none;
    }
  }
}

现在是难点:使指纹交互。您可以 在这里阅读有关 SVG 线条动画的信息。这是我们将用于填充每个单独路径的方法。

让我们创建一个描述路径元素的类,以便稍后更轻松地操作路径。

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(完全可见)。填充动画将在此时结束。

如果您从未使用这种逐帧方法进行任何动画,这里有一个非常简单的演示,可以帮助您了解它的工作原理

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 60fps raf 动画概念验证

我们还应处理用户停止点击或按鼠标按钮的情况。在这种情况下,我们将以相反的方向进行动画(每一帧从偏移量中减去一个常数值,直到它再次回到 -1)。

让我们创建计算每一帧偏移增量的函数,这在稍后会很有用。

function getPropertyIncrement(startValue, endValue, transitionDuration) {
  // We animate at 60 fps
  const TICK_TIME = 1000 / 60;
  const ticksToComplete = transitionDuration / TICK_TIME;
  return (endValue - startValue) / ticksToComplete;
}

现在是动画化的时候了!我们将把指纹路径保存在单个数组中

let fprintPaths = [];

// We create an instance of Path for every existing path. 
// We don't want the paths to be visible at first and then 
// disappear after the JavaScript runs, so we set them to 
// be invisible in CSS. That way we can offset them first 
// and then make them visible.
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) {
  
  // We don't want to paint if less than 1000 / 65 ms elapsed 
  // since the last frame (because there are faster screens 
  // out there and we want the animation to look the same on 
  // all devices). We use 65 instead of 60 because, even on 
  // 60 Hz screens, `requestAnimationFrame` can sometimes be called 
  // a little sooner, which can result in a skipped frame.
  if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
    lastRafCallTimestamp = timestamp;
    curFprintPathsOffset += fprintTick * fprintProgressionDirection;
    offsetAllFprintPaths(curFprintPathsOffset);
  }
  
  // Schedule the next frame if the animation isn't over
  if (curFprintPathsOffset >= -1 && curFprintPathsOffset <= 0) {
    isFprintAnimationInProgress = true;
    window.requestAnimationFrame(fprintFrame);
  }
  
  // The animation is over. We can schedule next animation steps
  else if (curFprintPathsOffset > 0) {
    curFprintPathsOffset = 0;
    offsetAllFprintPaths(curFprintPathsOffset);
    isFprintAnimationInProgress = false;
    isFprintAnimationOver = true;
    // Remove the background with grey paths
    $fprint.addClass('demo__fprint--no-bg');
    // Schedule the next animation step - transforming one of the paths into a string 
    // (this function is not implemented at this step yet, but we'll do that soon)
    startElasticAnimation();
    // Schedule the fingerprint removal (removeFprint function will be implemented in the next section)
    window.requestAnimationFrame(removeFprint);
  }
  // The fingerprint is back to the original state (the user has stopped holding the mouse down)
  else if (curFprintPathsOffset < -1) {
    curFprintPathsOffset = -1;
    offsetAllFprintPaths(curFprintPathsOffset);
    isFprintAnimationInProgress = false;
  }
}

我们将附加一些事件监听器到演示

$screen.on('mousedown touchstart', function() {
  fprintProgressionDirection = 1;
  // If the animation is already in progress,
  // we don't schedule the next frame since it's 
  // already scheduled in the `fprintFrame`. Also, 
  // we obviously don't schedule it if the animation 
  // is already over. That's why we have two separate 
  // flags for these conditions.
  if (!isFprintAnimationInProgress && !isFprintAnimationOver)
    window.requestAnimationFrame(fprintFrame);
})

// On `mouseup` / `touchend` we flip the animation direction
$(document).on('mouseup touchend', function() {
  fprintProgressionDirection = -1;
  if (!isFprintAnimationInProgress && !isFprintAnimationOver)
    window.requestAnimationFrame(fprintFrame);
})

…现在我们应该完成了第一步!以下是我们在这一步中的工作成果

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 css-t. 步骤 1

移除指纹

这部分与第一部分非常相似,只是现在我们必须考虑一些路径向前移除,而其他路径向后移除。这就是为什么我们之前添加了 --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) {
  // Drop the frame if we're faster than 65 fps
  if (timestamp - lastRafCallTimestamp >= 1000 / 65) {
    curFprintPathsOffset += fprintTick * fprintProgressionDirection;
    offsetFprintPathsByHalves(curFprintPathsOffset);
    lastRafCallTimestamp = timestamp;
  }
  // Schedule the next frame if the animation isn't over
  if (curFprintPathsOffset >= -1)
    window.requestAnimationFrame(removeFprintFrame);
  else {
    // Due to the floating point errors, the final offset might be 
    // slightly less than -1, so if it exceeds that, we'll just 
    // assign -1 to it and animate one more frame
    curFprintPathsOffset = -1;
    offsetAllFprintPaths(curFprintPathsOffset);
  }
}

function removeFprint() {
  fprintProgressionDirection = -1;
  window.requestAnimationFrame(removeFprintFrame);
}

现在剩下的就是填充指纹后调用 removeFprint

function fprintFrame(timestamp) {
  // ...
  else if (curFprintPathsOffset > 0) {
    // ...
    window.requestAnimationFrame(removeFprint);
  }
  // ...
}

现在让我们检查一下我们的工作成果

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 css-t. 第 2 部分

动画化路径端点

您可以看到,当指纹几乎被移除时,它的一些路径比一开始更长。我把它们移动到单独的路径中,这些路径在正确的时间开始动画化。我可以将它们合并到现有的路径中,但这要困难得多,而且在 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"/>
<!-- and 5 more paths like this -->

…并应用一些基本样式

&__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);
  // ...
}

现在,我们的工作真的开始成型了

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 css-t. 第 3 部分

变形指纹

好的,我发现这部分很难自己做,但使用 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',
      // I played with the easing a bit to get that "vibration" effect
      ease: Elastic.easeOut.config(1, 0.3)
    })
}

当指纹填充后,我们将在 fprintFrame 中调用此函数

function fprintFrame(timestamp) {
  // ...
  else if (curFprintPathsOffset > 0) {
    // ...
    startElasticAnimation();
    // ...
  }
  // ...
}

结果如下

查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 css-t. 第 4 部分

动画化漂浮的子弹

为此,我使用了一些简单的直接 CSS 动画。我选择了时序函数来模拟重力。您可以 在这里在这里 尝试不同的时序函数。

让我们创建一个 div

<div class="demo__bullet"></div>

…并对其应用一些样式

&__bullet {
  position: absolute;
  width: 4px * $scale;
  height: 4px * $scale;
  background-color: white;
  border-radius: 50%;
  top: 210px * $scale;
  left: 88px * $scale;
  opacity: 0;
  transition: all 0.7s cubic-bezier(0.455, 0.030, 0.515, 0.955);
  will-change: transform, opacity;
  
  // This will be applied after the bullet has descended, to create a transparent "aura" around it
  &--with-aura {
    box-shadow: 0 0 0 3px * $scale rgba(255, 255, 255, 0.3);
  }
  // This will be applied to make the bullet go up
  &--elevated {
    transform: translate3d(0, -250px * $scale, 0);
    opacity: 1;
  }
  // This will be applied to make the bullet go down
  &--descended {
    transform: translate3d(0, 30px * $scale, 0);
    opacity: 1;
    transition: all 0.6s cubic-bezier(0.285, 0.210, 0.605, 0.910);
  }
}

然后,我们通过根据用户的交互添加和删除类来将它们绑定在一起

const DELAY_TO_BULLET_AURA = 300;
const ELEVATION_TIME = 700;
const DELAY_AFTER_ELEVATION = 700;

const $bullet = $('.demo__bullet');

function elevateBullet() {
  $bullet.addClass('demo__bullet--elevated');
}

function descendBullet() {
  $bullet.addClass('demo__bullet--descended').removeClass('demo__bullet--elevated');
  animateBulletAura();
}
  
function animateBulletAura() {
  setTimeout(() => $bullet.addClass('demo__bullet--with-aura'), DELAY_TO_BULLET_AURA);
}

function animateBullet() {
  elevateBullet();
  $screen.removeClass('demo__screen--clickable');
  setTimeout(descendBullet, ELEVATION_TIME + DELAY_AFTER_ELEVATION);
}

现在,我们需要调用 `animateBullet` 函数

function startElasticAnimation() {
  // ...
  animateBullet();
}

这是我们目前所处的位置

查看示例 css-t. part 5 由 Kirill Kiyutin (@kiyutink) 发布在 CodePen 上。

将字符串转换为图形

现在,让我们将该字符串转换为一个图形,使弹点可以落在上面。我们将添加另一个关键帧到 `morphSVG` 动画中。

<path class="demo__hidden-path" id='demo__curve' d="M0,140.2c13.1-10.5,34.7-17,48.5-4.1c5.5,5.2,7.6,12.1,9.2,19.2c2.4,10.5,4.3,21,7.2,31.4c2.4,8.6,4.3,19.6,10.4,26.7c4.3,5,17.7,13.4,23.1,4.8c5.9-9.4,6.8-22.5,9.7-33c4.9-17.8,13-14.6,15.7-14.6c1.8,0,9,2.3,15.4,5.4c6.2,3,11.9,7.7,17.9,11.2c7,4.1,16.5,9.2,22.8,6.6"/>

我们将此关键帧添加到时间轴中,如下所示

const DELAY_TO_CURVE = 350;
const ELASTIC_TRANSITION_TIME_TO_CURVED = 300;

function startElasticAnimation() {
  // ...
  elasticAnimationTimeline
    // ...
    .to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_CURVED / 1000, {
      delay: DELAY_TO_CURVE / 1000, 
      morphSVG: '#demo__curve'
    })
    // ...
}

我们得到以下结果

查看示例 css-t. part 6 由 Kirill Kiyutin (@kiyutink) 发布在 CodePen 上。

爆炸粒子

这是一个有趣的动画。首先,我们将创建几个新的 div,其中包含爆炸的粒子

<div class="demo__logo-particles">
    <div class="demo__logo-particle"></div>
    <!-- and several more of these -->
</div>
<div class="demo__money-particles">
    <div class="demo__money-particle"></div>
    <!-- and several more of these -->
</div>

两次爆炸几乎完全相同,除了几个参数。这就是 SCSS 混合器派上用场的地方。我们可以编写一次函数,然后在我们的 div 上使用它。

@mixin particlesContainer($top) {
  position: absolute;
  width: 2px * $scale;
  height: 2px * $scale;
  left: 89px * $scale;
  top: $top * $scale;
  // We'll hide the whole container to not show the particles initially
  opacity: 0;

  &--visible {
    opacity: 1;
  }
}

// The $sweep parameter shows how far from the center (horizontally) the initial positions of the particles can be
@mixin particle($sweep, $time) {
  width: 1.5px * $scale;
  height: 1.5px * $scale;
  border-radius: 50%;
  background-color: white;
  opacity: 1;
  transition: all $time ease;
  position: absolute;
  will-change: transform;
  
  // Phones can't handle the particles very well :(
  @media (max-width: 400px) {
    display: none;
  }

  @for $i from 1 through 30 {
    &:nth-child(#{$i}) {
      left: (random($sweep) - $sweep / 2) * $scale + px;
      @if random(100) > 50 {
        background-color: $purplish-color;
      }
      @else {
        background-color: $pinkish-color;
      }
    }
    &--exploded:nth-child(#{$i}) {
      transform: translate3d((random(110) - 55) * $scale + px, random(35) * $scale + px, 0);
      opacity: 0;
    }
  }
}

请注意代码中的注释,粒子在功能较弱的设备(例如手机)上表现不佳。如果有人有想法并想加入,也许这里有另一种方法可以解决这个问题。

好的,让我们在元素上使用混合器

&__logo-particles {
  @include particlesContainer(15px);
}

&__logo-particle {
  @include particle(50, 1.7s);
}

&__money-particles {
  @include particlesContainer(100px);
}

&__money-particle {
  @include particle(100, 1.5s);
}

现在,我们将在 JavaScript 中的合适时间将类添加到 div 中

const DELAY_TO_ANIMATE_MONEY_PARTICLES = 300;
const DELAY_TO_ANIMATE_LOGO_PARTICLES = 500;

const $moneyParticles = $('.demo__money-particle');
const $moneyParticlesContainer = $('.demo__money-particles');
const $logoParticlesContainer = $('.demo__logo-particles');
const $logoParticles = $('.demo__logo-particle');

function animateMoneyParticles() {
  setTimeout(() => {
    $moneyParticlesContainer.addClass('demo__money-particles--visible')
    $moneyParticles.addClass('demo__money-particle--exploded');
  }, DELAY_TO_ANIMATE_MONEY_PARTICLES);    
}

function animateLogoParticles() {
  setTimeout(() => {
    $logoParticlesContainer.addClass('demo__logo-particles--visible')
    $logoParticles.addClass('demo__logo-particle--exploded');
  }, DELAY_TO_ANIMATE_LOGO_PARTICLES);    
}

function elevateBullet() {
  // ...
  animateMoneyParticles();
  animateLogoParticles();
}

这是我们目前所处的位置

查看示例 css-t. part 7 由 Kirill Kiyutin (@kiyutink) 发布在 CodePen 上。

为账户余额设置动画

每个数字将包含一些我们将在其中滚动的随机数字

<div class="demo__money">
  <div class="demo__money-currency">$</div>
  <!-- every digit will be a div like this one -->
  <div class="demo__money-digit">
    1
    2
    3
    4
    5
    6
    7
    8
    1
  </div>
  // ...
</div>

我们将在所有数字上设置不同的过渡时间,以便动画交错进行。我们可以使用 SCSS 循环来实现这一点

&__money-digit {
  // ...
  // we start from 2 because the first child is the currency sign :)
  @for $i from 2 through 6 {
    &:nth-child(#{$i}) {
      transition: transform 0.1s * $i + 0.2s ease;
      transition-delay: 0.3s;
      transform: translate3d(0, -26px * $scale * 8, 0);
    }
    
    &--visible:nth-child(#{$i}) {
      transform: none;
    }
  }
}

剩下的就是及时添加 CSS 类

const $money = $('.demo__money');
const $moneyDigits = $('.demo__money-digit');

function animateMoney() {
  $money.addClass('demo__money--visible');
  $moneyDigits.addClass('demo__money-digit--visible');
}

function descendBullet() {
  // ...
  animateMoney();
  // ...
}

现在,坐下来欣赏我们的作品

查看示例 css-t. part 8 由 Kirill Kiyutin (@kiyutink) 发布在 CodePen 上。

其余的动画非常简单,涉及轻微的 CSS 过渡,因此我不会深入介绍它们,以保持内容简洁。您可以在完成的演示中查看所有最终代码。

查看演示

结语

  • 在我早期的尝试中,我尝试使用 CSS 过渡来完成所有动画工作。我发现几乎无法控制动画的进度和方向,因此我很快放弃了这个想法,并在开始再次尝试之前等了一个月左右。实际上,如果当时我知道 Web Animations API 存在,我会尝试使用它。
  • 我尝试使用 Canvas 制作爆炸效果以获得更好的性能(参考 这篇文章),但我发现很难使用两个单独的 `requestAnimationFrame` 链来控制帧速率。如果您知道如何做到这一点,那么也许您可以在评论中告诉我(或为 CSS-Tricks 写一篇文章 🙂)。
  • 在我得到第一个可用的原型后,我对它的性能很不满意。我在 PC 上的帧速率约为 40-50fps,更不用说手机了。我花了大量时间优化代码,这篇文章 提供了很大帮助。
  • 您可以看到图形有一个渐变。我通过直接在 SVG `defs` 块中声明渐变来实现这一点
<defs>
  <linearGradient id="linear" x1="0%" y1="0%" x2="100%" y2="0%">
    <stop offset="0%"   stop-color="#8742cc"/>
    <stop offset="100%" stop-color="#a94a8c"/>
  </linearGradient>
</defs>

…然后在 CSS 属性中应用它

fill: url(#linear);
stroke: url(#linear);

整个过程从头到尾——发现 Dribbble 截图到完成工作——花费了我大约一年的时间。我在这里和那里休息了几个月,要么是因为我不知道如何处理某个特定方面,要么是因为我没有足够的空闲时间来处理它。整个过程是一次非常宝贵的经历,我在此过程中学到了很多新东西。

话虽如此,从中学到的最大教训是,无需回避雄心勃勃的任务,也不要因为一开始不知道如何处理而感到沮丧。网络是一个很大的地方,有很多空间可以边走边摸索。