1. 程式人生 > >canvas實現有遞增動畫的環形進度條

canvas實現有遞增動畫的環形進度條

 

哈?標題不知道啥意思?

老規矩,直接看圖!

效果如下:

 

高清大圖!

 

碼農多年,老眼昏花,動圖看不清?!那就看靜態截圖!!!

不同分值效果如下:

        

 

看完了賣家秀,我們來看產品的製作過程吧!

canvas繪製圓環

1、vue中,<template lang="pug">裡的程式碼如下:

canvas#baseCanvas是底部的灰色圓環

canvas#myCanvas是上邊的彩色圓環

需要用css樣式幫助我們把彩色圓環蓋到灰色圓環上邊。

2、css樣式:

 

3、js-canvas的樣式繪製程式碼

這段程式碼也很簡單,看canvas的api即可

3-1、vue元件中,script標籤頂部定義需要用的變數

3-2、vue的methos物件中,定義方法三個:

drawBaseCanvas:用來繪製底部灰色圓環。由於灰色圓環沒有動畫效果,所以一開始就繪製一個完整的灰色圓環即可。 drawClrCanvas:用來繪製上邊的彩色圓環。 clearCanvas:用來清空畫布。這是彩色圓環動畫需要。 因為我們圓環動畫效果的核心就是,每隔一段時間就把彩色圓環清空一下,然後把結束角度值增大、重畫,這樣連續起來就是動畫。

以下是三個方法的程式碼:

上邊三個方法裡邊的程式碼,幾乎都是對canvas API的應用,看教程即可。

只有draoClrCanvas方法中,canvas圓形的繪製時,arc的引數裡關於開始值、結束值的設定。

開始值決定了圓環的起始繪製位置,結束值決定了結束的位置(我好像說了一句廢話,但是冥思苦想後的思想描述文字,不想刪掉哈哈哈)

這個結束值的計算,對於我來說還是比較麻煩的。

 

count變數為什麼要這麼計算,我也忘了我是怎麼鼓搗出來的了。

 

this.grade是100以內的正整數,表示分值。被定義在data中,預設是0分。

所以一開始彩色圓環就看不見,因為起始點和結束點都是0點。

如果更改grade的值,從0-100,canvas彩色圓環的值也就會更改。

 

這樣,只要我們逐漸修改grade的值,重新繪製,彩色圓環就會逐漸遞增,實現動畫效果。 

 

圓環動畫效果

由於我這裡需求特殊,需要使用者每次翻到canvas所在swiper時,才會觸發動畫(後來更麻煩一點需要柱狀圖和canvas部分有個入場效果後,動畫才開始。效果就是上圖中最長的那張gif動畫那樣)。

所以我得藉助swiper才能實現。在swiper切換的回撥函式中,從0開始不停遞增grade分數,並重新觸發彩色圓環的繪製,進而實現動畫效果。

vue中我用的swiper是'vue-awesome-swiper'。她的用法我在其他文章中寫過步驟。

swiper在vue-data中的配置裡,有一個on物件。在on物件中的slideChange函式,就是每次翻頁swiper時會觸發的回撥函式。

 

這裡我說一下幾個比較特殊的點:
(1)vm:是我早就在vue的script中儲存的變數,初始化為null,然後在mounted中,將其賦值為vue例項物件。

初始化資料、繪製灰色圓環

通過這種方法,我在vue例項物件 - data - swiper - 回撥函式中去拿vue例項物件 - data中的grade和gradeTarget屬性值,並對其進行修改。

ps:我也不知道這麼做是不是很傻的一種做法,當時做到這裡時是我遇到的一個難題,不知道怎麼在swiper的on回撥中獲取vue例項。於是就有了這麼曲線救國的方法。如果看官有更好的解決方案,希望可以給我提供一個新的思路,感激不盡哦親

 

(2) (this.activeIndex == 2 && vm.isStar) || (this.activeIndex == 1 && !vm.isStar)

這裡是因為業務,才這麼判斷,可以忽略。

this在swiperChange函式中指向swiper物件。this.activeIndex是swiper例項的屬性,用官方的話說“返回當前活動塊(啟用塊)的索引。”可以理解他指的是當前翻到的是哪一頁,就是當前你所看的swiper-slide的下標。

我因為使用者的身份,會判斷性的決定當前canvas所在swiper前一頁是否展示。 如果不展示就根本不會繪製前一頁,那麼相應的當前頁的swiper的下標就會變成(index-1)。

總而言之,當滿足條件、使用者翻到canvas所在swiper頁面後,我就要觸發if裡邊的圓環繪製邏輯。否則就走到else裡初始化資料頁面的狀態、清除定時器暫停動畫、並把彩色圓環清空

 

(3)vm.aniShow

在我上篇《純css繪製柱狀圖》裡邊說了,柱狀圖的動畫要跟canvas的動畫一起說。因為他們的動畫實現需要配合swiper的切換。說的就是這裡的程式碼:

vue - data - aniShow屬性變為true時,div.row就會新增ani這個class類名:

同樣,aniShow為true,progress的高度就會附上自己的目標值,也就是這個progress的實際高度經過百分制轉化後被賦予給了style屬性的height。(具體換算規則還是見上篇《純css繪製柱狀圖》)

此時,因為progress的transition監聽了height變化,就開始有了高度漸增的柱狀圖遞增動畫了。

 

而ani類名下,progress的transition-delay實現了其高度錯開遞增效果。

可能只看文字描述很晦澀,再看一眼效果:

 

(4)彩色圓環繪製程式碼部分

gradeTarget是實際分值,是最終要繪製到的結果。

grade從0開始,自增到gradeTarget的大小。

這裡我沒有直接++vm.grade,我也不知道自己當時咋想的。

if判斷,如果grade遞增到了目標值gradeTarget或者大於目標值,就停止遞增,並讓grade=gradeTarget。屬於臨界值的判斷。在運動功能中,又算碰撞檢測。

反之,不到目標的話,就清除上一次繪製的canvas畫布,在grade遞增變化後重新繪製新的彩色圓環。

 

(5)所有這些放到setTimeout中,暫停500毫秒再執行,是為了等柱圖和環圖入場後,在開始繪製圓環的遞增效果。

 

其實上邊程式碼都是很簡單的邏輯處理,看官們讀一遍程式碼應該就差不離了。

 

新想法:

這個效果是我很久以前做的,今天在整理製作方法的時候,我想到自己程式碼的一種優化方案:

其實沒必要在定時器裡重新呼叫彩色圓環繪製方法。我們直接改的是this.grade屬性,監聽這個屬性的改變就好了其實。這樣此屬性在定時器中被修改,圓環方法就會自動執行。

這還是一個想法,還需要我的實踐。

 

中間文字的遞增效果:

因為grade是每次遞增的分數,所以利用vue的雙向資料繫結,直接把grade當作分數值繫結到對應dom檢視處即可。

 

 

最後,圓環和上邊柱狀圖的動畫結合,就是animation控制一下動畫延遲即可。很簡單的。

 

index.vue原始碼:

(注,原始碼稍作整理,單獨提取。為了完整性也為了保護其他業務程式碼,部分變數名做了修改,可能會和之前截圖中略微不同)

  1 <template lang='pug'>
  2   .indexs#Indexs.app-bg
  3     transition(name="fade")
  4       swiper#swiperBox(:options="swiperOption" ref="mySwiper")
  5         swiper-slide.swiper-slide1
  6           .container
  7           .up
  8         swiper-slide.swiper-slide2(v-if="isShow")
  9           .my-shark
 10           .up
 11         swiper-slide.swiper-slide3
 12           .container
 13             .data-cont
 14               .data.data01
 15                 .data01-charts
 16                   .row(v-for='item,index in Data' :key="index" :class='aniShow ? "ani":""')
 17                     .data-txt {{item.grade > 0 ? item.grade : '無資料'}}
 18                     .progress(:class='item.grade == 0 ? "nodata" : ""' :style="'height: ' + (aniShow ? (item.grade >= 100 ? (100 * 1.5) / 100 : item.grade == 0 ? 0.04 : item.grade * 1.5 / 100) : 0) +'rem'")
 19                       span.pg-data
 20                     .week {{item.week}}
 21               .data.data02
 22                 .data02-charts
 23                   .canvas-box
 24                     //- baseCanvas
 25                     canvas#baseCanvas.my-canvas(ref="baseCanvas" width="174" height="174")
 26                     //- canvas
 27                     canvas#myCanvas.my-canvas.clr-canvas(ref="myCanvas" width="174" height="174")
 28                     .canvas-data #[span.num {{grade}}]分
 29                   
 30 </template>
 31 <script>
 32 var vm = null,
 33   timer1 = null,
 34   /* canvas基礎值 */
 35   c = null, //document.getElementById("myCanvas");
 36   ctx = null, //canvas-2d畫布
 37   x = 161 / 2 + 1, //圓心座標
 38   r = (161 - 10) / 2; //半徑大小
 39 
 40 /* swiper元件 */
 41 import { swiper, swiperSlide } from "vue-awesome-swiper";
 42 import { getData } from "../io/getData";
 43 
 44 export default {
 45   name: "Indexs",
 46   components: {
 47     swiper,
 48     swiperSlide
 49   },
 50   data() {
 51     return {
 52       grade: 0, //圓環圖分數
 53       gradeTarget: 78.54, //實際得分數,可ajax請求資料後修改
 54       isShow: true,//是否展示第二頁swiper
 55       aniShow: false,//是否開啟柱圖動畫
 56       Data:[{
 57           week: "第一週",
 58           grade: 0
 59         },
 60         {
 61           week: "第二週",
 62           grade: 30
 63         },
 64         {
 65           week: "第三週",
 66           grade: 99.99
 67         },
 68         {
 69           week: "第四周",
 70           grade: 76.98
 71         },
 72         {
 73           week: "第五週",
 74           grade: 100
 75         }],
 76       
 77       swiperOption: {
 78         //swiper引數
 79         notNextTick: true,
 80         direction: "vertical",
 81         grabCursor: true,
 82         setWrapperSize: true,
 83         autoHeight: true,
 84         slidesPerView: 1,
 85         mousewheel: false,
 86         mousewheelControl: false,
 87         height: window.innerHeight, // 高度設定,佔滿裝置高度
 88         resistanceRatio: 0,
 89         observeParents: true,
 90         initialSlide: 2 - 1, //設定初始化時,swiper的預設展示頁面,從零開始
 91         on: {
 92           slideChange() {
 93             if (
 94               (this.activeIndex == 2 && vm.isShow) ||
 95               (this.activeIndex == 1 && !vm.isShow)
 96             ) {
 97               console.log(this.activeIndex, vm.isShow, "繪製動畫");
 98               setTimeout(function() {
 99                 // 配合展示柱狀圖動畫
100                 vm.aniShow = true;
101                 // 定時器不斷觸發繪製彩色圓環,實現圓環動畫效果
102                 timer1 = setInterval(function() {
103                   // 中間分數文案更改
104                   var num = vm.grade;
105                   num++;
106                   if (num >= vm.gradeTarget) {
107                     vm.grade = vm.gradeTarget;
108                     clearInterval(timer1);
109                   } else {
110                     vm.grade = num;
111                   }
112                   vm.clearCanvas();
113                   vm.drawClrCanvas();
114                 }, 1000 / 60);
115               }, 500);
116             } else {
117               // 翻頁後,初始化資料頁面的狀態、清除定時器暫停動畫、並把彩色圓環清空
118               console.log("其他頁");
119               clearInterval(timer1);
120               vm.grade = 0;
121               vm.aniShow = false;
122               vm.clearCanvas();
123             }
124           }
125         }
126       }
127     };
128   },
129   computed: {},
130   mounted() {
131     // 初始化資料、繪製灰色圓環
132     vm = this;
133     c = this.$refs.myCanvas;
134     ctx = c.getContext("2d");
135     this.drawBaseCanvas();
136   },
137   methods: {
138     drawBaseCanvas() {
139       // canvas繪製
140       /* 基礎值 */
141       var c = this.$refs.baseCanvas, //document.getElementById("myCanvas");
142         // debugger;
143         ctx = c.getContext("2d"),
144         o = x,
145         randius = r;
146       /* 預設灰色圓圈 */
147       ctx.strokeStyle = "#eee";
148       ctx.lineWidth = 10;
149       ctx.beginPath();
150       ctx.arc(o, o, randius, 0, 2 * Math.PI);
151       ctx.stroke();
152     },
153     clearCanvas() {
154       // 清除畫布
155       ctx.clearRect(0, 0, 200, 200);
156     },
157     drawClrCanvas() {
158       var gradient = ctx.createLinearGradient(75, 50, 5, 90);
159       gradient.addColorStop("0", "#C88EFF");
160       gradient.addColorStop("1.0", "#7E5CFF");
161       ctx.strokeStyle = gradient; // 用漸變進行填充
162       ctx.lineWidth = 10;
163       ctx.lineCap = "round";
164       ctx.shadowColor = "rgba(191,142,255, 0.36)";
165       ctx.shadowBlur = 8;
166       ctx.shadowOffsetY = 8;
167       ctx.beginPath();
168       var count = this.grade / (100 / 2) + 1;
169       ctx.arc(x, x, r, Math.PI, Math.PI * count, false);
170       ctx.stroke();
171     }
172   }
173 };
174 </script>
175 <style lang='scss'>
176 // 柱圖
177 .row {
178   position: relative;
179   z-index: 1;
180   width: 0.61rem;
181   margin-bottom: -0.28 - 0.08 - 0.38rem;
182   text-align: center;
183 }
184 
185 .data-txt {
186   font-size: 0.2rem;
187   line-height: 0.2rem;
188   margin-bottom: 0.09rem;
189 }
190 
191 .progress {
192   height: 0rem;
193   transition: height 0.5s ease-in-out;
194 }
195 
196 .ani {
197   @for $i from 1 to 6 {
198     &:nth-of-type(#{$i}) {
199       .progress {
200         transition-delay: #{$i * 0.15}s;
201       }
202     }
203   }
204   // &:nth-of-type(1) {
205   //   .progress {
206   //     transition-delay: .4s;
207   //   }
208   // }
209 
210   // &:nth-of-type(2) {
211   //   .progress {
212   //     transition-delay: .8s;
213   //   }
214   // }
215 
216   // &:nth-of-type(3) {
217   //   .progress {
218   //     transition-delay: 1s;
219   //   }
220   // }
221 
222   // &:nth-of-type(4) {
223   //   .progress {
224   //     transition-delay: 1.4s;
225   //   }
226   // }
227 
228   // &:nth-of-type(5) {
229   //   .progress {
230   //     transition-delay: 1.8s;
231   //   }
232   // }
233 }
234 
235 .pg-data {
236   display: block;
237   width: 0.12rem;
238   height: 100%;
239   margin: 0 auto;
240   background: linear-gradient(0deg, #c88eff 0%, #7e5cff 100%);
241   box-shadow: 0 -0.04rem 0.14rem 0 rgba(129, 93, 255, 0.4);
242   border-radius: 0.05rem 0.05rem 0 0;
243 }
244 
245 // 0分展示規則
246 .nodata {
247   .pg-data {
248     border-radius: 0;
249     background: #e7e7e7;
250     box-shadow: none;
251   }
252 }
253 
254 .week {
255   font-size: 0.2rem;
256   line-height: 0.2rem;
257   margin-top: 0.08rem;
258   color: #666;
259 }
260 // 環圖 - data02資料部分
261 .data02-charts {
262   margin-top: 0.32rem;
263   height: 1.61rem;
264 }
265 
266 .canvas-box {
267   position: relative;
268   float: left;
269   width: 1.61rem;
270   height: 1.61rem;
271   margin-left: 0.92rem;
272 }
273 
274 .my-canvas {
275   width: 1.61rem;
276   height: 1.61rem;
277 }
278 .clr-canvas {
279   position: absolute;
280   top: 0;
281   left: 0;
282 }
283 
284 .canvas-data {
285   position: absolute;
286   top: 0.56rem;
287   left: 0;
288   right: 0;
289   margin: auto;
290   margin-left: -0.1rem;
291   text-align: center;
292   font-size: 0.24rem;
293 
294   .num {
295     font-size: 0.32rem;
296     font-weight: 600;
297   }
298 }
299 </style>