Standard library · Web and browser

web/keys

Keyboard state for browser games.

import web.keys

In a game you usually ask once per frame which keys are down, rather than taking an event for every press. This package listens to the keydown and keyup events of the window for you and keeps the state of up to 32 keys. Call Init once, then ask Down or Pressed inside the EveryFrame function from dom, and draw with web/canvas. For a form or a text field, where you want each key as it comes, use dom.OnEventE with dom.EventKey instead.

Keys are named by the browser's key value: ArrowLeft, Enter, " " for the space bar, a for the A key. Things to know:

  • Only tracked keys are seen. Init tracks the four arrows, Enter and the space bar; add others with Track. Asking about any other key returns false.
  • Letters change with Shift and Caps Lock: a becomes A. Track and ask for both.
  • Tracked keys stop doing their normal job on the page, so they cannot be typed into a text box while the game runs.
  • A key can stay "down" after the window loses focus. When the player switches windows while holding a key, the browser sends no keyup, and Down stays true until that key is pressed and released again.

Holding a key and pressing it once

The two questions are different. Down is true for as long as the key is held, which suits walking. Pressed is true once per press, the next time you ask after the key went down, which suits jumping, shooting or opening a menu: holding the space bar does not make the player jump again and again.

web-keys-jump.salam
import dom
import web.canvas
import web.keys

mut x := 40.0
mut y := 160.0
mut vy := 0.0
mut c := 0 as i64

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "level")
    dom.SetAttr(el, "width", "400")
    dom.SetAttr(el, "height", "200")
    dom.Append(dom.Body(), el)
    c = canvas.Get("level")
    keys.Init()
    floor := 160.0

    dom.EveryFrame((ms: f64):
        _ := ms
        if keys.Down("ArrowLeft"):
            x -= 3
        end
        if keys.Down("ArrowRight"):
            x += 3
        end
        jump := keys.Pressed(" ")
        if jump and y >= floor:
            vy = -9
        end
        vy += 0.5
        y += vy
        if y > floor:
            y = floor
            vy = 0
        end
        canvas.ClearRect(c, 0, 0, 400, 200)
        canvas.FillStyle(c, "#444")
        canvas.FillRect(c, 0, floor + 20, 400, 20)
        canvas.FillStyle(c, "#e11d48")
        canvas.FillRect(c, x, y, 20, 20)
    end)
end

Notice that the program asks Pressed on every frame and only then checks whether the player stands on the floor. If it wrote y >= floor and keys.Pressed(" "), a press in mid air would not be asked about, would stay waiting, and the player would jump by itself on landing.

Your own keys: pause with P

Init tracks the arrow keys, Enter and the space bar. Add any other key with Track, using the browser's key name: a letter, Escape, Shift and so on. The names are exact text, so with Shift or Caps Lock on, the P key arrives as P, not p; track both when it matters.

web-keys-pause.salam
import dom
import web.canvas
import web.keys

mut paused := false
mut angle := 0.0
mut c := 0 as i64

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "game")
    dom.SetAttr(el, "width", "200")
    dom.SetAttr(el, "height", "200")
    dom.Append(dom.Body(), el)
    c = canvas.Get("game")
    keys.Init()
    keys.Track("p")
    keys.Track("P")
    keys.Track("Escape")

    dom.EveryFrame((now: f64):
        _ := now
        if keys.Pressed("p") or keys.Pressed("P") or keys.Pressed("Escape"):
            paused = not paused
        end
        if not paused:
            angle += 0.05
        end
        canvas.ClearRect(c, 0, 0, 200, 200)
        canvas.Save(c)
        canvas.Translate(c, 100, 100)
        canvas.Rotate(c, angle)
        canvas.FillStyle(c, "#22c55e")
        canvas.FillRect(c, -30, -30, 60, 60)
        canvas.Restore(c)
        if paused:
            canvas.FillStyle(c, "#000")
            canvas.Font(c, "18px sans-serif")
            canvas.TextAlign(c, "center")
            canvas.FillText(c, "Paused, press P", 100, 190)
        end
    end)
end

A tracked key no longer does its usual job in the page, because the package calls PreventDefault for it. That is what stops the arrows and the space bar from scrolling during a game, but it also means a tracked letter cannot be typed into a text box on the same page.

Like the dom examples, these run in the browser and are only checked here.

Smooth movement with arrows and WASD

Most games accept both the arrow keys and W, A, S and D. The helper held checks an arrow, a letter and its capital, so Caps Lock does not stop the player. Each axis becomes -1, 0 or 1; when both are set the program divides by the length, so moving diagonally is not faster than moving straight. The speed is in pixels per millisecond and is multiplied by ms, the time since the last frame that EveryFrame passes in, so the dot moves at the same speed on a 60 Hz and a 144 Hz screen.

web-keys-move.salam
import dom
import math
import str
import web.canvas
import web.keys

mut x := 200.0
mut y := 150.0
mut c := 0 as i64

func held(arrow: str, letter: str): bool:
    ret keys.Down(arrow) or keys.Down(letter) or keys.Down(str.Upper(letter))
end

func axis(minus_arrow: str, minus_letter: str, plus_arrow: str, plus_letter: str): f64:
    mut v := 0.0
    if held(minus_arrow, minus_letter): v -= 1.0 end
    if held(plus_arrow, plus_letter): v += 1.0 end
    ret v
end

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "field")
    dom.SetAttr(el, "width", "400")
    dom.SetAttr(el, "height", "300")
    dom.Append(dom.Body(), el)
    c = canvas.Get("field")
    keys.Init()
    each k in ["w", "a", "s", "d", "W", "A", "S", "D"]:
        keys.Track(k)
    end
    speed := 0.2

    dom.EveryFrame((ms: f64):
        mut dx := axis("ArrowLeft", "a", "ArrowRight", "d")
        mut dy := axis("ArrowUp", "w", "ArrowDown", "s")
        length := math.Sqrt(dx * dx + dy * dy)
        if length > 0.0:
            dx = dx / length
            dy = dy / length
        end
        x = math.ClampF(x + dx * speed * ms, 10.0, 390.0)
        y = math.ClampF(y + dy * speed * ms, 10.0, 290.0)
        canvas.FillStyle(c, "#0f172a")
        canvas.FillRect(c, 0, 0, 400, 300)
        canvas.FillStyle(c, "#22c55e")
        canvas.BeginPath(c)
        canvas.Arc(c, x, y, 10, 0, 6.2832)
        canvas.Fill(c)
    end)
end

Functions

Track

func Track(k: str)
Adds a key to the list of tracked keys; the key name is the browser key value, such as a. Up to 32 keys; more are ignored without a word. Works before or after Init.
web-keys-Game.salam
import dom
import web.canvas
import web.keys

mut x := 160.0
mut score := 0

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "game")
    dom.Append(dom.Body(), el)
    c := canvas.Get("game")
    keys.Init()
    keys.Track("a")
    keys.Track("d")
    dom.EveryFrame((now: f64):
        if keys.Down("ArrowLeft") or keys.Down("a"):
            x -= 4
        end
        if keys.Down("ArrowRight") or keys.Down("d"):
            x += 4
        end
        if keys.Pressed(" "):
            score += 1
        end
        canvas.ClearRect(c, 0, 0, canvas.Width(c), canvas.Height(c))
        canvas.FillRect(c, x - 10, 100, 20, 20)
        canvas.FillText(c, "score: " + score, 10, 20)
    end)
end

Init

func Init()
Starts listening on the window and tracks the arrow keys, Enter and the space bar. Tracked keys no longer scroll the page or type into fields. Call it once; a second call adds a second pair of listeners.

The example for this item is shown with the ones above: web-keys-Game.salam

Down

func Down(k: str): bool
The key is being held right now; for continuous movement.

The example for this item is shown with the ones above: web-keys-Game.salam

Pressed

func Pressed(k: str): bool
The key has been pressed since the last time you asked; every press returns true exactly once. For jumping or shooting.

The example for this item is shown with the ones above: web-keys-Game.salam