【前端冷知識】動畫:你知道的和你不知道的
摘要:如上面代碼,我們實現了一個連續的軌跡動畫,它效果如下圖所示:。然後我們把它和前面的軌跡動畫結合一下,就實現了扇翅膀飛行的效果。
前幾天我們在文章中討論了 如何優雅地實現軌跡動畫 。 軌跡動畫,只是最簡單的一類動畫,可以用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
這些內容的前兩部分,在課程《前端進階十日談》中有詳細介紹,後兩部份在不久後也會有專門的課程來講解的。
關於動畫,還有什麼想要討論的,歡迎給我們留言。更多內容,盡在《前端進階十日談》