哈? 标题不知道啥意思?
老规矩, 直接看图!
效果如下:
高清大图!
码农多年, 老眼昏花, 动图看不清?! 那就看静态截图!!!
不同分值效果如下:
看完了卖家秀, 我们来看产品的制作过程吧!
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 圆形的绘制 http://www.w3school.com.cn/tags/canvas_arc.asp 时, 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 源码:
- (注, 源码稍作整理, 单独提取. 为了完整性也为了保护其他业务代码, 部分变量名做了修改, 可能会和之前截图中略微不同)
- <template lang='pug'>
- .indexs#Indexs.App-bg
- transition(name="fade")
- swiper#swiperBox(:options="swiperOption" ref="mySwiper")
- swiper-slide.swiper-slide1
- .container
- .up
- swiper-slide.swiper-slide2(v-if="isShow")
- .my-shark
- .up
- swiper-slide.swiper-slide3
- .container
- .data-cont
- .data.data01
- .data01-charts
- .row(v-for='item,index in Data' :key="index" :class='aniShow ?"ani":""')
- .data-txt {{item.grade> 0 ? item.grade : '无数据'}}
- .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'")
- span.pg-data
- .week {{item.week}}
- .data.data02
- .data02-charts
- .canvas-box
- //- baseCanvas
- canvas#baseCanvas.my-canvas(ref="baseCanvas" width="174" height="174")
- //- canvas
- canvas#myCanvas.my-canvas.clr-canvas(ref="myCanvas" width="174" height="174")
- .canvas-data #[span.num {{grade}}]分
- </template>
- <script>
- var vm = null,
- timer1 = null,
- /* canvas 基础值 */
- c = null, //document.getElementById("myCanvas");
- ctx = null, //canvas-2d 画布
- x = 161 / 2 + 1, // 圆心坐标
- r = (161 - 10) / 2; // 半径大小
- /* swiper 组件 */
- import { swiper, swiperSlide } from "vue-awesome-swiper";
- import { getData } from "../io/getData";
- export default {
- name: "Indexs",
- components: {
- swiper,
- swiperSlide
- },
- data() {
- return {
- grade: 0, // 圆环图分数
- gradeTarget: 78.54, // 实际得分数, 可 Ajax 请求数据后修改
- isShow: true,// 是否展示第二页 swiper
- aniShow: false,// 是否开启柱图动画
- Data:[{
- week: "第一周",
- grade: 0
- },
- {
- week: "第二周",
- grade: 30
- },
- {
- week: "第三周",
- grade: 99.99
- },
- {
- week: "第四周",
- grade: 76.98
- },
- {
- week: "第五周",
- grade: 100
- }],
- swiperOption: {
- //swiper 参数
- notNextTick: true,
- direction: "vertical",
- grabCursor: true,
- setWrapperSize: true,
- autoHeight: true,
- slidesPerView: 1,
- mousewheel: false,
- mousewheelControl: false,
- height: Windows.innerHeight, // 高度设置, 占满设备高度
- resistanceRatio: 0,
- observeParents: true,
- initialSlide: 2 - 1, // 设置初始化时, swiper 的默认展示页面, 从零开始
- on: {
- slideChange() {
- if (
- (this.activeIndex == 2 && vm.isShow) ||
- (this.activeIndex == 1 && !vm.isShow)
- ) {
- console.log(this.activeIndex, vm.isShow, "绘制动画");
- setTimeout(function() {
- // 配合展示柱状图动画
- vm.aniShow = true;
- // 定时器不断触发绘制彩色圆环, 实现圆环动画效果
- timer1 = setInterval(function() {
- // 中间分数文案更改
- var num = vm.grade;
- num++;
- if (num>= vm.gradeTarget) {
- vm.grade = vm.gradeTarget;
- clearInterval(timer1);
- } else {
- vm.grade = num;
- }
- vm.clearCanvas();
- vm.drawClrCanvas();
- }, 1000 / 60);
- }, 500);
- } else {
- // 翻页后, 初始化数据页面的状态, 清除定时器暂停动画, 并把彩色圆环清空
- console.log("其他页");
- clearInterval(timer1);
- vm.grade = 0;
- vm.aniShow = false;
- vm.clearCanvas();
- }
- }
- }
- }
- };
- },
- computed: {},
- mounted() {
- // 初始化数据, 绘制灰色圆环
- vm = this;
- c = this.$refs.myCanvas;
- ctx = c.getContext("2d");
- this.drawBaseCanvas();
- },
- methods: {
- drawBaseCanvas() {
- // canvas 绘制
- /* 基础值 */
- var c = this.$refs.baseCanvas, //document.getElementById("myCanvas");
- // debugger;
- ctx = c.getContext("2d"),
- o = x,
- randius = r;
- /* 默认灰色圆圈 */
- ctx.strokeStyle = "#eee";
- ctx.lineWidth = 10;
- ctx.beginPath();
- ctx.arc(o, o, randius, 0, 2 * Math.PI);
- ctx.stroke();
- },
- clearCanvas() {
- // 清除画布
- ctx.clearRect(0, 0, 200, 200);
- },
- drawClrCanvas() {
- var gradient = ctx.createLinearGradient(75, 50, 5, 90);
- gradient.addColorStop("0", "#C88EFF");
- gradient.addColorStop("1.0", "#7E5CFF");
- ctx.strokeStyle = gradient; // 用渐变进行填充
- ctx.lineWidth = 10;
- ctx.lineCap = "round";
- ctx.shadowColor = "rgba(191,142,255, 0.36)";
- ctx.shadowBlur = 8;
- ctx.shadowOffsetY = 8;
- ctx.beginPath();
- var count = this.grade / (100 / 2) + 1;
- ctx.arc(x, x, r, Math.PI, Math.PI * count, false);
- ctx.stroke();
- }
- }
- };
- </script>
- <style lang='scss'>
- // 柱图
- .row {
- position: relative;
- z-index: 1;
- width: 0.61rem;
- margin-bottom: -0.28 - 0.08 - 0.38rem;
- text-align: center;
- }
- .data-txt {
- font-size: 0.2rem;
- line-height: 0.2rem;
- margin-bottom: 0.09rem;
- }
- .progress {
- height: 0rem;
- transition: height 0.5s ease-in-out;
- }
- .ani {
- @for $i from 1 to 6 {
- &:nth-of-type(#{$i}) {
- .progress {
- transition-delay: #{$i * 0.15}s;
- }
- }
- }
- // &:nth-of-type(1) {
- // .progress {
- // transition-delay: .4s;
- // }
- // }
- // &:nth-of-type(2) {
- // .progress {
- // transition-delay: .8s;
- // }
- // }
- // &:nth-of-type(3) {
- // .progress {
- // transition-delay: 1s;
- // }
- // }
- // &:nth-of-type(4) {
- // .progress {
- // transition-delay: 1.4s;
- // }
- // }
- // &:nth-of-type(5) {
- // .progress {
- // transition-delay: 1.8s;
- // }
- // }
- }
- .pg-data {
- display: block;
- width: 0.12rem;
- height: 100%;
- margin: 0 auto;
- background: linear-gradient(0deg, #c88eff 0%, #7e5cff 100%);
- box-shadow: 0 -0.04rem 0.14rem 0 rgba(129, 93, 255, 0.4);
- border-radius: 0.05rem 0.05rem 0 0;
- }
- // 0 分展示规则
- .nodata {
- .pg-data {
- border-radius: 0;
- background: #e7e7e7;
- box-shadow: none;
- }
- }
- .week {
- font-size: 0.2rem;
- line-height: 0.2rem;
- margin-top: 0.08rem;
- color: #666;
- }
- // 环图 - data02 数据部分
- .data02-charts {
- margin-top: 0.32rem;
- height: 1.61rem;
- }
- .canvas-box {
- position: relative;
- float: left;
- width: 1.61rem;
- height: 1.61rem;
- margin-left: 0.92rem;
- }
- .my-canvas {
- width: 1.61rem;
- height: 1.61rem;
- }
- .clr-canvas {
- position: absolute;
- top: 0;
- left: 0;
- }
- .canvas-data {
- position: absolute;
- top: 0.56rem;
- left: 0;
- right: 0;
- margin: auto;
- margin-left: -0.1rem;
- text-align: center;
- font-size: 0.24rem;
- .num {
- font-size: 0.32rem;
- font-weight: 600;
- }
- }
- </style>
来源: https://www.cnblogs.com/padding1015/p/11138606.html