Standard library · Web and browser

web/canvas

Drawing shapes, text and images on a browser canvas element.

import web.canvas

This package gives plain functions for two dimensional drawing on a <canvas> element: charts, simple games, signatures, diagrams. As with dom, the program is translated for the browser with salam js, and the elements, events and timers around the drawing come from dom. For key presses in a game loop add web/keys; for pictures saved to a file on the server side, see image.

Get the drawing context with Get (by element id) or Of (for an element you hold) and pass it to every function. Each function is one call on the browser's CanvasRenderingContext2D. The coordinates are in canvas pixels, start at the top left corner, and the vertical axis points down, so angles turn clockwise.

Things that catch people out:

  • Set the size with the width and height attributes, not CSS. CSS stretches the picture; the attributes set how many pixels there are.
  • Shapes built from a path pile up until BeginPath; see the last section.
  • An image draws nothing until it has loaded. Check dom.ImageReady before DrawImage.
  • Mouse positions from dom.EventX are window coordinates. Subtract the canvas position from getBoundingClientRect to get canvas coordinates.
  • Only the common calls are wrapped. Gradients, measureText and the rest are reached with dom.Call... and dom.Set... on the context.

A bar chart from data

Drawing a chart is mostly arithmetic: turn each value into a height, each index into an x position, and draw from the bottom up because y grows downward. Set the canvas size with the width and height attributes rather than CSS; CSS only stretches the picture, while the attributes set how many pixels you draw on. TextAlign(c, "center") makes FillText centre each label on the x you give.

web-canvas-bar-chart.salam
import dom
import web.canvas

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "chart")
    dom.SetAttr(el, "width", "420")
    dom.SetAttr(el, "height", "240")
    dom.Append(dom.Body(), el)
    c := canvas.Get("chart")

    months := ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
    sales := [42.0, 55.0, 38.0, 71.0, 64.0, 80.0]
    top := 100.0
    base := 200.0
    bar := 40.0
    gap := 24.0

    canvas.StrokeStyle(c, "#999")
    canvas.LineWidth(c, 1)
    canvas.BeginPath(c)
    canvas.MoveTo(c, 30, base)
    canvas.LineTo(c, 410, base)
    canvas.Stroke(c)

    canvas.Font(c, "13px sans-serif")
    canvas.TextAlign(c, "center")
    each i, value in sales:
        x := 40.0 + (i as f64) * (bar + gap)
        h := value / top * 160.0
        canvas.FillStyle(c, "#3b82f6")
        canvas.BeginPath(c)
        canvas.RoundRect(c, x, base - h, bar, h, 4)
        canvas.Fill(c)
        canvas.FillStyle(c, "#222")
        canvas.FillText(c, months[i], x + bar / 2, base + 18)
        canvas.FillText(c, "" + (value as int), x + bar / 2, base - h - 6)
    end
end

These programs run in the browser, so they are only checked here; see how to run them.

Animation: clear, move, draw

An animation redraws the whole picture on every frame. EveryFrame from dom calls your function before each repaint, usually 60 times a second. Each call moves the objects a little, paints the background over the last frame and draws everything again. Keep the state in globals so it lasts from one frame to the next.

web-canvas-bounce.salam
import dom
import web.canvas

mut x := 60.0
mut y := 60.0
mut dx := 3.0
mut dy := 2.0
mut c := 0 as i64

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "box")
    dom.SetAttr(el, "width", "360")
    dom.SetAttr(el, "height", "220")
    dom.Append(dom.Body(), el)
    c = canvas.Get("box")
    r := 14.0

    dom.EveryFrame((ms: f64):
        _ := ms
        w := canvas.Width(c)
        h := canvas.Height(c)
        x += dx
        y += dy
        if x < r or x > w - r:
            dx = 0.0 - dx
        end
        if y < r or y > h - r:
            dy = 0.0 - dy
        end
        canvas.FillStyle(c, "#10131a")
        canvas.FillRect(c, 0, 0, w, h)
        canvas.BeginPath(c)
        canvas.Arc(c, x, y, r, 0, 6.2832)
        canvas.FillStyle(c, "#f59e0b")
        canvas.Fill(c)
    end)
end

The speed here is per frame, so the ball moves faster on a 120 Hz screen. For the same speed everywhere, multiply by the argument of the frame function: it is the time since the last frame in milliseconds, at most 100 (so a hidden tab does not make things jump). The spinner below works that way.

Clicks and images

A drawing that reacts to the mouse needs the click position in canvas coordinates. dom.EventX and dom.EventY are measured from the window, so the program asks the canvas for its box with dom.Call0(board, "getBoundingClientRect") and subtracts left and top. Each click stamps a picture there. dom.LoadImage starts the download at once and returns the image; until dom.ImageReady says it has arrived, the click does nothing. A right click clears the board, after PreventDefault stops the browser menu.

web-canvas-stamp.salam
import dom
import web.canvas

mut c := 0 as i64
mut board := 0 as i64
mut stamp := 0 as i64
mut placed := 0

func clear_board():
    canvas.FillStyle(c, "#f1f5f9")
    canvas.FillRect(c, 0, 0, canvas.Width(c), canvas.Height(c))
    canvas.FillStyle(c, "#334155")
    canvas.Font(c, "14px sans-serif")
    canvas.TextAlign(c, "left")
    canvas.FillText(c, "Click to place a stamp", 10, 20)
end

func main:
    board = dom.Create("canvas")
    dom.SetAttr(board, "width", "400")
    dom.SetAttr(board, "height", "260")
    dom.Append(dom.Body(), board)
    c = canvas.Of(board)
    stamp = dom.LoadImage("stamp.png")
    clear_board()

    dom.OnEventE(board, "click", (e: i64):
        if not dom.ImageReady(stamp): ret end
        box := dom.Call0(board, "getBoundingClientRect")
        x := dom.EventX(e) - dom.GetNum(box, "left")
        y := dom.EventY(e) - dom.GetNum(box, "top")
        canvas.DrawImage(c, stamp, x - 16, y - 16, 32, 32)
        placed += 1
        dom.SetTitle(placed + " stamps")
    end)
    dom.OnEventE(board, "contextmenu", (e: i64):
        dom.PreventDefault(e)
        clear_board()
        placed = 0
    end)
end

Put a stamp.png next to the page. When CSS makes the canvas larger or smaller than its width attribute, also scale the position by canvas.Width(c) / box width.

Rotating with Save and Restore

Translate, Rotate and Scale move the coordinate system, not the shapes, and every later drawing call is affected. Wrap them in Save and Restore so the change ends where you want it to. The spinner below moves the origin to the centre, then draws the same small bar twelve times, turning a twelfth of a circle and raising Alpha each time.

web-canvas-spinner.salam
import dom
import web.canvas

mut c := 0 as i64
mut turn := 0.0

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "spin")
    dom.SetAttr(el, "width", "120")
    dom.SetAttr(el, "height", "120")
    dom.Append(dom.Body(), el)
    c = canvas.Get("spin")

    dom.EveryFrame((ms: f64):
        turn += ms / 400.0
        canvas.ClearRect(c, 0, 0, 120, 120)
        canvas.Save(c)
        canvas.Translate(c, 60, 60)
        canvas.Rotate(c, turn)
        repeat 12 in i:
            canvas.Alpha(c, (i as f64 + 1.0) / 12.0)
            canvas.FillStyle(c, "#0ea5e9")
            canvas.BeginPath(c)
            canvas.RoundRect(c, -4, -44, 8, 18, 4)
            canvas.Fill(c)
            canvas.Rotate(c, 6.2832 / 12.0)
        end
        canvas.Restore(c)
    end)
end

Angles are in radians: a full turn is about 6.2832. Alpha is part of the saved state too, so the Restore at the end also brings it back to 1.

One trap: RoundRect, MoveTo, LineTo and Arc add to the current path, and Fill fills all of it. Without BeginPath before each shape, the twelfth Fill here would paint all twelve bars again at full strength and the fading tail would disappear. FillRect and StrokeRect draw at once and do not touch the path.

Functions

Get

func Get(id: str): i64
The two dimensional drawing context of the canvas element with this id. The element must exist, or the call throws.
web-canvas-Draw.salam
import dom
import web.canvas

func main:
    el := dom.Create("canvas")
    dom.SetAttr(el, "id", "board")
    dom.SetAttr(el, "width", "320")
    dom.SetAttr(el, "height", "200")
    dom.Append(dom.Body(), el)
    c := canvas.Get("board")
    println canvas.Width(c), canvas.Height(c)

    canvas.FillStyle(c, "#f5f0e6")
    canvas.FillRect(c, 0, 0, canvas.Width(c), canvas.Height(c))
    canvas.FillStyle(c, "#2a7")
    canvas.RoundRect(c, 20, 20, 120, 60, 12)
    canvas.Fill(c)
    canvas.StrokeStyle(c, "#333")
    canvas.LineWidth(c, 2)
    canvas.StrokeRect(c, 160, 20, 120, 60)
    canvas.ClearRect(c, 170, 30, 20, 20)

    canvas.BeginPath(c)
    canvas.MoveTo(c, 20, 180)
    canvas.LineTo(c, 100, 100)
    canvas.LineTo(c, 180, 180)
    canvas.ClosePath(c)
    canvas.Stroke(c)
    canvas.BeginPath(c)
    canvas.Arc(c, 250, 140, 30, 0, 6.2832)
    canvas.Alpha(c, 0.5)
    canvas.Fill(c)
    canvas.Alpha(c, 1)

    canvas.Save(c)
    canvas.Translate(c, 160, 100)
    canvas.Rotate(c, 0.2)
    canvas.Scale(c, 1.5, 1.5)
    canvas.Font(c, "16px Inter")
    canvas.TextAlign(c, "center")
    canvas.FillStyle(c, "#000")
    canvas.FillText(c, "hello", 0, 0)
    canvas.Restore(c)

    image := dom.LoadImage("logo.png")
    if dom.ImageReady(image):
        canvas.DrawImage(c, image, 260, 10, 50, 50)
    end
    second := canvas.Of(dom.Create("canvas"))
    println second != 0
end

Of

func Of(el: i64): i64
The drawing context of a canvas element you already hold.

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

Width

func Width(c: i64): f64
The canvas width in pixels, from its width attribute.

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

Height

func Height(c: i64): f64
The canvas height.

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

FillStyle

func FillStyle(c: i64, v: str)
The fill colour, such as #2a7 or red.

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

StrokeStyle

func StrokeStyle(c: i64, v: str)
The colour of the lines.

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

LineWidth

func LineWidth(c: i64, v: f64)
The thickness of the lines.

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

Font

func Font(c: i64, v: str)
The text font in CSS terms, such as 16px Inter.

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

TextAlign

func TextAlign(c: i64, v: str)
The horizontal text alignment: left, center or right.

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

Alpha

func Alpha(c: i64, v: f64)
The transparency of everything drawn after this, from 0 to 1.

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

FillRect

func FillRect(c: i64, x: f64, y: f64, w: f64, h: f64)
A filled rectangle.

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

StrokeRect

func StrokeRect(c: i64, x: f64, y: f64, w: f64, h: f64)
The outline of a rectangle.

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

ClearRect

func ClearRect(c: i64, x: f64, y: f64, w: f64, h: f64)
Clears a part of the canvas and makes it transparent.

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

RoundRect

func RoundRect(c: i64, x: f64, y: f64, w: f64, h: f64, r: f64)
Adds a rectangle with rounded corners to the path; call Fill or Stroke afterwards. Browsers without roundRect get square corners.

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

BeginPath

func BeginPath(c: i64)
Starts a new path.

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

ClosePath

func ClosePath(c: i64)
Closes the path back to its starting point.

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

MoveTo

func MoveTo(c: i64, x: f64, y: f64)
Moves the pen to this point without drawing.

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

LineTo

func LineTo(c: i64, x: f64, y: f64)
Adds a line segment up to this point to the path.

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

Arc

func Arc(c: i64, x: f64, y: f64, r: f64, a: f64, b: f64)
An arc with the given centre and radius, from the start angle to the end angle in radians; a full circle is 0 to 2π.

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

Fill

func Fill(c: i64)
Fills the inside of the path.

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

Stroke

func Stroke(c: i64)
Draws the line of the path.

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

FillText

func FillText(c: i64, t: str, x: f64, y: f64)
Writes the text at this point.

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

Save

func Save(c: i64)
Keeps the current colours, font and transforms.

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

Restore

func Restore(c: i64)
Brings back the last kept state.

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

Translate

func Translate(c: i64, x: f64, y: f64)
Moves the origin of the coordinates.

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

Rotate

func Rotate(c: i64, a: f64)
Rotates everything drawn after this by this angle in radians.

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

Scale

func Scale(c: i64, x: f64, y: f64)
Makes everything drawn after this larger or smaller.

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

DrawImage

func DrawImage(c: i64, img: i64, x: f64, y: f64, w: f64, h: f64)
Draws an image you got from dom.LoadImage, stretched into this rectangle. An image that has not loaded yet draws nothing.

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