整合營銷服務商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          Canvas學習筆記 - 高級動畫(一)緩動動畫

          Canvas學習筆記 - 高級動畫(一)緩動動畫

          緩動動畫,指的是帶有一定緩沖效果的動畫。在動畫過程中,物體在某一段時間會“漸進加速”或“漸進減速”,從而讓物體運動看起來更為自然而逼真。 緩動動畫分為兩種,即緩入動畫和緩出動畫。 在Canvas中,想要實現(xiàn)緩動動畫,一般需要以下五個步驟: (1)定義一個0~1之間的緩動系統(tǒng)easing。 (2)計算出物體與終點之間的距離。 (3)計算出當前速度,其中當前速度=距離*緩動系統(tǒng)。 (4)計算新的位置,其中新的位置=當前位置+當前速度。 (5)重復執(zhí)行第2~4步,直到物體達到目標。

          語法

           var targetX=任意位置;
           var targetY=任意位置;
           // 動畫循環(huán)
           var vx=(targetX - object.x) * easing;
           var vy=(targetY - object.y) * easing;

          說明: targetX和targetY分別為目標的橫坐標和縱坐標,easing為緩動系統(tǒng),vx和vy分別為物體在x軸方向和y軸方向上的速度。

          示例

          示例一:X軸方向上的緩動動畫

           <!DOCTYPE html>
           <html lang="en">
             <head>
               <meta charset="UTF-8" />
               <title>X軸方向上的緩動動畫</title>
               <script type="text/javascript" src="ball.js"></script>
             </head>
             <body>
               <canvas id="canvas" width="400" height="200"
                 style="border: 1px dashed #333333" ></canvas>
               <script>
                 window.onload=function () {
                   // 1、獲取 Canvas 對象
                   var canvas=document.getElementById("canvas");
                   // 2、獲取上下文環(huán)境對象
                   var ctx=canvas.getContext("2d");
                   // 3、開始繪制圖形
           
                   var ball=new Ball(0, canvas.height / 2);
                   // (1)定義一個0~1之間的緩動系統(tǒng)easing。
                   var easing=0.05;
                   // (2)計算出物體與終點之間的距離。
                   var targetX=canvas.width * (3 / 4);
           
                   (function frame() {
                     ctx.clearRect(0, 0, canvas.width, canvas.height);
                     // (3)計算出當前速度,其中當前速度=距離x緩動系統(tǒng)。
                     var vx=(targetX - ball.x) * easing;
                     // (4)計算新的位置,其中新的位置=當前位置+當前速度。
                     ball.x +=vx;
                     ball.fill(ctx);
                     // (5)重復執(zhí)行第2~4步,直到物體達到目標。
                     window.requestAnimationFrame(frame);
                   })();
                 };
               </script>
             </body>
           </html>

          效果圖:

          前面兩篇文章,學習了JavaScript中DOM、BOM相關基礎知識后,今天我們將結合前面所學內容,利用DOM操作實現(xiàn)常見的網(wǎng)頁動畫效果;結合知識點有相關案例效果演示,另外本文所設計的源碼和配套素材可在文末獲取;

          JavaScript網(wǎng)頁特效

          學習目標

          • offset的屬性與作用
          • client的屬性與作用
          • scroll的屬性與作用
          • 封裝簡單動畫函數(shù)
          • 網(wǎng)頁輪播圖

          I、元素偏移量

          獲得元素距離帶有定位父元素的位置

          獲得元素自身的width和height

          1.offset常用的屬性

          2.offset與style的區(qū)別

          案例:獲取鼠標在盒子內的坐標

          思路:盒子內的坐標可以通過鼠標事件的e.pageX獲取到頁面坐標再減去element.offsetLeft來得到;

          var box=document.querySelector('.box');
          box.addEventListener('click',function(e){
              var x=e.pageX - this.offsetLeft;
              var y=e.pageY - this.offsetTop;
              console.log('x:'+x+',y:'+y);
          })

          案例:模態(tài)框拖拽

          需求:

          (1)點擊彈出層,會淡出模塊框,并且顯示灰色半透明的遮擋層;

          (2)點擊關閉按鈕,可以關閉模態(tài)框,并且同時關閉灰色半透明遮擋層;

          (3)鼠標放到模態(tài)框最上面一行,可以按住鼠標拖拽模態(tài)框在頁面中移動;

          (4)鼠標松開,可以停止拖動模態(tài)框移動;

          分析:

          (1)拖拽事件,鼠標是按著的狀態(tài),故mousemovemouseup須卸載mousedown事件里面;

          (2)在拖拽的過程中,鼠標的位置相對于組件的坐標是不變的,因此為了改變組件的位置,需要將頁面坐標減去相對組件坐標來獲取;

          var link=document.querySelector('#link');
          var login_window=document.querySelector('#login');
          var close_button=document.querySelector('#closeBtn');
          var bg=document.querySelector('#bg');
          link.addEventListener('click',function(){
              login_window.style.display='block';
              bg.style.display='block';
          })
          close_button.addEventListener('click',function(){
              login_window.style.display='none';
              bg.style.display='none';
          })
          // 拖動事件三個過程:mousedown、mousemove、mouseup
          var title=document.querySelector('#title');
          title.addEventListener('mousedown',function(e){
              var x=e.pageX - login_window.offsetLeft;
              var y=e.pageY - login_window.offsetTop;
              document.addEventListener('mousemove',move);
              function move(e){
                  login_window.style.left=e.pageX - x + 'px';
                  login_window.style.top=e.pageY -y + 'px';
              }
              document.addEventListener('mouseup',function (e) {
                  document.removeEventListener('mousemove',move);
              });
          });

          案例:仿京東放大鏡效果

          分析:

          (1)mask界面與big界面可以通過鼠標事件設置dispaly屬性即可;mouseover顯示mask,mouseleave隱藏mask、mousemove移動mask

          (2)mask的left與top的值可以通過鼠標在prew_img中的位置來獲取;

          (3)為了讓mask界面中央跟著鼠標指針走,則需要減去mask尺寸一半的偏移量;

          (4)防止mask頁面跑出prew_img,需要對偏移量進行判斷;

          (5)大圖片移動距離怎么計算?mask移動距離/mask最大移動距離=大圖片移動距離/大圖片最大移動距離;所以大圖片移動距離=mask移動距離 * 大圖片移動最大距離 / 遮擋層最大移動距離;

          window.addEventListener('load',function(){
              var prew_img=document.querySelector('.preview_img');
              var mask=document.querySelector('.mask');
              var big=document.querySelector('.big');
              prew_img.addEventListener('mouseover',function(){
                  mask.style.display='block';
                  big.style.display='block';
              })
              prew_img.addEventListener('mousemove',function(e){
                  // 獲取鼠標在prew_img元素內的坐標
                  var x=e.pageX - this.offsetLeft;
                  var y=e.pageY - this.offsetTop;
                  // 防止mask頁面跑出prew_img盒子
                  var maskX=x - mask.offsetWidth / 2;
                  // 遮擋層的最大移動距離maskMax
                  var maskMax=this.offsetWidth - mask.offsetWidth;
                  if(maskX <=0){
                      maskX=0;
                  }else if(maskX >=maskMax){
                      maskX=maskMax;
                  }
                  var maskY=y - mask.offsetHeight / 2;
                  if(maskY <=0){
                      maskY=0;
                  }else if(maskY >=maskMax){
                      maskY=maskMax;
                  }
                  // 讓mask界面中央跟著鼠標指針走
                  mask.style.left=maskX + 'px';
                  mask.style.top=maskY + 'px';
                  // 讓big界面內容跟著mask位置移動,
                  // 大圖片移動距離=mask移動距離 * 大圖片移動最大距離 / mask最大移動距離
                  var big_img=document.querySelector('.bigImg');
                  var bigMax=big_img.offsetWidth - big.offsetWidth;
                  // 大圖片移動距離
                  var bigMoveX=maskX * bigMax / maskMax;
                  var bigMoveY=maskY * bigMax / maskMax;
                  big_img.style.left=-bigMoveX + 'px';
                  big_img.style.top=-bigMoveY + 'px';
              });
          
              prew_img.addEventListener('mouseleave',function(){
                  mask.style.display='none';
                  big.style.display='none';
              });
          });

          II、元素的可視區(qū)


          立即執(zhí)行函數(shù)

          不需要調用,立馬能自己執(zhí)行的函數(shù);

          // 寫法一:
          (function(){})()
          // 寫法二:
          (function(){}())

          立即執(zhí)行函數(shù)也可以傳遞參數(shù):

          (function(a,b){
          console.log(a+b);
          })(1,2)

          立即執(zhí)行函數(shù)的作用是獨立創(chuàng)建了一個作用域,里面所有的變量都是局部變量,不會有命名沖突的情況;

          III、元素滾動scroll

          使用scroll系列相關屬性可以動態(tài)得到元素的大小、滾動距離;

          scroll系列屬性 作用 備注 element.scrollTop 返回被卷去的上側距離 返回的數(shù)值不帶單位 element.scrollLeft 返回被卷去的左側距離 返回的數(shù)值不帶單位 element.scrollWidth 返回自身的實際寬度,不含邊框 返回的數(shù)值不帶單位 element.scrollHeight 返回自身的實際高度,不含邊框 返回的數(shù)值不帶單位

          案例:仿淘寶固定側邊欄

          • 1.原先側邊欄是絕對定位
          • 2.當頁面滾動到一定的位置,側邊欄改為固定定位
          • 3.頁面繼續(xù)滾動,會讓頂部顯示出來

          頁面被卷去的頭部可以通過:window.pageYOffset來獲得

          // 1.獲取元素
          var sliderBar=document.querySelector('.slider-bar');
          var topBar=document.querySelector('.header');
          var banner=document.querySelector('.banner');
          var main=document.querySelector('.main');
          var goBack=document.querySelector('.goBack');
          
          var barHeight=banner.offsetTop;
          var sliderBarTop=sliderBar.offsetTop;
          var fixTop=sliderBarTop - barHeight;
          var mainTop=main.offsetTop;
          // 2.頁面滾動事件
          document.addEventListener('scroll',function(){
          
              if(window.pageYOffset >=barHeight){
                  sliderBar.style.position='fixed';
                  sliderBar.style.top=fixTop + 'px';
              }else{
                  sliderBar.style.position='absolute';
                  sliderBar.style.top=sliderBarTop + 'px';
              }
              if(window.pageYOffset >=mainTop){
                  goBack.style.display='block';
              }else{
                  goBack.style.display='none';
              }
          })

          IV、鼠標事件

          當鼠標移動到元素上時就會觸發(fā)mouseenter事件,類似mouseenter

          兩者的區(qū)別是:mouseover鼠標經過自身盒子會觸發(fā)、經過子盒子也會觸發(fā);而mouseenter只有經過自身盒子才會觸發(fā);

          原因是mouseenter不會冒泡;跟mouseenter搭配的通常是mouseleave

          V、動畫函數(shù)

          1.動畫實現(xiàn)的原理:

          通過定時器setInterval()不斷移動盒子位置;

          實現(xiàn)步驟:

          • 獲得盒子當前位置
          • 讓盒子在當前位置加上一個移動距離
          • 加上一個結束定時器的條件
          • 盒子需要添加定位,才能使用element.style.left
          //利用setInterval設置一個box元素從左向右移動400像素的動畫
          var box=document.querySelector('.box');
          var timer=setInterval(function(){
              if(box.offsetLeft > 200){
                  // 停止動畫,本質上是清除定時器
                  clearInterval(timer);
              }
              box.style.left=box.offsetLeft + 5 + 'px';
          },50);

          2.動畫函數(shù)的封裝

          將動畫函數(shù)封裝起來,利用JS是動態(tài)語言的特性,通過設置對象的方式,給每個不同的元素指定不同的定時器;

          // 簡單動畫函數(shù)的封裝,目標對象obj,目標位置target
          function animate(obj,target){
              // 如果設置一個按鈕啟動動畫,會存在一個bug,只要不斷點擊按鈕,動畫會越走越快;
              // 原因是啟動了太多定時器,解決方案是,讓元素只有一個定時器執(zhí)行
              clearInterval(obj.timer);
              obj.timer=setInterval(function(){
              if(obj.offsetLeft > target){
                  // 停止動畫,本質上是清除定時器
                  clearInterval(obj.timer);
              }
              obj.style.left=obj.offsetLeft + 5 + 'px';
          },50);
          }
          //利用setInterval設置動畫
          var btn=document.querySelector('button');
          var box1=document.querySelector('.box1');
          btn.addEventListener('click',function(){
              animate(box1,400);
          });

          3.緩動動畫原理

          緩動動畫就是讓元素運動速度有所變換,最常見的就是讓速度慢慢停下來;

          思路:讓盒子每次移動的距離慢慢變小;

          核心算法每次移動的步長=(目標值-現(xiàn)在的位置)/10這里的10是份數(shù)可以是9也可以是8;

          停止的條件是:當前盒子的位置等于目標位置就停止定時器;

          // 緩動動畫
          function animate(obj,target){
              // 清除定時器
              clearInterval(obj.timer);
              obj.timer=setInterval(function() {
                  // 每次運行定時器都要重新計算步長值,所以步長值寫在定時器里面
                  var step=(target - obj.offsetLeft)/10;
                  // 如果step是正值則往大取整,如果step是負值則往小取整
                  step=step >=0 ? Math.ceil(step) : Math.floor(step);
                  if(obj.offsetLeft==target){
                      clearInterval(obj.timer);
                  }
                  // 緩動動畫核心算法:前進步長=(目標-當前)/ 10
                  obj.style.left=obj.offsetLeft + step + 'px';
              },50);
          }

          動畫函數(shù)添加回調函數(shù)

          回調函數(shù)原理:函數(shù)可以作為一個參數(shù),將這個函數(shù)作為參數(shù)傳入到另一個函數(shù)里面,當這個函數(shù)執(zhí)行完成后,再執(zhí)行傳進去的這個函數(shù),這個過程叫做回調。

          回調函數(shù)位置:寫到定時器結束的位置

          // 緩動動畫
          function animate(obj,target,callback){
              // 清除定時器
              clearInterval(obj.timer);
              obj.timer=setInterval(function() {
                  // 每次運行定時器都要重新計算步長值,所以步長值寫在定時器里面
                  var step=(target - obj.offsetLeft)/10;
                  // 如果step是正值則往大取整,如果step是負值則往小取整
                  step=step >=0 ? Math.ceil(step) : Math.floor(step);
                  if(obj.offsetLeft==target){
                      clearInterval(obj.timer);
                      if(callback){
                          // 調用函數(shù)
                          callback();
                      }
                  }
                  // 緩動動畫核心算法:前進步長=(目標-當前)/ 10
                  obj.style.left=obj.offsetLeft + step + 'px';
              },50);
          }

          將動畫函數(shù)封裝到單獨的JS文件

          在head里引入JS文件

          <script src="animate.js"></script>

          實際調用過程中可以直接調用JS中定義的函數(shù)

          var sliderbar=document.querySelector('.sliderbar');
          var con=document.querySelector('.con');
          sliderbar.addEventListener('mouseenter',function(){
              animate(con,-160,function () {
                  // 動畫執(zhí)行完畢,將左箭頭轉為右箭頭
                  sliderbar.children[1].innerHTML='→';
              });
          })
          sliderbar.addEventListener('mouseleave',function(){
              animate(con,0,function () {
                  sliderbar.children[1].innerHTML='←';
              });
          })

          案例:網(wǎng)頁輪播圖

          功能需求:

          • 鼠標經過輪播圖模塊,左右按鈕顯示,離開隱藏左右按鈕;
          • 鼠標點擊右側按鈕一次,圖片往左播放一張,以此內推,左側按鈕同理;
          • 圖標播放的同時,下面小圓圈模塊跟隨一起變化;
          • 點擊小圓圈,可以播放相應圖片;
          • 鼠標不經過輪播圖,輪播圖也會自動播放圖片;
          • 鼠標經過,輪播圖模塊,自動播放停止
          window.addEventListener('load', function () {
              //===0.定義變量,獲取元素
              var focus=document.querySelector('.focus');
              var arrowL=document.querySelector('.arrow-l');
              var arrowR=document.querySelector('.arrow-r');
          
              //===1.鼠標經過輪播圖模塊,左右按鈕顯示,離開隱藏左右按鈕;=====focus.addEventListener('mouseenter', function () {
                  arrowL.style.display='block';
                  arrowR.style.display='block';
                  clearInterval(timer);
                  timer=null;
              });
              focus.addEventListener('mouseleave', function () {
                  arrowL.style.display='none';
                  arrowR.style.display='none';
                  timer=window.setInterval(function () {
                      arrowR.click();
                  }, 3000);
              });
              //===2.根據(jù)輪播圖的數(shù)量自動生成小圓圈
              var ul=focus.querySelector('ul');
              var ol=focus.querySelector('.circle');
              var pictureWidth=focus.offsetWidth;
              // 獲取到輪播圖圖片的數(shù)量length
              var length=ul.children.length;
              // 顯示當前圖片的索引
              var circle=0;
              // 顯示當前圖片的索引
              var num=0;
              // 創(chuàng)建元素插入到ol中
              for (var i=0; i < length; i++) {
                  var li=document.createElement('li');
                  // 給沒一個小圓圈添加一個index屬性,方便點擊事件的ul移動長度的確定
                  li.setAttribute('index', i);
                  // 給每一個小圓圈添加點擊事件
                  li.addEventListener('click', function () {
                      // 移除其他小圓圈的選中屬性
                      for (var i=0; i < length; i++) {
                          ol.children[i].className='';
                      }
                      // 設置當前選中小圓圈的選中屬性
                      this.className='current';
                      // 定義選中的小圓圈的索引
                      var index=this.getAttribute('index');
                      num=index;
                      circle=index;
                      animate(ul, -pictureWidth * index);
                  });
                  ol.appendChild(li);
              }
              ol.children[0].className='current';
          
              //===3.鼠標點擊右側按鈕一次,圖片往左播放一張,以此內推,左側按鈕同理;==// 為了實現(xiàn)無縫滾動,需要將第一張圖添加到最后一張圖中
              var lastLi=ul.children[0].cloneNode(true);
              ul.appendChild(lastLi);
              // 設置一個節(jié)流閥,防止按鈕在短時間內點擊過多
              var flag=true;
              arrowR.addEventListener('click', function () {
                  if (flag) {
                      flag=false;
                      if (num==ul.children.length - 1) {
                          ul.style.left=0;
                          num=0;
                      }
                      num++;
                      animate(ul, -pictureWidth * num, function () {
                          flag=true;
                      });
                      circle++;
                      if (circle==ol.children.length) {
                          circle=0;
                      }
                      activateCircle(circle);
                  }
              });
          
              arrowL.addEventListener('click', function () {
                  if (flag) {
                      flag=false;
                      if (num==0) {
                          num=ul.children.length - 1;
                          ul.style.left=-num * pictureWidth + 'px';
                      }
                      num--;
                      animate(ul, -num * pictureWidth, function () {
                          flag=true;
                      });
                      if (circle==0) {
                          circle=ol.children.length;
                      }
                      circle--;
                      activateCircle(circle);
                  }
              });
          
              // 排他法思想點亮小圓圈
              function activateCircle(index) {
                  for (var i=0; i < ol.children.length; i++) {
                      ol.children[i].className='';
                  }
                  ol.children[index].className='current';
              }
          
              // 實現(xiàn)輪播圖自動播放
              var timer=window.setInterval(function () {
                  arrowR.click();
              }, 3000);
          });

          顯示效果:

          輪播圖自動播放的原理是:設置一個定時器使用click()函數(shù)模擬按鈕點擊事件;

          輪播圖實現(xiàn)無縫滾動的原理是:將第一個圖動態(tài)加在最后一張圖上,當切到最后一張圖時,重新設置元素布局距離,再執(zhí)行滑動動畫。

          防止同一個按鈕在短時間內點擊次數(shù)過多,可以設置一個節(jié)流閥,將啟動節(jié)流閥的操作寫在動畫回調函數(shù)里,這樣只有當動畫執(zhí)行完畢后才才可以重新恢復按鈕的點擊功能;

          頁面滾動動畫可以使用函數(shù):window.scroll(x,y)可以滾動至頁面的指定坐標位置;


          最后:

          本文配套源碼與素材獲取:https://gitee.com/yushengtan/jscode

          關于JavaScript在網(wǎng)頁動畫中的使用就介紹到這里,下一節(jié)我們將繼續(xù)深入介紹JavaScript的異步調用相關知識的學習~

          文將比較全面細致的梳理一下 CSS 動畫的方方面面,針對每個屬性用法的講解及進階用法的示意,希望能成為一個比較好的從入門到進階的教程。

          CSS 動畫介紹及語法

          首先,我們來簡單介紹一下 CSS 動畫。

          最新版本的 CSS 動畫由規(guī)范 -- CSS Animations Level 1 定義。

          CSS 動畫用于實現(xiàn)元素從一個 CSS 樣式配置轉換到另一個 CSS 樣式配置。

          動畫包括兩個部分: 描述動畫的樣式規(guī)則和用于指定動畫開始、結束以及中間點樣式的關鍵幀。

          簡單來說,看下面的例子:

          div {
              animation: change 3s;
          }
          
          @keyframes change {
              0% {
                  color: #f00;
              }
              100% {
                  color: #000;
              }
          }
          
          1. animation: move 1s 部分就是動畫的第一部分,用于描述動畫的各個規(guī)則;
          2. @keyframes move {} 部分就是動畫的第二部分,用于指定動畫開始、結束以及中間點樣式的關鍵幀;

          一個 CSS 動畫一定要由上述兩部分組成。

          CSS 動畫的語法

          接下來,我們簡單看看 CSS 動畫的語法。

          創(chuàng)建動畫序列,需要使用 animation 屬性或其子屬性,該屬性允許配置動畫時間、時長以及其他動畫細節(jié),但該屬性不能配置動畫的實際表現(xiàn),動畫的實際表現(xiàn)是由 @keyframes 規(guī)則實現(xiàn)。

          animation 的子屬性有:

          • animation-name:指定由 @keyframes 描述的關鍵幀名稱。
          • animation-duration:設置動畫一個周期的時長。
          • animation-delay:設置延時,即從元素加載完成之后到動畫序列開始執(zhí)行的這段時間。
          • animation-direction:設置動畫在每次運行完后是反向運行還是重新回到開始位置重復運行。
          • animation-iteration-count:設置動畫重復次數(shù), 可以指定 infinite 無限次重復動畫
          • animation-play-state:允許暫停和恢復動畫。
          • animation-timing-function:設置動畫速度, 即通過建立加速度曲線,設置動畫在關鍵幀之間是如何變化。
          • animation-fill-mode:指定動畫執(zhí)行前后如何為目標元素應用樣式
          • @keyframes 規(guī)則,當然,一個動畫想要運行,還應該包括 @keyframes 規(guī)則,在內部設定動畫關鍵幀

          其中,對于一個動畫:

          • 必須項animation-nameanimation-duration@keyframes規(guī)則
          • 非必須項animation-delayanimation-directionanimation-iteration-countanimation-play-stateanimation-timing-functionanimation-fill-mode,當然不是說它們不重要,只是不設置時,它們都有默認值

          上面已經給了一個簡單的 DEMO, 就用上述的 DEMO,看看結果:

          這就是一個最基本的 CSS 動畫,本文將從 animation 的各個子屬性入手,探究 CSS 動畫的方方面面。

          animation-name / animation-duration 詳解

          整體而言,單個的 animation-nameanimation-duration 沒有太多的技巧,非常好理解,放在一起。

          首先介紹一下 animation-name,通過 animation-name,CSS 引擎將會找到對應的 @keyframes 規(guī)則。

          當然,它和 CSS 規(guī)則命名一樣,也存在一些騷操作。譬如,他是支持 emoji 表情的,所以代碼中的 animation-name 命名也可以這樣寫:

          div {
              animation:  3s;
          }
          
          @keyframes  {
              0% {
                  color: #f00;
              }
              100% {
                  color: #000;
              }
          }
          

          animation-duration 設置動畫一個周期的時長,上述 DEMO 中,就是設定動畫整體持續(xù) 3s,這個也非常好理解。

          animation-delay 詳解

          animation-delay 就比較有意思了,它可以設置動畫延時,即從元素加載完成之后到動畫序列開始執(zhí)行的這段時間。

          簡單的一個 DEMO:

          <div></div>
          <div></div>
          
          div {
              width: 100px;
              height: 100px;
              background: #000;
              animation-name: move;
              animation-duration: 2s;
          }
          
          div:nth-child(2) {
              animation-delay: 1s;
          }
          @keyframes move {
              0% {
                  transform: translate(0);
              }
              100% {
                  transform: translate(200px);
              }
          }
          

          比較下列兩個動畫,一個添加了 animation-delay,一個沒有,非常直觀:

          上述第二個 div,關于 animation 屬性,也可以簡寫為 animation: move 2s 1s,第一個時間值表示持續(xù)時間,第二個時間值表示延遲時間。

          animation-delay 可以為負值

          關于 animation-delay,最有意思的技巧在于,它可以是負數(shù)。也就是說,雖然屬性名是動畫延遲時間,但是運用了負數(shù)之后,動畫可以提前進行

          假設我們要實現(xiàn)這樣一個 loading 動畫效果:

          有幾種思路:

          1. 初始 3 個球的位置就是間隔 120°,同時開始旋轉,但是這樣代碼量會稍微多一點
          2. 另外一種思路,同一個動畫,3 個元素的其中兩個延遲整個動畫的 1/3,2/3 時間出發(fā)

          方案 2 的核心偽代碼如下:

          .item:nth-child(1) {
              animation: rotate 3s infinite linear;
          }
          .item:nth-child(2) {
              animation: rotate 3s infinite 1s linear;
          }
          .item:nth-child(3) {
              animation: rotate 3s infinite 2s linear;
          }
          

          但是,在動畫的前 2s,另外兩個元素是不會動的,只有 2s 過后,整個動畫才是我們想要的:

          此時,我們可以讓第 2、3 個元素的延遲時間,改為負值,這樣可以讓動畫延遲進行 -1s-2s,也就是提前進行 1s2s

          .item:nth-child(1) {
              animation: rotate 3s infinite linear;
          }
          .item:nth-child(2) {
              animation: rotate 3s infinite -1s linear;
          }
          .item:nth-child(3) {
              animation: rotate 3s infinite -2s linear;
          }
          

          這樣,每個元素都無需等待,直接就是運動狀態(tài)中的,并且元素間隔位置是我們想要的結果:

          利用 animation-duration 和 animation-delay 構建隨機效果

          還有一個有意思的小技巧。

          同一個動畫,我們利用一定范圍內隨機的 animation-duration 和一定范圍內隨機的 animation-delay,可以有效的構建更為隨機的動畫效果,讓動畫更加的自然。

          我在下述兩個純 CSS 動畫中,都使用了這樣的技巧:

          1. 純 CSS 實現(xiàn)華為充電動畫:
          1. 純 CSS 實現(xiàn)火焰動畫:

          純 CSS 實現(xiàn)華為充電動畫為例子,簡單講解一下。

          仔細觀察這一部分,上升的一個一個圓球,拋去這里的一些融合效果,只關注不斷上升的圓球,看著像是沒有什么規(guī)律可言:

          我們來模擬一下,如果是使用 10 個 animation-durationanimation-delay 都一致的圓的話,核心偽代碼:

          <ul>
              <li></li>
              <!--共 10 個...--> 
              <li></li>
          </ul>
          
          ul {
              display: flex;
              flex-wrap: nowrap;
              gap: 5px;
          }
          li {
              background: #000;
              animation: move 3s infinite 1s linear;
          }
          @keyframes move {
              0% {
                  transform: translate(0, 0);
              }
              100% {
                  transform: translate(0, -100px);
              }
          }
          

          這樣,小球的運動會是這樣的整齊劃一:

          要讓小球的運動顯得非常的隨機,只需要讓 animation-durationanimation-delay 都在一定范圍內浮動即可,改造下 CSS:

          @for $i from 1 to 11 {
              li:nth-child(#{$i}) {
                  animation-duration: #{random(2000)/1000 + 2}s;
                  animation-delay: #{random(1000)/1000 + 1}s;
              }
          }
          

          我們利用 SASS 的循環(huán)和 random() 函數(shù),讓 animation-duration 在 2-4 秒范圍內隨機,讓 animation-delay 在 1-2 秒范圍內隨機,這樣,我們就可以得到非常自然且不同的上升動畫效果,基本不會出現(xiàn)重復的畫面,很好的模擬了隨機效果:

          CodePen Demo -- 利用范圍隨機 animation-duration 和 animation-delay 實現(xiàn)隨機動畫效果

          animation-timing-function 緩動函數(shù)

          緩動函數(shù)在動畫中非常重要,它定義了動畫在每一動畫周期中執(zhí)行的節(jié)奏。

          緩動主要分為兩類:

          1. cubic-bezier-timing-function 三次貝塞爾曲線緩動函數(shù)
          2. step-timing-function 步驟緩動函數(shù)(這個翻譯是我自己翻的,可能有點奇怪)

          三次貝塞爾曲線緩動函數(shù)

          首先先看看三次貝塞爾曲線緩動函數(shù)。在 CSS 中,支持一些緩動函數(shù)關鍵字。

          /* Keyword values */
          animation-timing-function: ease;  // 動畫以低速開始,然后加快,在結束前變慢
          animation-timing-function: ease-in;  // 動畫以低速開始
          animation-timing-function: ease-out; // 動畫以低速結束
          animation-timing-function: ease-in-out; // 動畫以低速開始和結束
          animation-timing-function: linear; // 勻速,動畫從頭到尾的速度是相同的
          

          關于它們之間的效果對比:

          除了 CSS 支持的這 5 個關鍵字,我們還可以使用 cubic-bezier() 方法自定義三次貝塞爾曲線:

          animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);
          

          這里有個非常好用的網(wǎng)站 -- cubic-bezier 用于創(chuàng)建和調試生成不同的貝塞爾曲線參數(shù)。

          三次貝塞爾曲線緩動對動畫的影響

          關于緩動函數(shù)對動畫的影響,這里有一個非常好的示例。這里我們使用了純 CSS 實現(xiàn)了一個鐘的效果,對于其中的動畫的運動,如果是 animation-timing-function: linear,效果如下:

          而如果我們我把緩動函數(shù)替換一下,變成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲線對應如下:

          整個鐘的動畫律動效果將變成這樣,完全不一樣的感覺:

          CodePen Demo - 緩動不同效果不同

          對于許多精益求精的動畫,在設計中其實都考慮到了緩動函數(shù)。我很久之前看到過一篇《基于物理學的動畫用戶體驗設計》,可惜如今已經無法找到原文。其中傳達出的一些概念是,動畫的設計依據(jù)實際在生活中的表現(xiàn)去考量。

          譬如 linear 這個緩動,實際應用于某些動畫中會顯得很不自然,因為由于空氣阻力的存在,程序模擬的勻速直線運動在現(xiàn)實生活中是很難實現(xiàn)的。因此對于這樣一個用戶平時很少感知到的運動是很難建立信任感的。這樣的勻速直線運動也是我們在進行動效設計時需要極力避免的。

          步驟緩動函數(shù)

          接下來再講講步驟緩動函數(shù)。在 CSS 的 animation-timing-function 中,它有如下幾種表現(xiàn)形態(tài):

          {
              /* Keyword values */
              animation-timing-function: step-start;
              animation-timing-function: step-end;
          
              /* Function values */
              animation-timing-function: steps(6, start)
              animation-timing-function: steps(4, end);
          }
          

          在 CSS 中,使用步驟緩動函數(shù)最多的,就是利用其來實現(xiàn)逐幀動畫。假設我們有這樣一張圖(圖片大小為 1536 x 256,圖片來源于網(wǎng)絡):

          可以發(fā)現(xiàn)它其實是一個人物行進過程中的 6 種狀態(tài),或者可以為 6 幀,我們利用 animation-timing-function: steps(6) 可以將其用一個 CSS 動畫串聯(lián)起來,代碼非常的簡單:

          <div class="box"></div>
          
          .box {
            width: 256px;
            height: 256px;
            background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
            animation: sprite .6s steps(6, end) infinite;
          }
          @keyframes sprite {
            0% { 
              background-position: 0 0;
            }
            100% { 
              background-position: -1536px 0;
            }
          }
          

          簡單解釋一下上述代碼,首先要知道,剛好 256 x 6=1536,所以上述圖片其實可以剛好均分為 6 段:

          1. 我們設定了一個大小都為 256px 的 div,給這個 div 賦予了一個 animation: sprite .6s steps(6) infinite 動畫;
          2. 其中 steps(6) 的意思就是將設定的 @keyframes 動畫分為 6 次(6幀)執(zhí)行,而整體的動畫時間是 0.6s,所以每一幀的停頓時長為 0.1s
          3. 動畫效果是由 background-position: 0 0background-position: -1536px 0,由于上述的 CSS 代碼沒有設置 background-repeat,所以其實 background-position: 0 0 是等價于 background-position: -1536px 0,就是圖片在整個動畫過程中推進了一輪,只不過每一幀停在了特點的地方,一共 6 幀;

          將上述 1、2、3,3 個步驟畫在圖上簡單示意:

          從上圖可知,其實在動畫過程中,background-position 的取值其實只有 background-position: 0 0background-position: -256px 0background-position: -512px 0 依次類推一直到 background-position: -1536px 0,由于背景的 repeat 的特性,其實剛好回到原點,由此又重新開始新一輪同樣的動畫。

          所以,整個動畫就會是這樣,每一幀停留 0.1s 后切換到下一幀(注意這里是個無限循環(huán)動畫),:

          完整的代碼你可以戳這里 -- CodePen Demo -- Sprite Animation with steps()

          animation-duration 動畫長短對動畫的影響

          在這里再插入一個小章節(jié),animation-duration 動畫長短對動畫的影響也是非常明顯的。

          在上述代碼的基礎上,我們再修改 animation-duration,縮短每一幀的時間就可以讓步行的效果變成跑步的效果,同理,也可以增加每一幀的停留時間。讓每一步變得緩慢,就像是在步行一樣。

          需要提出的是,上文說的每一幀,和瀏覽器渲染過程中的 FPS 的每一幀不是同一個概念。

          看看效果,設置不同的 animation-duration 的效果(這里是 0.6s -> 0.2s),GIF 錄屏丟失了一些關鍵幀,實際效果會更好點:

          當然,在 steps() 中,還有 steps(6, start)steps(6, end) 的差異,也就是其中關鍵字 startend 的差異。對于上述的無限動畫而言,其實基本是可以忽略不計的,它主要是控制動畫第一幀的開始和持續(xù)時長,比較小的一個知識點但是想講明白需要比較長的篇幅,限于本文的內容,在這里不做展開,讀者可以自行了解。

          同個動畫效果的補間動畫和逐幀動畫演繹對比

          上述的三次貝塞爾曲線緩動和步驟緩動,其實就是對應的補間動畫和逐幀動畫。

          對于同個動畫而言,有的時候兩種緩動都是適用的。我們在具體使用的時候需要具體分析選取。

          假設我們用 CSS 實現(xiàn)了這樣一個圖形:

          現(xiàn)在想利用這個圖形制作一個 Loading 效果,如果利用補間動畫,也就是三次貝塞爾曲線緩動的話,讓它旋轉起來,得到的效果非常的一般:

          .g-container{
              animation: rotate 2s linear infinite;
          }
          @keyframes rotate {
              0% {
                  transform: rotate(0);
              }
              100% {
                  transform: rotate(360deg);
              }
          }
          

          動畫效果如下:

          但是如果這里,我們將補間動畫換成逐幀動畫,因為有 20 個點,所以設置成 steps(20),再看看效果,會得到完全不一樣的感覺:

          .g-container{
              animation: rotate 2s steps(20) infinite;
          }
          @keyframes rotate {
              0% {
                  transform: rotate(0);
              }
              100% {
                  transform: rotate(360deg);
              }
          }
          

          動畫效果如下:

          整個 loading 的圈圈看上去好像也在旋轉,實際上只是 20 幀關鍵幀在切換,整體的效果感覺更適合 Loading 的效果。

          因此,兩種動畫效果都是很有必要掌握的,在實際使用的時候靈活嘗試,選擇更適合的。

          上述 DEMO 效果完整的代碼:CodePen Demo -- Scale Loading steps vs linear

          animation-play-state

          接下來,我們講講 animation-play-state,顧名思義,它可以控制動畫的狀態(tài) -- 運行或者暫停。類似于視頻播放器的開始和暫停。是 CSS 動畫中有限的控制動畫狀態(tài)的手段之一。

          它的取值只有兩個(默認為 running):

          {
              animation-play-state: paused | running;
          }
          

          使用起來也非常簡單,看下面這個例子,我們在 hover 按鈕的時候,實現(xiàn)動畫的暫停:

          <div class="btn stop">stop</div>
          <div class="animation"></div>
          
          .animation {
              width: 100px;
              height: 100px;
              background: deeppink;
              animation: move 2s linear infinite alternate;
          }
          
          @keyframes move {
              100% {
                  transform: translate(100px, 0);
              }
          }
          
          .stop:hover ~ .animation {
              animation-play-state: paused;
          }
          

          一個簡單的 CSS 動畫,但是當我們 hover 按鈕的時候,給動畫元素添加上 animation-play-state: paused

          animation-play-state 小技巧,默認暫停,點擊運行

          正常而言,按照正常思路使用 animation-play-state: paused 是非常簡單的。

          但是,如果我們想創(chuàng)造一些有意思的 CSS 動畫效果,不如反其道而行之。

          我們都知道,正常情況下,動畫應該是運行狀態(tài),那如果我們將一些動畫的默認狀態(tài)設置為暫停,只有當鼠標點擊或者 hover 的時候,才設置其 animation-play-state: running,這樣就可以得到很多有趣的 CSS 效果。

          看個倒酒的例子,這是一個純 CSS 動畫,但是默認狀態(tài)下,動畫處于 animation-play-state: paused,也就是暫停狀態(tài),只有當鼠標點擊杯子的時,才設置 animation-play-state: running,讓酒倒下,利用 animation-play-state 實現(xiàn)了一個非常有意思的交互效果:

          完整的 DEMO 你可以戳這里:CodePen Demo -- CSS Beer!

          在非常多 Web 創(chuàng)意交互動畫我們都可以看到這個技巧的身影。

          1. 頁面 render 后,無任何操作,動畫不會開始。只有當鼠標對元素進行 click ,通過觸發(fā)元素的 :active 偽類效果的時候,賦予動畫 animation-play-state: running,動畫才開始進行;
          2. 動畫進行到任意時刻,鼠標停止點擊,偽類消失,則動畫停止;

          animation-fill-mode 控制元素在各個階段的狀態(tài)

          下一個屬性 animation-fill-mode,很多人會誤認為它只是用于控制元素在動畫結束后是否復位。這個其實是不準確的,不全面的。

          看看它的取值:

          {
              // 默認值,當動畫未執(zhí)行時,動畫將不會將任何樣式應用于目標,而是使用賦予給該元素的 CSS 規(guī)則來顯示該元素的狀態(tài)
              animation-fill-mode: none;
              // 動畫將在應用于目標時立即應用第一個關鍵幀中定義的值,并在 `animation-delay` 期間保留此值,
              animation-fill-mode: backwards; 
              // 目標將保留由執(zhí)行期間遇到的最后一個關鍵幀計算值。 最后一個關鍵幀取決于 `animation-direction` 和 `animation-iteration-count`
              animation-fill-mode: forwards;    
              // 動畫將遵循 `forwards` 和 `backwards` 的規(guī)則,從而在兩個方向上擴展動畫屬性
              animation-fill-mode: both; 
          }
          

          對于 animation-fill-mode 的解讀,我在 Segment Fault 上的一個問答中(SF - 如何理解 animation-fill-mode)看到了 4 副很好的解讀圖,這里借用一下:

          假設 HTML 如下:

          <div class="box"></div>
          

          CSS如下:

          .box{
              transform: translateY(0);
          }
          .box.on{
              animation: move 1s;
          }
          
          @keyframes move{
              from{transform: translateY(-50px)}
              to  {transform: translateY( 50px)}
          }
          

          使用圖片來表示 translateY 的值與 時間 的關系:

          • 橫軸為表示 時間,為 0 時表示動畫開始的時間,也就是向 box 加上 on 類名的時間,橫軸一格表示 0.5s
          • 縱軸表示 translateY 的值,為 0 時表示 translateY 的值為 0,縱軸一格表示 50px
          1. animation-fill-mode: none 表現(xiàn)如圖:

          一句話總結,元素在動畫時間之外,樣式只受到它的 CSS 規(guī)則限制,與 @keyframes 內的關鍵幀定義無關。

          1. animation-fill-mode: backwards 表現(xiàn)如圖:

          一句話總結,元素在動畫開始之前(包含未觸發(fā)動畫階段及 animation-delay 期間)的樣式為動畫運行時的第一幀,而動畫結束后的樣式則恢復為 CSS 規(guī)則設定的樣式。

          1. animation-fill-mode: forwards 表現(xiàn)如圖:

          一句話總結,元素在動畫開始之前的樣式為 CSS 規(guī)則設定的樣式,而動畫結束后的樣式則表現(xiàn)為由執(zhí)行期間遇到的最后一個關鍵幀計算值(也就是停在最后一幀)。

          1. animation-fill-mode: both 表現(xiàn)如圖:

          一句話總結,綜合了 animation-fill-mode: backwardsanimation-fill-mode: forwards 的設定。動畫開始前的樣式為動畫運行時的第一幀,動畫結束后停在最后一幀。

          animation-iteration-count/animation-direction 動畫循環(huán)次數(shù)和方向

          講到了 animation-fill-mode,我們就可以順帶講講這個兩個比較好理解的屬性 -- animation-iteration-countanimation-direction

          • animation-iteration-count 控制動畫運行的次數(shù),可以是數(shù)字或者 infinite,注意,數(shù)字可以是小數(shù)
          • animation-direction 控制動畫的方向,正向、反向、正向交替與反向交替

          在上面講述 animation-fill-mode 時,我使用了動畫運行時的第一幀替代了@keyframes 中定義的第一幀這種說法,因為動畫運行的第一幀和最后一幀的實際狀態(tài)還會受到動畫運行方向 animation-directionanimation-iteration-count 的影響。

          在 CSS 動畫中,由 animation-iteration-countanimation-direction 共同決定動畫運行時的第一幀和最后一幀的狀態(tài)。

          1. 動畫運行的第一幀由 animation-direction 決定
          2. 動畫運行的最后一幀由 animation-iteration-countanimation-direction 決定

          動畫的最后一幀,也就是動畫運行的最終狀態(tài),并且我們可以利用 animation-fill-mode: forwards 讓動畫在結束后停留在這一幀,這個還是比較好理解的,但是 animation-fill-mode: backwardsanimation-direction 的關系很容易弄不清楚,這里簡答講解下。

          設置一個 100px x 100px 的滑塊,在一個 400px x 100px 的容器中,其代碼如下:

          <div class="g-father">
              <div class="g-box"></div>
          </div>
          
          .g-father {
              width: 400px;
              height: 100px;
              border: 1px solid #000;
          }
          .g-box {
              width: 100px;
              height: 100px;
              background: #333;
          }
          

          表現(xiàn)如下:

          那么,加入 animation 之后,在不同的 animation-iteration-countanimation-direction 作用下,動畫的初始和結束狀態(tài)都不一樣。

          如果設置了 animation-fill-mode: backwards,則元素在動畫未開始前的狀態(tài)由 animation-direction 決定:

          .g-box {
              ...
              animation: move 4s linear;
              animation-play-state: paused;
              transform: translate(0, 0);
          }
          @keyframes move {
              0% {
                  transform: translate(100px, 0);
              }
              100% {
                  transform: translate(300px, 0);
              }
          }
          

          注意這里 CSS 規(guī)則中,元素沒有設置位移 transform: translate(0, 0),而在動畫中,第一個關鍵幀和最后一個關鍵的 translateX 分別是 100px300px,配合不同的 animation-direction 初始狀態(tài)如下。

          下圖假設我們設置了動畫默認是暫停的 -- animation-play-state: paused,那么動畫在開始前的狀態(tài)為:

          動畫的分治與復用

          講完了每一個屬性,我們再來看看一些動畫使用過程中的細節(jié)。

          看這樣一個動畫:

          <div></div>
          
          div {
              width: 100px;
              height: 100px;
              background: #000;
              animation: combine 2s;
          }
          @keyframes combine {
              100% {
                  transform: translate(0, 150px);
                  opacity: 0;
              }
          }
          

          這里我們實現(xiàn)了一個 div 塊下落動畫,下落的同時產生透明度的變化:

          對于這樣一個多個屬性變化的動畫,它其實等價于:

          div {
              animation: falldown 2s, fadeIn 2s;
          }
          
          @keyframes falldown {
              100% {
                  transform: translate(0, 150px);
              }
          }
          @keyframes fadeIn {
              100% {
                  opacity: 0;
              }
          }
          

          在 CSS 動畫規(guī)則中,animation 是可以接收多個動畫的,這樣做的目的不僅僅只是為了復用,同時也是為了分治,我們對每一個屬性層面的動畫能夠有著更為精確的控制。

          keyframes 規(guī)則的設定

          我們經常能夠在各種不同的 CSS 代碼見到如下兩種 CSS @keyframes 的設定:

          1. 使用百分比
          @keyframes fadeIn {
              0% {
                  opacity: 1;
              }
              100% {
                  opacity: 0;
              }
          }
          
          1. 使用 fromto
          @keyframes fadeIn {
              from {
                  opacity: 1;
              }
              to {
                  opacity: 0;
              }
          }
          

          在 CSS 動畫 @keyframes 的定義中,from 等同于 0%,而 to 等同于 100%

          當然,當我們的關鍵幀不止 2 幀的時,更推薦使用百分比定義的方式。

          除此之外,當動畫的起始幀等同于 CSS 規(guī)則中賦予的值并且沒有設定 animation-fill-mode0%from 這一幀是可以刪除的。

          動畫狀態(tài)的高優(yōu)先級性

          我曾經在這篇文章中 -- 深入理解 CSS(Cascading Style Sheets)中的層疊(Cascading) 講過一個很有意思的 CSS 現(xiàn)象。

          這也是很多人對 CSS 優(yōu)先級的一個認知誤區(qū),在 CSS 中,優(yōu)先級還需要考慮選擇器的層疊(級聯(lián))順序

          只有在層疊順序相等時,使用哪個值才取決于樣式的優(yōu)先級。

          那什么是層疊順序呢?

          根據(jù) CSS Cascading 4 最新標準:

          CSS Cascading and Inheritance Level 5(Current Work)

          定義的當前規(guī)范下申明的層疊順序優(yōu)先級如下(越往下的優(yōu)先級越高,下面的規(guī)則按升序排列):

          • Normal user agent declarations
          • Normal user declarations
          • Normal author declarations
          • Animation declarations
          • Important author declarations
          • Important user declarations
          • Important user agent declarations
          • Transition declarations

          簡單翻譯一下:

          按照上述算法,大概是這樣:

          過渡動畫過程中每一幀的樣式 > 用戶代理、用戶、頁面作者設置的!important樣式 > 動畫過程中每一幀的樣式優(yōu)先級 > 頁面作者、用戶、用戶代理普通樣式。

          然而,經過多個瀏覽器的測試,實際上并不是這樣。(尷尬了)

          舉個例子,我們可以通過這個特性,覆蓋掉行內樣式中的 !important 樣式:

          <p class="txt" style="color:red!important">123456789</p>
          
          .txt {
              animation: colorGreen 2s infinite;
          }
          @keyframes colorGreen {
              0%,
              100% {
                  color: green;
              }
          }
          

          在 Safari 瀏覽器下,上述 DEMO 文本的顏色為綠色,也就是說,處于動畫狀態(tài)中的樣式,能夠覆蓋掉行內樣式中的 !important 樣式,屬于最最高優(yōu)先級的一種樣式,我們可以通過無限動畫、或者 animation-fill-mode: forwards,利用這個技巧,覆蓋掉本來應該是優(yōu)先級非常非常高的行內樣式中的 !important 樣式。

          我在早兩年的 Chrome 中也能得到同樣的結果,但是到今天(2022-01-10),最新版的 Chrome 已經不支持動畫過程中關鍵幀樣式優(yōu)先級覆蓋行內樣式 !important 的特性。

          對于不同瀏覽器,感興趣的同學可以利用我這個 DEMO 自行嘗試,CodePen Demo - the priority of CSS Animation

          CSS 動畫的優(yōu)化

          這也是非常多人非常關心的一個重點。

          我的 CSS 動畫很卡,我應該如何去優(yōu)化它?

          動畫元素生成獨立的 GraphicsLayer,強制開始 GPU 加速

          CSS 動畫很卡,其實是一個現(xiàn)象描述,它的本質其實是在動畫過程中,瀏覽器刷新渲染頁面的幀率過低。通常而言,目前大多數(shù)瀏覽器刷新率為 60 次/秒,所以通常來講 FPS 為 60 frame/s 時動畫效果較好,也就是每幀的消耗時間為 16.67ms。

          頁面處于動畫變化時,當幀率低于一定數(shù)值時,我們就感覺到頁面的卡頓。

          而造成幀率低的原因就是瀏覽器在一幀之間處理的事情太多了,超過了 16.67ms,要優(yōu)化每一幀的時間,又需要完整地知道瀏覽器在每一幀干了什么,這個就又涉及到了老生常談的瀏覽器渲染頁面。

          到今天,雖然不同瀏覽器的渲染過程不完全相同,但是基本上大同小異,基本上都是:

          簡化一下也就是這個圖:

          這兩張圖,你可以在非常多不同的文章中看到。

          回歸本文的重點,Web 動畫很大一部分開銷在于層的重繪,以層為基礎的復合模型對渲染性能有著深遠的影響。當不需要繪制時,復合操作的開銷可以忽略不計,因此在試著調試渲染性能問題時,首要目標就是要避免層的重繪。那么這就給動畫的性能優(yōu)化提供了方向,減少元素的重繪與回流

          這其中,如何減少頁面的回流與重繪呢,這里就會運用到我們常說的** GPU 加速**。

          GPU 加速的本質其實是減少瀏覽器渲染頁面每一幀過程中的 reflow 和 repaint,其根本,就是讓需要進行動畫的元素,生成自己的 GraphicsLayer

          瀏覽器渲染一個頁面時,它使用了許多沒有暴露給開發(fā)者的中間表現(xiàn)形式,其中最重要的結構便是層(layer)。

          在 Chrome 中,存在有不同類型的層: RenderLayer(負責 DOM 子樹),GraphicsLayer(負責 RenderLayer 的子樹)。

          GraphicsLayer ,它對于我們的 Web 動畫而言非常重要,通常,Chrome 會將一個層的內容在作為紋理上傳到 GPU 前先繪制(paint)進一個位圖中。如果內容不會改變,那么就沒有必要重繪(repaint)層。

          而當元素生成了自己的 GraphicsLayer 之后,在動畫過程中,Chrome 并不會始終重繪整個層,它會嘗試智能地去重繪 DOM 中失效的部分,也就是發(fā)生動畫的部分,在 Composite 之前,頁面是處于一種分層狀態(tài),借助 GPU,瀏覽器僅僅在每一幀對生成了自己獨立 GraphicsLayer 元素層進行重繪,如此,大大的降低了整個頁面重排重繪的開銷,提升了頁面渲染的效率。

          因此,CSS 動畫(Web 動畫同理)優(yōu)化的第一條準則就是讓需要動畫的元素生成了自己獨立的 GraphicsLayer,強制開始 GPU 加速,而我們需要知道是,GPU 加速的本質是利用讓元素生成了自己獨立的 GraphicsLayer,降低了頁面在渲染過程中重繪重排的開銷。

          當然,生成自己的獨立的 GraphicsLayer,不僅僅只有 transform3d api,還有非常多的方式。對于上述一大段非常繞的內容,你可以再看看這幾篇文章:

          • 【W(wǎng)eb動畫】CSS3 3D 行星運轉 && 瀏覽器渲染原理
          • Accelerated Rendering in Chrome

          除了上述準則之外,還有一些提升 CSS 動畫性能的建議:

          減少使用耗性能樣式

          不同樣式在消耗性能方面是不同的,改變一些屬性的開銷比改變其他屬性要多,因此更可能使動畫卡頓。

          例如,與改變元素的文本顏色相比,改變元素的 box-shadow 將需要開銷大很多的繪圖操作。box-shadow 屬性,從渲染角度來講十分耗性能,原因就是與其他樣式相比,它們的繪制代碼執(zhí)行時間過長。這就是說,如果一個耗性能嚴重的樣式經常需要重繪,那么你就會遇到性能問題。

          類似的還有 CSS 3D 變換、mix-blend-modefilter,這些樣式相比其他一些簡單的操作,會更加的消耗性能。我們應該盡可能的在動畫過程中降低其使用的頻率或者尋找替代方案。

          當然,沒有不變的事情,在今天性能很差的樣式,可能明天就被優(yōu)化,并且瀏覽器之間也存在差異。

          因此關鍵在于,我們需要針對每一起卡頓的例子,借助開發(fā)工具來分辨出性能瓶頸所在,然后設法減少瀏覽器的工作量。學會 Chrome 開發(fā)者工具的 Performance 面板及其他渲染相關的面板非常重要,當然這不是本文的重點。大家可以自行探索。

          使用 will-change 提高頁面滾動、動畫等渲染性能

          will-change 為 Web 開發(fā)者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發(fā)生變化之前提前做好對應的優(yōu)化準備工作。 這種優(yōu)化可以將一部分復雜的計算工作提前準備好,使頁面的反應更為快速靈敏。

          值得注意的是,用好這個屬性并不是很容易:

          • 不要將 will-change 應用到太多元素上:瀏覽器已經盡力嘗試去優(yōu)化一切可以優(yōu)化的東西了。有一些更強力的優(yōu)化,如果與 will-change 結合在一起的話,有可能會消耗很多機器資源,如果過度使用的話,可能導致頁面響應緩慢或者消耗非常多的資源。
          • 有節(jié)制地使用:通常,當元素恢復到初始狀態(tài)時,瀏覽器會丟棄掉之前做的優(yōu)化工作。但是如果直接在樣式表中顯式聲明了 will-change 屬性,則表示目標元素可能會經常變化,瀏覽器會將優(yōu)化工作保存得比之前更久。所以最佳實踐是當元素變化之前和之后通過腳本來切換 will-change 的值。
          • 不要過早應用 will-change 優(yōu)化:如果你的頁面在性能方面沒什么問題,則不要添加 will-change 屬性來榨取一丁點的速度。 will-change 的設計初衷是作為最后的優(yōu)化手段,用來嘗試解決現(xiàn)有的性能問題。它不應該被用來預防性能問題。過度使用 will-change 會導致大量的內存占用,并會導致更復雜的渲染過程,因為瀏覽器會試圖準備可能存在的變化過程。這會導致更嚴重的性能問題。
          • 給它足夠的工作時間:這個屬性是用來讓頁面開發(fā)者告知瀏覽器哪些屬性可能會變化的。然后瀏覽器可以選擇在變化發(fā)生前提前去做一些優(yōu)化工作。所以給瀏覽器一點時間去真正做這些優(yōu)化工作是非常重要的。使用時需要嘗試去找到一些方法提前一定時間獲知元素可能發(fā)生的變化,然后為它加上 will-change 屬性。

          有人說 will-change 是良藥,也有人說是毒藥,在具體使用的時候,可以多測試一下。

          最后

          好了,本文從多個方面,由淺入深地描述了 CSS 動畫我認為的一些比較重要、值得一講、需要注意的點。當然很多地方點到即止,或者限于篇幅沒有完全展開,很多細節(jié)還需要讀者進一步閱讀規(guī)范或者自行嘗試驗證,實踐出真知,紙上得來終覺淺。

          OK,本文到此結束,希望本文對你有所幫助 :)


          主站蜘蛛池模板: 精品一区二区三区在线观看视频| 国产高清一区二区三区| 精品国产一区二区22| 91精品一区二区三区久久久久| 国产伦精品一区二区三区免费迷| 色狠狠一区二区三区香蕉| 精品乱子伦一区二区三区高清免费播放 | 99精品国产高清一区二区| 一区二区精品在线观看| 国产在线不卡一区| 亚洲乱码av中文一区二区 | 国产激情视频一区二区三区| 一区二区三区四区在线观看视频| 久久一区二区三区精华液使用方法| 日韩人妻精品无码一区二区三区| 中文字幕亚洲一区| 国产成人无码AV一区二区在线观看 | 99精品一区二区免费视频| 日本一区二区不卡视频| 制服中文字幕一区二区| 国模吧无码一区二区三区| 精品国产毛片一区二区无码| 久久99国产一区二区三区| 无码人妻一区二区三区在线水卜樱 | 亚无码乱人伦一区二区| 亚洲欧美日韩一区二区三区在线| 亚洲国产成人久久综合一区| 2018高清国产一区二区三区| 无码少妇一区二区浪潮免费| 国精产品999一区二区三区有限 | 国产综合无码一区二区三区| 国产午夜精品一区二区三区嫩草 | 人妻体内射精一区二区| 欲色影视天天一区二区三区色香欲| 视频一区视频二区在线观看| 国产福利电影一区二区三区,亚洲国模精品一区 | 日韩一区二区三区在线精品| 久久精品无码一区二区三区日韩| 国产无线乱码一区二三区 | 精品国产一区二区麻豆| 一区二区不卡视频在线观看|