前端實現旗幟飄動效果系列(Ⅰ):dom+css實現

李一花發表於2018-07-17

hello,民娜桑~~我又來開新坑了( ̄ε(# ̄)╰╮o( ̄皿 ̄///),這次儘量保證把這個坑填完~

本系列我會分四篇來完成主題,分別是① DIV+CSS的實現,② canvas2D的簡單實現,③ canvas2D的進階實現,④ webgl+著色器的實現 以及 ⑤ 包裝成jquery外掛併發布為npm模組 。

這是整個系列完成以後的最終效果:

開始閱讀之前請確保您對高中的三角函式還有一定的印象以及瞭解基本的canvas繪圖操作——當然如果你確實不瞭解也沒事,這篇文章是使用div和css的實現,暫時沒有用到以上的知識。

首先講一下實現的原理,拿到一張圖片後,獲取其寬度,然後在效能允許的情況下,切成儘可能細的豎直切片,每個切片都用同一張背景圖片並將背景圖片的位置移動到切片的對應位置,然後通過css3關鍵幀動畫使切片元素以不同的時間軸來進行上下移動。很簡單是吧,如果你覺得so easy或者想根據原理自己試著實現一遍,那本文的後面你就可以直接跳過了。

html結構很簡單:

<!doctype html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>飄動的旗幟~</title>
    <style>


        * {
            margin: 0;
            padding: 0;
        }

        html, body {
            height: 100%;
            width: 100%;
            background-color: lightgrey;
        }

        body {
            text-align: center;
            position: relative;
        }

        ul, li {
            list-style: none;
        }

        #flag {
            position: absolute;
            left: 50%;
            top: 50%;
        }
        
        /* 這裡是核心css樣式 */

    </style>
</head>
<body>
<ul id="flag"></ul>
<script>
  (function () {
    // 這裡是js程式碼
  })();
</script>
</body>
</html>

然後,準備一張圖片,比如這張艹貓路飛團的海盜旗,哎呀手滑,是草帽路飛團 (๑•̀ㅂ•́)و

接下來新增核心css程式碼:

/* 這裡是核心css樣式 */

#flag > li {
    height: 100%;
    float: left;
    background-image: url("./img/flag.jpg");
    background-size: auto 100%;
    animation: flag ease-in-out infinite;
}

是的,你沒看錯,就是這麼點~~事實上並非如此,為了實現程式碼的靈活性,比如自定義週期數、週期長度、振幅、切片數量等,我使用js程式碼動態建立了style標籤,並將屬性計算後寫入。

下面是js程式碼,圖片地址我暫時是寫死的,通過上傳圖片自動生成動畫我會在最後一節封裝外掛時作為補充來說明。

// 這裡是js程式碼
var flagEle = document.getElementById(`flag`)
var image = new Image()
image.src = `./img/flag.jpg`

var IMG_MAX_WIDTH = 600
var IMG_MAX_HEIGHT = 600
var imgHeight
var imgWidth
image.onload = function () {
    imgWidth = image.width
    imgHeight = image.height
    var ratio = image.width / image.height
    if (imgWidth > IMG_MAX_WIDTH) {
      imgWidth = IMG_MAX_WIDTH
      imgHeight = imgWidth / ratio
    }
    if (imgHeight > IMG_MAX_HEIGHT) {
      imgHeight = IMG_MAX_HEIGHT
      imgWidth = imgHeight * ratio
    }
    
    flagEle.style.width = imgWidth + `px`
    flagEle.style.height = imgHeight + `px`
    flagEle.style.marginLeft = -imgWidth / 2 + `px`
    flagEle.style.marginTop = -imgHeight / 2 + `px`
    
    splitImg(100, 20, 2, 2)
}

雖然在圖片載入後有一堆程式碼,但是除了 splitImg(100, 20, 2, 2) ,事實上其他都無關緊要,前面那段程式碼的主要作用是定義一個容器的最大寬高,如果超過將會被等比例縮放。(不過並不推薦使用大圖,效能會是一個大問題)

下面使這段程式的核心方法——splitImg:

 /**
   * 分割圖片
   * @param sliceCount 切片數量
   * @param amplitude 振幅
   * @param period 固定週期個數
   * @param duration 一個週期的時長
   */
  function splitImg (sliceCount, amplitude, period, duration) {
    var styleEle = document.createElement(`style`)
    // styleEle.innerHTML = `body{background: red}`
    var styleHtmlAry = []
    var sliceCountPerPeriod = Math.floor(sliceCount / period)
    var sliceWidth = imgWidth / sliceCount
    var formula = sliceCountPerPeriod + `n+`
    var interval = duration * period / sliceCount

    // 新增動畫延時
    for (var i = 0; i < sliceCount; i++) {
      if (i < sliceCountPerPeriod) {
        styleHtmlAry.push(`#flag > li:nth-child(` + formula + i + `) { `)
        styleHtmlAry.push(`animation-delay: -` + (interval * (sliceCountPerPeriod - i)) + `s;`)
        styleHtmlAry.push(`}`)
      }
      styleHtmlAry.push(`#flag > li:nth-child(` + i + `) { background-position: -` + (i * sliceWidth) + `px 0; }`) // 設定切片背景
    }

    // 新增關鍵幀動畫
    styleHtmlAry.push(`@keyframes flag {`)
    styleHtmlAry.push(`0% { transform: translate3d(0, ` + amplitude + `px, 0); }`)
    styleHtmlAry.push(`50% { transform: translate3d(0, -` + amplitude + `px, 0); }`)
    styleHtmlAry.push(`100% { transform: translate3d(0, ` + amplitude + `px, 0); }`)
    styleHtmlAry.push(`}`)

    // 切片樣式
    styleHtmlAry.push(`#flag > li {`)
    styleHtmlAry.push(`animation-duration: ` + duration + `s;`) // 新增週期時長
    styleHtmlAry.push(`width: ` + (imgWidth / sliceCount) + `px;`) // 設定切片寬度
    styleHtmlAry.push(`}`)

    styleEle.innerHTML = styleHtmlAry.join(``)

    // 建立切片元素
    flagEle.innerHTML = new Array(sliceCount + 1).join(`<li></li>`)

    document.documentElement.appendChild(styleEle)
  }

① 這裡的波形圖是使用的cos函式的表示形式,新增了三個關鍵幀:從波峰到波谷,再回到波峰。

② 因為使用了ease-in-out的動畫曲線,所以可以模擬出三角函式的波形圖

③ 原理和程式碼都比較簡單,可能比較需要注意的是這句 styleHtmlAry.push(`#flag > li:nth-child(` + formula + i + `) { `),對css3瞭解的朋友應該知道:nth-child的用法,括號裡面的是一個等差數列表示式,項數規定用n表示,那麼公差是多少呢,由於我們的動畫是週期性的,所以公差應該是每個週期包含的切片數量(正整數),即 var sliceCountPerPeriod = Math.floor(sliceCount / period)

寫完以上程式碼,我們的基本雛形就出來了,這是切片數為80份,振幅20單位,2個週期,週期時長為2秒 時的效果圖:

是不是看著有點不對勁?旗子不是應該一邊固定的麼?怎麼兩邊一起動了?

辦法也簡單,只要我們在容器上加一個反方向的運動不就好了?

修改#flag樣式,新增如下樣式:animation: flag-reverse ease-in-out infinite;


#flag {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate3d(-50%,-50%,0);
    animation: flag-reverse ease-in-out infinite;
}

如下位置新增js程式碼:


// 新增關鍵幀動畫
...


// 新增反向關鍵幀動畫
styleHtmlAry.push(`@keyframes flag-reverse {`)
styleHtmlAry.push(`0% { transform: translate3d(0, ` + (-amplitude) + `px, 0); }`)
styleHtmlAry.push(`50% { transform: translate3d(0, ` + amplitude + `px, 0); }`)
styleHtmlAry.push(`100% { transform: translate3d(0, ` + (-amplitude) + `px, 0); }`)
styleHtmlAry.push(`}`)

// 容器應用flag-reverse動畫
styleHtmlAry.push(`#flag {`)
styleHtmlAry.push(`animation-duration: ` + duration + `s;`) // 新增週期時長
styleHtmlAry.push(`animation-delay: -` + (interval * sliceCountPerPeriod) + `s;`)
styleHtmlAry.push(`}`)

// 切片樣式
...

似乎沒問題了,看看效果:

納尼?怎麼兩邊都固定了?原來是因為我們指定2個週期,只要不是週期的整數倍就行了,在原來的基礎上改為1.5個週期試試:

到這裡我們的dom+css的實現方式就結束啦,這種方式的優點很明顯,就是實現簡單;缺點也不少,比如無法新增高光效果,整體振幅一致不符合常理,切片過多容易造成的頁面阻塞與記憶體洩露,下一節 我會用canvas2D畫素級的操作實現該效果,可以很大程度上避免這些問題。

完整程式碼戳這裡

Demo:See the Pen flag waving by dom+css by Kay (@oj8kay) on CodePen.

原文釋出時間為:2018年07月03日
原文作者:oj8kay 

本文來源:開源中國 如需轉載請聯絡原作者


相關文章