<divclass="animlinear"></div><divclass="animsteps"></div>
.anim{width:120px;height:160px;background-image:url(./spirit.png);/合成图/background-position:00;background-size:100%auto;}.liear{animation:anim-walk0.4slinear0sinfinite;}.steps{animation:anim-walk0.4ssteps(1)0sinfinite;}@keyframesanim-walk{0%{background-position:0px0px;}50%{background-position:0px-160px;}100%{background-position:0px0px;}}
效果:linear:steps:在 CSS 代码里,我们定义了一个 叫 anim-walk 的一组关键帧,关键帧 0% 时 background-position-y 是 0, 50% 时 为 -160 ,100% 时又回到 0从效果图里可以看出,不同的 animation-timing-function 设置对动画效果的影响linear 因为是线性变化,所以 0 ~ -160 ~ 0 之间的数据计算出来就是 0 ~ -40 ~ -80 ~ -120 ~ -160 ~ -120 ~ -80 ~ -40 ~ 0steps 因为是非线性变化, 所以 0 ~ -160 ~ 0 之间的数据计算出来就是 0 ~ 0 ~ 0 ~ 0 ~ -160 ~ -160 ~ -160 ~ -160 ~ 03.3 路径动画( CSS 怎么做曲线路径动画?)在京喜工厂项目里,小人是要在工厂的几个点内移动3.3.1 CSS offset-path最简单的方法是用 offset-path,用法在张鑫旭的这篇文章写得非常详细了:offset-path-css-animation[2]缺点是兼容性较差,这里就不详细说明了3.3.2 利用时间函数为贝塞尔曲线的副作用在京喜工厂项目里小人移动的路径可以从下面这个设定图,标注的圆点都是要停留工作的path-all可以确定的是,这些标注的圆点位置在 CSS 动画里肯定是一个关键帧,而圆点与圆点之间的直线路径还好办,那曲线呢?这里我用到「CSS分层动画」和「时间函数为贝塞尔曲线的副作用」简单来说,就是通过使用两个或多个元素实现动画效果(分层),我们可以更加细粒度地控制某个元素的路径,沿着 X 轴运动使用一种 timing-function ,沿着 Y 轴运动使用另一种 timing-function 假设有 A[0,0]、B[100,100] 两点从 A 移动到 B ,我们可以分拆成 X 轴的变化量,和 Y 轴的变化量直线移动时,就是 X 轴与 Y 轴的累计变化量是一样的:<divclass="anim-x"><divclass="anim-y"></div></div>
.anim-x{animation:anim-x1000ms0slinearinfinite;}.anim-y{animation:anim-y1000ms0slinearinfinite;}@keyframesanim-x{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(100px,0,0)}}@keyframesanim-y{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(0,100px,0)}}
exp2反过来,如果 X轴 与 Y轴 的累计变化量不一样,就会走出曲线:<divclass="anim-x"><divclass="anim-y"></div></div>
.anim-x{animation:anim-x1000ms0sease-ininfinite;}.anim-y{animation:anim-y1000ms0sease-outinfinite;}@keyframesanim-x{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(100px,0,0)}}@keyframesanim-y{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(0,100px,0)}}
exp3这篇文章把原理写得很详细:CSS分层动画可以让元素沿弧形路径运动[3]3.4 组合起来路径动画的问题解决了,小人走路和工作的帧动画也准备好,下面还有两个小问题:(1)小人走路和工作的帧动画不能同时出现(2)路径动画从左走到右时小人的朝向,应该与从右走到左时相反这里的解决方法也是「CSS分层动画」和 「非线性动画」再加多一层转向动画,一层控制 「小人走路帧动画」 的动画、一层控制 「小人工作帧动画」 的动画,这三个控制动画都是「非线性动画」大概的代码可以这样写:<divclass="anim-turn"><divclass="anim-walk"></div><divclass="anim-work"></div></div>
.anim-turn{animation:anim-turn${allTime}ms0ssteps(1)infinite;}.anim-walk{animation:anim-walk-opacity${allTime}ms0ssteps(1)infinite,anim-walk0.4ssteps(1)0sinfinite;}.anim-work{animation:anim-work-opacity${allTime}ms0ssteps(1)infinite,anim-working0.4ssteps(1)0sinfinite;}@keyframesanim-turn{//转向动画0%{transform:scale(1,1)}//正向50%{transform:scale(-1,1)}//反向100%{transform:scale(1,1)}//正向}@keyframesanim-walk-opacity{//控制「小人走路帧动画」的动画0%{opacity:1}50%{opacity:0}100%{opacity:1}}@keyframesanim-work-opacity{//控制「小人工作帧动画」的动画0%{opacity:0}50%{opacity:1}100%{opacity:0}}
再加上 X轴 和 Y轴 的分层 CSS:<divclass="anim-x"><divclass="anim-y"><divclass="anim-turn"><divclass="anim-walk"></div><divclass="anim-work"></div></div></div></div>
.anim-x{animation:anim-x${allTime}ms0slinearinfinite;}.anim-y{animation:anim-y${allTime}ms0slinearinfinite;}.anim-turn{animation:anim-turn${allTime}ms0ssteps(1)infinite;}.anim-walk{animation:anim-walk-opacity${allTime}ms0ssteps(1)infinite,anim-walk0.4ssteps(1)0sinfinite;}.anim-work{animation:anim-work-opacity${allTime}ms0ssteps(1)infinite,anim-working0.4ssteps(1)0sinfinite;}@keyframes...
3.5 写个可视化工具,提升效率上面只是简单的写了这个动画的简单架构,而具体的动画数据 @keyframes才是重点,而且这些关键帧肯定不是手写工欲善其事,必先利其器所以我们来用 Vue 打造一个可视化工具[doge]大概长这样子:基本操作是「添加关键帧」、「调整每个关键帧的属性」、「生成测试动画」、「输出动画CSS」「添加关键帧」:添加关键帧「调整每个关键帧的属性」:调整每个关键帧的属性「生成测试动画-输出动画CSS」:生成测试动画-输出动画CSS工具的具体实现略过,这里说一些关键细节:(1)如何画出动画路径?(2)动画时间怎么算?3.6 画出动画路径在路径动画里,每两个关键帧确定了一段时间内元素的起点与终点,而时间函数着决定了这段时间内 X轴 与 Y轴 的变化量,我们可以将这段时间平均分为 N 端,然后分别求出这 N 端时间终点时元素的位置,用直线连起来就可以得到一条近似的曲线point-line举个例子:<divclass="anim-x"><divclass="anim-y"></div></div>
@keyframesanim-x{0%{transform:translate3d(0,0,0);animation-timing-function:linear}100%{transform:translate3d(300px,0,0)}}@keyframesanim-y{0%{transform:translate3d(0,0,0);animation-timing-function:cubic-bezier(0,.26,.74,1)}100%{transform:translate3d(0,300px,0)}}.anim-x{animation:anim-x1000ms0s;}.anim-y{animation:anim-y1000ms0s;}
在这个例子里,元素的 X轴 从 0 ~ 300 , animation-timing-function 是linear, Y轴 从 0 ~ 300, animation-timing-function 是 cubic-bezier(0,.26,.74,1),然后将时间长度定义为 1,平均分为 100 段,使用 for 循环求出不同进度时的 X 和 Y:constmoveTo=[0,0];conststep=100;constdX=300;constdY=300;consttimeFunX="linear";consttimeFunY="cubic-bezier(0,.26,.74,1)";if(Math.abs(dX)>0||Math.abs(dY)>0){ctx.moveTo(moveTo[0],moveTo[1]);for(leti=0;i<=step;i++){constx=getTimeFunctionValue(timeFunX,i/step)dX+moveTo[0];//求出timeFunX(linear)对应时间进度下的xconsty=getTimeFunctionValue(timeFunY,i/step)dY+moveTo[1];//求出timeFunY(cubic-bezier(0,.26,.74,1))对应时间进度下的yctx.fillRect(x,y,1,1);if(i%10===0){ctx.font="16pxserif";ctx.fillText(`(${x},${(y).toFixed(2)})`,x+20,y+20);}}}
效果如下:point-text剩下就是这个 getTimeFunctionValue(时间函数,时间进度[0,1]) 应该怎么写?首先要把 linear 和 其他的贝塞尔曲线分开, linear 其实就是一条直线,时间进度输入任何值,都返回相同的值functiongetTimeFunctionValue(timeFunctionName="linear",x=0){...if(timeFunctionName==="linear")returnx;...}
贝塞尔曲线呢?先来补习一下 CSS 动画里的贝塞尔曲线时间函数3.7 CSS 动画里的贝塞尔曲线时间函数「贝塞尔曲线」是一种参数函数计算机中应用比较广泛的是「三次贝塞尔曲线」P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线曲线起始于P0走向P1,并从P2的方向来到P3一般不会经过P1或P2;这两个点只是在那里提供方向信息P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”曲线的参数形式为:CSS 动画里的贝塞尔曲线时间函数是一个简化版的「三次贝塞尔曲线」,P0 固定为 [0,0], P3 固定为 [1,1]而且其直角坐标系是区别于几何坐标(x,y),而是有其他意义的,横轴代表的是「时间进度(time)」,取值为[0% ~ 100%]竖轴代表的是属性的「变化程度(progression)」,这个取值一般会在[0% ~ 100%],可以小于0%,也可以大于100%所以 这个简化版的 CSS 贝塞尔曲线可以用下面这两个方程表示(代入 P0[0,0] P3[1,1]):T(时间进度) = ...P(变化百分比) = ...cubic-bezier(0,.26,.74,1) 里面的参数其实就是(P1_time,P1_progression,P2_time,P2_progression)如下图所示:exp取 cubic-bezier(0,.26,.74,1) 里面的参数(P1[0,0.26],P2[0.74,1]),代入上面的两个公式,得到下面的结果(方程):T(时间进度) = ...P(变化百分比) = ...第一条方程中的 T 就是时间进度,是入参,解出这条 关于 t 的一元三次函数的根,代入第二条方程中,就可以求得 PP 就是 T 「时间进度」下的「变化程度」注意:三次函数有3个根,但是只有实数而且在[0 ~ 1]之间的是正解3.8 动画里时间怎么算?上面我们用积分的方法将动画路径近似的画出来,就相当于我们可以求出动画路径的长度的近似值长度 / 速度 = 动画时间其中速度可以自定义4 其他4.1 解决层级不正确的问题(translateZ)京喜工厂还有一个传送带动画,大家可以看看下图的最初版本:before上面这张图,可以看到货物从左往右沿着传送带移动最初左边看着还挺正常,但是到了右边会出现后方货物把前边货物盖压的现象原因也很简单,因为这几个货物是并排的元素,后面的元素层级总会比前面的高就像下面这样:<div><div>1</div><!--显示层级最低--><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><!--显示层级最高--></div>
但这个动画想表现的层级是中间高,两边低有些同学可能会想到用 z-index ,可惜 z-index 在 CSS 动画里是不起作用的正确的解决方案是用 translateZ 将其转换成 3D 显示,从而实现中间高,两边低的层级:@keyframesanim-z{0%{transform:perspective(500px)translateZ(0);}50%{transform:perspective(500px)translateZ(50px);}100%{transform:perspective(500px)translateZ(0);}}
增加后的效果:after4.2 解决逐帧动画抖动问题dou帧动画这里还有一个抖动的问题,看上面的 gif 可以发现小人有点抖动这张 gif 是在 iPhone 6 Plus(手机屏幕像素是 414 px)上的显示效果问题是出在单位转换上:移动端的适配时,通常是用 rem ,小程序使用 rpx,他们在计算成 px 过程中可能会出现取整的问题,从而造成帧动画抖动逐帧动画抖动的研究,看 「凹凸实验室」 的这篇文章就够了:《CSS技巧:逐帧动画抖动解决方案》[4]这篇文章提出了三个方案 A、B、C ,其中方案C是「终极解决方案」可惜的是,这个方案用到的是 SVG,而小程序是不支持 SVG 的退而求其次,我选择了方案 A ,就是用 CSS 的媒体查询来写断点,断点里都用 px 做单位/300~349之间用iphone5(320)的数据/@mediascreenand(min-width:300px)and(max-width:349px){.m_worker_employee{width:51px;height:68px}@keyframesanim-working{0%{background-position:0px-204px}50%{background-position:0px-272px}100%{background-position:0px-204px}}}/350~399之间用iphone6(375)的数据/@mediascreenand(min-width:350px)and(max-width:399px){.m_worker_employee{width:60px;height:80px}@keyframesanim-working{0%{background-position:0px-240px}50%{background-position:0px-320px}100%{background-position:0px-240px}}}/400~449之间用iphone6P(414)的数据/@mediascreenand(min-width:400px)and(max-width:449px){.m_worker_employee{width:66px;height:88px}@keyframesanim-working{0%{background-position:0px-264px}50%{background-position:0px-352px}100%{background-position:0px-264px}}}
budou断点应用后,帧动画就不抖了最后总结一句:前端动画方案有很多种,但是要根据使用的环境选择最合适的;调试动画很繁琐,关键是要用合适的工具,没有就自己造一个5 公式相关本文的公式是使用 「TeX」软件,然后利用「MathJax」输出为 SVG ,在此推荐一下:https://www.mathjax.org/#demo作者:吴冠禧 WecTeam转发连接:https://mp.weixin.qq.com/s/u5GHsA0vHz8A_MmGslRw2g(图片来源网络,侵删)
0 评论