引力效果[2] - 绘制引力线条

说明

本意是模拟一个引力环境,做出粒子相互之间引力捕获的效果。为了方便调试的时候查看每个粒子受到其它哪些粒子的引力,就加了引力线的显示效果。这与某框架(名字忘了)的特效一致。

一如既往,这里如果用原生canvas当时是没问题的,使用pixi无非是快速实现想法。这里绘制引力线条的思路当然很简单,基本没难点,问题是使用pixi直接快速调用graphics绘图和擦除会出现不连贯的问题,这个在直线《pixi如何实现绘画功能》一文里有提到,所以依旧使用canvas转pixi材质,然后创建pixi的sprite展示。每次绘图完成之后,材质update即可。

1
2
3
<div id="preview-box" style="text-align:center"></div>
<button id="start">start</button>
<button id="stop">stop</button>
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
// 简单的数组随机取值函数
function random(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}

document.getElementById('start').onclick = () => PIXI.Ticker.shared.start()
document.getElementById('stop').onclick = () => PIXI.Ticker.shared.stop()

let game = new PIXI.Application({
width: 640,
height: 320,
backgroundColor: 0x333333
})
document.getElementById('preview-box').appendChild(game.view)
game.view.style.width = "480px"
game.view.style.maxWidth = "100%"

let width = game.renderer.width
let height = game.renderer.height

// 舞台分割粒度
let cellSize = 20
// 二维数组 y做为第1维度,x作为第2维度
let team = []
// 列数
let colNum = Math.ceil(width / cellSize)
// 行数
let rowNum = Math.ceil(height / cellSize)
// 格子数
let boxNum = 0
// 总粒子数
let pNum = 120
// 总运行帧数 可以在达到一定值之后置0
let frame = 0
// 引力影响格子数 R=1 则影响包括自身格子的 9格 (1 + 2R)^2
// 也就是说 认为这些距离的粒子才会对当前粒子产生引力效果 虽然不严谨 但是考虑到计算性能 只能折中考虑
let R = 1

// 引力线绘制层 需用原生canvas实现 原理参考之前的一片关于pixi实现画板的文章
let lineCanvas = document.createElement('canvas');
lineCanvas.width = width
lineCanvas.height = height
let ctx = lineCanvas.getContext('2d');

// tx 作为材质 需要在每次绘制后更新 或者批量绘制后统一更新
let tx = PIXI.Texture.from(lineCanvas);
let lineLayout = new PIXI.Sprite(tx)
lineLayout.alpha = .2

let container = new PIXI.ParticleContainer(pNum)
game.stage.addChild(lineLayout)
game.stage.addChild(container)

// 预先准备格子
for (let x = 0; x < colNum; x++) {
let col = []
for (let y = 0; y < rowNum; y++) {
let box = {}
boxNum++
col.push(box)
}
team.push(col)
}

// 创建粒子
for (let n = 0; n < pNum; n++) {
let px = Math.random() * width
let py = Math.random() * height
let col = ~~(px / cellSize)
let row = ~~(py / cellSize)

// 粒子基本属性
let p = new PIXI.Sprite(PIXI.Texture.WHITE)
p.name = n;
p.x = px;
p.y = py;
p.anchor.set(0.5)
p.rotation = Math.PI/4
p.tint = 0x91ff00
p.alpha = .45
// 附加属性 可以考虑用WeakMap存储 到时候做个对比,看性能差异
// 行列数
p.col = col;
p.row = row;
// 质量
p.m = 2
p.scale.set(.3)
// 速度
p.vx = (Math.random() - 0.5)
p.vy = (Math.random() - 0.5)
// 加速度
p.ax = 0
p.ay = 0

team[col][row][n] = p;
container.addChild(p)
}

// 计算行动一次的时间
let time = 1;
// 定时器
// 舞台等分为 多少格,然后粒子每次运动结束,记录当前属于第几个格子,移动到对应的数组存储
PIXI.Ticker.shared.add(() => {
if (frame % 1 === 0) {
ctx.clearRect(0, 0, width, height)

container.children.forEach(p => {
gravityCalc(p, R)

let fcol = ~~(p.x / cellSize)
let frow = ~~(p.y / cellSize)

let tx = p.x + p.vx
let ty = p.y + p.vy

// 计算碰撞反弹 模拟摩擦力为0 能量无损耗的理想环境
if (tx < p.width / 2 || tx > width - p.width / 2) {
p.vx *= -1
p.x = tx < p.width / 2 ? p.width / 2 : width - p.width / 2
} else {
p.x += p.vx
}

if (ty < p.height / 2 || ty > height - p.height / 2) {
p.vy *= -1
p.y = ty < p.height / 2 ? p.height / 2 : height - p.height / 2
} else {
p.y += p.vy
}

// 移动完毕 更新所在的格子
let tcol = ~~(p.x / cellSize)
let trow = ~~(p.y / cellSize)
if (fcol !== tcol || frow !== trow) {
p.col = tcol;
p.row = trow;
delete team[fcol][frow][p.name]
team[tcol][trow][p.name] = p;
}
});

tx.update()
}
frame++;
})

// 每个粒子检查周边 (1+2R)^2个 格子
function gravityCalc(p, range) {
let { col: centerCol, row: centerRow } = p;
let g = 0.9;

for (let x = 0; x < range * 2 + 1; x++) {
for (let y = 0; y < range * 2 + 1; y++) {
let
testCol = centerCol - range + x,
testRow = centerRow - range + y;

// 非法行列数 目标盒子不存在
if (testCol < 0 || testCol >= colNum || testRow < 0 || testRow >= rowNum) continue;
let testBox = team[testCol][testRow];

// 目标盒子没有内容
if (Object.keys(testBox).length <= 0) continue;

// 目标盒子是当前盒子 只有自己
if (testCol === centerCol && testRow === centerRow && Object.keys(testBox).length <= 1) continue;

// 剩余情况 目标盒子与自己产生引力效应,影响速度
for (let name in testBox) {
if (testBox.hasOwnProperty(name)) {

// 引力点
let tp = testBox[name];

// 绘制引力线条 这里暂不刷新 前面有
ctx.beginPath()
ctx.lineWidth = .5;
ctx.strokeStyle = '#ffffff'
ctx.moveTo(tp.x, tp.y)
ctx.lineTo(p.x, p.y)
ctx.stroke()

// 求出引力点,相对于当前点p 的位置 弧度 及距离
let rad = Math.atan2((tp.x - p.x), (tp.y - p.y))
let dis = Math.pow(Math.pow(tp.x - p.x, 2) + Math.pow(tp.y - p.y, 2), .5)

// 引力公式 f = g(M*m/r^2)
let f = g * (tp.m * p.m / Math.pow(dis, 2))
f = f > 2 ? 2 : f

// xy轴的引力向量
let fx = Math.sin(rad) * f
let fy = Math.cos(rad) * f

// 加速度 收到影响
p.ax += fx / p.m
p.ay += fy / p.m
}
}
}
}
}
  1. 说明