Skip to content

Instantly share code, notes, and snippets.

@willbailey
Created September 16, 2012 16:30
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save willbailey/3733089 to your computer and use it in GitHub Desktop.
Save willbailey/3733089 to your computer and use it in GitHub Desktop.
flocking algorithm
###
CoffeeScript port of http://processing.org/learning/topics/flocking.html
###
window.FlockingSketch = class FlockingSketch
# setup the environment and start the draw loop
constructor: ->
@setup()
@draw()
# setup the canvas and create the flock of boids
setup: ->
@canvas = document.getElementById "sketch"
@ctx = @canvas.getContext "2d"
@canvas.addEventListener 'click', @onclick
@size 500, 500
@flock = new Flock @
i = 150
while(i--)
# vector = new Vector random(0, @width), random(0, @height)
vector = new Vector @width/2, @height/2
boid = new Boid vector, 3, 0.05
@flock.addBoid boid
# clear the screen
clear: -> @ctx.clearRect 0, 0, @width, @height
# draw the frame based on the current boids flock state
draw: =>
@clear()
@flock.run()
requestAnimationFrame @draw
# add more boids where the user clicked
onclick: (ev) =>
[x, y] = [ev.offsetX, ev.offsetY]
vector = new Vector x, y
boid = new Boid vector, 2, 0.05
@flock.addBoid boid
# set the size of the sketch
size: (width, height) -> [@width, @height] = arguments
# wrapper for all the boids in the simulation
class Flock
# create the flock container and keep a reference to the sketch
constructor: (sketch)-> [@sketch, @boids] = [sketch, []]
# run each boid in the flock
run: -> boid.run(@boids) for boid in @boids
# add a boid to the flock
addBoid: (boid) ->
boid.sketch = @sketch
@boids.push boid
class Boid
# sketch the boid is a part of
sketch: null
# current location of the boid
loc: null
# current velocity of the boid
vel: null
# current acceleration of the boid
acc: null
# size of the boids sides
r: 3
# maximum force that can be exerted by any vector
maxforce: null
# maximum speed of the boid
maxspeed: null
# setup the initial boid state
constructor: (l, ms, mf) ->
@acc = new Vector 0, 0
@vel = new Vector random(-1,1), random(-1,1)
@loc = l.get()
@maxspeed = ms
@maxforce = mf
run: (boids) ->
@flock boids
@update()
@borders()
@render()
flock: (boids) ->
# calculate the three rules: separation, alignment, cohesion
@sep = @separate boids
@ali = @align boids
@coh = @cohesion boids
# weight the forces -- controllable in the ui via slider
@sep.mult document.getElementById('sep').value
@ali.mult document.getElementById('ali').value
@coh.mult document.getElementById('coh').value
# add the forces to the boid's acceleration vector
@acc.add @sep
@acc.add @ali
@acc.add @coh
# move the boid up to maxspeed
update: ->
@vel.add @acc
@vel.limit @maxspeed
@loc.add @vel
@acc.mult 0
# steer the boid toward a target
steer: (target, slowdown) ->
desired = Vector.sub target, @loc
d = desired.mag()
if d > 0
desired.normalize()
if slowdown and d < 100
desired.mult @maxspeed*(d/100)
else
desired.mult @maxspeed
steer = Vector.sub desired, @vel
steer.limit @maxforce
else
steer = new Vector
steer
render: ->
ctx = @sketch.ctx
theta = @vel.heading2D() + 270 * (Math.PI/180);
ctx.fillStyle = '#333333';
ctx.save()
ctx.translate @loc.x, @loc.y
ctx.rotate theta
ctx.beginPath()
ctx.moveTo 0, @r*2
ctx.lineTo @r, -@r*2
ctx.lineTo -@r, -@r*2
ctx.lineTo 0, @r*2
ctx.fill()
ctx.closePath()
ctx.restore()
# wrap around
borders: ->
@loc.x = @sketch.width + @r if @loc.x < -@r
@loc.y = @sketch.height + @r if @loc.y < -@r
@loc.x = -@r if (@loc.x > @sketch.width + @r)
@loc.y = -@r if (@loc.y > @sketch.width + @r)
# check for nearby boids and steer away
separate: (boids)->
desiredseparation = 20
steer = new Vector
count = 0
# check if any are too close
for other in boids
d = Vector.dist @loc, other.loc
if d > 0 and d < desiredseparation
diff = Vector.sub @loc, other.loc
diff.normalize()
diff.div d
steer.add diff
count++
steer.div count if count > 0
if steer.mag() > 0
steer.normalize()
steer.mult @maxspeed
steer.sub @vel
steer.limit @maxforce
steer
# for every nearby boid in the system calculate the average # velocity
align: (boids)->
neighbordist = 25
steer = new Vector
count = 0
for other in boids
d = Vector.dist @loc, other.loc
if d > 0 && d < neighbordist
steer.add other.vel
count++
steer.div count if count > 0
if steer.mag() > 0
steer.normalize()
steer.mult @maxspeed
steer.sub @vel
steer.limit @maxforce
steer
# for the average location (i.e. center) of all nearby
# boids, calculate steering vector towards that location
cohesion: (boids)->
neighbordist = 25
sum = new Vector
count = 0
for other in boids
d = @loc.dist other.loc
if d > 0 and d < neighbordist
sum.add other.loc
count++
if count > 0
sum.div count
@steer sum, false
else
sum
# listen for document load and startup the Flocking sketch
document.addEventListener 'DOMContentLoaded', -> new FlockingSketch
window.random = random = (low, hi)->
r = Math.abs(low) + Math.abs(hi) + 1
res = Math.random() * r
res = Math.floor(res) + low
window.requestAnimationFrame = (func)->
webkitRequestAnimationFrame func
# setTimeout func, 10
window.Vector = class Vector
x: 0
y: 0
z: 0
constructor: (x=0,y=0,z=0)-> @set x, y, z
add: (v)->
@x += v.x
@y += v.y
@z += v.z
get: -> new Vector @x, @y, @z
cross: (v)->
new Vector @y * v.z - v.y * @z, @z * v.x - v.z * @x, @x * v.y - v.x * @y
div: (v)->
v = new Vector v, v, v if typeof v is 'number'
@x /= v.x
@y /= v.y
@z /= v.z
dist: (v)->
v = new Vector v, v, v if typeof v is 'number'
dx = v.x - @x
dy = v.y - @y
dz = v.z - @z
Math.sqrt dx*dx + dy*dy + dz*dz
dot: (v)-> @x * v.x + @y * v.y + @z * v.z
heading2D: -> -Math.atan2 -@y, @x
limit: (high)->
if @mag() > high
@normalize()
@mult(high)
mag: -> Math.sqrt @x * @x + @y * @y + @z * @z
mult: (v)->
@x *= v.x || v
@y *= v.y || v
@z *= v.z || v
normalize: ->
m = @mag()
@div m if m > 0
set: (x=0, y=0, z=0)-> [ @x, @y, @z ] = [ x, y, z ]
sub: (v)->
@x -= v.z
@y -= v.y
@z -= v.z
toArray: -> [@x, @y, @z]
toString: -> "x:#{@x}, y:#{@y}, z:#{@z}"
Vector.sub = (v1, v2, target)->
if target
target.set v1.x - v2.x, v1.y - v2.y, v1.z - v2.z
else
target = new Vector v1.x - v2.x, v1.y - v2.y, v1.z - v2.z
Vector.dist = (v1, v2)-> v1.dist v2
Vector.dot = (v1, v2)-> v1.dot(v2)
Vector.cross = (v1, v2)-> v1.cross(v2)
Vector.angleBetween = (v1, v2)-> Math.acos v1.dot(v2) / (v1.mag() * v2.mag())
exports.Vector = Vector if exports?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment