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
widthandheightattributes, 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.ImageReadybeforeDrawImage. - Mouse positions from
dom.EventXare window coordinates. Subtract the canvas position fromgetBoundingClientRectto get canvas coordinates. - Only the common calls are wrapped. Gradients,
measureTextand the rest are reached withdom.Call...anddom.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.
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
endThese 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.
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)
endThe 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.
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)
endPut 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.
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)
endAngles 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): i64import 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
endOf
func Of(el: i64): i64The example for this item is shown with the ones above: web-canvas-Draw.salam
Width
func Width(c: i64): f64width attribute.The example for this item is shown with the ones above: web-canvas-Draw.salam
Height
func Height(c: i64): f64The example for this item is shown with the ones above: web-canvas-Draw.salam
FillStyle
func FillStyle(c: i64, v: str)#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 example for this item is shown with the ones above: web-canvas-Draw.salam
LineWidth
func LineWidth(c: i64, v: f64)The example for this item is shown with the ones above: web-canvas-Draw.salam
Font
func Font(c: i64, v: str)16px Inter.The example for this item is shown with the ones above: web-canvas-Draw.salam
TextAlign
func TextAlign(c: i64, v: str)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 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)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 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)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)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)The example for this item is shown with the ones above: web-canvas-Draw.salam
ClosePath
func ClosePath(c: i64)The example for this item is shown with the ones above: web-canvas-Draw.salam
MoveTo
func MoveTo(c: i64, x: f64, y: f64)The example for this item is shown with the ones above: web-canvas-Draw.salam
LineTo
func LineTo(c: i64, x: f64, y: f64)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)The example for this item is shown with the ones above: web-canvas-Draw.salam
Fill
func Fill(c: i64)The example for this item is shown with the ones above: web-canvas-Draw.salam
Stroke
func Stroke(c: i64)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)The example for this item is shown with the ones above: web-canvas-Draw.salam
Save
func Save(c: i64)The example for this item is shown with the ones above: web-canvas-Draw.salam
Restore
func Restore(c: i64)The example for this item is shown with the ones above: web-canvas-Draw.salam
Translate
func Translate(c: i64, x: f64, y: f64)The example for this item is shown with the ones above: web-canvas-Draw.salam
Rotate
func Rotate(c: i64, a: f64)The example for this item is shown with the ones above: web-canvas-Draw.salam
Scale
func Scale(c: i64, x: f64, y: f64)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)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