cssAudio - Activefile-genericCSS - ActiveGeneric - ActiveHTML - ActiveImage - ActiveJS - ActiveSVG - ActiveText - Activefile-genericVideo - ActiveLovehtmlicon-new-collectionicon-personicon-teamlog-outoctocatpop-outspinnerstartv

Pen Settings

CSS Base

Vendor Prefixing

Add External CSS

These stylesheets will be added in this order and before the code you write in the CSS editor. You can also add another Pen here, and it will pull the CSS from it. Try typing "font" or "ribbon" below.

Quick-add: + add another resource

Add External JavaScript

These scripts will run in this order and before the code in the JavaScript editor. You can also link to another Pen here, and it will run the JavaScript from it. Also try typing the name of any popular library.

Quick-add: + add another resource

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

            
              <canvas id="canvas" width="400" height="400"></canvas>
<ul>
  <li>move the mouse around the field to generate flow fields leading to that point</li>
  <li>the system is optimized by breaking the map into many smaller sectors to generate the flow fields then stitching them together</li>
</ul>
            
          
!
            
              rng = new Chance 19870910

options = {
  width: 5
  height: 5
  sector:
    width: 4
    height: 4
  tile:
    width: 20
    height: 20
}
options.grid = {
  width: options.sector.width * options.tile.width
  height: options.sector.height * options.tile.height
}

class FlowField
  neighborDirections: [
    [ 0, -1]
    [ 1,  0]
    [ 0,  1]
    [-1,  0]
    #[ 1, -1]
    #[ 1,  1]
    #[-1,  1]
    #[-1, -1]
  ]

  constructor: (@x, @y, @gridData) ->

  callback: (node) =>
    return unless node.data.open
    node.visited = true
    node.iteration = 1
    @openNodes.push node

  directionFlow: (vX, vY, costCallback, flowCallback) ->
    @reset()
    @openNodes = []
    {width, height} = options.sector
    lX = if vX is -1 then 0 else width - 1
    lY = if vY is -1 then 0 else height - 1
    offsetX = options.sector.width * @x
    offsetY = options.sector.height * @y
    if vX is - 1 or vX is 1
      for y in [0...height]
        node = @gridData.getNode lX + offsetX, y + offsetY
        @callback node
    if vY is -1 or vY is 1
      for x in [0...width]
        node = @gridData.getNode x + offsetX, lY + offsetY
        @callback node
    @costMap 1, costCallback
    @flowMap flowCallback
    if vX is -1 or vX is 1
      for y in [0...height]
        node = @gridData.getNode lX + offsetX, y + offsetY
        continue unless node.data.open
        vector = @neighborVector node, vX, 0
        flowCallback lX, y, vector
    if vY is -1 or vY is 1
      for x in [0...width]
        node = @gridData.getNode x + offsetX, lY + offsetY
        continue unless node.data.open
        vector = @neighborVector node, 0, vY
        flowCallback x, lY, vector

  neighborVector: (node, vX, vY) -> # bug here, sometimes vectors point towards eachother
    neighborNode = @gridData.getNode node.x + vX, node.y + vY
    if neighborNode.data.open
      vector = {x: vX, y: vY}
    else
      offsetX = options.sector.width * @x
      offsetY = options.sector.height * @y
      {width, height} = options.sector
      neighborNodes = @collectNeighbors node, (n) ->
        rX = n.x - offsetX
        rY = n.y - offsetY
        return false unless rX < width and rY < height
        return false unless rX >= 0 and rY >= 0
        n.iteration >= 0
      for n in neighborNodes
        if n.data.open
          vector = {
            x: n.x - node.x
            y: n.y - node.y
          }
          break
    vector

  process: (x, y, costCallback, flowCallback) ->
    @reset()
    offsetX = options.sector.width * @x
    offsetY = options.sector.height * @y
    @openNodes = [@gridData.getNode x + offsetX, y + offsetY]
    @costMap 0, costCallback
    @flowMap flowCallback

  flowMap: (flowCallback) ->
    {width, height} = options.sector
    offsetX = options.sector.width * @x
    offsetY = options.sector.height * @y
    for y in [0...height]
      for x in [0...width]
        node = @gridData.getNode x + offsetX, y + offsetY
        continue unless node.data.open
        continue unless node.visited
        neighbors = @collectNeighbors node, (n) ->
          rX = n.x - offsetX
          rY = n.y - offsetY
          return false unless rX < width and rY < height
          return false unless rX >= 0 and rY >= 0
          n.iteration >= 0
        lowestNode = neighbors.pop()
        neighbors.map (n) ->
          return unless n.data.open
          return unless n.iteration?
          lowestNode = n if lowestNode.iteration > n.iteration
        continue unless lowestNode
        vector = {
          x: lowestNode.x - node.x
          y: lowestNode.y - node.y
        }
        vector = {x: 0, y: 0} if node.iteration is 0
        flowCallback x, y, vector

  reset: ->
    offsetX = options.sector.width * @x
    offsetY = options.sector.height * @y
    {width, height} = options.sector
    for y in [0...height]
      for x in [0...width]
        node = @gridData.getNode x + offsetX, y + offsetY
        node.visited = false

  floodCallback: (node) -> # doesn't feel right to have this here
    return false unless node.data.open
    return false if node.visited
    node.visited = true
    true

  costMap: (iteration, costCallback) ->
    newNodes = []
    while node = @openNodes.pop()
      node.visited = true
      node.iteration = iteration
      {x, y} = node
      costCallback x, y
      nodes = @collectNeighbors node, @floodCallback
      newNodes = newNodes.concat nodes
    @openNodes = @openNodes.concat newNodes
    if @openNodes.length
      @costMap iteration + 1, costCallback

  collectNeighbors: ({x, y}, checkCallback) ->
    {width, height} = options.sector
    nodes = []
    for [dX, dY] in @neighborDirections
      tX = x + dX
      tY = y + dY
      node = @gridData.getNode tX, tY
      continue unless node
      continue unless checkCallback node
      nodes.push node
    nodes

class FlowNode
  constructor: (@x, @y, @iteration, @data = {}) ->

class WorldData
  constructor: (mapData) ->
    @buildData mapData

  buildData: (mapData) ->
    @width = mapData[0].length
    @height = mapData.length
    @nodes = []
    for y in [0...@height]
      @nodes.push []
      for x in [0...@width]
        node = new FlowNode x, y, -Infinity, { open: !!mapData[y][x] }
        @nodes[y][x] = node

  getNode: (x, y) ->
    unless @nodes[y]? and @nodes[y][x]?
      return node = new FlowNode x, y, -Infinity, { open: false }
    @nodes[y][x]

class SectorTile
  directionLookup: {
    '1_0': 0
    '2_1': 90
    '1_2': 180
    '0_1': 270
    '2_0': 45
    '2_2': 135
    '0_2': 225
    '0_0': 315
    '1_1': 0
  }

  constructor: (x, y, @node) ->
    @el = new createjs.Container
    @el.x = x
    @el.y = y
    @render()

  render: ->
    @drawBackground()
    @drawArrow()

  drawBackground: ->
    return if @node.data.open
    g = new createjs.Graphics
    backgroundEl = new createjs.Shape g
    g.f('#880000').dr 0, 0, options.tile.width, options.tile.height
    @el.addChild backgroundEl

  drawArrow: ->
    g = new createjs.Graphics
    @arrowEl = new createjs.Shape g
    width = options.tile.width * 0.25
    height = options.tile.height * 0.75
    x = (options.tile.width - width) / 2
    y = (options.tile.height - height) / 2
    g.lf(['#333333', '#dedede'], [0, 1], 0, 0, 0, height).dr x, y, width, height
    @arrowEl.regX = options.tile.width / 2
    @arrowEl.regY = options.tile.height / 2
    @arrowEl.x = options.tile.width / 2
    @arrowEl.y = options.tile.height / 2
    #@arrowEl.visible = false
    @el.addChild @arrowEl

  setVector: ({x, y}) ->
    return unless @node.data.open
    return if x is 0 and y is 0
    @arrowEl.visible = true
    @arrowEl.rotation = @directionLookup["#{x + 1}_#{y + 1}"]

  clearVector: ->
    @arrowEl.visible = false

class SectorView
  constructor: (@worldData, @x, @y) ->
    @el = new createjs.Container
    @render()
    @flowField = new FlowField @x, @y, @worldData

  reset: ->
    for row, y in @sectorTiles
      for sectorTile, x in row
        sectorTile.clearVector()

  markCost: ->

  markFlow: (x, y, vector) =>
    @sectorTiles[y][x].setVector vector

  render: ->
    @drawTiles()
    @el.cache 0, 0, options.grid.width, options.grid.height

  drawTiles: ->
    @sectorTiles = []
    {width, height} = options.sector
    for y in [0...height]
      @sectorTiles[y] = []
      for x in [0...width]
        pX = x * options.tile.width
        pY = y * options.tile.height
        wX = @x * options.sector.width + x
        wY = @y * options.sector.height + y
        node = @worldData.getNode wX, wY
        sectorTile = new SectorTile pX, pY, node
        @el.addChild sectorTile.el
        @sectorTiles[y][x] = sectorTile

  process: (x, y) ->
    @vX = @vY = undefined
    return unless @sectorTiles[y][x].node.data.open
    @flowField.process x, y, @markCost, @markFlow
    @el.updateCache()

  directionFlow: (vX, vY) ->
    return if vX is @vX and vY is @vY
    @vX = vX
    @vY = vY
    @flowField.directionFlow vX, vY, @markCost, @markFlow
    @el.updateCache()

class GridView
  constructor: (@width, @height) ->
    @el = new createjs.Container
    @createWorldData()
    @addSectors()
    @addEventListeners()
  
  createWorldData: ->
    width = options.width * options.sector.width
    height = options.height * options.sector.height
    @worldData = new WorldData mapData(width, height)

  addSectors: ->
    @sectors = []
    for y in [0...options.height]
      @sectors[y] = []
      for x in [0...options.width]
        pX = x * options.grid.width
        pY = y * options.grid.height
        sectorView = @addSector pX, pY, x, y
        @el.addChild sectorView.el
        @sectors[y][x] = sectorView

  addSector: (x, y, sectorX, sectorY) ->
    sectorView = new SectorView @worldData, sectorX, sectorY
    sectorView.el.x = x
    sectorView.el.y = y
    sectorView

  addEventListeners: ->
    {width, height} = options
    @cX = @cY = undefined
    $('canvas').mousemove @onMouseMove

  onMouseMove: ({offsetX, offsetY}) =>
    sectorX = ~~(offsetX / options.grid.width)
    sectorY = ~~(offsetY / options.grid.height)
    mX = offsetX % options.grid.width
    mY = offsetY % options.grid.height
    x = ~~(mX / options.tile.width)
    y = ~~(mY / options.tile.height)
    if @cX isnt x or @cY isnt y
      @cX = x
      @cY = y
      for sY in [0...options.height]
        for sX in [0...options.width]
          sector = @sectors[sY][sX]
          sector.reset()
          if sX is sectorX and sY is sectorY
            sector.process x, y
          else
            vX = 0
            vY = 0
            if sectorX > sX
              vX = 1
            if sectorX < sX
              vX = -1
            if sectorY > sY
              vY = 1
            if sectorY < sY
              vY = -1
            sector.directionFlow vX, vY

mapData = (width, height) ->
  data = []
  for y in [0...height]
    data[y] = []
    for x in [0...width]
      data[y][x] = rng.bool {likelihood: 92}
  data

createGrid = ->
  gridView = new GridView options.width, options.height
  stageEl.addChild gridView.el
  gridView.onMouseMove {offsetX: 0, offsetY: 0}

stageEl = new createjs.Stage 'canvas'
createjs.Ticker.setFPS 60
createjs.Ticker.addEventListener 'tick', ->
  stageEl.update()

createGrid()

            
          
!
999px
Loading ..................

Console