博客 / 詳情

返回

我用四種方式實現某場所碼箭頭動畫(css3+canvas+svg+js)講解超詳細

大家好!我是貓小白,本期給大家帶來一個簡單的實戰demo,希望大家食用得開心,有收穫。

==首先聲明:此demo存粹為了學習其中的動畫知識,請不要用於真實的場景,否則被JC叔叔抓起包吃包住就不應該了瑟!==
此項目基於==成都天府健康通==,其它地區的場所碼不知道是否有類似的動畫,如果沒有也沒關係,可以學習下是如何實現的。
為啥子要做這個呢?那是因為有一天我出去買菜,回來保安叔叔恩是要我掃一下場所碼,作為一個5星好公民,肯定是非常配合的掏出手機開始掃 。叮~,屏幕上出現一個不挺放大縮小的箭頭。我就想,要不用代碼來實現一下這個動畫吧。

於是就有了這篇文章,我一共是實現了4種方法:

  1. 第一種,css,零js代碼(最簡單)
  2. 第二種,svg,零js代碼 (較簡單)
  3. 第三種,js+dom (較複雜)
  4. 第四種,js+canvas (較複雜)

全文源碼地址:點擊跳轉到github,可以拉下來親操練操練,順便給一個start!
微信圖片_20220503174339.jpg

接下來,我們一個一個來分析如何實現的。

準備工作

首先我們假裝出去買個菜(未解封的同胞除外例如上海的朋友),回來的時候去保安那裏故作鎮定,連貫的掏出手機打開天府健康通(成都的哈)掃一下,然後給保安亮一下,不退出界面,淡定回家。

回到家後,拿出手機,打開剛剛的界面,截一個圖:
bg_gaitubao_291x517.png

加入html中,作為div容器的背景圖片,然後我們創建一個白色的框把中間的圓圈和勾遮住,因為我們要自己實現其中的動畫效果。
實現效果如下:

遮板.png

遮板的css代碼:

/**遮板**/
.shutter {
  position: absolute;
  display: block;
  width: 120px;
  height: 120px;
  background-color: #fff;
  top: 206px;
  left: 150px;
}

核心html代碼結構如下,:

<body>
    <div class="app">
      <div class="content">
        <!--遮板把原來的箭頭用白色背景遮住,自己實現箭頭-->
        <div class="shutter"></div>
        <!--自己實現的箭頭和圓-->
        <div class="circle"></div>
      </div>
    </div>
  </body>
只是列舉關鍵的代碼,完整代碼請到github倉庫查看。
準備工作完成,開始正片!

第一種,css,零js代碼

使用css3animation動畫,可以很容易實現這樣的效果,不用一行js代碼。單關鍵是要看明白我們目標動畫是由哪些部分組成的,動畫是怎麼變化的。所以先再次看看這個gif,注意其中動畫部分:
gif.gif

不難發現:

  • 圓圈是由小到大,再由大到小
  • 內圈放大到一定程度時,外圈(擴散部分)出現,並且半徑不停增大,當內圈放到最大時,擴散圈消失
  • 內部的勾也會一起放大縮小,頻率和內圈相同
    根據上面的規律,我們先實現一個內圈(綠色圈部分)

內部圈css代碼:

/**內部圓圈**/
.circle {
  position: absolute;
  z-index: 9;
  width: var(--circle-size);
  height: var(--circle-size);
  top: 218px;
  left: 165px;
  background-color: var(--theme-green);
  /* background-color: red; */
  border-radius: 45px;
  animation: circleMove 0.7s alternate infinite cubic-bezier(1, 1, 1, 1);
}
--circle-size、--theme-green是我們在開頭定義的變量,方便後續使用。
:root {
  --theme-green: #4ba776; /**主題綠色**/
  --circle-size: 90px; /**圓圈大小 寬高一樣**/
}

內部圈畫好了,還差一個白色的"√",為了簡單我直接使用了"√"這個字符串,加入偽類元素的content中。你們也可以用圖片或者svg。
"√"的css代碼如下:

/**內部的勾,使用`.circle`的`before`偽類**/
.circle::before {
  z-index: 2;//z-index要比外圈(闊散圈)大,否則要被背景覆蓋
  position: absolute;
  display: block;
  content: "✔";
  font-size: 63px;
  color: #fff;
  left: 18px;
  top: -1px;
}

讓我們預覽下現在的效果:
圈效果1.png
喲也~看起來是那麼回事了。

現在我們按照內圈相同的顏色和大小,畫一個外部圈(擴散圈)。
參考上面的"√"的實現,我們用after偽類來實現:

/**動畫時外部漸變圈*/
.circle::after {
  z-index: 1;//z-index要比內部的勾小,否則要覆蓋掉勾
  position: absolute;
  display: block;
  content: "";
  width: calc(var(--circle-size));
  height: calc(var(--circle-size));
  box-sizing: border-box;
  border-radius: 50%;
  background-color: #4ba776;
  transform: scale(1.2);
  /**外圈動畫 一輪時間是內圈的2倍 內部圈放大0.7s+縮小0.7s=1.4s  infinite無限重複**/
  /* animation: outCircleMove 1.4s infinite cubic-bezier(1, 1, 1, 1); */
}

內圈加入放大縮小動畫,在.circle中計入animation

/**內部圓圈**/
.circle {
  position: absolute;
  z-index: 9;
  width: var(--circle-size);
  height: var(--circle-size);
  top: 218px;
  left: 165px;
  background-color: var(--theme-green);
  /* background-color: red; */
  border-radius: 45px;
  /*加入放大縮小動畫  alternate:輪流播放,放大再放小  infinite:無限播放  ease-in-out:變化函數*/
  animation: circleMove 0.7s alternate infinite ease-in-out;
}
/**定義放大縮小動畫,最大放大到1.2倍**/
@keyframes circleMove {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(1.2);
  }
}
解讀:時間設置為0.7s,我們定義了circleMove 動畫,意思是在0秒的時候圓圈保持1倍大小,在0.1~0.7s圓圈逐漸變為1.2倍大小。變化的快慢是由animation-timing-function參數決定的,文中設置的ease-in-out是進入和退出較慢,中間速度較快的一種變化曲線。
我們可以打開開發者工具,點擊animation屬性的曲線圖標,查看和選擇想要的動畫曲線
動畫曲線.png

好內圈動畫已經完成,我們來看下效果:
內圈.gif

perfect!現在我們加入外圈動畫,讓我們回顧一下:外圈動畫是要在內圈放大到一定時間外圈才出現,我們暫定1/5,也就是20%的時候顯示並且開始放大(放大對應屬性:scale);放大後外圈會越來越便透明,直到內圈放到最大,此時完全透明,感覺像是擴散的效果(透明對應屬性:opacity)。
知道這些後,我們設置外圈動畫如下:
/*動畫時外部漸變圈/

.circle::after {
  z-index: 1;
  position: absolute;
  display: block;
  content: "";
  width: calc(var(--circle-size));
  height: calc(var(--circle-size));
  box-sizing: border-box;
  border-radius: 50%;
  background-color: #4ba776;
  /**外圈動畫 一輪時間是內圈的2倍 內部圈放大0.7s+縮小0.7s=1.4s  infinite無限重複**/
  animation: outCircleMove 1.4s infinite ease-in-out;
}
/**定義擴散圈的放大縮小動畫**/
@keyframes outCircleMove {
  /**0%~50% 0s~ 0.7s前的時間 此時內圈放大,擴散圈等待時機出現**/
  0% {
    opacity: 0;
  }
  /**10% 此時內圈已經放大一段時間, 擴散全顯示opacity設為1,倍率為1 **/
  10% {
    opacity: 1;
    transform: scale(1);
  }
  /**50%到100%是動畫0.7s~1.4s的時間 此時內圈縮小,擴散圈變為1.3倍,但是透明度設為0**/
  50%,
  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}

至此就大功告成了,看下效果:
最終效果.gif

第二種,svg,零js代碼 (較簡單)

上面了利用css3的動畫屬性,操作起來非常簡單。現在介紹一種用svg動畫的方式實現相同的效果。我們知道svg也是有比較豐富的動畫特性支持。
我們簡單列舉一下svg實現動畫的幾種方法:

1.set

set元素是SVG動畫元素中最簡單的元素。在經過特定時間間隔後,它只是將屬性設置為特定值。因此,形狀不會連續進行動畫處理,而只是更改屬性值一次。

2.animate

<animate>元素通常放置到一個SVG圖像元素裏面,用來定義這個圖像元素的某個屬性的動畫變化過程。

3.animateMotion

<animateMotion>元素也是放置一個圖像元素裏面,它可以引用一個事先定義好的動畫路徑,讓圖像元素按路徑定義的方式運動。

4.animateTransform

動畫上一個目標元素變換屬性,從而使動畫控制平移,縮放,旋轉或傾斜。
簡單瞭解過後,我們今天主要用animate來實現動畫。

首先,我們還是拿出上面的基礎佈局,祖傳代碼(背景+遮罩)

<div class="app">
  <div class="content">
    <!--遮板把原來的箭頭用白色背景遮住,自己實現箭頭-->
    <div class="shutter"></div>
    <!--自己實現的箭頭動畫-svg-->
    <svg></svg>
  </div>
</div>
</body>
基本框架的樣式和上一節css動畫的樣式相同,就不重複粘貼了。

然後我們畫一個圓和勾,也就是內部的部分;svg中畫圓是用<circle>,於是我們有這樣的代碼:

<div class="app">
  <div class="content">
    <!--遮板把原來的箭頭用白色背景遮住,自己實現箭頭-->
    <div class="shutter"></div>
    <!--自己實現的箭頭和圓-->
  <svg
    class="circle"
    width="130"
    height="130"
    xmlns="http://www.w3.org/2000/svg"
  >
    <g>
      <circle cx="45" cy="45" r="45" fill="#4ba776"></circle>
    </g>
  </svg>
  </div>
</div>
</body>

效果如下:
svg1.png

繼續添加中間的白色"√"。為了簡介,我們用<text>文字標籤:
添加<text>後的代碼如下:

<div class="app">
  <div class="conten ">
    <!--遮板把原來的箭頭用白色背景遮住,自己實現箭頭-->
    <div class="shutter"></div>
    <!--自己實現的箭頭和圓-->
  <svg
    class="circle"
    width="130"
    height="130"
    xmlns="http://www.w3.org/2000/svg"
  >
    <g>
      <circle cx="65" cy="65" r="45" fill="#4ba776"></circle>
      <!--text添加-->
      <text font-size="63" fill="#fff" y="90" x="38">✔</text>
    </g>
  </svg>
  </div>
</div>
</body>

添加文字後效果如下:
svg2.png
好了,內圈添加好了,我們再添加一個外圈(擴散圈),還是用<circle>,大小和位置都和內圈一模一樣,這裏就不粘貼代碼了。

現在來添加動畫,根據上一章節的css3動畫,我們需要不挺的放大縮小內圈的大小。所以我們通過動畫改編圓的半徑就ok了。上面介紹了<animate>標籤可以動態改變某一屬,這裏的屬性就是半徑R:

...其它代碼
<circle cx="65" cy="65" r="45" fill="#4ba776">
  <!--添加到circle標籤內部,指定attributeName為半徑r,values是半徑變化的值,我們讓它先變大再變小,dur是動畫時間 repeatCount:indefinite無限循環-->
  <animate
    attributeName="r"
    values="45;54;45"
    dur="1.4s"
    repeatCount="indefinite"
  />
</circle>
...其它代碼

實現效果:
svg.gif
完美!看起來就是那麼回事,後面的步驟其實都是依葫蘆畫瓢。
添加內部"√"的動畫,用<animate>改變font-size

<text font-size="63" fill="#fff" y="90" x="38">
  ✔
  <animate
    attributeName="font-size"
    values="63;69;63"
    dur="1.4s"
    repeatCount="indefinite"
  />
</text>

添加後效果我們來看下:
svgtext.gif

最後,我們添加擴散效果。擴散效果其實分兩步,第一步是放大半徑,另一步是從不透明到透明的過程。所以我們用<animate>改變半徑的同時,改變它的透明度。

...其它代碼
<!--擴散圈--->
<circle cx="65" cy="65" r="45" fill="#4ba776">
  <!--設置放大縮小動畫--->
  <animate
    attributeName="r"
    values="45;50;65;65;50;45"
    dur="1.4s"
    repeatCount="indefinite"
  />
  <!--設置透明度變化動畫--->
  <animate
    attributeName="opacity"
    values="1;1;0.2;0;0;0"
    dur="1.4s"
    repeatCount="indefinite"
  />
</circle>
...其它代碼

最終效果如下:
svg最終效果.gif

第三種,js+dom (較複雜)

前面都是通過css或者svg自帶特性完成的動畫效果,下面這種是通過js動態改變圓圈div大小實現的,較為複雜,理解起來需要一定的js基礎,在做項目優先選擇css3,我們一起來看下是如何實現的。
首先,我們還是拿出上面的基礎佈局,添加內外圈的div和佈局。
關鍵css部分:

/**外部圓圈**/
.circle,
.outCircle {
  z-index: 1;
  position: absolute;
  width: var(--circle-size);
  height: var(--circle-size);
  top: 218px;
  left: 165px;
  background-color: var(--theme-green);
  /* background-color: red; */
  border-radius: 45px;
}
/**內部的勾**/
.circle::before {
  position: absolute;
  display: block;
  content: "✔";
  font-size: 63px;
  color: #fff;
  left: 18px;
  top: -1px;
}

html部分:

<body>
  <div class="app">
    <div class="content">
      <!--遮板把原來的箭頭用白色背景遮住,自己實現箭頭-->
      <div class="shutter"></div>
      <!--自己實現的箭頭和圓-->
      <div class="circle"></div>
      <div class="outCircle"></div>
    </div>
  </div>
</body>

至此,元素已經畫好了,就差動畫部分了,簡單分析一波:
內圈需要逐漸放大再逐漸放小,我們用一個變量size_inner來代表放大縮小的倍數,STEP_INNER代表內圈遞增或者遞減的步長,最終設置到style上的transform屬性。
外圈也需要有一個放大過程,且放大的速度比內圈快,我們用size_outer來代表放大倍數,STEP_OUT代表外圈遞增步長。
外圈需要一個透明度逐漸小的值opacity_out ,透明度變化的步長用OPA_STEP代替,最終設置style上的opacity屬性。
動畫是持續不斷的,所以我們需要不停的執行,先暫時採用setInterval函數;
放大到一定倍數然後再縮小,最大倍數我們用MAX_SCALE常量代表,如果遞增到最大然後STEP_INNER設置為負數,意味着開始縮小。

最終的源代碼如下:

<script>
  const circle = document.querySelector(".circle");
  const outCircle = document.querySelector(".outCircle");
  let size_inner = 1.0; //內圈的放大倍數
  let size_outer = 1.0; //內圈的放大倍數
  let STEP_INNER = 0.009; //內圈的遞增步值
  let STEP_OUT = STEP_INNER + 0.009; //外圈要比內圈擴散倍速大一點
  let opacity_out = 1; //外圈透明度
  const OPA_STEP = 0.03;
  const MAX_SCALE = 1.4;
  function randerInner() {
    //放大過程
    if (STEP_INNER >= 0) {
      if (size_inner <= MAX_SCALE) {
        size_inner += STEP_INNER;
        //控制外圈光環--start
        if (size_inner >= 1.1) {
          //內圈放大到1.1倍 外圈光環才出現,大小和內圈一致,透明度為1
          if (opacity_out === 0) {
            opacity_out = 1;
            size_outer = size_inner;
          }
          size_outer += STEP_OUT;
          opacity_out -= OPA_STEP;//透明度逐漸
        }
        //控制外圈光環--end
      } else {
        //放大到MAX_SCALE倍了 重新縮小
        size_inner = MAX_SCALE;
        STEP_INNER = STEP_INNER * -1; //步長乘以-1,開始方方向變化
        //控制外圈光環-隱藏
        opacity_out = 0;
      }
    } else {
      //縮小過程
      if (size_inner > 1) {
        size_inner += STEP_INNER;
      } else {
        //縮到1倍了 重新放大
        STEP_INNER = STEP_INNER * -1; //步長乘以-1,開始方方向變化
        size_inner = 1;
      }
    }
    circle.style.transform = `scale(${size_inner})`; //設這內圈放大樣式
    outCircle.style.transform = `scale(${size_outer})`; //設這內圈放大樣式
    outCircle.style.opacity = opacity_out; //設這內圈透明度
  }
  setInterval(() => {
    randerInner();
  }, 17);
</script>

因篇幅有限,不逐一解釋各行代碼的含義,有不理解的可以私聊我。
最終效果:
js1最終效果.gif

眾所周知,因為瀏覽器的eventloop機制,setInterval往往並不能按照設置的時間執行,在稍微複雜一些的頁面中會出現卡頓。固可採用requestAnimationFrameapi代替。
代碼如下:

<script>
    const circle = document.querySelector(".circle");
    const outCircle = document.querySelector(".outCircle");
    let size_inner = 1.0; //內圈的放大倍數
    let size_outer = 1.0; //內圈的放大倍數
    let STEP_INNER = 0.009; //內圈的遞增步值
    let STEP_OUT = STEP_INNER + 0.009; //外圈要比內圈擴散倍速大一點
    let opacity_out = 1; //外圈透明度
    const OPA_STEP = 0.03;
    const MAX_SCALE = 1.4;
    (function randerInner() {
      //放大過程
      if (STEP_INNER >= 0) {
        if (size_inner <= MAX_SCALE) {
          size_inner += STEP_INNER;
          //控制外圈光環--start
          if (size_inner >= 1.1) {
            //內圈放大到1.2倍 外圈光環才出現大小一致
            if (opacity_out === 0) {
              opacity_out = 1;
              size_outer = size_inner;
            }
            size_outer += STEP_OUT;
            opacity_out -= OPA_STEP;
          }
          //控制外圈光環--end
        } else {
          //放大到MAX_SCALE倍了 重新縮小
          size_inner = MAX_SCALE;
          STEP_INNER = STEP_INNER * -1;

          //控制外圈光環-隱藏
          opacity_out = 0;
        }
      } else {
        //縮小過程
        if (size_inner > 1) {
          size_inner += STEP_INNER;
        } else {
          //縮到1倍了 重新放大
          STEP_INNER = STEP_INNER * -1;
          size_inner = 1;
        }
      }
      circle.style.transform = `scale(${size_inner})`; //設這內圈放大樣式
      outCircle.style.transform = `scale(${size_outer})`; //設這內圈放大樣式

      outCircle.style.opacity = opacity_out; //設這內圈透明度
      requestAnimationFrame(() => {
        randerInner();
        _test();
      });
    })()
</script>

第四種,js+canvas (較複雜)

經過上面章節的洗禮,canvas版本和js+dom版本非常相似,唯一的不同就是繪畫圓或者文字的api不同。
我們分析下要點:

  1. 需要在畫布上用arcapi繪製2個圓,一個內圈一個外圈,並且不停改變圓圈的大小或透明度。
  2. 需要用fillText繪製一個字樣,並且改變字體大小。
  3. 需要不停執行繪製函數,這裏使用流暢的requestAnimationFrameapi。
  4. 每次繪製前,需清空畫布clearRect
    其實原理和第三種差不多,都需要用變量記錄內部圈變化的大小,到最大值後方向遞減。外部圈是需要等到內圈放大到一定倍數(1.1)倍,才顯示出來,並且放大速度要比內圈快,隨着外圈變大,透明度逐漸減小到0。

我想第四種和第三種其實都有這樣的邏輯,為此我花了一點事件把這個動作進行了封裝,採用了一種訂閲發佈模式來實現。
為了大家瀏覽我這篇文章的同時,除了能學習到動畫知識,也能學習到一些其它的比較重要的js知識。大家應該瞭解或者聽過js的各種模式,什麼工廠模式、單列模式、觀察者模式、發佈訂閲模式等等,都是學習到了理論知識。今天我就用這個例子介紹下發布訂閲模式是如何在實踐中使用的。

剛我們分析了canvas實現的方式和js+dom的方式,這兩種方式都需要用變量來記錄一個值(這裏是放大倍數),而且每次執行渲染函數時都要遞增一個常量值(成為步長),到達某個最大值(比如1.3倍大小)後開始反向遞減(步長乘以-1),然後遞減到最初值(1倍大小)。同時還有一個細節,當內圈遞增到一定倍數(比如1.1倍)要讓外圈開始加入動畫,當內圈遞增到最大時,外圈要消失(透明度設置為0)

基於這個分析,我們得出封裝的類需要設計成這樣:

/***
 * 放大縮小數字變化類
 * 根據放大縮小初始值和步長等遞增
 * 提供到達某一數值時的回調
 * @params {Number} from 數字初始值
 * @params {Number} to 數字最大值
 * @params {Number} step 數字步長
 * @params {Array} dingNums 達到dingNums的區間觸發回調 只再放大輪迴觸發
 */
export default class MoveCrycle {
  constructor({ from = 0, to = 100, step = 1, dingNums = [] }) {
    this.from = from;
    this.to = to;
    this.step = step;
    this.dingNums = dingNums;

    this.currValue = this.from;
    this._turnNum = 0; //放大縮小輪次 一次放大一次縮小為一輪
    this._event = {
      onchange: [], //每一次變化觸發的回調
      onlarge: [], //每次到最大值觸發的回調
      onsmall: [], //每次到最小值觸發的回調
      onding: [], //每次到dingNums區間值時觸發的回調
    };
    this._init();
  }
  _init() {
    //註冊放大事件
    //記錄輪次 每次放大+1
    this.on("onlarge", function turn() {
      this._turnNum++;
    });
  }
  //開始滾動數字
  jump() {
    if (this.step > 0) {
      //放大過程
      if (this.currValue <= this.to) {
        //遞增過程中
        this.currValue += this.step;
        //判斷是否在dingNums區間
        if (
          this.currValue >= this.dingNums[0] &&
          this.currValue <= this.dingNums[1]
        ) {
          this._emit("onding", this.currValue, this._turnNum);
        }
      } else {
        this._emit("onsmall", this.currValue);
        this.currValue = this.to;
        this.step *= -1;
      }
    } else {
      //縮小過程
      if (this.currValue > this.from) {
        this.currValue += this.step;
      } else {
        //縮到初始值了 重新放大
        this.step *= -1;
        this.currValue = this.from;
        this._emit("onlarge", this.currValue);
      }
    }
    this._emit("onchange", this.currValue, this.step > 0, this._turnNum);
  }
  //觸發事件函數
  _emit(type, ...args) {
    //觸發每次變動函數
    this._event[type].forEach((f) => {
      try {
        f.apply({}, args);
      } catch (e) {
        console.error(e);
      }
    });
  }

  //註冊回調
  on(type, func) {
    if (
      this._event[type] &&
      !this._event[type].filter((f) => f === func).length
    ) {
      this._event[type].push(func);
    }
  }
  //註銷回調
  off(type, func) {
    if (this._event[type]) {
      this._event[type] = this._event[type].filter((f) => f !== func);
    }
  }
}

一看比較長,別急慢慢分析:

  1. constructor部分就是接收入參,沒有什麼好説的。需要注意的是this._init(),初始化時自己也註冊一個onlarge事件,來記錄每次輪迴的次數_turnNum
  2. jump是核心代碼,主要管理每一次被執行時的遞增或遞減狀態,同時通過this.emit()函數觸發響應的回調事件。放大過程中判斷是否到達dingNums的區間,如果在其中則不停觸發onding事件,當到達最大值時觸發onsmall事件代表即將變小,同時this.step *= -1步長取反方向值。到達最小值時觸發onlarge事件,代表即將增大,同時this.step *= -1步長取反方向值。jump函數並沒有這個類中進行遞歸調用,是因為把這個調用權交給使用者會更加靈活,後面會介紹如何調用。
  3. _emit函數是觸發事件的控制中心,根據傳入的type類型找到事件對象中對應的函數數組,遍歷執行。用try{} catch(e){}是為了避免在某個回調函數中出錯而影響了其它回調函數正常執行。
  4. onoff顧名思義,就是註冊和註銷回調的入口。on函數把傳入的事件類型和函數加入對應的數組中,如果重複添加同一個函數是無效的。off函數接收事件類型和函數,然後數組中過濾掉。這裏留給你想像一下,如果用匿名函數註冊的事件能取消掉嗎?

好了,MoveCrycle類封裝好了,回到我們的主題,看下如何在這個箭頭動畫中使用。

HTML部分

<body>
  <div class="app">
    <div class="content">
      <!--canvas替換箭頭部分-->
      <canvas
        id="canvas"
        class="canvas"
        width="140px"
        height="140px"
      ></canvas>
    </div>
  </div>
</body>

script部分

引入MoveCrycle類,初始化

import MoveCrycle from "./js/MoveCrycle.js";

let cavas = document.getElementById("canvas");
var ctx = cavas.getContext("2d");
//定義的一些常量和變量下面會用到
const R_SMALL = 45;//內圈初始大小
...省略部分代碼

//看着裏!!!!初始化封裝的類
let move = new MoveCrycle({
  from: R_SMALL, //初始值
  to: R_SMALL * 1.35, //最大1.35倍
  step: 0.3, //步長
  dingNums: [R_SMALL * 1.1, R_SMALL * 1.35], //外圈開始出現的區間內圈的1.1倍到1.35倍
});
...

註冊onchange事件,監聽每一次變化,然後渲染畫布

//註冊數值變化事件,每次變化獲取值渲染
move.on("onchange", (value, step) => {
  //繪製內圓
  clear(); //清空畫布
  //繪製內圈
  drawInnerCircle(value);
  //繪製外圈
  drawOutCircle();
  //文字放在最後,不然會被上面的圓屬性fillStyle覆蓋
  drawText(value);
});

註冊onding事件,控制外圈的變大,透明度減小

//再設定的區間觸發事件,控制外圈的變大,透明度減小
move.on("onding", (value) => {
  r_out += R_OUT_STEP; //外圈變大
  opa -= OPA_STEP; //外圈變大的同時透明度減小
});

註冊onlarge事件,控制外圈

//動畫圈開始變大控制外圈
move.on("onlarge", (value) => {
  opa = 1; //開始變大,透明度為1
  r_out = 45; //外部開始變大時半徑回覆到初始值
});

註冊onsmall事件,控制外圈透明度為0

move.on("onsmall", (value) => {
  //控制外圈光環-隱藏
  opa = 0;
});

繪製內圈、外圈、文字函數

//繪製內圓
function drawInnerCircle(val) {
  ctx.beginPath();
  ctx.arc(SIZE / 2, SIZE / 2, val, 0, 2 * Math.PI);
  ctx.fillStyle = `rgba(${COLOR}, 1)`;
  ctx.fill();
}
//繪製外圈圓 變大的同時透明度下降
function drawOutCircle() {
  ctx.beginPath();
  ctx.arc(SIZE / 2, SIZE / 2, r_out, 0, 2 * Math.PI);
  ctx.fillStyle = `rgba(${COLOR}, ${opa})`;
  ctx.fill();
}
//放大字體 大小隨內圈一起變化
function drawText(val) {
  //font放大倍數和內圈一樣
  ctx.font = (val / R_SMALL) * 60 + "px Arial";
  ctx.textBaseline = "center"; //設置字體底線居中
  ctx.textAlign = "center"; //設置字體對齊方式居中
  ctx.fillStyle = `rgba(255,255,255,1)`;
  ctx.fillText("✔", 70, 90);
}

清除畫布

//清除畫布
function clear() {
  ctx.clearRect(0, 0, SIZE, SIZE);
}

循環遞歸函數

//循環遞歸函數
(function start() {
  move.jump(); //開始執行
  requestAnimationFrame(() => {
    start();
  });
})

script部分完整代碼:

<script type="module">
  import MoveCrycle from "./js/MoveCrycle.js";

  let cavas = document.getElementById("canvas");
  var ctx = cavas.getContext("2d");

  const COLOR = "75, 167, 118";
  const R_SMALL = 45;//內圈初始大小
  const SIZE = 140; //畫布寬高
  const R_OUT_STEP = 0.3 * 2.5; //外圈變化係數
  const OPA_STEP = 0.028;

  let r_out = R_SMALL; //外圈半徑累加值
  let opa = 1; //外圈透明度累加值

  let move = new MoveCrycle({
    from: R_SMALL, //初始值
    to: R_SMALL * 1.35, //最大1.35倍
    step: 0.3, //步長
    dingNums: [R_SMALL * 1.1, R_SMALL * 1.35], //外圈開始出現的區間內圈的1.1倍到1.35倍
  });
  //註冊數值變化事件,每次變化獲取值渲染
  move.on("onchange", (value, step) => {
    //繪製內圓
    clear(); //清空畫布
    //繪製內圈
    drawInnerCircle(value);
    //繪製外圈
    drawOutCircle();
    //文字放在最後,不然會被上面的圓屬性fillStyle覆蓋
    drawText(value);
  });
  //再設定的區間觸發事件
  move.on("onding", (value) => {
    r_out += R_OUT_STEP; //外圈變大
    opa -= OPA_STEP; //外圈變大的同時透明度減小
  });
  //動畫圈開始變大
  move.on("onlarge", (value) => {
    opa = 1; //開始變大,透明度為1
    r_out = 45; //開始變大時半徑回覆到初始值
  });
  //動畫圈開始變小
  move.on("onsmall", (value) => {
    //控制外圈光環-隱藏
    opa = 0;
  });

  //繪製內圓
  function drawInnerCircle(val) {
    ctx.beginPath();
    ctx.arc(SIZE / 2, SIZE / 2, val, 0, 2 * Math.PI);
    ctx.fillStyle = `rgba(${COLOR}, 1)`;
    ctx.fill();
  }
  //繪製外圈圓 變大的同時透明度下降
  function drawOutCircle() {
    ctx.beginPath();
    ctx.arc(SIZE / 2, SIZE / 2, r_out, 0, 2 * Math.PI);
    ctx.fillStyle = `rgba(${COLOR}, ${opa})`;
    ctx.fill();
  }
  //放大字體 大小隨內圈一起變化
  function drawText(val) {
    //font放大倍數和內圈一樣
    ctx.font = (val / R_SMALL) * 60 + "px Arial";
    ctx.textBaseline = "center"; //設置字體底線居中
    ctx.textAlign = "center"; //設置字體對齊方式居中
    ctx.fillStyle = `rgba(255,255,255,1)`;
    ctx.fillText("✔", 70, 90);
  }
  //循環遞歸函數
  (function start() {
    move.jump(); //開始執行
    requestAnimationFrame(() => {
      start();
    });
  })

  //清除畫布
  function clear() {
    ctx.clearRect(0, 0, SIZE, SIZE);
  }
  //開始函數
  start();
</script>

以上。
完整代碼可以到我的github倉庫獲取!
各位大佬,不要忘了給我點贊+評論+收藏

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.