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.