1 分析需求

画出心形并放大缩小模拟跳动

心形外部围绕一圈粒子

内部粒子随机分布,越靠近外部边缘的粒子越明显,跳动幅度越大

2 先画一个跳动的心

2.1 设置画布

1
2
3
4
5
6
7
8
9
const canvas = document.createElement('canvas');
const width = window.innerWidth;
const height = window.innerHeight;
document.body.append(canvas);
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#EEAEEE'
ctx.fillStyle = '#EEAEEE'
//将画笔移动到画布中央
ctx.translate(width / 2,height / 2);

2.2 先画个心形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function calX(p){
return 16*16*Math.pow(Math.sin(p),3);
}

function calY(p){
return -16*(13*Math.cos(p)-5*Math.cos(2*p)-2*Math.cos(3*p)-Math.cos(4*p))
}

function draw(ctx){
let radian_cal = 0 ;
let radian_add = Math.PI/180;
let x,y;
ctx.beginPath();
ctx.moveTo(calX(radian_cal),calY(radian_cal))
while ( radian_cal <= 2*Math.PI ){
radian_cal += radian_add;
x = calX(radian_cal);
y = calY(radian_cal);
ctx.lineTo(x,y);
ctx.stroke()
}
}

然后调用draw方法 得到一个心形heart_static.png

但此时并不会动, 我们让它动起来

2.3 加入动画

将计算坐标的函数改一下

1
2
3
4
5
6
function calX(p,size){
return size*16*Math.pow(Math.sin(p),3);
}
function calY(p,size){
return -size*(13*Math.cos(p)-5*Math.cos(2*p)-2*Math.cos(3*p)-Math.cos(4*p))
}

此时我们就可以在每次刷新时,通过传入不同的size来控制变大变小。

size需要在一定区间内不断加减,所以将size抽离 放在动画中计算大小。

为了灵活控制 额外定义三个变量

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
  //初始大小
let initSize = 16;
//最大大小
let maxSize = 18;
//当前变化的大小
let changeSize = initSize;
//当前是变大还是变小
let isadd = true;
//停顿时间
let stop = new Date();
function draw(ctx){
let radian_cal = 0 ;
let radian_add = Math.PI/180;
let x,y;
ctx.beginPath();
ctx.moveTo(calX(radian_cal,changeSize),calY(radian_cal,changeSize))
while ( radian_cal <= 2*Math.PI ){
radian_cal += radian_add;
x = calX(radian_cal,changeSize);
y = calY(radian_cal,changeSize);
ctx.lineTo(x,y);
ctx.stroke()
}
}
function loop(){
//每次跳动停顿500毫秒
if (new Date().getTime() - stop.getTime() > 500){
//清空画布
ctx.clearRect(-300,-300,width,height)
//判断是改变状态 变大还是变小
if (changeSize >= maxSize){
isadd = false
} else if (changeSize <= initSize){
isadd = true;
//标记完成一次跳动
stop = new Date();
}
if (isadd){changeSize += 0.1}else {changeSize -= 0.1}
draw(ctx)
}
requestAnimationFrame(loop)
}
requestAnimationFrame(loop)


运行代码 此时我们就得到了一个会跳动的心形

change_heart.gif

2 处理粒子

2.1 生成随机数

由于需要随机生成粒子.将会大量用到随机数,所以开始之前先将生成随机数的方法抽离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    /**
* 返回两个值之间的随机数
* @param min 最小值
* @param max 最大值
* @returns {*} 随机数
*/
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}

/**
* 返回两个值之间的随机整数
* @param min 最小值
* @param max 最大值
* @returns {*} 随机数
*/
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min);
}

2.2 准备数据结构

粒子分为外层粒子和内层粒子

外层较为简单,就是围绕心形包裹一圈。内层看似随机分布,实际上靠近边缘的粒子会更亮,并且跳动时幅度也会更大。所有粒子所在位置、亮度、跳动幅度都和心形有关。
所以抽象一个心形类Heart,每个Heart自己处理自己周围的粒子

将粒子抽象为Point类 记录坐标及变化大小

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 outPointNum = 5
//内部扩散系数
const inPointNum = 8
//粒子类
class Point{
x = 10;
y = 10;
size = 0;
radian_cal = 0;
change_size = 0;
}
//心形类
class Heart {
x = 0;
y = 0;
radian_cal = 0;
//外部扩散
out_points = [];
//内部扩散
in_points = [];

constructor() {
}
}

2.3 处理粒子运动逻辑

在Heart中添加build和change方法,加载时先生成心形以及所有粒子坐标,每次刷新调用change变化

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
class Heart {
x = 0;
y = 0;
radian_cal = 0;
//外部扩散
out_points = [];
//内部扩散
in_points = [];

constructor() {
}
//初始化构建
build(radian_cal,x,y){
this.x = x;
this.y = y;
this.radian_cal = radian_cal;
for (let i = 0; i < outPointNum; i++) {
//外部扩散
let p = new Point();
//大小+1
let outx = calX(this.radian_cal,changeSize + 1);
let outy = calY(this.radian_cal,changeSize + 1);
//随机大小
p.size = getRandomInt(0,2)
//随机偏移坐标
p.x = getRandomInt(outx - 10,outx + 10)
p.y = getRandomInt(outy - 10,outy + 10)
this.out_points.push(p);
}
let r = getRandomInt(0,5);
for (let i = 0; i < inPointNum; i++) {
//内部扩散 控制随机坐标概率 要不然分布不均 会有很多在中间
let randomChangeSize ;
if ( r % 2 === 0 && i % 2 === 1 ){
randomChangeSize = getRandomArbitrary(-1,initSize-1);
}else if (r % 2 === 0 || i % 2===0){
randomChangeSize = getRandomArbitrary(initSize/5,initSize-1);
}else if (i % 2 === 0){
randomChangeSize = getRandomArbitrary(initSize/2,initSize-1);
}else {
randomChangeSize = getRandomArbitrary(initSize-5,initSize-1);
}
let p = new Point();
p.x = calX(radian_cal,randomChangeSize);
p.y = calY(radian_cal,randomChangeSize);
p.size = 1;
p.radian_cal = radian_cal;
p.change_size = randomChangeSize;
this.in_points.push(p);
}
}
//每次变化偏移计算
change(){
let diffx = calX(this.radian_cal,changeSize)
let diffy = calY(this.radian_cal,changeSize)
//计算便宜差值
let dx = diffx - this.x
let dy = diffy - this.y
this.y = diffy ;
this.x = diffx ;
for (let i = 0; i < this.out_points.length; i++) {
//外部扩散直接加偏移量
this.out_points[i].x = this.out_points[i].x + dx
this.out_points[i].y = this.out_points[i].y + dy
ctx.fillRect(this.out_points[i].x,this.out_points[i].y,this.out_points[i].size,this.out_points[i].size);
}
for (let i = 0; i < this.in_points.length; i++) {
//内部扩散 计算差值比例 保证外面的更大
let initDuffSize = initSize - this.in_points[i].change_size
let currDuffSize = changeSize - this.in_points[i].change_size
let inChangeSize = this.in_points[i].change_size + (currDuffSize / initDuffSize)
let diffx = calX(this.in_points[i].radian_cal,inChangeSize )
let diffy = calY(this.in_points[i].radian_cal,inChangeSize)
let dx = diffx - this.in_points[i].x
let dy = diffy - this.in_points[i].y
let luminance = inChangeSize/ 10 + changeSize / 20
ctx.fillRect(this.in_points[i].x +dx,this.in_points[i].y+dy,luminance,luminance);
}
}
}

const heartList = []
//初始化所有坐标
while ( radian_cal <= 2*Math.PI ){
radian_cal += radian_add;
let x = calX(radian_cal,initSize);
let y = calY(radian_cal,initSize);
let p = new Heart();
p.build(radian_cal,x,y)
heartList.push(p)
}

将loop方法中调用draw 改为

1
2
3
for (let i = 0; i < heartList.length; i++) {
heartList[i].change();
}

再次运行,此时我们将得到跳动的心形粒子

change_heart_point.gif

3 最后的处理

3.1 将背景改为黑色

1
2
3
4
5
6
7
<style>
canvas{
background-color: #000000;
display: block;
margin: 0 auto;
}
</style>

3.2 在loop中画出心形

1
2
3
4
5
6
ctx.beginPath()
for (let i = 0; i < heartList.length; i++) {
ctx.lineTo(heartList[i].x,heartList[i].y)
}
ctx.stroke()
ctx.closePath()

3.3 补上心形中间的直线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function line(){
let minX = 0;
let minY = 0;
let tempX = 0;
let tempY = 0;
for (let i = 0; i < heartList.length; i++) {
if ( minY < heartList[i].y){
minY = heartList[i].y
minX = heartList[i].x;
}
tempX = heartList[i].x;
tempY = heartList[i].y;
}
ctx.beginPath()
ctx.moveTo(tempX,tempY)
ctx.lineTo(minX,minY)
ctx.stroke()
ctx.closePath()
}

4 完结

至此 大功告成 点击以下链接看看效果吧~

https://www.yfniubi.club/html/heart.html

finish_heart.gif