我之前偶然发现了 Jakub Reis 的这个很棒的 Dribbble 作品。它引起了我的注意,我知道我必须尝试用代码重新创建它。当时,我不知道怎么做。我尝试了很多不同的方法,大约一年后,我终于成功地 制作了这个演示。
我在这个过程中学到了一些东西,所以让我带您踏上一段小旅程,了解我做了什么来制作它,因为您也可能学到一两件事。
查看 CodePen 上 Kirill Kiyutin (@kiyutink) 的 银行应用程序的启动屏幕。
步骤 1:将工作分解成部分
我多次观看原始 GIF。我的目标是将动画分解成小的、易于理解的片段,我能够将其分解如下
- 填充指纹
- 移除指纹
- 动画路径端点
- 将弯曲的指纹线之一变形为水平线
- 动画化由线条发射的“子弹”
- 将字符串变形为图表
- 动画化小的爆炸粒子
- 动画化帐户余额
- 以及一些次要 CSS 过渡的其他小动画
我知道,这看起来很多,但我们可以做到!
步骤 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 截图到完成工作——花费了我大约一年的时间。我在这里和那里休息了几个月,要么是因为我不知道如何处理某个特定方面,要么是因为我没有足够的空闲时间来处理它。整个过程是一次非常宝贵的经历,我在此过程中学到了很多新东西。
话虽如此,从中学到的最大教训是,无需回避雄心勃勃的任务,也不要因为一开始不知道如何处理而感到沮丧。网络是一个很大的地方,有很多空间可以边走边摸索。
我很喜欢阅读这篇文章。感谢您分享您的努力。
这太棒了,复制这个真是太棒了!我发现它有点令人着迷,并不断重新加载页面以再次看到它旋转。
如果 Apple 实现了某种 TouchID Web API,使其真正有效,那就太好了。即使只是点击一下,它也是一个非常有趣的进入内容的门户。
真棒!我从中学到了很多。谢谢!
我有一个关于动画时间的问题。`getPropertyIncrement()` 传递 `transitionDuration`,您似乎期望动画在该时间内完成。(手指路径填充颜色的时间为 700 毫秒)。但是,当我添加一个在动画开始时触发的 `setTimeout`(在 700 毫秒时),并使用停止动画的回调,它显示动画没有完全完成。
动画似乎没有按预期的时间完成。我很想知道为什么会出现这种情况
嗨,Jordan,
您的问题很有道理。出现这种情况的原因是有些动画帧丢失了
1. 您可以看到,在最终示例的第 223 行中,我们没有在响应用户鼠标按下/手指触控事件时立即调用 `fprintFrame`。我们实际上使用 `requestAnimationFrame` 来调度第一帧,因此在此处损失了 16.666 毫秒(60fps 时的一帧)。
2. 正如我在文章中提到的,由于浮点误差,偏移量最终可能略大于 0,因此我们动画化了另一帧(最终示例的第 194 行)——又损失了一帧。
除此之外,如果您使用的是刷新率不同于 60 的屏幕,则会导致不同的计时。尽管我已经考虑了这一点,但我使用的是 65fps 而不是 60fps,并且有些帧可能与之不一致,因为例如 144 既不能被 60 也不被 65 整除(许多屏幕的刷新率为 144Hz)。
用原子钟的精度制作动画毫无意义(由于异步 JS 的工作方式,即使是不可实现的)。如果您需要连接到动画的特定部分,您应该避免使用 `timeout`,因为它们对此不可靠,而是直接在所需位置添加函数调用(或表达式)。
另一个可能影响计时的是计算机的速度(如果您的计算机速度很慢,同步代码的执行可能需要比分配给帧的时间更长)——尽管这不太可能,并且在大多数情况下可以忽略不计。
感谢您对本文的关注!