0%

基础用法

示例效果

1、先画个中国地图

html部分

1
2
<div ref="myEchart"
:style="{height:'100%%',width:'100%'}"/>

引入中国地图

1
2
import echarts from "echarts";
import "echarts/map/js/china";

js部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let myChart = echarts.init(this.$refs.myEchart); // 这里是为了获得容器所在位置
window.onresize = myChart.resize();
myChart.setOption({
backgroundColor: "transparent", // 画布背景颜色
geo: {
show: true,
map: "china",
zoom: 1.1,
aspectScale: 0.8, // 长宽比
top: "5%",
layoutSize: "100%"
roam: false
},
series: [
{
type: "map",
map: "china",
geoIndex: 0,
zoom: 1.1,
top: "5%",
layoutSize: "100%",
aspectScale: 0.8 // 长宽比
}
]
});

效果如下,灰白白一片,啥也没有

添加边框

echarts的geo配置里面有个边框属性,但是如果盲目的给加上这个边框,会发现每个省市都有,边框还好说,但是设计图里面有个阴影,加上去那真是惨不忍睹。所以咱换个思路讲,在geo里面添加边框和阴影,在series里面再给每个item设置一个背景色,遮盖住geo里面的阴影和边框,效果就会变得好看起来。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
myChart.setOption({
backgroundColor: "transparent", // 画布背景颜色
tooltip: { // 鼠标移到图里面的浮动提示框
show: false
},
geo: {
...
roam: false,
itemStyle: {
normal: {
areaColor: "#00A0E6",
borderWidth: 0, // 设置外层边框
borderColor: "#05D8ED",
shadowColor: "#1442A4",
shadowOffsetX: 0,
shadowOffsetY: 18
}
}
},
series: [
{
geoIndex: 1, // 关键代码就是将index设为1 这样就能在geo的上面去覆盖边框和阴影
...
label: { // 这一段代码是为了显示各省份的名称
normal: {
show: true,
formatter: "{b}",
color: "#ffffff",
fontSize: 14,
position: "inside"
},
emphasis: {
show: false,
textStyle: {
color: "#fff"
}
}
},
roam: false,
itemStyle: {
normal: {
areaColor: "#00BAE6",
borderColor: "#01E6F6",
borderWidth: 2
},
emphasis: {
areaColor: "#009EE6"
}
}
}
]
});

尾巴彩蛋

设计图需求还有一个是要显示每个省市对应的人数,如果用tooltip的话不能多个一起显示,于是利用了scatter给每个item设置一个框显示数据,框的样式可以叫UI切个背景图,效果如下图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
myChart.setOption({
backgroundColor: "transparent", // 画布背景颜色
tooltip: { // 鼠标移到图里面的浮动提示框
show: false
},
geo: {
...
},
series: [
{
...
{ // 悬浮窗
name: "悬浮窗",
type: "scatter",
coordinateSystem: "geo",
symbol: icon,
symbolSize: [1.92 * this.fontSize, 2.43 * this.fontSize], // [25, 30]
symbolOffset: [0, -2.14 * this.fontSize],
showEffectOn: "render",
rippleEffect: { // 涟漪特效
brushType: "stroke" // 波纹绘制方式 stroke, fill
},
data: this.convertDataDian(),
hoverAnimation: true,
zlevel: 1
},
// 框
{
name: "悬浮窗",
type: "scatter",
coordinateSystem: "geo",
symbol: icon1,
symbolSize: [10.6 * this.fontSize, 5 * this.fontSize], // [25, 30]
symbolOffset: [0, -5.71 * this.fontSize],
label: {
normal: {
show: true,
textStyle: {
color: "#fff",
fontSize: this.fontSize * 0.9
},
formatter: (params) =>
{
return `{a|${params.name}}\n{b|${params.data.value[2]}}`;
},
rich: {
a: {
color: "#01D8FF",
fontWeight: "bold",
fontSize: this.fontSize,
align: "center"
},
b: {
color: "#ffffff",
fontSize: this.fontSize * 1.2,
fontWeight: "bold",
padding: 10,
align: "center"
}
}
}
},
showEffectOn: "render",
rippleEffect: { // 涟漪特效
brushType: "stroke" // 波纹绘制方式 stroke, fill
},
data: this.convertDataDian(),
hoverAnimation: true,
zlevel: 1
}
}
]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
convertDataDian()
{
let geoCoordMap = this.geoCoordMap[0];
let res = [];
for (let i = 0; i < this.dataList.length; i++)
{
let nativePlace = this.dataList[i].nativePlace ? this.dataList[i].nativePlace.replace("省", "") : "";
let geoCoord = geoCoordMap[nativePlace];
if (geoCoord)
{
res.push({
name: this.dataList[i].nativePlace,
value: geoCoord.concat(this.dataList[i].peopleCounting)
});
}
}
return res;
}

最后附上完整代码

运行可直接使用

echarts:4.2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<template>
<div id="chart-map"
ref="myEchart"
:style="{height:'88vh',width:'100%'}"
class="map-contanier"/>
</template>

<script>
import echarts from "echarts";
import "echarts/map/js/china";
require("echarts/theme/macarons"); // echarts theme
import { debounce } from "@/utils/common/utils";
export default {
data()
{
return {
chart: null,
geoCoordMap: [
{
"甘肃": [103.73, 36.03],
"青海": [101.74, 36.56],
"四川": [104.06, 30.67],
"河北": [114.48, 38.03],
"云南": [102.73, 25.04],
"贵州": [106.71, 26.57],
"湖北": [114.31, 30.52],
"河南": [113.65, 34.76],
"山东": [117, 36.65],
"江苏": [118.78, 32.04],
"安徽": [117.27, 31.86],
"浙江": [120.19, 30.26],
"江西": [115.89, 28.68],
"福建": [119.3, 26.08],
"广东": [113.23, 23.16],
"湖南": [113, 28.21],
"海南": [110.35, 20.02],
"辽宁": [123.38, 41.8],
"吉林": [125.35, 43.88],
"黑龙江": [126.63, 45.75],
"山西": [112.53, 37.87],
"陕西": [108.95, 34.27],
"台湾": [121.30, 25.03],
"北京": [116.46, 39.92],
"上海": [121.48, 31.22],
"重庆": [106.54, 29.59],
"天津": [117.2, 39.13],
"内蒙古": [111.65, 40.82],
"广西": [108.33, 22.84],
"西藏": [91.11, 29.97],
"宁夏": [106.27, 38.47],
"新疆": [87.68, 43.77],
"香港": [114.17, 22.28],
"澳门": [113.54, 22.19]
}
],
dataList: [
{ nativePlace: "黑龙江", peopleCounting: 2635 },
{ nativePlace: "北京", peopleCounting: 2635 },
{ nativePlace: "新疆", peopleCounting: 2635 },
{ nativePlace: "青海", peopleCounting: 2635 },
{ nativePlace: "湖北", peopleCounting: 2653 },
{ nativePlace: "贵州", peopleCounting: 2635 }
]
};
},
mounted()
{
this.initChart();
this.__resizeHandler = debounce(() =>
{
if (this.chart)
{
this.chart.resize();
}
}, 100);
window.addEventListener("chart-map", this.__resizeHandler);
},
beforeDestroy()
{
if (!this.chart)
{
return;
}
this.chart.dispose();
this.chart = null;
},
methods: {
initChart()
{
let icon = "image://" + require("@/assets/images/reform/ready.png"); // location图标
let icon1 = "image://" + require("@/assets/images/reform/hint.png"); // 弹窗的背景图片
let myChart = echarts.init(this.$refs.myEchart); // 这里是为了获得容器所在位置
window.onresize = myChart.resize();
myChart.setOption({
backgroundColor: "transparent", // 画布背景颜色
tooltip: { // 鼠标移到图里面的浮动提示框
show: false
},
geo: {
show: true,
map: "china",
zoom: 1.1,
aspectScale: 0.8, // 长宽比
top: "5%",
layoutSize: "100%",
label: {
normal: {
show: false
},
emphasis: {
show: false
}
},
roam: false,
itemStyle: {
normal: {
areaColor: "#00A0E6",
borderWidth: 0, // 设置外层边框
borderColor: "#05D8ED",
shadowColor: "#1442A4",
shadowOffsetX: 0,
shadowOffsetY: 18
}
}
},
series: [
{
type: "map",
map: "china",
geoIndex: 1,
zoom: 1.1,
top: "5%",
layoutSize: "100%",
aspectScale: 0.8, // 长宽比
showLegendSymbol: false, // 存在legend时显示
label: {
normal: {
show: true,
formatter: "{b}",
color: "#ffffff",
fontSize: 14,
position: "inside"
},
emphasis: {
show: false,
textStyle: {
color: "#fff"
}
}
},
selectedMode: "multiple",
colorBy: "series",
roam: false,
itemStyle: {
normal: {
areaColor: "#00BAE6",
borderColor: "#01E6F6",
borderWidth: 2
},
emphasis: {
areaColor: "#009EE6"
}
}
},
// 地图中闪烁的点
{
name: "闪点",
type: "effectScatter",
coordinateSystem: "geo",
symbolOffset: [0, -30],
data: this.convertDataDian(),
showEffectOn: "render",
encode: {
value: 5
},
rippleEffect: { // 涟漪特效
brushType: "stroke", // 波纹绘制方式 stroke, fill
period: 5, // 动画时间,值越小速度越快
scale: 5 // 波纹圆环最大限制,值越大波纹越大
},
hoverAnimation: true,
itemStyle: {
color: "#0154BE",
shadowBlur: 10,
shadowColor: "#0154BE"
},
zlevel: 1
},
{ // 悬浮窗
name: "悬浮窗",
type: "scatter",
coordinateSystem: "geo",
symbol: icon,
symbolSize: [27, 34],
symbolOffset: [0, -30],
showEffectOn: "render",
rippleEffect: { // 涟漪特效
brushType: "stroke" // 波纹绘制方式 stroke, fill
},
data: this.convertDataDian(),
hoverAnimation: true,
zlevel: 1
},
// 框
{
name: "悬浮窗",
type: "scatter",
coordinateSystem: "geo",
symbol: icon1,
symbolSize: [149, 70],
symbolOffset: [0, -80],
label: {
normal: {
show: true,
textStyle: {
color: "#fff",
fontSize: this.fontSize * 0.9
},
formatter: (params) =>
{
return `{a|${params.name}}\n{b|${params.data.value[2]}}`;
},
rich: {
a: {
color: "#01D8FF",
fontWeight: "bold",
fontSize: this.fontSize,
align: "center"
},
b: {
color: "#ffffff",
fontSize: this.fontSize * 1.2,
fontWeight: "bold",
padding: 10,
align: "center"
}
}
}
},
showEffectOn: "render",
rippleEffect: { // 涟漪特效
brushType: "stroke" // 波纹绘制方式 stroke, fill
},
data: this.convertDataDian(),
hoverAnimation: true,
zlevel: 1
}
]
});
},
convertDataDian()
{
let geoCoordMap = this.geoCoordMap[0];
let res = [];
for (let i = 0; i < this.dataList.length; i++)
{
let geoCoord = geoCoordMap[this.dataList[i].nativePlace];
if (geoCoord)
{
res.push({
name: this.dataList[i].nativePlace,
value: geoCoord.concat(this.dataList[i].peopleCounting)
});
}
}
return res;
}
}
};
</script>

应用场景

在web页面上调起手机摄像头,并增加人脸框进行拍照,然后把图片上传到服务器。

实现原理

1、基本原理

使用getUserMedia()方法获取视频流,通过video标签在页面上播放,通过canvas画布截取图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!--video用于显示媒体设备的视频流,自动播放-->
<div class="camera-wrapper">
<!-- 人脸框照片 -->
<image class="face-icon" src="@/static/images/home/collecte/face.png"</image>
<video class="video"
id="video"
autoplay
height="100%"
width="100%"
:controls="false"
:show-play-btn="false"
:show-center-play-btn="false"
:show-mute-btn="false"
:muted="true"></video>
</div>
<!--拍照按钮-->
<div class="bottom-btn">
<button class="btn-cell bottom-btn-left" @click="stopMedia">取消</button>
<button class="btn-cell bottom-btn-middle" @click="captureImg">
<image class="capture-icon" src="@/static/images/home/collecte/capture.png"></image>
</button>
<button class="btn-cell bottom-btn-right" @click="clipMedia">
<image src="@/static/images/home/collecte/flip.png" class="flip-icon"></image>
</button>
<!--拍照图片-->
<canvas id="canvas"></canvas>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// getUserMedia()获取视频流
getUserMedia(){
const constraints = window.constraints = {
// 关闭视频声音
audio: false,
// 摄像头参数 user 前置摄像头 environment 后置摄像头
// 默认前置 通过变量isFront控制展示前置还是后置
video: {facingMode: this.isFront ? "user" : "environment"}
};
if(navigator.mediaDevices.getUserMedia){
//最新标准API navigator.mediaDevices.getUserMedia(constraints).then(this.success).catch(this.error);
} else if (navigator.webkitGetUserMedia){
//webkit内核浏览器
navigator.webkitGetUserMedia(constraints,this.success, this.error)
} else if (navigator.mozGetUserMedia){
//Firefox浏览器
navigator.mozGetUserMedia(constraints, this.success, this.error);
} else if (navigator.getUserMedia){
//旧版API
navigator.getUserMedia(constraints, this.success, this.error);
}
},
init()
{
if ((navigator.mediaDevices && navigator.mediaDevices.getUserMedia) || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia){
this.getUserMedia();
} else {
console.log("你的浏览器不支持访问用户媒体设备");
}
},
error(error){
uni.showToast({
title: error.message,
icon: 'none'
});
console.log(error.name+error.message);
},
success(stream){
const video = document.getelementIdBy('video');
const videoTracks = stream.getVideoTracks();
console.log('Got stream with constraints:', constraints);
this.mediaStreamTrack = stream;
video.srcObject = stream;
video.play();
},
//关闭视频流
stopMedia()
{
this.mediaStreamTrack.getTracks().forEach( (track) =>
{
track.stop();
});
},
// 拍照片
captureImg()
{
var videoHtml = document.getElementById("video");
var canvasHtml = document.getelementById("canvas");
var context = canvasHtml.getContext("2d");
context.drawImage(videoHtml, 0, 0, 100, 100);
},
// 翻转摄像头
clipMedia()
{
this.isFront = !this.isFront;
this.getUserMedia();
},
2、坑1:getContext is not undefined

uniapp里面获取的canvas画布没法转getContext,官方给的是用uni.createCanvasContext获取,但是用这个截取不了视频流,就是没法获取照片。

解决办法:利用原生的方法创建一个canvas画布,利用这个canvas画布截取视频流,再转成图片路径展示。拍照函数做下改动。

1
2
3
4
5
var videoHtml = document.getElementById("video");
var canvasHtml = document.createElement("canvas");
var context = canvasHtml.getContext("2d");
context.drawImage(videoHtml, 0, 0, 100, 100);
let img = canvasHtml.toDataURL("image/png", 1); // 获取的图片路径,可直接在页面显示
3、坑2:截取视频流时报错,数据不是视频流

与上一个坑一个意思,利用id去获取的vedio标签其实是uniapp包装以后的div盒子,所以直接获取是不行的,还是要用原生的方法获取vedio标签。

解决办法:改动代码如下。

1
2
3
4
5
var videoHtml = document.getElementById("video").querySelector("video");
var canvasHtml = document.createElement("canvas");
var context = canvasHtml.getContext("2d");
context.drawImage(videoHtml, 0, 0, width, height);
let img = canvasHtml.toDataURL("image/png", 1); // 获取的图片路径,可直接在页面显示
4、坑三:画出的图片不完整

图片不完整是因为图片的宽高设置有问题,drawImage需要传入起点终点及宽高数据,直接写的话会有适配问题。

解决办法:获取视频vedio标签的宽高,作为截取的宽高赋值,别忘了也给canvas赋值。

1
2
3
4
5
6
7
8
9
var videoHtml = document.getElementById("video").querySelector("video");
var width = document.getElementById("video").clientWidth;
var height = document.getElementById("video").clientHeight;
var canvasHtml = document.createElement("canvas");
canvasHtml.width = width;
canvasHtml.height = height;
var context = canvasHtml.getContext("2d");
context.drawImage(videoHtml, 0, 0, width, height);
let img = canvasHtml.toDataURL("image/png", 1);
5、补充

getUserMedia()方法需要在https协议或者localhost才能调用,如果电脑开发,需要手机上看效果的话,可以在manifest.json中设置”https” : true,就可以在手机上运行测试了。

配置代码如下:

1
2
3
4
5
"h5" : {
"devServer":{
"https" : true
}
}

基础用法

1、官网代码
1
2
3
4
<!-- 接受被移动元素 -->
<div id="div1" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<!-- 被移动元素 -->
<img loading="lazy" id="drag1" src="/images/logo.png" draggable="true" ondragstart="drag(event)" width="336" height="69">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 拖动元素放置的容器 
function allowDrop(ev)
{
ev.preventDefault();
}

function drag(ev)
{
ev.dataTransfer.setData("Text",ev.target.id);
}

function drop(ev)
{
ev.preventDefault();
var data=ev.dataTransfer.getData("Text");
ev.target.appendChild(document.getElementById(data));
}

代码解释:

  • 调用 preventDefault() 来避免浏览器对数据的默认处理(drop 事件的默认行为是以链接形式打开)
  • 通过 dataTransfer.getData(“Text”) 方法获得被拖的数据。该方法将返回在 setData() 方法中设置为相同类型的任何数据。
  • 被拖数据是被拖元素的 id (“drag1”)
  • 把被拖元素追加到放置元素(目标元素)中

鼠标默认显示

1、效果描述

一个课表,每个格子都是一个课程。拖到空白元素的可新增,而有课程的地方需要显示禁选。理想效果如下图:

2、代码:
1
2
3
4
5
6
7
8
allowDrop(e, week, index, id)
{
// 如果格子有元素 才阻断默认行为(其实默认行为就是鼠标icon为禁选,取消就是可以新增)
if (!week)
{
e.preventDefault();
}
}

拖动中样式设置

1、效果描述

拖动过程中,我希望我能知道我现在悬停在哪个元素上。实现效果如下:

2、实现思路

dragover事件监听的其实是被拖到的位置,于是可以在dragover事件中获取当前元素,给它一个标记。

由于我的代码是用vue循环得出,于是我给了一个下标值标记。

伪代码如下:

1
2
3
4
5
6
7
8
9
10
<!-- dragHoverIndex 代表当前悬浮的元素  drag-hover-cell 为悬浮元素的样式 -->
<td v-for="(week, wi) in d.arrangeList"
:key="'daily_week_'+wi"
:class="{'drag-hover-cell': dragHoverIndex === (wi + '_' + d.id)}""
class="course-cell has-class"
@drop="drop($event, d, wi)"
@dragover="allowDrop($event, week, wi, d.id)"
>
...
</td>
1
.drag-hover-cell{border: 1px solid #409EFF;}
1
2
3
4
5
6
7
8
9
10
11
12
13
allowDrop(e, week, index, id)
{
// week 有值 则有课程 无值则显示悬浮样式
if (!week)
{
e.preventDefault();
this.dragHoverIndex = index + "_" + id;
}
else
{
this.dragHoverIndex = -1;
}
}

效果展示

实现原理

1、路径绘图

图标主要是用svg的path属性,首先先了解一下,path有哪些命令。

命令 属性
M 移动到某个点
L 画直线
H 水平线
V 垂直线
C 二次赛贝尔曲线
S 平滑二次赛贝尔曲线
Q 三次赛贝尔曲线
T 平滑赛贝尔曲线
A 弧线
Z 结束点
2、简单绘图

现在先简单的绘制一个屋顶三角形,图标大小为45 X 45。

1
2
3
4
5
6
7
<svg width="45" height="45" class="path">
<path d="M 1 22
L 22.5 1
L 44 22
Z">
</path>
</svg>

3、添加圆角

因为图标所有线条的连接点都是圆滑的曲线,只是单纯的用坐标绘制会得到如上图所示生硬的结果,于是二次赛贝尔曲线就出现了。利用三次赛贝尔曲线的原理,以转折点的左边坐标点为起始点,然后以转折点为控制点,转折点右边坐标点为终点。先拿顶角为例,顶角坐标为 (22.5,1),圆角的实现为:

起始点 (21.5 2)、控制点(22.5,1)、终点(23.5,2)

1
2
3
4
5
6
7
<svg width="45" height="45" class="path">
<path d="M 1 22
L 21.5 2 Q 22.5 1 23.5 2
L 44 22
Z">
</path>
</svg>

4、画icon

有了上面的基础,我们只需要把icon的每个坐标点都通过三次赛贝尔曲线描绘,就可以得到一个完整的小房子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<svg width="45" height="45" class="path">
<path d="M 25 3 Q 22.5 1 20 3
L 2 18 Q 1 22 5 22
L 7 20
L 7 35 Q 7 43 12 43
L 17 43 Q 19 43 19 41
L 19 31 Q 19 29 20 29
L 25 29 Q 26 29 26 31
L 26 41 Q 26 43 28 43
L 33 43 Q 38 43 38 35
L 38 20
L 40 22 Q 44 22 43 18 Z"
stroke="#333"
stroke-width="1.5"
fill="none">
</path>
<path d="M 23.5 25 Q 22.5 25 21.5 25
L 19 25 Q 18 25 18 24
L 18 21
L 18 19 Q 18 18 19 18
L 20 18 Q 21 18 22 18
L 20 18 Q 21 18 22 18
L 23 18 Q 24 18 24 18
L 23 18 Q 24 18 24 18
L 26 18 Q 27 18 27 19
L 27 21
L 27 24 Q 27 25 26 25
Z"
fill="#0BCCB0"
stroke="none"></path>
</svg>
5、添加动画

动画的原理是改变SVG的路径,变成另一个图形,前提是这两个图形的坐标值数量必须一样。因此为了正方形能够转换变成房子,将正方形的坐标拆解成多个。利用css的animation动画,实现路径变化的动态效果。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
html:
<label>
<input type="checkbox" id="home" />
<svg width="45px" height="45px">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="0" y2="100%">
<stop offset="0%" style="stop-color:#67F1DD;stop-opacity:1" />
<stop offset="100%" style="stop-color:#15CCB2;stop-opacity:1" />
</linearGradient>
</defs>
<path class="background"></path>
<path class="border"></path>
</svg>
</label>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#home{
display: none;
}
.background{
fill: url(#grad1);
animation: toSmall .5s linear forwards;
}
.border{
fill: none;
stroke: #333;
stroke-width: 1.5;
animation: fade-in .5s linear forwards;
opacity: 0;
d: path('M 25 3 Q 22.5 1 20 3 L 2 18 Q 1 22 5 22 L 7 20 L 7 35 Q 7 43 12 43 L 17 43 Q 19 43 19 41 L 19 31 Q 19 29 20 29 L 25 29 Q 26 29 26 31 L 26 41 Q 26 43 28 43 L 33 43 Q 38 43 38 35 L 38 20 L 40 22 Q 44 22 43 18 Z');
}
#home:checked + svg .background {
stroke-width: 1.5;
stroke: url(#grad1);
animation: bigger .5s linear forwards;
}
#home:checked + svg .border {
opacity: 1;
animation: fade-out .3s linear forwards;
}
@keyframes bigger {
0% {
d: path('M 23.5 18 Q 22.5 18 21.5 18 L 19 18 Q 18 18 18 19 L 18 21 L 18 24 Q 18 25 19 25 L 20 25 Q 21 25 22 25 L 20 25 Q 21 25 22 25 L 23 25 Q 24 25 24 25 L 23 25 Q 24 25 24 25 L 26 25 Q 27 25 27 24 L 27 21 L 27 19 Q 27 18 26 18 Z');
}
50% {
d: path('M 25 3 Q 22.5 1 20 3 L 2 18 Q 1 22 5 22 L 7 20 L 7 35 Q 7 43 12 43 L 17 43 Q 19 43 19 43 L 19 43 Q 19 43 20 43 L 25 43 Q 26 43 26 43 L 26 43 Q 26 43 28 43 L 33 43 Q 38 43 38 35 L 38 20 L 40 22 Q 44 22 43 18 Z');
}

100% {
d: path('M 25 3 Q 22.5 1 20 3 L 2 18 Q 1 22 5 22 L 7 20 L 7 35 Q 7 43 12 43 L 17 43 Q 19 43 19 41 L 19 31 Q 19 29 20 29 L 25 29 Q 26 29 26 31 L 26 41 Q 26 43 28 43 L 33 43 Q 38 43 38 35 L 38 20 L 40 22 Q 44 22 43 18 Z');
}
}
@keyframes toSmall{
0%{
d: path('M 25 3 Q 22.5 1 20 3 L 2 18 Q 1 22 5 22 L 7 20 L 7 35 Q 7 43 12 43 L 17 43 Q 19 43 19 41 L 19 31 Q 19 29 20 29 L 25 29 Q 26 29 26 31 L 26 41 Q 26 43 28 43 L 33 43 Q 38 43 38 35 L 38 20 L 40 22 Q 44 22 43 18 Z');
}
50%{
d: path('M 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 L 22.5 22.5 L 22.5 22.5 Q 22.5 22.5 22.5 22.5 Z');
}
100%{
d: path('M 23.5 18 Q 22.5 18 21.5 18 L 19 18 Q 18 18 18 19 L 18 21 L 18 24 Q 18 25 19 25 L 20 25 Q 21 25 22 25 L 20 25 Q 21 25 22 25 L 23 25 Q 24 25 24 25 L 23 25 Q 24 25 24 25 L 26 25 Q 27 25 27 24 L 27 21 L 27 19 Q 27 18 26 18 Z');
}
}
@keyframes fade-in{
from{opacity: 0;}
to{ opacity: 1;}
}
@keyframes fade-out{
from{opacity: 1;}
to{opacity: 0;}
}

应用场景

报表统计某个项目中使用的关键字数量集合,由于关键字是用户自定义输入,存在不可控性,可能会有大量不同的关键字存在,于是便想到了用字符云做统计。实现效果:

踩雷过程

1、安装依赖
1
2
npm install echarts;
npm install echarts-wordcloud
2、页面引入

options的代码我是复制的官方示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import echarts from "echarts";
import "echarts-wordcloud";
this.chart.setOption(
{
tooltip: {},
series: [{
type: "wordCloud",
gridSize: 5,
textStyle: {
color: () =>
{
return "rgb(" + [
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(",") + ")";
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: "#333"
}
},
data: [
{
name: "Sam S Club",
value: 6987
},
{
name: "Macys",
value: 6181
},
{
name: "Amy Schumer",
value: 4386
},
{
name: "Jurassic World",
value: 4055
},
{
name: "Charter Communications",
value: 2467
},
{
name: "Chick Fil A",
value: 2244
},
{
name: "Planet Fitness",
value: 1898
},
{
name: "Pitch Perfect",
value: 1484
},
{
name: "Express",
value: 1112
},
{
name: "Home",
value: 965
},
{
name: "Johnny Depp",
value: 847
},
{
name: "Lena Dunham",
value: 582
},
{
name: "Lewis Hamilton",
value: 555
},
{
name: "KXAN",
value: 550
},
{
name: "Mary Ellen Mark",
value: 462
},
{
name: "Farrah Abraham",
value: 366
},
{
name: "Rita Ora",
value: 360
},
{
name: "Serena Williams",
value: 282
},
{
name: "NCAA baseball tournament",
value: 273
},
{
name: "Point Break",
value: 265
}
]
}]
});
3、踩雷在途

上面一顿操作之后,页面运行直接报错”TypeError: echarts_lib_echarts__WEBPACK_IMPORTED_MODULE_0__.helper.createDimensions is not a function”

百度之后也没整明白啥问题,然后在官网上看到了一句话。

NOTE:

echarts-wordcloud@2 is for echarts@5 echarts-wordcloud@1 is for echarts@4

image-20210416105319164

我的echarts版本是4.0.1,于是卸载echarts-wordcloud重新安装,再次运行。

1
2
npm uninstall echarts-wordcloud
npm install echarts-wordcloud@1.1.2

欢喜运行,再次打开页面,报了另一个错。

echarts_lib_echarts__WEBPACK_IMPORTED_MODULE_0__.helper.createTextStyle is not a function

沉着思考,冷静分析。。。分析不出来,开始疯狂百度,于是我又换了一个echarts的版本,从4.0.1换成了3.7.2。

再次欢喜运行。(其实重复安装依赖安装了非常多次,经过我多次试验,我发现需要先安装echarts@3.7.2的版本,再安装echarts-wordcloud@1.1.2的版本,欢喜运行就能出来)

4、曙光在即

页面是欢欢喜喜的运行出来了,然而字体却还是有点差强人意。仔细对照了官方给的文档,但是字体颜色就是不生效,出来全是一个色。

疯狂百度无果,正当山穷水尽疑无路之时,我默默的看了一眼echarts-wordcloud的源码,好家伙,还真让我发现了一个东西,color颜色定义放在textStyle的normal下面,把代码改了一下之后,字体颜色如期展示了。页面源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<template>
<div class="middle-chart">
<div ref="chart"
:style="{height:height,width:width}"/>
</div>
</template>
<script>
import echarts from "echarts";
import "echarts-wordcloud";
import { debounce } from "@/scripts/common/utils";
export default {
props: {
height: {
type: String,
default: "100%"
},
width: {
type: String,
default: "100%"
}
},
data()
{
return {
};
},
mounted()
{
this.initChart();
this.__resizeHandler = debounce(() =>
{
if (this.chart)
{
this.chart.resize();
}
}, 100);
window.addEventListener("resize", this.__resizeHandler);
},
beforeDestroy()
{
if (!this.chart)
{
return;
}
window.removeEventListener("resize", this.__resizeHandler);
this.chart.dispose();
this.chart = null;
},
methods: {
initChart: function()
{
this.chart = echarts.init(this.$refs.chart);

this.chart.setOption(
{
tooltip: {},
series: [{
type: "wordCloud",
gridSize: 5,
textStyle: {
normal: {
color: () =>
{
return "rgb(" + [
Math.round(Math.random() * 160) + 45,
Math.round(Math.random() * 160) + 45,
Math.round(Math.random() * 160) + 45
].join(",") + ")";
}
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: "#333"
}
},
data: [
{
name: "Sam S Club",
value: 6987
},
{
name: "Macys",
value: 6181
},
{
name: "Amy Schumer",
value: 4386
},
{
name: "Jurassic World",
value: 4055
},
{
name: "Charter Communications",
value: 2467
},
{
name: "Chick Fil A",
value: 2244
},
{
name: "Planet Fitness",
value: 1898
},
{
name: "Pitch Perfect",
value: 1484
},
{
name: "Express",
value: 1112
},
{
name: "Home",
value: 965
},
{
name: "Johnny Depp",
value: 847
},
{
name: "Lena Dunham",
value: 582
},
{
name: "Lewis Hamilton",
value: 555
},
{
name: "KXAN",
value: 550
},
{
name: "Mary Ellen Mark",
value: 462
},
{
name: "Farrah Abraham",
value: 366
},
{
name: "Rita Ora",
value: 360
},
{
name: "Serena Williams",
value: 282
},
{
name: "NCAA baseball tournament",
value: 273
},
{
name: "Point Break",
value: 265
}
]
}]
});
}
}
};
</script>
<style lang="scss" scoped>
@import url("./chart.scss");
</style>
6、结语

官方给的随机颜色是在0-160区间,红黄蓝都在这个区间显示的字体颜色是深色的,比较适合浅色背景,如果和我一样是深色背景的可以将rgba的色值调高一点,一般160-255之间的出来颜色会偏浅。

应用场景

某些网站中拥有大量图片,但用户可视区范围内,只展示一小部分的图片,如果不对图片进行懒加载,在网速较慢或图片资源过大的情况下,会出现加载缓慢卡顿的现象,影响用户体验。针对这种情况提出先加载用户可视范围内的图片,其他图片等用户进行操作(滑动或点击按钮)时再进行加载。

实现原理

1、图片列表

先不对图片标签赋值图片的地址,将地址放到父级里面利用data-source保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="imgWrapper">
<ul id="imgList">
{taskImg.map((item, index) =>(
<li key={`img${index}`}
lazyload="true"
data-original={download() + item.imageId}>
<Image
width={150}
height={150}
src=""/>
</li>
))}
</ul>
</div>
2、页面滑动

横向滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取未显示的图片列表
const imgList = document.getElementById("imgList").querySelectorAll("li[data-original][lazyload]");
// 获取主容器左边距离和右边距离
const wrapper = document.getElementById("imgWrapper");
const initRect = wrapper.getBoundingClientRect();
const initLeft = initRect.left;
const initRight = initRect.right;
imgList.forEach((item, index) =>
{
const img = item.firstChild.firstChild;//获取图片元素
const rect = item.getBoundingClientRect();
let rectL = rect.left + rect.width;// 图片到可视区域左边距离
let rectR = rect.right - rect.width;// 图片到可视区域右边距离
// 判断是否在可视区域内
if ((rect.left >= initLeft && rectR <= initRight) || (rectL >= initLeft && rect.right <= initRight))
{
img.src = item.dataset.original;
item.removeAttribute("data-original");// 移除属性,下次不再遍历
item.removeAttribute("lazyload");
}

});

竖向滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取未显示的图片列表
const imgList = document.getElementById("imgList").querySelectorAll("li[data-original][lazyload]");
imgList.forEach((item, index) =>
{
const img = item.firstChild.firstChild;//获取图片元素
// 判断是否在可视区域内
if (img.offsetTop < window.innerHeight + window.pageYOffset)
{
img.src = item.dataset.original;
item.removeAttribute("data-original");// 移除属性,下次不再遍历
item.removeAttribute("lazyload");
}

});

应用场景

对图片进行标注,包括画矩形框、多边形框和线条。官网传送门

实现原理

1、环境准备
1
2
// 安装ailabel依赖
# npm install ailabel --save
1
2
// 页面引入ailabel
const AILabel = require("AILabel");
2、Map对象实例化
1
const gMap = new AILabel.Map("AiLabelImg", { zoom: 0, cx: 0, cy: 0, zoomMax: 400 * 10, zoomMin: 400 / 10, autoPan: true, drawZoom: true });
1
2
3
4
5
6
render()
{
return (
<div id="AiLabelImg"></div>
);
}

map实例参数说明:

参数 说明
zoom 初始缩放级别
cx 初始中心点坐标x
cy 初始中心点坐标y
zoomMax 缩放最大级别
zoomMin 缩放最小级别
autoPan 绘制过程中是否禁止自动平移
drawZoom 绘制过程中是否禁止滑轮缩放
autoFeatureSelect 默认是否双击选中feature
3、添加图像层

为避免图像在容器中出现变形,先计算容器的宽高和图片的宽高,根据宽高比例自适应显示图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 获取容器的宽高
initWidth = document.getElementById("AiLabelImg").clientWidth;
initHeight = document.getElementById("AiLabelImg").clientHeight;
const image = new Image();
image.src = imgUrl;// 图片地址
image.onload = () =>
{
imgInitW = image.width;
imgInitH = image.height;
imgSjW = initWidth;
imgSjH = initHeight;
const sR = imgInitW / imgInitH;
const iR = initWidth / initHeight;
// 如果图片宽度大于高度 则宽度百分百 高度自适应
if (sR >= iR)
{
imgSjH = Math.floor(initWidth / imgInitW * imgInitH);
}
else
{
// 如果图片高度大于宽度 则高度百分百 宽度自适应
imgSjW = Math.floor(initHeight / imgInitH * imgInitW);
}
// 图片层实例\添加
const gImageLayer = new AILabel.Layer.Image("img", props.url, { h: imgSjH, w: imgSjW }, {});
gMap.addLayer(gImageLayer);
const gFeatureLayer = new AILabel.Layer.Feature(`featureid`, { opacity: 1, zIndex: 4 });
};
}
4、画矩形框
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const gTextStyle = new AILabel.Style({ strokeColor: "#E43446", lineWeight: 2, fillColor: "rgba(0,0,0,0)" });
// 设置模式为矩形框
gMap.setMode("drawRect", gTextStyle);
// 监听绘制过程
// 绘制完成,将绘制的坐标值显示
const count = 0;
gMap.events.on("geometryDrawDone", (type, points) =>
{
// 每次画框的featureId都需要不一样,可以用count计数
feature = new AILabel.Feature.Rect(`rectid${count}`, points, { name: "矩形框" }, style);
gFeatureLayer.addFeature(feature);
count += 1;
});
// 编辑完成,替换新的点坐标值
gMap.events.on("geometryEditDone", (type, feature, newPoints) =>
{
feature.show();
feature.update({ points: newPoints });
});
// 绘制过程中,将之前的框先隐藏
gMap.events.on("geometryEditing", (type, feature) =>
{
feature.hide();
});
5、完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import React, {
useImperativeHandle,
forwardRef,
useEffect
} from "react";
import styles from "./index.less";
import { message } from "antd";
import { canvasToAil, aiToCanvas } from "@/utils/utils";
import PropTypes from "prop-types";

const AILabel = require("AILabel");

let gMap;
let gImageLayer;
let count = 0;
let gFeatureLayer;
let selectedFeature = null;
let showFlag = true;
let initWidth = 700;// 盒子宽
let initHeight = 400;// 盒子的高
let initZoom;// 初始放大值
let initCenter;// 初始中心点
let gTextStyle; // 画笔样式
let preTextStyle;
let labelList = [];// 所有框的坐标和标签数据
let current = {}; // 当前框的坐标和标签数据
/* 用于坐标值转换参数 */
let imgInitH; // 图片的初始宽度
let imgInitW; // 图片的初始高度
let imgSjH;
let imgSjW;
const MONITOR_CANVAS = (props, ref) =>
{
const textAndRect = (points, created, index, isPreAnnotation) =>
{
let feature;
let style;
if (isPreAnnotation === "1")
{
style = preTextStyle;
}
else
{
style = gTextStyle;
}
// 实例化矩形形
if (props.type === "2" || props.type === "1")
{
feature = new AILabel.Feature.Rect(`rectid${count}`, points, { name: "矩形框" }, style);
}
else if (props.type === "4")
{
feature = new AILabel.Feature.Polygon(`rectid${count}`, points, { name: "多变型" }, style);
}
gFeatureLayer.addFeature(feature);
count += 1;
};
useEffect(() =>
{
if (props.url)
{
initWidth = document.getElementById("AiLabelImgWrapper").clientWidth;
initHeight = document.getElementById("AiLabelImgWrapper").clientHeight;
const image = new Image();
image.src = props.url;
image.onload = () =>
{
imgInitW = image.width;
imgInitH = image.height;
imgSjW = initWidth;
imgSjH = initHeight;
const sR = imgInitW / imgInitH;
const iR = initWidth / initHeight;
// 如果图片宽度大于高度 则宽度百分百 高度自适应
if (sR >= iR)
{
imgSjH = Math.floor(initWidth / imgInitW * imgInitH);
}
else
{
// 如果图片高度大于宽度 则高度百分百 宽度自适应
imgSjW = Math.floor(initHeight / imgInitH * imgInitW);
}
count = 0;
labelList = [];
// document.getElementById("AiLabelImg").style.width = `${imgSjW}px`;
// document.getElementById("AiLabelImg").style.height = `${imgSjH}px`;
gMap = new AILabel.Map("AiLabelImg", { zoom: 0, cx: 0, cy: 0, zoomMax: 400 * 10, zoomMin: 400 / 10, autoPan: true, drawZoom: true });
gMap.tipLayer.hideTips(); // 关闭tip提示
// 图片层实例\添加
gImageLayer = new AILabel.Layer.Image("img", props.url, { h: imgSjH, w: imgSjW }, {});
gMap.addLayer(gImageLayer);
gFeatureLayer = new AILabel.Layer.Feature(`featureid`, { opacity: 1, zIndex: 4 });
gMap.addLayer(gFeatureLayer);
initZoom = gMap.getZoom();
initCenter = gMap.getCenter();
// 样式
gTextStyle = new AILabel.Style({ strokeColor: "#E43446", lineWeight: 2, fillColor: "rgba(0,0,0,0)" });
preTextStyle = new AILabel.Style({ strokeColor: "#0FA883", lineWeight: 2, fillColor: "rgba(0,0,0,0)" });
gMap.events.on("featureSelected", feature =>
{
// TODO 框被选中
});
gMap.events.on("featureStatusReset", () =>
{
// TODO 框失去焦点
});
};
}
}, [props.url]);
useImperativeHandle(ref, () => ({
// 窗口自适应
autoWindow()
{
gMap.centerAndZoom(initCenter, initZoom);
},
//回复浏览模式
autoWidth()
{
gMap.setMode("pan");
},
// 原始尺寸
initWidth()
{
gMap.centerAndZoom(initCenter, initZoom);
},
imgZoomIn()
{
gMap.zoomIn();
},
imgZoomOut()
{
gMap.zoomOut();
},
draw()
{
if (gFeatureLayer)
{
if (props.type === "2" || props.type === "1")
{
gMap.setMode("drawRect", gTextStyle);
}
else if (props.type === "4")
{
gMap.setMode("drawPolygon", gTextStyle);
}
gMap.events.on("geometryDrawDone", (type, points) =>
{
textAndRect(points, true, 0, null, "0");
});
gMap.events.on("geometryEditDone", (type, feature, newPoints) =>
{
feature.show();
feature.update({ points: newPoints });
});
gMap.events.on("geometryEditing", (type, feature) =>
{
feature.hide();
});
}
},
// 复制框
copyLabel()
{
if (selectedFeature === null)
{
message.error("未选择框进行复制操作");
}
else
{
const copyPoint = [];
selectedFeature.points.forEach(item =>
{
copyPoint.push({
x: item.x + 10,
y: item.y - 10
});
});
selectedFeature.deActive();
textAndRect(copyPoint, true, 0, null, "0");
}
},
// 删除框
deleteLabel()
{
if (selectedFeature === null)
{
message.error("未选择框进行删除操作");
}
else
{
// 修改操作
gFeatureLayer.removeFeature(selectedFeature);
}
},
showHideAll()
{
const allFeatures = gFeatureLayer.getAllFeatures(); // 返回所有要素数据
allFeatures.forEach(feature =>
{
if (showFlag)
{
feature.hide();
}
else
{
feature.show();
}
});
showFlag = !showFlag;
}
}));

return (
<div id="AiLabelImgWrapper" className={styles["ailabel-img"]}>
<div id="AiLabelImg" className={styles["ailabel-img"]}></div>
</div>
);
};
export default forwardRef(MONITOR_CANVAS);

应用场景

文件上传之后,获得文件的唯一标识(MD5值),将文件切割成多个分块,根据文件标识查询文件未上传的分块,所有分块上传完之后,将分块进行合并。

实现原理

1、上传文件
1
2
3
4
5
const uploadProps = {
name: "file",
accept: ".zip",
maxCount: 1,
}
1
2
3
4
5
6
7
8
9
<Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或将文件拖拽到这里上传</p>
<p className="ant-upload-hint">
支持文件扩展名:.zip
</p>
</Dragger>
2、文件预处理

在文件上传之前,beforeUpload里对文件进行切割预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 兼容性的处理
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunkSize = 1024 * 1024 * 5; // 切片每次5M
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0; // 当前上传的chunk
const spark = new SparkMD5.ArrayBuffer();
// 对arrayBuffer数据进行md5加密,产生一个md5字符串
const chunkFileReader = new FileReader(); // 用于计算出每个chunkMd5
const params = { chunks: [], file: {}}; // 用于上传所有分片的md5信息
const arrayBufferDataT = []; // 用于存储每个chunk的arrayBuffer对象,用于分片上传使用
/* 文件合并所需参数 */
params.file.fileName = file.name; // 文件名
params.file.totalSize = file.size; // 文件总大小
params.file.fileType = file.type; // 文件类型
params.file.relativePath = file.name; // 相对路径
params.file.refProjectId = "dbxt-sf-gd-sz"; // 文件唯一标识

function loadNext()
{
const start = currentChunk * chunkSize;
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
chunkFileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
let identifier;
chunkFileReader.onload = function(e)
{
// 对每一片分片进行md5加密
if (currentChunk === 0)
{
spark.append(e.target.result);
identifier = spark.end();
}
// 每一个分片需要包含的信息
const obj = {
totalSize: file.size,
relativePath: file.name,
chunkNumber: currentChunk + 1, // 块编号
fileName: file.name,
identifier, // 块编号
totalChunks: chunks, // 总分片数
chunkSize, // 块大小
currentChunkSize: ((currentChunk * chunkSize + chunkSize) >= file.size) ? file.size - currentChunk * chunkSize : chunkSize // 当前块大小
};
// 每一次分片onload,currentChunk都需要增加,以便来计算分片的次数
currentChunk += 1;
params.chunks.push(obj);

// 将每一块分片的arrayBuffer存储起来,用来partUpload
const tmp = {
chunk: obj.chunkNumber,
currentBuffer: e.target.result
};
arrayBufferDataT.push(tmp);
if (currentChunk < chunks)
{
// 当前切片总数没有达到总数时
loadNext();
// 计算预处理进度
setpreUploading(true);
setpreUploadPercent(Number((currentChunk / chunks * 100).toFixed(2)));
}
else
{
setchunksSize(chunks);
setpreUploadPercent(100);
setFileList([file]);
// 记录所有chunks的长度
params.file.identifier = identifier;
params.file.fileChunks = params.chunks.length;
// 表示预处理结束,将上传的参数,arrayBuffer的数据存储起来
setarrayBufferData(arrayBufferDataT);
setpreUploading(false);
setuploadParams(params);
}
};

chunkFileReader.onerror = function()
{
console.warn("oops, something went wrong.");
};
3、上传分块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

try
{
// 判断是否上传
const data = await examChunk(chunks[0]);
let { uploadedChunks } = data || []; // 已上传文件
uploadedChunks = Array.from(new Set(uploadedChunks));
if (uploadedChunks.length === uploadParams.file.fileChunks || data.skipUpload) // 如果全部文件快已上传 则直接完成 实现秒传
{
setuploadPercent(100); // 文件上传进度
mergeFile();// 合并文件
}
else
{
setuploadPercent(Number((uploadedChunks.length / uploadParams.file.fileChunks * 100).toFixed(2)));
const uploadList = chunks.filter(item => { return uploadedChunks.indexOf(item.chunkNumber) < 0; });// 过滤出未上传分片
handlePartUpload(uploadList, uploadList.length); // 分片上传
}
}
catch (error)
{
console.log(error);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 分片上传
const handlePartUpload = (uploadList, currentChunks) =>
{
let current = currentChunks;
uploadList.forEach(async(element) =>
{
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const { identifier, chunkNumber } = element;
const formData = new FormData();
// 新建一个Blob对象,将对应分片的arrayBuffer加入Blob中
const blob = new Blob([arrayBufferData[chunkNumber - 1].currentBuffer], { type: "application/octet-stream" });
// 将生成blob塞入到formdata中传入服务端
formData.append("upfile", blob, identifier);
Object.keys(element).forEach(el =>
{
formData.append(el, element[el]);
});
try
{
const uploadData = await upload(formData);
if (uploadData === 200)
{
current -= 1;
setuploadPercent(Number(((uploadParams.file.fileChunks - current) / uploadParams.file.fileChunks * 100).toFixed(2)));
if (current === 0)
{
mergeFile();
}
}
else
{
setUploadFailed(true);
setspinning(false);
}
}
catch (error)
{
setUploadFailed(true);
setspinning(false);
console.log(error);
}
});
};
4、合并文件块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const mergeFile = async() =>
{
try
{
const mergeData = await merge(uploadParams.file);
if (mergeData.id)
{
// setUpfileId(mergeData.id);
// message.success('上传成功');
createData(mergeData.id);
setuploading(false);
setuploaded(true);
}
else
{
setspinning(false);
setUploadFailed(true);
}
}
catch (error)
{
setspinning(false);
setUploadFailed(true);
}
};

效果图

前言

项目有个场景需求是要实现点击时,出现烟花绽放的效果,在网上百度了很多关于烟花的效果图,免费的不好看,好看的要钱,贫穷的打工人选择自己上手。

实现步骤

1、分解

将所有烟花都分解成一个类似水滴形状的瓣,控制它的大小、坐标和旋转角度,叠加在一起就成了一朵好看的烟花。一个瓣可以看做是两条弯曲的曲线构成,曲线可由三次贝塞尔曲线绘制。

2、水平绘制

假设初始坐标值为(x,y),瓣的宽度为w,高度为h,第一个拐角点设在宽度0.9位置(经过我的多次测试,这个位置绘制出的烟花最好看!)。

1
2
3
4
ctx.beginPath();
ctx.moveTo(x,y); // 先将点放到初始坐标值
ctx.bezierCurveTo(x+w*0.9,y-h/2,x+w, y-h/2, x+w,y);
ctx.bezierCurveTo(x+w,y+h/2,x+w*0.9,y+h/2,x,y);// 第二条曲线以第一条曲线的结束点为起始点
3、旋转角度

假设旋转角度为45度,如图计算出每个坐标的坐标值。

4、花瓣合成

一层烟花由八朵烟花花瓣组成,分为左、右、上、下、左上、右上、左下、右下。由于canvas画布的坐标只有正数没有负数,因此可以计算出:

// 左 及 左上
目标横坐标 = 初始位置横坐标 - 对应点横坐标
目标纵坐标 = 初始位置纵坐标 - 对应点点纵坐标
// 上 及 右上
目标横坐标 = 初始位置横坐标 + 对应点点横坐标
目标纵坐标 = 初始位置纵坐标 - 对应点点纵坐标
// 右 及 右下
目标横坐标 = 初始位置横坐标 + 对应点点横坐标
目标纵坐标 = 初始位置纵坐标 + 对应点点纵坐标
// 下 及 左下
目标横坐标 = 初始位置横坐标 - 对应点点横坐标
目标纵坐标 = 初始位置纵坐标 + 对应点点纵坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 假设a为偏移的角度,(x,y)为起始点坐标值,h为花瓣高度,w为花瓣宽度
// 左边花瓣代码示例
var angel = a;//偏移的角度
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x - ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y - (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x - ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y - (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x - w * cosAngel;
var p3y = y - w * sinAngel;
var p4x = x - ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y - (w + h / 2 / tanAngel) * sinAngel;
var p5x = x - ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y - (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.stroke();
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
5、圆点坐标值

烟花可看做一圈一圈不同半径上的点为起始坐标的花瓣组成,每一圈有八个花瓣,根据三角函数可计算出点的坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 假设偏移角度为angle,圆心坐标为(x,y),半径为r
// 左
var p1x = x - r * Math.cos(angle * Math.PI / 180);
var p1y = y - r * Math.sin(angle * Math.PI / 180);
// 右
var p2x = x + r * Math.cos(angle * Math.PI / 180);
var p2y = y + r * Math.sin(angle * Math.PI / 180);
// 上
var p3x = x + r * Math.sin(angle * Math.PI / 180);
var p3y = y - r * Math.cos(angle * Math.PI / 180);
// 下
var p4x = x - r * Math.sin(angle * Math.PI / 180);
var p4y = y + r * Math.cos(angle * Math.PI / 180);
// 左上
var p5x = x - r * Math.cos((angle + 45) * Math.PI / 180);
var p5y = y - r * Math.sin((angle + 45) * Math.PI / 180);
// 右上
var p6x = x + r * Math.cos((45 - angle) * Math.PI / 180);
var p6y = y - r * Math.sin((45 - angle) * Math.PI / 180);
// 左下
var p7x = x - r * Math.cos((45 - angle) * Math.PI / 180);
var p7y = y + r * Math.sin((45 - angle) * Math.PI / 180);
// 右下
var p8x = x + r * Math.cos((angle + 45) * Math.PI / 180);
var p8y = y + r * Math.sin((angle + 45) * Math.PI / 180);
6、最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// 圆心横坐标、    圆心纵坐标、   半径、   偏移角度、     瓣高  、     瓣宽、    颜色 
// 第一圈
calcEight(pointR, pointR, canvasW * 0.05, 1,canvasW * 0.02, canvasW * 0.15, "#E4AF6A");
calcEight(pointR, pointR, canvasW * 0.05, 11,canvasW * 0.02, canvasW * 0.15, "#F8EEB0");
// 第二圈
calcEight(pointR, pointR, canvasW * 0.1, 28,canvasW * 0.08, canvasW * 0.35, "#E4AF6A");
calcEight(pointR, pointR, canvasW * 0.1, 36,canvasW * 0.08, canvasW * 0.35, "#F8EEB0");
// 第三圈
calcEight(pointR, pointR, canvasW * 0.075, 20,canvasW * 0.058, canvasW * 0.3, "#E4AF6A");
calcEight(pointR, pointR, canvasW * 0.075, 28,canvasW * 0.058, canvasW * 0.3, "#F8EEB0");
// 第四圈
calcEight(pointR, pointR, canvasW * 0.29, 9,canvasW * 0.03, canvasW * 0.2, "#E4AF6A");
calcEight(pointR, pointR, canvasW * 0.29, 1,canvasW * 0.03, canvasW * 0.2, "#F8EEB0");
// 左
function draw1(x,y,h,w, r,a,color)
{
var angel = a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x - ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y - (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x - ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y - (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x - w * cosAngel;
var p3y = y - w * sinAngel;
var p4x = x - ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y - (w + h / 2 / tanAngel) * sinAngel;
var p5x = x - ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y - (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.stroke();
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 右
function draw2(x,y,h,w, r,a,color)
{
var angel = a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x + ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y + (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x + ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y + (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x + w * cosAngel;
var p3y = y + w * sinAngel;
var p4x = x + ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y + (w + h / 2 / tanAngel) * sinAngel;
var p5x = x + ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y + (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 上
function draw3(x,y,h,w, r,a,color)
{
var angel = 90 - a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x + ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y - (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x + ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y - (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x + w * cosAngel;
var p3y = y - w * sinAngel;
var p4x = x + ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y - (w + h / 2 / tanAngel) * sinAngel;
var p5x = x + ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y - (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 下
function draw4(x,y,h,w, r,a,color)
{
var angel = 90 - a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x - ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y + (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x - ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y + (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x - w * cosAngel;
var p3y = y + w * sinAngel;
var p4x = x - ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y + (w + h / 2 / tanAngel) * sinAngel;
var p5x = x - ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y + (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 左上
function draw5(x,y,h,w, r,a,color)
{
var angel = a + 45;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x - ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y - (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x - ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y - (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x - w * cosAngel;
var p3y = y - w * sinAngel;
var p4x = x - ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y - (w + h / 2 / tanAngel) * sinAngel;
var p5x = x - ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y - (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.stroke();
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 右上
function draw6(x,y,h,w, r,a,color)
{
var angel = 45 - a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x + ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y - (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x + ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y - (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x + w * cosAngel;
var p3y = y - w * sinAngel;
var p4x = x + ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y - (w + h / 2 / tanAngel) * sinAngel;
var p5x = x + ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y - (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 左下
function draw7(x,y,h,w, r,a,color)
{
var angel = 45 - a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x - ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y + (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x - ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y + (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x - w * cosAngel;
var p3y = y + w * sinAngel;
var p4x = x - ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y + (w + h / 2 / tanAngel) * sinAngel;
var p5x = x - ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y + (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 右下
function draw8(x,y,h,w, r,a,color)
{
var angel = 45 + a;
var sinAngel = Math.sin(angel*Math.PI / 180);
var cosAngel = Math.cos(angel*Math.PI / 180);
var tanAngel = Math.tan(angel*Math.PI / 180);
var hypotenuse = h / 2 / Math.sin(angel*Math.PI / 180);
var p1x = x + ((w * rate - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p1y = y + (w * rate - h / 2 / tanAngel) * sinAngel;
var p2x = x + ((w - h / 2 / tanAngel) * cosAngel + hypotenuse);
var p2y = y + (w - h / 2 / tanAngel) * sinAngel ;
var p3x = x + w * cosAngel;
var p3y = y + w * sinAngel;
var p4x = x + ((w + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p4y = y + (w + h / 2 / tanAngel) * sinAngel;
var p5x = x + ((w * rate + h / 2 / tanAngel) * cosAngel - hypotenuse);
var p5y = y + (w * rate + h / 2 / tanAngel) * sinAngel;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.bezierCurveTo(p1x,p1y,p2x, p2y,p3x,p3y);
ctx.bezierCurveTo(p4x, p4y,p5x,p5y,x,y);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function calcEight(x,y,r, angle,h, w, color)
{
// 左
var p1x = x - r * Math.cos(angle * Math.PI / 180);
var p1y = y - r * Math.sin(angle * Math.PI / 180);
// 右
var p2x = x + r * Math.cos(angle * Math.PI / 180);
var p2y = y + r * Math.sin(angle * Math.PI / 180);
// 上
var p3x = x + r * Math.sin(angle * Math.PI / 180);
var p3y = y - r * Math.cos(angle * Math.PI / 180);
// 下
var p4x = x - r * Math.sin(angle * Math.PI / 180);
var p4y = y + r * Math.cos(angle * Math.PI / 180);
// 左上
var p5x = x - r * Math.cos((angle + 45) * Math.PI / 180);
var p5y = y - r * Math.sin((angle + 45) * Math.PI / 180);
// 右上
var p6x = x + r * Math.cos((45 - angle) * Math.PI / 180);
var p6y = y - r * Math.sin((45 - angle) * Math.PI / 180);
// 左下
var p7x = x - r * Math.cos((45 - angle) * Math.PI / 180);
var p7y = y + r * Math.sin((45 - angle) * Math.PI / 180);
// 右下
var p8x = x + r * Math.cos((angle + 45) * Math.PI / 180);
var p8y = y + r * Math.sin((angle + 45) * Math.PI / 180);
// 左
draw1(p1x, p1y, h, w, Math.PI,angle,color);
// 右
draw2(p2x, p2y, h, w, Math.PI,angle,color);
// 上
draw3(p3x, p3y, h, w, Math.PI / 2,angle,color);
// 下
draw4(p4x, p4y, h, w, Math.PI / 2,angle,color);
// 左上
draw5(p5x, p5y, h, w, Math.PI,angle,color);
// 右上
draw6(p6x, p6y, h, w, Math.PI,angle,color);
// 左下
draw7(p7x, p7y, h, w, Math.PI / 2,angle,color);
// 右下
draw8(p8x, p8y, h, w, Math.PI / 2,angle,color);
}
7、动画效果

将画布的大小分成30,每50ms加一份,以此来改变每个花瓣的大小以及初始坐标的位置,达到逐渐变大的动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var pointR = canvasWidth / 2;
var cw = 0;
var aT = 0;
var cwSpeed = canvasWidth / 30;// 画布大小增长速度
var aSpeed = 45 / 30;
var timer = setInterval(function()
{
if(cw < canvasWidth)
{
cw += cwSpeed;
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
ctx.fill();
draw(cw, pointR, aT);
}
else
{
ctx.globalAlpha = 0;
clearInterval(timer);
}
}, 50);