• shapes.coffee

  • ¶

    A very simple shapes module for drawing NetLogo-like agents.

    Shapes = do ->
  • ¶

    Each shape is a named object with two members: a boolean rotate and a draw procedure and two optional properties: img for images, and shortcut for a transform-less version of draw.

    The shape is used in the following context with a color set and a transform such that the shape should be drawn in a -.5 to .5 square The draw procedure has an optional strokeColor which can be used, if specified, see “bug” for example (“#000000” is the un-set color value). Also see “filledRing” which uses the strokeColor as an outer fillStyle. The shape doesn’t call “fill” which is called by the shape user.

    ctx.save()
    ctx.fillStyle = color.css
    ctx.strokeStyle = strokeColor.css if strokeColor
    ctx.translate x, y; ctx.scale size, size;
    ctx.rotate heading if shape.rotate
    ctx.beginPath(); shape.draw(ctx); ctx.closePath()
    ctx.fill()
    ctx.restore()
    

    The list of current shapes, via ABM.Shapes.names() below, is:

    ["default", "triangle", "arrow", "bug", "pyramid", "circle",
     "square", "pentagon", "ring", "filledRing", "person"]
    
  • ¶

    A polygon utility: c is the 2D context, and a is an array of 2D points. c.closePath() and c.fill() will be called by the calling turtle, see initial discription of drawing context. It is used in adding a new shape above.

      poly = (c, a) ->
        for p, i in a
          if i is 0 then c.moveTo p[0], p[1] else c.lineTo p[0], p[1]
        null
  • ¶

    Centered drawing primitives: centered on x,y with a given width/height size. Useful for shortcuts

      circ = (c, x, y, s) ->  # centered circle
        c.arc x, y, s/2, 0, 2*Math.PI
      ccirc = (c, x, y, s) -> # counter clockwise circ
        c.arc x, y, s/2, 0, 2*Math.PI, true
      cimg = (c, x, y, s, img)-> # Image, flippd for cartesian, y-up.
        c.scale 1, -1
        c.drawImage img, x-s/2, y-s/2, s, s
        c.scale 1, -1
      csq = (c, x, y, s) -> # centered square
        c.fillRect x-s/2, y-s/2, s, s
  • ¶

    An async utility for delayed drawing of images into sprite slots

      fillSlot = (slot, img) ->
        slot.ctx.save()
        slot.ctx.scale 1, -1
        slot.ctx.drawImage img, slot.x, -(slot.y+slot.spriteSize), slot.spriteSize, slot.spriteSize
        slot.ctx.restore()
  • ¶

    The spritesheet data, indexed by sprite size

      spriteSheets = []
  • ¶

    The module returns the following object:

      default:
        rotate: true
        draw: (c) -> poly c, [[.5,0],[-.5,-.5],[-.25,0],[-.5,.5]]
      triangle:
        rotate: true
        draw: (c) -> poly c, [[.5,0],[-.5,-.4],[-.5,.4]]
      arrow:
        rotate: true
        draw: (c) -> poly c, [[.5,0],[0,.5],[0,.2],[-.5,.2],[-.5,-.2],[0,-.2],[0,-.5]]
      bug:
        rotate: true
        draw: (c) ->
          c.strokeStyle = c.fillStyle if c.strokeStyle is "#000000"
          c.lineWidth = .05; poly c, [[.4,.225],[.2,0],[.4,-.225]]; c.stroke()
          c.beginPath(); circ c,.12,0,.26; circ c,-.05,0,.26; circ c,-.27,0,.4
      pyramid:
        rotate: false
        draw: (c) -> poly c, [[0,.5],[-.433,-.25],[.433,-.25]]
      circle: # Note: NetLogo's dot is simply circle with a small size
        shortcut: (c,x,y,s) -> c.beginPath(); circ c,x,y,s; c.closePath(); c.fill()
        rotate: false
        draw: (c) -> circ c,0,0,1 # c.arc 0,0,.5,0,2*Math.PI
      square:
        shortcut: (c,x,y,s) -> csq c,x,y,s
        rotate: false
        draw: (c) -> csq c,0,0,1 #c.fillRect -.5,-.5,1,1
      pentagon:
        rotate: false
        draw: (c) -> poly c, [[0,.45],[-.45,.1],[-.3,-.45],[.3,-.45],[.45,.1]]
      ring:
        rotate: false
        draw: (c) -> circ c,0,0,1; c.closePath(); ccirc c,0,0,.6
      filledRing:
        rotate: false
        draw: (c) ->
          circ(c,0,0,1)
          tempStyle = c.fillStyle # save fill style
          c.fillStyle = c.strokeStyle # use stroke style for larger circle
          c.fill()
          c.fillStyle = tempStyle
          c.beginPath()
          circ(c,0,0,.8)
      person:
        rotate: false
        draw: (c) ->
          poly c, [  [.15,.2],[.3,0],[.125,-.1],[.125,.05],
          [.1,-.15],[.25,-.5],[.05,-.5],[0,-.25],
          [-.05,-.5],[-.25,-.5],[-.1,-.15],[-.125,.05],
          [-.125,-.1],[-.3,0],[-.15,.2]  ]
          c.closePath(); circ c,0,.35,.30
  • ¶

    Return a list of the available shapes, see above.

      names: ->
        (name for own name, val of @ when val.rotate? and val.draw?)
  • ¶

    Add your own shape. Will be included in names list. Usage:

    ABM.Shapes.add "test", true, (c) -> # bowtie/hourglass
      ABM.Shapes.poly c, [[-.5,-.5],[.5,.5],[-.5,.5],[.5,-.5]]
    

    Note: an image that is not rotated automatically gets a shortcut.

      add: (name, rotate, draw, shortcut) -> # draw can be an image, shortcut defaults to null
        s = @[name] =
          if u.isFunction draw then {rotate,draw} else {rotate,img:draw,draw:(c)->cimg c,.5,.5,1,@img}
        (s.shortcut = (c,x,y,s) -> cimg c,x,y,s,@img) if s.img? and not s.rotate
        s.shortcut = shortcut if shortcut? # can override img default shortcut if needed
  • ¶

    Add local private objects for use by add() and debugging

      poly:poly, circ:circ, ccirc:ccirc, cimg:cimg, csq:csq
      spriteSheets:spriteSheets # export spriteSheets for debugging, showing in html
  • ¶

    Two draw procedures, one for shapes, the other for sprites made from shapes.

      draw: (ctx, shape, x, y, size, rad, color, strokeColor) ->
        if shape.shortcut?
          ctx.fillStyle = color.css unless shape.img? # u.colorStr color
          shape.shortcut ctx,x,y,size
        else
          ctx.save()
          ctx.translate x, y
          ctx.scale size, size if size isnt 1
          ctx.rotate rad if rad isnt 0
          if shape.img? # is an image, not a path function
            shape.draw ctx
          else
            ctx.fillStyle = color.css # u.colorStr color
            ctx.strokeStyle = strokeColor.css if strokeColor # u.colorStr color
            ctx.beginPath(); shape.draw ctx; ctx.closePath()
            ctx.fill()
          ctx.restore()
        shape
  • ¶

    Draw a sprite, called by turtles. The world transform is in effect. see this post for drawing centered rotated images The sprite (s) properties are in pixels, x,y,size in world coordinates.

      drawSprite: (ctx, s, x, y, size, rad) ->
        if rad is 0
          ctx.drawImage s.ctx.canvas, s.x, s.y, s.spriteSize, s.spriteSize,
            x-size/2, y-size/2, size, size
        else
          ctx.save()
          ctx.translate x, y
          ctx.rotate rad
          ctx.drawImage s.ctx.canvas, s.x, s.y, s.spriteSize, s.spriteSize,
            -size/2,-size/2, size, size
          ctx.restore()
        s
  • ¶

    Convert a shape to a sprite by allocating a sprite sheet “slot” and drawing the shape to fit it. Return existing sprite if duplicate.

      shapeToSprite: (name, color, size, strokeColor) ->
        color = Color.convertColor color, "css" # if we're called directly by pgmr
        strokeColor = Color.convertColor strokeColor, "css" if strokeColor?
        spriteSize = Math.ceil size
        shape = @[name]
        index = if shape.img? then name else "#{name}-#{color}"
        ctx = spriteSheets[spriteSize]
  • ¶

    Create sheet for this bit size if it does not yet exist

        unless ctx?
          spriteSheets[spriteSize] = ctx = u.createCtx spriteSize*10, spriteSize
          ctx.nextX = 0; ctx.nextY = 0; ctx.index = {}
  • ¶

    Return matching sprite if index match found

        return foundSlot if (foundSlot = ctx.index[index])?
  • ¶

    Extend the sheet if we’re out of space

        if spriteSize*ctx.nextX is ctx.canvas.width
          u.resizeCtx ctx, ctx.canvas.width, ctx.canvas.height+spriteSize
          ctx.nextX = 0; ctx.nextY++
  • ¶

    Create the sprite “slot” object and install in index object

        x = spriteSize*ctx.nextX; y = spriteSize*ctx.nextY
        slot = {ctx, x, y, spriteSize, name, color, strokeColor, index}
        ctx.index[index] = slot
  • ¶

    Draw the shape into the sprite slot

        if (img=shape.img)? # is an image, not a path function
          if img.height isnt 0 then fillSlot(slot, img)
          else img.onload = -> fillSlot(slot, img)
        else
          ctx.save()
          ctx.scale spriteSize, spriteSize
          ctx.translate ctx.nextX+.5, ctx.nextY+.5
          ctx.fillStyle = color # u.colorStr color
          ctx.strokeStyle = strokeColor if strokeColor
          ctx.beginPath(); shape.draw ctx; ctx.closePath()
          ctx.fill()
          ctx.restore()
        ctx.nextX++
        slot # return the sprite (slot)