Shapes = do ->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]
nullCentered 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, sAn 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,.30Return 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 neededAdd 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 htmlTwo 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()
shapeDraw 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()
sConvert 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] = slotDraw 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)