【前端冷知识】动画:你知道的和你不知道的
摘要:如上面代码,我们实现了一个连续的轨迹动画,它效果如下图所示:。然后我们把它和前面的轨迹动画结合一下,就实现了扇翅膀飞行的效果。
前几天我们在文章中讨论了 如何优雅地实现轨迹动画 。 轨迹动画,只是最简单的一类动画,可以用CSS也可以用JS来实现。
实际上轨迹动画只是最简单的一类动画。在今天,我们继续来看一看Web上动画究竟能做到什么程度。
更复杂的轨迹动画
假设我们要让一个物体连续地经过固定的一系列点,完成一个完整的动画,那么我们可以用Promise来简单实现:
function lerp(start, end, p) {
return start * ( 1 - p ) + end * p ;
}
function animate ( target , duration , progress ) {
const startTime = Date . now ( ) ;
return new Promise ( ( resolve ) = > {
function update ( ) {
const t = Date . now ( ) - startTime ;
const p = Math . min ( t / duration , 1 ) ;
progress ( target , p ) ;
if ( p < 1 ) {
requestAnimationFrame ( update ) ;
} else {
resolve ( p ) ;
}
}
update ( ) ;
} ) ;
}
const spots = Array . from ( document . querySelectorAll ( '.target' ) )
. map ( t = > [ t . offsetLeft , t . offsetTop ] ) ;
( async function ( ) {
// noprotect
for ( let i = 0 ; i < spots . length ; i ++ ) {
let left = bird . offsetLeft ;
let top = bird . offsetTop ;
await animate ( bird , 1000 , ( target , p ) = > {
const x = lerp ( left , spots [ i ] [ 0 ] , p ) ;
const y = lerp ( top , spots [ i ] [ 1 ] , p ) ;
bird . style . left = ` ${ x } px` ;
bird . style . top = ` ${ y } px` ;
} ) ;
}
} ( ) ) ;
如上面代码,我们实现了一个连续的轨迹动画,它效果如下图所示:
不过这个动画实际上是一段折线动画,它虽然连续,但是不平滑。如果我们要想保证连续且平滑,我们可以用贝塞尔曲线函数拟合。
我们用一下连续的二阶贝塞尔曲线:
function lerp(start, end, p) {
return start * ( 1 - p ) + end * p ;
}
function quadraticBezier ( [ x0 , y0 ] , [ x1 , y1 ] , [ x2 , y2 ] , p ) {
const t = 1 - p ;
const x = x0 * t ** 2 + 2 * x1 * t * p + x2 * p ** 2 ;
const y = y0 * t ** 2 + 2 * y1 * t * p + y2 * p ** 2 ;
return [ x , y ] ;
}
function animate ( target , duration , progress ) {
const startTime = Date . now ( ) ;
return new Promise ( ( resolve ) = > {
function update ( ) {
const t = Date . now ( ) - startTime ;
const p = Math . min ( t / duration , 1 ) ;
progress ( target , p ) ;
if ( p < 1 ) {
requestAnimationFrame ( update ) ;
} else {
resolve ( p ) ;
}
}
update ( ) ;
} ) ;
}
const spots = Array . from ( document . querySelectorAll ( '.target' ) )
. map ( t = > [ t . offsetLeft , t . offsetTop ] ) ;
// noprotect
( async function ( ) {
let p0 = [ bird . offsetLeft , bird . offsetTop ] ,
p1 = spots [ 0 ] ,
c0 = [ 0.5 * ( p0 [ 0 ] + p1 [ 0 ] ) ,
0.5 * ( p0 [ 1 ] + p1 [ 1 ] ) ] ;
await animate ( bird , 1000 , ( target , p ) = > {
const [ x , y ] = quadraticBezier ( p0 , c0 , p1 , p ) ;
bird . style . left = ` ${ x } px` ;
bird . style . top = ` ${ y } px` ;
} ) ;
for ( let i = 1 ; i < spots . length ; i ++ ) {
p0 = p1 ;
c0 = [ p1 [ 0 ] * 2 - c0 [ 0 ] ,
p1 [ 1 ] * 2 - c0 [ 1 ] ] ;
p1 = spots [ i ] ;
await animate ( bird , 1000 , ( target , p ) = > {
const [ x , y ] = quadraticBezier ( p0 , c0 , p1 , p ) ;
bird . style . left = ` ${ x } px` ;
bird . style . top = ` ${ y } px` ;
} ) ;
}
} ( ) ) ;
效果如下图所示:
我们看到,飞鸟是平滑地经过这些点的,但是,它的轨迹偏离点的幅度有点大,尤其是第三个点到第四个点,要解决这个问题,可以改用三阶贝塞尔曲线,或者用Catmull Rom样条来做插值。
总之我们还有其他办法改进动画效果,让它变得更完美,在这里限于篇幅就不一一赘述了。
帧动画
上面的动画有一个遗憾,就是鸟在飞的过程中翅膀没有动,这可以通过帧动画来实现。
帧动画比固定轨迹动画更简单,我们只要用CSS将元素的背景切换一下就可以了。
@keyframes flap {
0% {
background-position : 0 0 px ;
}
33% {
background-position : 0 - 62 px ;
}
66% {
background-position : 0 - 124 px ;
}
100% {
background-position : 0 0 px ;
}
}
#bird {
position : absolute ;
background-image : url(https://p1.ssl.qhimg.com/t01dc66725cf859e983.png) ;
width : 86 px ;
height : 60 px ;
background-position : 0 - 62 px ;
transform : translate ( - 50% ,- 50% ) scale ( 0.5 ) ;
z-index : 100 ;
animation : flap .5 s step-end infinite ;
}
我们给飞鸟的元素加一个切换背景 background-position 的关键帧动画,让这个动画的缓动函数设为 step-end ,这样就实现了飞鸟的扇翅膀动画。
然后我们把它和前面的轨迹动画结合一下,就实现了扇翅膀飞行的效果。
gradient 和 mask 动画
CSS的渐变和遮罩,是非常有用的两个属性,用它们配合,可以实现各种比较好玩的动画。
比如我们在前面介绍CSS gradients这篇文章中,就介绍过遮罩实现百叶窗渐显的效果。
像素动画
要实现更加复杂的动画,我们还可以用Canvas2D或者WebGL直接操作图像的像素,对像素应用动画。这个就更加复杂了,但是能实现更有趣的效果。
Canvas2D因为CPU处理能力的限制,处理不了太复杂的效果。但是用WebGL的shader,就可以处理更加炫酷的效果了,比如类似下面的水纹滤镜:
还有更加炫酷的效果可以做出来哦。
这些动画效果有没有让你觉得惊艳?
这些效果都可以实现,它的基础是以下这些:
-
JavaScript固定轨迹动画和连续动画
-
CSS特殊属性的特殊动画
-
Canvas的像素操作
-
WebGL的FragmentShader
这些内容的前两部分,在课程《前端进阶十日谈》中有详细介绍,后两部份在不久后也会有专门的课程来讲解的。
关于动画,还有什么想要讨论的,欢迎给我们留言。更多内容,尽在《前端进阶十日谈》