- (window or exports).limber = limber =
- version: "0.0.2"
- geometry: {}
- component: {}
- entity: {}
- trait: {}
- engine: {}
- #convenience methods
- vector: (x, y) -> new limber.geometry.Vector(x, y)
- box: (x0, y0, x1, y1) -> new limber.geometry.Box(x0, y0, x1, y1)
- class limber.EventEmitter
- on: (event, cb) ->
- @listeners ||= {}
- @listeners[event] ||= []
- @listeners[event].push(cb)
- @
- echo: (emitter, event, asEvent) ->
- emitter.on event, @trigger(asEvent or event)
- emit: (event, args...) ->
- @listeners ||= {}
- cb.apply(this, args) for cb in @listeners[event] when cb if @listeners[event]
- @
- removeListener: (event, cb) ->
- @listeners ||= {}
- if listeners = @listeners[event]
- index = listeners.indexOf(cb)
- unless index == -1
- listeners[index] = null
- listeners.splice(index, 1)
- @
- removeAllListeners: ->
- @listeners[i] = null for i in @listeners if @listeners
- @
- trigger: (event) ->
- self = this
- (args...) -> self.emit(event, args...)
- class limber.Timer
- constructor: ->
- @lastTick = @currTick = @+ new Date
- tick: ->
- [@currTick, @lastTick] = [+ new Date, @currTick]
- @delta = @currTick - @lastTick
- #-----------#
- # COMPONENT #
- #-----------#
- # limber.Component is the basic component used in the engine
- class limber.component.Component extends limber.EventEmitter
- attach: (component) ->
- @children ||= []
- @children.push(component)
- @emit "attach", component
- component.emit("attached", this)
- @
- attachTo: (component) ->
- component.attach(this)
- @
- detach: (component) ->
- @children ||= []
- unless -1 == (index = @children.indexOf(component))
- @emit "detach", component.emit("detached", this)
- @children[index] = null
- @children.splice(index, 1)
- @
- class limber.component.FPS extends limber.component.Component
- constructor: (id) ->
- @el = document.getElementById(id)
- @size = 20
- @index = 0
- @sum = 0
- @sample = []
- @sample[i] = 0 for i in [0 ... @size]
- @on "attached", (component) ->
- component.on "update", @update
- component.on "render", @render
- update: (engine) =>
- @sum -= @sample[@index]
- @sum += engine.timer.delta
- @sample[@index] = engine.timer.delta
- @index += 1
- @index = 0 if @index == @size
- render: (engine) =>
- @el.innerHTML = Math.floor(1000 / (@sum / @size)) + " FPS"
- #--------- #
- # GEOMETRY #
- #----------#
- class limber.geometry.Vector
- constructor: (x, y) ->
- if x instanceof limber.geometry.Vector
- @x = x.x
- @y = x.y
- else if Array.isArray(x)
- @x = x[0]
- @y = x[1]
- else
- @x = x or 0
- @y = y or 0
- clone: -> new limber.geometry.Vector(this)
- reset: ->
- @x = 0
- @y = 0
- @
- scale: (scalar) ->
- @x *= scalar
- @y *= scalar
- @
- shrink: (scalar) ->
- @x /= scalar
- @y /= scalar
- @
- product: (x, y) ->
- vector = new limber.geometry.Vector(x, y)
- @x *= vector.x
- @y *= vector.y
- @
- add: (x, y) ->
- vector = new limber.geometry.Vector(x, y)
- @x += vector.x
- @y += vector.y
- @
- subtract: (x, y) ->
- vector = new limber.geometry.Vector(x, y)
- @x -= vector.x
- @y -= vector.y
- @
- normalize: ->
- if @x == 0 and @y
- @y = if @y > 0 then 1 else -1
- else if @y == 0 and @y
- @x = if @x > 0 then 1 else -1
- else if (magnitude = @magnitude())
- @x = @x / magnitude
- @y = @y / magnitude
- @
- reflect: (x, y) ->
- normal = new limber.geometry.Vector(x, y).normalize().rotateRight()
- @add(normal.scale(-2 * @dot(normal)))
- @
- dot: (x, y) ->
- if x instanceof limber.geometry.Vector then return @x * x.x + @y * x.y
- else return @x * x + @y * y
- magnitude: -> Math.sqrt(@x * @x + @y * @y)
- magnitudeSq: -> @x * @x + @y * @y
- flipX: ->
- @x = - @x
- @
- flipY: ->
- @y = - @y
- @
- flip: ->
- @x = - @x
- @y = - @y
- @
- rotateRight: ->
- [@x, @y] = [@y, - @x]
- @
- rotateLeft: ->
- [@x, @y] = [- @y, @x]
- @
- class limber.geometry.Projection
- constructor: (min, max) ->
- if min instanceof limber.geometry.Projection
- @min = min.min
- @max = min.max
- else
- @min = min
- @max = max
- overlap: (x, y) ->
- proj = new limber.geometry.Projection(x, y)
- overlap = 0
- unless @max < proj.min or @min > proj.max
- if @min > proj.min and @min < proj.max then overlap = proj.max - @min
- else if @max < proj.max and @max > proj.min then overlap = proj.min - @max
- else
- test = proj.max - proj.min
- overlap = @max - @min
- overlap = test if test < overlap
- overlap = Number.MAX_VALUE if overlap is Number.POSITIVE_INFINITY
- overlap = -Number.MAX_VALUE if overlap is Number.NEGATIVE_INFINITY
- #proj = null
- overlap
- class limber.geometry.MTV
- constructor: (axis, @overlap) ->
- @axis = limber.vector(axis)
- flip: ->
- @axis.flip()
- @
- class limber.geometry.ConvexHull
- constructor: (vertices...) ->
- @vertices = for vertex in vertices then limber.vector(vertex)
- add: (x, y) ->
- vertex.add(x, y) for vertex in @vertices
- @
- subtract: (x, y) ->
- vertex.subtract(x, y) for vertex in @vertices
- @
- testCollision: (convex) ->
- return @testPolygonPolygon(convex)
- test = "test"
- test +=
- if this instanceof limber.geometry.Polygon then "Polygon"
- else if this instanceof limber.geometry.Circle then "Circle"
- test +=
- if convex instanceof limber.geometry.Polygon then "Polygon"
- else if convex instanceof limber.geometry.Circle then "Circle"
- throw new Error("Collisions method not implemented: #{test}") unless this[test]
- method = this[test]
- method.call(this, convex)
- testPolygonPolygon: (convex) ->
- minOverlap = Number.POSITIVE_INFINITY
- minAxis = null
- multAxes = false
- axes = @getFaceNormals()
- axes.concat(convex.getFaceNormals()) unless this instanceof limber.geometry.AABB and convex instanceof limber.geometry.AABB
- for axis in axes
- proj1 = @projectOnto(axis)
- proj2 = convex.projectOnto(axis)
- overlap = proj1.overlap(proj2)
- return unless overlap
- if overlap == minOverlap
- minAxis.add(axis)
- multAxes = true
- else if overlap < minOverlap
- multAxes = false
- minOverlap = overlap
- minAxis = axis.clone()
- minAxis.normalize() if multAxes
- new limber.geometry.MTV(minAxis, minOverlap)
- projectOnto: (x, y) ->
- return new limber.geometry.Projection(0, 0) unless @vertices and @vertices.length
- axis = limber.vector(x, y)
- min = Number.POSITIVE_INFINITY
- max = Number.NEGATIVE_INFINITY
- for vertex in @vertices
- d = axis.dot(vertex)
- min = if d < min then d else min
- max = if d > max then d else max
- return new limber.geometry.Projection(min, max)
- getFaceNormals: ->
- @normals or @normals = for vertex, i in @vertices
- @vertices[(i + 1) % @vertices.length].clone().subtract(vertex).rotateRight().normalize()
- addNormal: (x, y) ->
- @normals ||= []
- @normals.push(limber.vector(x, y))
- @
- addVertex: (x, y) ->
- @vertices ||= []
- @vertices.push(limber.vector(x, y))
- @
- getVertices: -> @vertices or []
- class limber.geometry.Polygon extends limber.geometry.ConvexHull
- class limber.geometry.AABB extends limber.geometry.Polygon
- constructor: (x0, y0, x1, y1) ->
- if x0 instanceof limber.geometry.AABB
- @bl = x0.bl.clone()
- @tr = x0.tr.clone()
- else if arguments.length == 2
- @bl = new limber.geometry.Vector(x0)
- @tr = new limber.geometry.Vector(y0)
- else if arguments.length == 4
- @bl = new limber.geometry.Vector(x0, y0)
- @tr = new limber.geometry.Vector(x1, y1)
- @addVertex(@bl.x, @bl.y)
- @addVertex(@bl.x, @tr.y)
- @addVertex(@tr.x, @tr.y)
- @addVertex(@tr.x, @bl.y)
- @normals = [limber.vector(1, 0), limber.vector(0, 1)]
- clone: -> new limber.geometry.AABB(@bl, @tr)
- add: (x, y) ->
- @bl.add(x, y)
- @tr.add(x, y)
- super(x, y)
- subtract: (x, y) ->
- @bl.subtract(x, y)
- @tr.subtract(x, y)
- super(x, y)
- getFaceNormals: -> @normals
- toVector: -> @tr.clone().subtract(@bl)
- class limber.geometry.Wall extends limber.geometry.AABB
- constructor: (x, y, offset, yoff) ->
- #NOTE: ONLY WORKS FOR AABB WALLS FOR NOW
- if x
- @addVertex(offset, -Number.MAX_VALUE)
- @addVertex(offset, +Number.MAX_VALUE)
- @addVertex(-x * Number.MAX_VALUE, yoff)
- else
- @addVertex(-Number.MAX_VALUE, offset)
- @addVertex(+Number.MAX_VALUE, offset)
- @addVertex(yoff, -y * Number.MAX_VALUE)
- @addNormal(x, y)
- normal = null
- # Special case for Walls
- testPolygonCircle: (circle) ->
- class limber.geometry.Circle extends limber.geometry.ConvexHull
- #--------#
- # ENGINE #
- #--------#
- class limber.engine.Canvas2D extends limber.component.Component
- constructor: (id, width, height) ->
- @canvas = document.getElementById(id)
- @canvas.addEventListener("mousemove", @onMouseMove, false)
- #WebGL2D.enable(@canvas)
- @canvas.width = width
- @canvas.height = height
- @context = @canvas.getContext("2d")
- @timer = new limber.Timer
- @mousePosition = limber.vector()
- @context.translate(0, height)
- @context.scale(1, -1)
- animate: ->
- renderer = this
- canvas = @canvas
- nextFrame = window.requestAnimationFrame or
- window.webkitRequestAnimationFrame or
- window.mozRequestAnimationFrame or
- window.oRequestAnimationFrame or
- window.msRequestAnimationFrame or
- null
- @timer.tick()
- innerFrame = ->
- renderer.timer.tick()
- renderer.emit("update", renderer)
- renderer.context.clearRect(0, 0, canvas.width, canvas.height)
- renderer.emit("render", renderer)
- if nextFrame != null
- recursiveFrame = ->
- innerFrame()
- nextFrame(recursiveFrame)
- nextFrame(recursiveFrame)
- else
- ONE_FRAME_TIME = 1000.0 / 60.0
- setInterval(innerFrame, ONE_FRAME_TIME)
- onMouseMove: (e) =>
- @mousePosition =
- if e.pageX or e.pageY then limber.vector(e.pageX, e.pageY)
- else limber.vector(e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft, e.clientY + document.body.scrollTop + document.documentElement.scrollTop)
- @mousePosition
- .flipY().add(0, @canvas.height) # To switch to our coordinate scale
- .subtract(@canvas.offsetLeft, @canvas.offsetTop)
- #--------#
- # ENTITY #
- #--------#
- class limber.entity.Entity extends limber.component.Component
- constructor: (args...) ->
- self = this
- update = (args...) -> self.emit "update", args...
- render = (args...) -> self.emit "render", args...
- @components = []
- @on "update", @update
- @on "render", @render
- @on "attached", (component) ->
- component.on "update", update
- component.on "render", render
- self.components.push(component)
- @on "detached", (component) ->
- component.removeListener "update", update
- component.removeListener "render", render
- @initialize(args...)
- destroy: ->
- @emit "detached", component for component in @components
- @emit "destroy", this
- #@mixout(trait) for provide, trait of @traits if @traits
- @removeAllListeners()
- mixin: (trait) ->
- provides = if Array.isArray(trait.provides) then trait.provides else [trait.provides]
- requires = if Array.isArray(trait.requires) then trait.requires else [trait.requires]
- @traits ||= {}
- @requireTraits(requires)
- trait.emit "mixin", this
- trait.augment(this)
- @traits[name] = trait for name in provides
- @
- mixout: (trait) ->
- provides = if Array.isArray(trait.provides) then trait.provides else [trait.provides]
- trait.emit "mixout", this
- @traits ||= {}
- for provide in provides
- @traits[provide] = null
- delete @traits[provide]
- @
- requireTraits: (requires...) ->
- requires = requires[0] if Array.isArray(requires[0])
- for name in requires
- throw new Error("Missing required trait: #{name}") unless @traits[name]
- @
- attached: -> @
- detached: -> @
- initialize: -> @
- update: (component) -> @
- render: (component) -> @
- #-------#
- # TRAIT #
- #-------#
- class limber.trait.Trait extends limber.EventEmitter
- provides: []
- requires: []
- constructor: (args...) ->
- self = this
- @initialize(args...)
- destroy: ->
- @removeAllListeners()
- initialize: -> @
- augment: -> @
- class limber.trait.Position extends limber.trait.Trait
- provides: "position"
- initialize: (x, y) -> @position = limber.vector(x, y)
- augment: (entity) -> entity.position = @position
- class limber.trait.Velocity extends limber.trait.Trait
- provides: "velocity"
- requires: "position"
- initialize: (x, y) -> @velocity = limber.vector(x, y)
- augment: (entity) ->
- entity.velocity = @velocity
- entity.on "update", (engine) ->
- @position.add @velocity.clone().scale(engine.timer.delta / 1000)
- class limber.trait.Acceleration extends limber.trait.Trait
- provides: "acceleration"
- requires: "velocity"
- initialize: (x, y) -> @acceleration = limber.vector(x, y)
- augment: (entity) ->
- entity.acceleration = @acceleration
- entity.on "update", (engine) ->
- @velocity.add @acceleration
- @acceleration.reset()
- class limber.trait.AABBBody extends limber.trait.Trait
- provides: "body"
- initialize: (x0, y0, x1, y1) -> @aabb = new limber.geometry.AABB(x0, y0, x1, y1)
- augment: (entity) -> entity.body = @aabb
- class limber.trait.Bounded extends limber.trait.Trait
- requires: ["position", "body"]
- constructor: (x0, y0, x1, y1) ->
- boundaries = [
- new limber.geometry.Wall(1, 0, x0, y1)
- new limber.geometry.Wall(0, 1, y0, x1)
- new limber.geometry.Wall(-1, 0, x1, y0)
- new limber.geometry.Wall(0, -1, y1, x0)
- ]
- self = this
- @on "augment", (entity) ->
- entity.on "update", (engine) ->
- entity = @
- bounds = entity.body.clone().add(entity.position)
- for wall in boundaries
- if mtv = bounds.testCollision(wall)
- @emit "collision", engine, mtv, wall
- class limber.trait.Wrapped extends limber.trait.Trait
- requires: ["position", "body"]
- initialize: (@x0, @y0, @x1, @y1) ->
- augment: (entity) ->
- self = this
- entity.on "update", (engine) ->
- if @position.x < self.x0 then @position.x = self.x1 - self.x0 - entity.position.x
- else if @position.x > self.x1 then @position.x = self.x0 - self.x1 + entity.position.x
- if @position.y < self.y0 then @position.y = self.y1 - self.y0 - entity.position.y
- else if @position.y > self.y1 then @position.y = self.y0 - self.y1 + entity.position.x
- @
- class limber.trait.Comparator extends limber.trait.Trait
- provides: "comparator"
- initialize: -> @setup = false
- augment: (entity) ->
- self = this
- entity.on "render", (engine) ->
- self.seenInFrame = []
- entity.emit "comparator:teardown", engine, this
- entity.on "update", (engine) ->
- self.seenInFrame or self.seenInFrame = []
- entity.emit "comparator:setup", engine, this
- entity.emit "comparator:compare", engine, this, other for other in self.seenInFrame
- self.seenInFrame.push(this)
- class limber.trait.CollisionDetection extends limber.trait.Trait
- provides: "area"
- requires: ["position", "body", "comparator"]
- augment: (entity) ->
- entity.on "comparator:setup", @setup
- entity.on "comparator:compare", @compare
- setup: (engine, entity) ->
- entity.area = entity.body.clone().add(entity.position)
- compare: (engine, entity, other) ->
- if mtv = entity.area.testCollision(other.area)
- entity.emit "collision", engine, mtv, other
- other.emit "collision", engine, mtv.flip(), entity
- class limber.trait.CollisionAvoidance extends limber.trait.Trait
- requires: ["position", "velocity", "acceleration", "body", "comparator"]
- provides: "avoidance"
- initialize: (@short, @far, @closeWidth, @farWidth) ->
- augment: (entity) ->
- entity.on "comparator:setup", @setup
- entity.on "comparator:compare", @compare
- setup: (engine, entity) =>
- #console.log "Avoid.setup", arguments...
- accel = new limber.geometry.Vector
- vel = entity.velocity.clone()
- dir = vel.clone().normalize()
- pos = entity.position.clone().add(vel.clone().scale(@short))
- normal = dir.clone().rotateRight()
- ahead = @far - @short
- w = (@farWidth - @closeWidth) / 2
- entity.avoidance = new limber.geometry.Polygon
- entity.avoidance.addVertex(pos.add(normal.clone().scale(@closeWidth / 2))) #side, right
- entity.avoidance.addVertex(pos.add(vel.clone().scale(ahead).add(normal.clone().scale(w)))) #front, right
- entity.avoidance.addVertex(pos.add(normal.clone().scale(- @farWidth))) #front, left
- entity.avoidance.addVertex(pos.add(vel.clone().scale(- ahead).add(normal.clone().scale(w)))) #side, left
- entity.avoidance.avoid = false
- compare: (engine, entity, other) =>
- #console.log "Avoid.compare", arguments...
- if mtv = entity.avoidance.testCollision(other.avoidance)
- entity.avoidance.avoid = true
- entity.acceleration.add(mtv.axis.scale(mtv.overlap))
- other.avoidance.avoid = true
- other.acceleration.subtract(mtv.axis) #Already scaled above
- class limber.trait.Flocking extends limber.trait.Trait
- requires: ["position", "velocity", "acceleration"]
- initialize: ->
- @flock = []
- augment: (entity) ->
- self = this
- self.flock.push(entity)
- entity.on "update", -> self.steer(entity)
- steer: (entity) ->
- approach = new limber.geometry.Vector(0, 0)
- avoid = new limber.geometry.Vector(0, 0)
- match = new limber.geometry.Vector(0, 0)
- accel = new limber.geometry.Vector(0, 0)
- approach.n = avoid.n = match.n = 0
- for boid in @flock when boid != entity
- dist = boid.position.clone().subtract(entity.position)
- dist2 = dist.magnitudeSq()
- if dist2 < 10000
- approach.add(dist)
- approach.n++
- if dist2 < 1000
- avoid.subtract(dist)
- avoid.n++
- if dist2 < 10000
- match.add(boid.velocity.clone())
- match.n++
- approach.shrink(approach.n) if approach.n
- avoid.shrink(avoid.n) if avoid.n
- match.shrink(match.n) if match.n
- accel
- .add(approach).shrink(20)
- .add(avoid)
- .add(match).shrink(10)
- .normalize()
- .scale(20)
- #unless not @once and accel.magnitude()
- # @once = true
- # console.log "WTF", accel
- entity.acceleration.add(accel)
- class limber.trait.CollisionResponse extends limber.trait.Trait
- requires: "velocity"
- constructor: ->
- @on "augment", (entity) ->
- entity.on "render", -> @collided = false
- entity.on "collision", (engine, mtv, other) ->
- @collided = true
- @position.add(mtv.axis.clone().scale(mtv.overlap / 2))
- class limber.trait.RandomAcceleration extends limber.trait.Trait
- requires: "acceleration"
- constructor: (@speed = 20) ->
- augment: (entity) ->
- speed = @speed
- twiceSpeed = speed * 2
- entity.on "update", (engine) ->
- @acceleration.add(Math.random() * twiceSpeed - speed, Math.random() * twiceSpeed - speed)
- class limber.trait.ConstrainVelocity extends limber.trait.Trait
- requires: "velocity"
- constructor: (@min = 20, @max = 200) ->
- augment: (entity) ->
- min = @min
- max = @max
- minSq = min * min
- maxSq = max * max
- entity.on "update", (engine) ->
- v2 = @velocity.magnitudeSq()
- if v2 > maxSq then @velocity.scale(Math.sqrt(v2) / maxSq)
- else if v2 < minSq then @velocity.scale(min / Math.sqrt(v2))
- class limber.trait.ConstrainAcceleration extends limber.trait.Trait
- requires: "velocity"
- constructor: (@max = 20) ->
- augment: (entity) ->
- max = @max
- maxSq = max * max
- entity.on "update", (engine) ->
- v2 = @acceleration.magnitudeSq()
- if v2 > maxSq then @acceleration.normalize().scale(max)
- class limber.trait.AnimatedSprite extends limber.trait.Trait
- @imgCache = []
- requires: ["position", "velocity"]
- provides: "sprite"
- initialize: (src, @frameW, @frameH, @w, @h, @framesPerRow = 1, @frameCount = 1, @frameTime = 100) ->
- cache = limber.trait.AnimatedSprite.imgCache
- unless cache[src]
- cache[src] = new Image
- cache[src].src = src
- @image = cache[src]
- @frame = 0
- @xOff = 0
- @yOff = 0
- @nextFrame = @frameTime
- augment: (entity) ->
- self = this
- render = (engine) ->
- engine.context.save()
- engine.context.translate(entity.position.x, entity.position.y)
- engine.context.rotate(Math.atan2(entity.velocity.y, entity.velocity.x) + Math.PI / 2)
- engine.context.drawImage(self.image, self.frameW * self.xOff, self.frameH * self.yOff, self.frameW, self.frameH, - self.w / 2, - self.h / 2, self.w, self.h)
- engine.context.restore()
- update = (engine) ->
- self.nextFrame -= engine.timer.delta
- if self.nextFrame <= 0
- self.frame += 1
- if self.frame == self.frameCount
- self.frame = 0
- @emit "sprite:reset", this
- self.nextFrame = self.frameTime + self.nextFrame
- self.xOff = self.frame % self.framesPerRow
- self.yOff = Math.floor(self.frame / self.framesPerRow)
- entity.on "render", render
- entity.on "update", update
- entity.on "destroy", (entity) ->
- entity.removeListener "render", render
- entity.removeListener "update", update