-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathparticles2.ts
More file actions
274 lines (244 loc) · 9.36 KB
/
Copy pathparticles2.ts
File metadata and controls
274 lines (244 loc) · 9.36 KB
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
271
272
273
274
namespace Simulation {
const vecAllocator = {
allocate(): Vec2 { return Vec2.allocateFromPool() },
release(v: Vec2): void { v.returnToPool() }
}
const Position = new ComponentType("Position", vecAllocator)
const Velocity = new ComponentType("Velocity", vecAllocator)
function moveSystem(context: Context) {
const group = context.createGroupWith(Position, Velocity)
const dp = new Vec2
return function move(dt: number) {
group.each((p, v) => {
// dp = v dt
dp.copyFrom(v)
dp.scale(dt)
p.add(dp)
})
}
}
class RigidBodyInfo {
force: Vec2 = new Vec2()
mass: number = 1.0
clear() { this.force.clear(); this.mass = 1.0 }
}
const RigidBody = new ComponentType("RigidBody", new PoolAllocator(RigidBodyInfo))
function accelerationSystem(context: Context) {
const group = context.createGroupWith(Velocity, RigidBody)
const dv = new Vec2
return function accelerate(dt: number) {
group.each((v, body) => {
// dv = a dt = F/m dt
dv.copyFrom(body.force)
dv.scale(dt / body.mass)
v.add(dv)
body.force.clear()
})
}
}
function gravitySystem(context: Context) {
const group = context.createGroupWith(RigidBody)
const df = new Vec2
return function applyGravity() {
group.each((body, entity) => {
df.set(0, -9.8)
df.scale(body.mass)
body.force.add(df)
// df = -9.8 * m [down]
})
}
}
const RAINDROP_MASS = 0.004 // kg
const GRAVITY = 9.8 // m/s^2
const TERMINAL_VELOCITY = 10.0 // m/s
const TERMINAL_VELOCITY2 = TERMINAL_VELOCITY * TERMINAL_VELOCITY
function dragSystem(context: Context) {
const group = context.createGroupWith(Velocity, RigidBody)
const df = new Vec2
const vDiff = new Vec2
const dragFactor = (RAINDROP_MASS * GRAVITY) / TERMINAL_VELOCITY2
return function applyDrag(vWind: Vec2) {
group.each((v, body, entity) => {
vDiff.copyFrom(v)
vDiff.subtract(vWind)
df.copyFrom(vDiff)
df.scale(-dragFactor * vDiff.length())
body.force.add(df)
// df = -dragFactor * v
})
}
}
function recycleParticlesSystem(context: Context, width: number, height: number) {
const toRecycle: Entity[] = []
const group = context.createGroupWith(Position, ParticleAppearance)
return function recycleParticlesSystem(dt: number) {
group.each((position, _, entity) => {
if (position.y < 0) {
resetParticle(entity)
}
})
}
}
/**
* Render particles
*/
const TRAIL_SIZE = 6
class ParticleAppearanceInfo {
radius: number
color: string
trail: Vec2[] // Circular buffer of size TRAIL_SIZE
trailIndex: number
constructor() {
this.trail = []
for (let i = 0; i < TRAIL_SIZE; i++) {
this.trail.push(Vec2.allocateFromPool())
}
}
set(radius: number, color: string) { this.radius = radius; this.color = color }
resetTrail(position: Vec2) {
this.trailIndex = 0
for (let i = 0; i < TRAIL_SIZE; i++) {
this.trail[i].copyFrom(position)
}
}
addToTrail(position: Vec2) {
this.trail[this.trailIndex++].copyFrom(position)
this.trailIndex %= TRAIL_SIZE
}
}
const ParticleAppearance = new ComponentType("ParticleAppearance", new PoolAllocator(ParticleAppearanceInfo))
function particleRenderSystem(context: Context, ctx: CanvasRenderingContext2D, width: number, height: number) {
const PIXELS_PER_METER = 100
const group = context.createGroupWith(Position, ParticleAppearance)
return function renderParticles() {
group.each((p, appearance) => {
appearance.addToTrail(p)
ctx.strokeStyle = appearance.color
ctx.beginPath()
ctx.moveTo(p.x * PIXELS_PER_METER, height - p.y * PIXELS_PER_METER)
for (let i = 0; i < TRAIL_SIZE; i++) {
const index = (i + appearance.trailIndex) % TRAIL_SIZE
const trailPos = appearance.trail[index]
ctx.lineTo(trailPos.x * PIXELS_PER_METER, height - trailPos.y * PIXELS_PER_METER)
}
ctx.stroke()
// ctx.fillRect(p.x * PIXELS_PER_METER, height - p.y * PIXELS_PER_METER, appearance.radius, appearance.radius)
})
}
}
function resetParticle(entity: Entity): void {
entity.get(Position).set(3 + 12 * (Math.random() * 2 - 1), 6)
entity.get(Velocity).set(0, -5 - Math.random() * 5) // Random speed between 5m/s and 10m/s down
const rigidBody = entity.get(RigidBody)
rigidBody.clear()
rigidBody.mass = 0.004 // kg
const appearance = entity.get(ParticleAppearance)
appearance.resetTrail(entity.get(Position))
appearance.set(1, "rgba(0, 0, 0, 0.1)")
}
/**
* Bounce particles off of umbrella
*/
function particleBounceSystem(context: Context) {
const group = context.createGroupWith(Position, Velocity, ParticleAppearance)
const umbrellaCenter = new Vec2(3, 1.5)
const radius = 0.75
const r2 = radius * radius
const v1 = new Vec2()
const n = new Vec2()
const e = 0.2 // coefficient of restititution
const e2 = e * e
return function bounceParticles(dt: number) {
group.each((p, v, appearance) => {
// n = p - center
n.copyFrom(p)
n.subtract(umbrellaCenter)
if (n.length2() < r2 && n.dot(v) < 0) {
// Collision!
// Solve for the collision impulse effect
const a = n.length2()
const b = 2 * v.dot(n)
const c = (1 - e2) * v.length2()
// The coefficient of restitution is not always
// satisfiable. In those cases, setting the determinant to
// zero has the same effect as using the maximum possible
// coefficient of restitution.
const det = Math.max(0, b*b - 4 * a * c)
const k = (-b + Math.sqrt(det)) / (2 * a)
v1.copyFrom(n)
v1.scale(k)
v1.add(v)
// Update!
v.copyFrom(v1)
// Correct position to be at the surface of the umbrella
p.copyFrom(n)
p.scale(radius / n.length())
p.add(umbrellaCenter)
}
})
}
}
/**
* Generate new particle
*/
function createParticle(context: Context, width: number, height: number): void {
const entity = context.createEntity()
entity.add(Position)
entity.add(Velocity)
entity.add(RigidBody)
entity.add(ParticleAppearance)
resetParticle(entity)
}
function startTick(cb: () => void) {
// Chrome seems to leak memory from requestAnimationFrame loops.
// https://bugs.chromium.org/p/chromium/issues/detail?id=120186
(function tick() {
cb()
requestAnimationFrame(tick)
})()
}
export function main(canvas?: HTMLCanvasElement) {
if (!canvas) {
canvas = document.createElement("canvas")
document.body.appendChild(canvas)
}
const ctx = canvas.getContext("2d")!
const width = 600
const height = 600
canvas.width = width
canvas.height = height
const simContext = new Context()
// Set up systems
const recycleParticles = recycleParticlesSystem(simContext, 6, 6)
const applyGravity = gravitySystem(simContext)
const applyDrag = dragSystem(simContext)
const accelerate = accelerationSystem(simContext)
const move = moveSystem(simContext)
const bounceParticles = particleBounceSystem(simContext)
const renderParticles = particleRenderSystem(simContext, ctx, width, height)
const windSpeed = new Vec2(0, 0)
const MAX_WIND_SPEED = 40.0 // m/s
canvas.addEventListener("mousemove", (ev) => {
const xDirection = ev.clientX / width - 0.5
windSpeed.set(MAX_WIND_SPEED * xDirection, 0)
})
function tick() {
if (simContext.entities.length < 2000) {
for (let i = 0; i < 5; i++) {
createParticle(simContext, width, height)
}
}
const dt = 1/60.0
recycleParticles(dt)
applyGravity()
applyDrag(windSpeed)
accelerate(dt)
move(dt)
bounceParticles(dt)
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, width, height)
renderParticles()
}
startTick(tick)
}
}