Tutorial Nim / Canvas

A Nim tutorial, about how to access html5's canvas.

Here is below the file "test1.nim":

import pages

let cnv = getElementById("c1")
let ctx = cnv.getContext2d()

ctx.beginPath()
ctx.fillStyle(10, 230, 80)
ctx.rect(10, 10, 60, 40)
ctx.fill()

And the html page "test1.html":

<!DOCTYPE html>
<html>
<head>
  <title>Canvas Nim</title>
</head>
<body>
  <canvas id="c1" width="240" height="160" style="border:1px solid gray;"></canvas>
  <script src="test1.js"></script>
</body>
</html>

Result, You should see something like this:

There are already several Nim wrappers for the canvas api (jscanvas, and html5_canvas), but here we create our own one for the purpose of learning. Here are just the functions needed for the test-1:

import jsffi

type
  JsCanvas* = JsObject
  Ctx2D* = JsObject

proc getElementById*(id: cstring): JsCanvas
  {.importjs: "document.getElementById(#)".}

proc getContext2d*(cnv: JsCanvas): Ctx2D
  {.importjs: "#.getContext('2d')".}

proc rect*(ctx: Ctx2D, x: cint, y: cint, w: cint, h: cint)
  {.importjs: "#.rect(#, #, #, #)".}

proc beginPath*(ctx: Ctx2D) {.importjs: "#.beginPath()".}
proc strokeStyle*(ctx: Ctx2D, r: cint, g: cint, b: cint) {.importjs: "#.strokeStyle = 'rgb(#, #, #)'".}
proc fillStyle*(ctx: Ctx2D, r: cint, g: cint, b: cint) {.importjs: "#.fillStyle = 'rgb(#, #, #)'".}
proc fill*(ctx: Ctx2D) {.importjs: "#.fill()".}
proc stroke*(ctx: Ctx2D) {.importjs: "#.stroke()".}

proc log*(msg: cstring) {.importjs: "console.log(#)".}

We call our wrapper "nim-pages", and we put the code in a file called pages.nim above.

We can compile the example at the very beginning of this page test1.nim, with the path as argument where to find pages.nim:

nim js -d:release --path:../pages/ test1.nim

The generated javascript file "test1.js" will look like this:

/* Generated by the Nim Compiler v1.6.10 */
var cnv_469762050 = document.getElementById("c1");
var ctx_469762051 = cnv_469762050.getContext('2d');
ctx_469762051.beginPath();
ctx_469762051.fillStyle = 'rgb(10, 230, 80)';
ctx_469762051.rect(10, 10, 60, 40);
ctx_469762051.fill();

We can now make a second example, adding a "draw-circle" function:

import pages

let cnv = getElementById("c1")
let ctx = cnv.getContext2d()

ctx.beginPath()
ctx.fillStyle(10, 230, 80)
ctx.rect(10, 10, 60, 40)
ctx.fill()

ctx.beginPath()
ctx.fillStyle(240, 180, 20)
ctx.circle(76, 42, 26)
ctx.fill()

Here is its binding in "pages":

proc circle*(ctx: Ctx2D, cx: cdouble, cy: cdouble, r: cdouble)
  {.importjs: "#.arc(#, #, #, 0, 2 * Math.PI)".}

We get this result:


Here a basic way about how to wrap "event-listeners":

import pages

let cnv = getElementById("c1")
let ctx = cnv.getContext2d()

proc key_event(k: KeyEvent) =
  alert("Key-Down")

proc mouse_event(m: MouseEvent) =
  alert("Mouse-Down")

addKeyDownEventListener(key_event)
addMouseDownEventListener(mouse_event)
import jsffi

type
  KeyEvent* = JsObject
  MouseEvent* = JsObject

proc addKeyDownEventListener*(f: proc(k: KeyEvent))
  {.importjs: "window.addEventListener('keydown', #)".}

proc addKeyUpEventListener*(f: proc(k: KeyEvent))
  {.importjs: "window.addEventListener('keyup', #)".}

proc addMouseDownEventListener*(f: proc(m: MouseEvent))
  {.importjs: "window.addEventListener('mousedown', #)".}

proc addMouseUpEventListener*(f: proc(m: MouseEvent))
  {.importjs: "window.addEventListener('mouseup', #)".}

proc alert*(msg: cstring) {.importjs: "alert(#)".}

Here is a basic way about how to move a square with the arrow keys:

import pages

type
  Pos = object
    x, y: cint

var p = Pos(x: 0, y: 0)

let cnv = getElementById("c1")
let ctx = cnv.getContext2d()

proc draw_bg() =
  ctx.beginPath()
  ctx.fillStyle(255, 255, 255)
  ctx.rect(0, 0, 330, 240)
  ctx.fill()

proc draw_r(x: cint, y: cint) =
  ctx.beginPath()
  ctx.fillStyle(10, 230, 80)
  ctx.rect(x, y, 30, 30)
  ctx.fill()

proc animate() =
  draw_bg()
  draw_r(p.x, p.y)

proc key_event(k: KeyEvent) =
  let c = getKeyCode(k)
  case c
  of 37:
    p.x -= 30
  of 39:
    p.x += 30
  of 38:
    p.y -= 30
  of 40:
    p.y += 30
  else: discard

addKeyDownEventListener(key_event)
setInterval(animate, 1000 div 6)

We see that if we press arrow keys (left, right, up, down), the square moves.

But the result is not very smooth, because the "animate" callback, and the "key-event-listener" callback are not synchronised.

A simple solution can be to set a variable to the current requested direction in the "key_event" callback, and increment/decrement the (x, y) position in an "update" function in the animate callback, which is run at a constant frame-rate.

import pages

type
  Pos = object
    x, y: cint

type
  Dir = enum
    Left, Right, Up, Down, NoDir

var p = Pos(x: 0, y: 0)
var d = NoDir

let cnv = getElementById("c1")
let ctx = cnv.getContext2d()

proc draw_bg() =
  ctx.beginPath()
  ctx.fillStyle(255, 255, 255)
  ctx.rect(0, 0, 330, 240)
  ctx.fill()

proc draw_r(x: cint, y: cint) =
  ctx.beginPath()
  ctx.fillStyle(10, 230, 80)
  ctx.rect(x, y, 30, 30)
  ctx.fill()

proc update() =
  case d
  of Left:
    p.x -= 30
  of Right:
    p.x += 30
  of Up:
    p.y -= 30
  of Down:
    p.y += 30
  else: discard
  if p.x > 330: p.x = 0
  if p.y > 240: p.y = 0
  if p.x < 0: p.x = 330
  if p.y < 0: p.y = 240

proc animate() =
  update()
  draw_bg()
  draw_r(p.x, p.y)

proc key_event(k: KeyEvent) =
  let c = getKeyCode(k)
  case c
  of 37:
    d = if d == Right: NoDir else: Left
  of 39:
    d = if d == Left: NoDir else: Right
  of 38:
    d = if d == Down: NoDir else: Up
  of 40:
    d = if d == Up: NoDir else: Down
  else: discard

addKeyDownEventListener(key_event)
setInterval(animate, 1000 div 5)

The "getKeyCode" function is binded like that:

import jsffi

type
  KeyEvent* = JsObject

proc getKeyCode*(k: KeyEvent): cint {.importjs: "#.keyCode".}
proc getKey*(k: KeyEvent): cstring {.importjs: "#.key".}


Now we can add a "trail" field to the player to make a basic snake.

We also add an apple at a random position. When the snake eat the apple its "trail" will grow, and another apple will appear at a new random position.

import random
import pages

type
  Pos = object
    x, y: cint

type
  Dir = enum
    Left, Right, Up, Down, NoDir

type
  Apple = object
    pos: Pos

type
  Player = object
    pos: Pos
    dir: Dir
    trail: seq[Pos]
    size: cint

randomize()  # initialises the seed

proc rand_pos(): Pos =
  result = Pos(x: cint(30 * rand(0..11)), y: cint(30 * rand(0..8)))

var player = Player(pos: Pos(x: 0, y: 0), dir: NoDir, trail: @[], size: 4)
var apple = Apple(pos: rand_pos())

let cnv = getElementById("c1")
let ctx = cnv.getContext2d()

proc draw_bg() =
  ctx.beginPath()
  ctx.fillStyle(255, 255, 255)
  ctx.rect(0, 0, 360, 270)
  ctx.fill()

proc draw_r(p: Pos) =
  ctx.beginPath()
  ctx.fillStyle(10, 230, 80)
  ctx.rect(p.x, p.y, 30, 30)
  ctx.fill()

proc draw_t(trail: seq[Pos]) =
  ctx.beginPath()
  ctx.fillStyle(40, 255, 160)
  for p in trail:
    ctx.rect(p.x, p.y, 30, 30)
  ctx.fill()

proc draw_a(p: Pos) =
  ctx.beginPath()
  ctx.fillStyle(240, 180, 20)
  ctx.rect(p.x, p.y, 30, 30)
  ctx.fill()

proc player_moves(p: var Pos, d: Dir) =
  case d
  of Left:
    p.x -= 30
  of Right:
    p.x += 30
  of Up:
    p.y -= 30
  of Down:
    p.y += 30
  else: discard

proc wrap_sides(p: var Pos) =
  if p.x > 330: p.x = 0
  if p.y > 240: p.y = 0
  if p.x < 0: p.x = 330
  if p.y < 0: p.y = 240

proc new_apple(p: var Pos, exclude: seq[Pos]) =
  p = rand_pos()
  while p in exclude:
    p = rand_pos()

proc update() =
  player_moves(player.pos, player.dir)
  wrap_sides(player.pos)
  if player.pos == apple.pos:
    new_apple(apple.pos, player.trail & @[player.pos])
    player.size += 1
  player.trail.insert(player.pos, 0)
  while player.trail.len > player.size:
    discard player.trail.pop()

proc animate() =
  update()
  draw_bg()
  draw_a(apple.pos)
  draw_t(player.trail)
  draw_r(player.pos)

proc key_event(k: KeyEvent) =
  var d = addr player.dir
  let c = getKeyCode(k)
  case c
  of 37:
    d[] = if d[] == Right: NoDir else: Left
  of 39:
    d[] = if d[] == Left: NoDir else: Right
  of 38:
    d[] = if d[] == Down: NoDir else: Up
  of 40:
    d[] = if d[] == Up: NoDir else: Down
  else: discard

addKeyDownEventListener(key_event)
setInterval(animate, 1000 div 5)

The end.