Rprimo drawing with draw
▶Open Rprimo

Rprimodrawing with draw

draw is the drawing context built into Rprimo. You write library(Rprimo), call a few of its functions, and the picture appears in the Draw tab of the console. It is made to give you a friendly start into R: draw something on your first day.

Rprimo is the package the editor provides; nothing to install, it is simply there when a program runs here. draw is its drawing context, one object that a program draws through, much as a web page asks its canvas for a context and draws through that. Under the names, draw is the browser's own canvas. Every function and every style on this page is the canvas's, with the canvas's own names, so what you learn here is what a web page draws with, and the canvas documentation on MDN is a second reference for the same object. This page is the full reference, with worked examples. The handbook covers where the Draw tab lives and how it interacts with the debugger.

Contents#

  1. 1Getting started
  2. 2The coordinate system
  3. 3The canvas, mirrored
  4. 4Colours and styles
  5. 5Reference
  6. 6Examples
  7. 7Drawing and the debugger
  8. 8Limits and gotchas

1Getting started#

Nothing to install. A drawing program starts with one library() line and then talks to draw:

rOpen
library(Rprimo)

draw$clear("midnightblue")
draw$fillStyle <- "gold"
draw$fillCircle(200, 150, 60)
draw$write(20, 20, "Hello!")

Every function is reached through draw with a $, and a style such as fillStyle is set by assignment: draw$fillStyle <- "gold", as on the canvas. When you run the program, the console switches to the Draw tab and the picture appears there. Examples 2, 4 and 7 in the examples menu are ready-made starting points, 13 is the Game of Life, and 14 is a whole game.

Rprimo is in fact attached before your program starts, so library(Rprimo) has nothing to do. Write it anyway: it says what the program uses, the way library() does in every R script, and Rprimo::draw works too, for those who like to spell it out.

draw also works at the console. Each line you type draws as soon as you press Enter, which is a quick way to try a style or ask a question:

r>Console
draw$size()
draw$fillStyle <- "tomato"
draw$fillRect(20, 20, 80, 50)
draw$fillStyle

R's own graphics, plot(), hist(), barplot() and the rest, are a different thing: they draw a finished chart onto R's graphics device, and it appears in the Plots tab. draw is for pictures you build yourself, shape by shape, and for anything that moves.

2The coordinate system#

  • (0, 0) is the top-left corner.
  • x grows to the right, y grows downward, the opposite of a maths graph, but the same as every screen.
  • Units are drawing units. On an ordinary display one unit is one pixel; on a high-resolution display the canvas is backed by two or three times as many physical pixels, but you keep using the same numbers and everything simply looks sharper.
  • Coordinates need not be whole numbers. draw$fillCircle(100.5, 80.25, 30) is fine.

The canvas fills the Draw tab, so its size depends on how big the console is. Always ask rather than guess:

rOpen
library(Rprimo)

s <- draw$size()
draw$strokeCircle(s$width / 2, s$height / 2, min(s$width, s$height) / 3)

Make the console taller and run again and the circle grows with it.

3The canvas, mirrored#

Every web browser has a drawing surface called the canvas, and a page draws on it through an object with a long name, CanvasRenderingContext2D. draw is that object, name for name; a page gets it with canvas.getContext("2d"), a program here gets it with library(Rprimo). The names are not translated at all: what the canvas documentation calls fillRect is draw$fillRect here, and lineWidth is draw$lineWidth. The only change is R's $ where JavaScript writes a dot.

On the canvas (MDN)In draw
ctx.fillRect(x, y, w, h)draw$fillRect(x, y, w, h)
ctx.fillStyle = "gold"draw$fillStyle <- "gold"
ctx.beginPath(), ctx.moveTo(), ctx.lineTo()draw$beginPath(), draw$moveTo(), draw$lineTo()
ctx.arc(x, y, r, 0, Math.PI)draw$arc(x, y, r, 0, 180)
ctx.createLinearGradient(...)draw$createLinearGradient(...)
ctx.measureText(s).widthdraw$measureText(s)

So when you wonder what lineJoin does, or how bezierCurveTo bends, look it up on MDN: the answer is the same. That is the reason draw is built this way. A library with names of its own would have to explain everything itself, and what you learned would stop at the edge of Rprimo; the canvas is documented in depth, and it is what you meet the moment you draw on a real web page.

Three things to know before reading the canvas pages:

  • Styles are set by assignment. draw$lineWidth <- 4 and draw$fillStyle <- "gold", exactly like ctx.lineWidth = 4 on the canvas. There is no setLineWidth() function. Reading a style back gives what you last set.
  • Every call that paints says how. The canvas has no rect() that draws; it has fillRect and strokeRect, and fillText and strokeText, and a general fill() and stroke() for a path you built. Fill means solid, in the fill colour; stroke means outline, in the stroke colour, at the line width. draw keeps that rule for its own additions too: fillCircle and strokeCircle, fillPolygon and strokePolygon.
  • Angles are in degrees. The canvas wants radians; draw takes degrees everywhere an angle appears (arc, ellipse, rotate, createConicGradient) and converts, because 90 reads better than pi / 2 on your first day. Angles go clockwise, since y grows downward: 0° points right, 90° points down.

What the canvas does not have, draw adds, and these are the only names you will not find on MDN: size(), clear(), color(), flush(), write(), line(), the six fill/stroke shape shortcuts, the keyboard and the mouse. What the canvas has and draw leaves out is anything to do with images and raw pixels (drawImage, getImageData, patterns); see Limits.

4Colours and styles#

Colours#

Every style or function that takes a colour accepts what a web browser accepts:

FormExamples
Named colour"red", "skyblue", "gold", "midnightblue", "rebeccapurple"
Hex code"#ff0000", "#f00", "#ff000080" (last two digits = transparency)
RGB / RGBA"rgb(255, 0, 0)", "rgba(0, 0, 0, 0.5)"
HSL"hsl(200, 80%, 50%)", handy for rainbows: vary the first number

There are about 150 named colours, listed with swatches on MDN under named colours; that list is the one draw uses. It is the browser's list, not R's: most names are on both ("gold", "steelblue"), but R's numbered shades such as "gray40" or "steelblue3" are not colours here. A gradient (see Gradients) is accepted in place of a colour by fillStyle, strokeStyle and color(), which sets both, and nowhere else: the canvas draws a shadow in one colour, so shadowColor takes a colour and stops with an error that says so when given a gradient.

If you pass something that isn't a colour, draw stops with an error instead of silently drawing nothing:

Error: "gren" is not a colour. Try a name like "red", a code like "#ff0000", or "rgb(255, 0, 0)".

Two pens#

The canvas has two colours, not one: fillStyle paints the inside of shapes and the body of text, strokeStyle draws outlines and lines. They are set by assignment, and each stays as it is until you change it:

rOpen
library(Rprimo)

draw$fillStyle <- "gold"
draw$strokeStyle <- "black"
draw$lineWidth <- 3
draw$fillCircle(100, 100, 40)     # gold
draw$strokeCircle(100, 100, 40)   # black ring, 3 units wide

draw$color("gold") sets both at once, which is what a beginner usually wants: one pen, one colour, until the picture needs an outline in a second colour.

The styles#

These are all the styles, with what they start at in each run. Most are the canvas's own starting values; the first seven are set by draw so that a first line draws something visible, and so that text lands where shapes do.

StyleStarts atWhat it does
fillStyle"black"Colour (or gradient) for fill* calls and write
strokeStyle"black"Colour (or gradient) for stroke* calls and line
lineWidth2Thickness of lines and outlines, in drawing units
lineCap"round"Line ends: "butt", "round" or "square"
lineJoin"round"Corners: "round", "bevel" or "miter"
textBaseline"top"Which part of the text sits on y: "top", "middle", "alphabetic", "bottom"
font"16px sans-serif"A CSS font, e.g. "bold 24px serif", for fillText and strokeText
textAlign"start"Which part of the text sits on x: "start" (the left, in left-to-right text), "end", "left", "center", "right"
letterSpacing"0px"Extra space between letters, as a CSS length
miterLimit10How sharp a "miter" corner may get before it is cut off
lineDashOffset0Where along the dash pattern a stroke starts; see setLineDash
globalAlpha1Transparency of everything drawn afterwards, 0 to 1
globalCompositeOperation"source-over"How new paint mixes with what is there, e.g. "lighter", "multiply"
shadowColor"rgba(0, 0, 0, 0)"The colour of a drop shadow under everything drawn afterwards; transparent, so no shadow, until set. A colour only, never a gradient
shadowBlur, shadowOffsetX, shadowOffsetY0How soft the shadow is, and how far it sits to the right and down
filter"none"A CSS filter, e.g. "blur(4px)"
imageSmoothingEnabledTRUEThe canvas has it, so draw has it; without images it changes nothing

Reading a style back, draw$lineWidth at the console or print(draw$lineWidth) in a program, gives the value in force, which is what you last set or the starting value in the table. A style is remembered by save() and put back by restore(), along with the transform and the clip; see State.

A style takes one value. draw$lineWidth <- c(2, 4) or draw$fillStyle <- NA stops with an error that says so, rather than sending the canvas something it cannot use.

5Reference#

Setup#

draw$size() → list(width, height) The drawing area in drawing units, as a list with two names: s <- draw$size(), then s$width and s$height. Call it at the start and derive your positions from it.

draw$clear(c = "white") Fill the whole canvas with one colour. draw$clear() gives a white page; draw$clear(NULL) erases to transparent. Usually the first line of a drawing. It paints the whole area whatever transform is in force, and leaves every style as it was, including fillStyle.

draw$color(c) Set fillStyle and strokeStyle together.

draw$flush() Shows everything drawn so far, right now. Drawing calls travel to the page in batches; Sys.sleep(), readline(), and every call that needs an answer from the page (the keyboard, the mouse, measureText, the hit tests) already send what is waiting, so a loop that draws and sleeps needs no flush(). Call it before a long calculation whose frame should already be showing.

Shapes in one call#

These are draw's shortcuts: each builds a path and fills or strokes it, so that a circle is one line rather than three. All of them use the styles in force.

draw$line(x1, y1, x2, y2) Straight line between two points, in strokeStyle.

draw$fillCircle(x, y, r) / draw$strokeCircle(x, y, r) Circle centred on (x, y) with radius r.

draw$fillEllipse(x, y, rx, ry) / draw$strokeEllipse(x, y, rx, ry) Ellipse centred on (x, y), rx wide and ry tall (those are half-widths, like a radius).

draw$fillPolygon(points) / draw$strokePolygon(points) A closed shape through a list of points; the last point is joined back to the first. Needs at least two points. The points can come in whichever form is handy: a two-column matrix of x and y (cbind(xs, ys)), a data frame with two columns, a list of pairs (list(c(10, 10), c(90, 10), c(50, 80))), or one vector c(x1, y1, x2, y2, ...).

And the canvas's own one-call rectangles:

draw$fillRect(x, y, w, h) / draw$strokeRect(x, y, w, h) Rectangle with its top-left corner at (x, y).

draw$clearRect(x, y, w, h) Erase a rectangle to transparent.

Many shapes in one call#

fillRect, strokeRect, clearRect, rect, fillCircle, strokeCircle and line take vectors, the way R's own functions do: each argument is either one value, used for every shape, or one value per shape. draw$fillCircle(xs, ys, 5) draws a circle at every pair of xs and ys, all of radius 5, in the current fillStyle. It draws exactly what a for loop over the positions would, but much faster: every call from R to the canvas has a cost of its own, so one call for a thousand circles takes about as long as a dozen single ones.

rOpen
xs <- seq(20, 380, by = 20)
draw$fillStyle <- "steelblue"
draw$fillCircle(xs, 150 + 60 * sin(xs / 40), 6)

Arguments of different lengths other than one are refused, as are NA and Inf anywhere in them. A vector with nothing in it draws nothing, so which() finding no matches is fine. Styles stay one value each: to paint shapes in two colours, make one call per colour.

Paths#

Everything else is drawn the canvas way: start a path, add lines and curves to it, then paint it once, with fill() or stroke() or both. A path is not visible until painted, and it stays the current path until the next beginPath(), which is why a shape can be filled and then stroked without being described twice.

rOpen
library(Rprimo)

draw$beginPath()
draw$moveTo(50, 150)
draw$lineTo(150, 30)
draw$lineTo(250, 150)
draw$closePath()
draw$fillStyle <- "tomato"
draw$fill()
draw$stroke()

draw$beginPath() Start a new path, forgetting the old one.

draw$moveTo(x, y) Lift the pen and put it down at (x, y): the start of a line.

draw$lineTo(x, y) A straight line from the pen to (x, y).

draw$closePath() Join the end back to the start. fill() closes a shape by itself; stroke() does not, so an outline needs this or it has a gap.

draw$arc(x, y, r, start, end, anticlockwise = FALSE) Part of a circle around (x, y), from start to end degrees, clockwise. draw$arc(x, y, r, 0, 180) is the bottom half; 180, 360 is the top half; 0, 360 is the whole circle. If the pen is already down somewhere, a straight line joins it to the start of the arc, which is what makes a pie slice: moveTo the centre, arc, closePath, fill.

draw$ellipse(x, y, rx, ry, rotation = 0, start = 0, end = 360, anticlockwise = FALSE) Part of an ellipse, turned by rotation degrees. With the defaults, the whole ellipse.

draw$arcTo(x1, y1, x2, y2, r) A rounded corner of radius r on the way from the pen towards (x1, y1) and on to (x2, y2).

draw$rect(x, y, w, h) / draw$roundRect(x, y, w, h, radii = 0) Add a rectangle to the path, the second with rounded corners: one radius for all four, or a vector of up to four, c(10, 10, 0, 0).

draw$quadraticCurveTo(cpx, cpy, x, y) / draw$bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) A curve to (x, y), bent towards one control point or two.

draw$fill() / draw$stroke() Paint the current path, solid in fillStyle or as an outline in strokeStyle.

draw$clip() From now on, paint only inside the current path. There is no un-clip; wrap it in save() and restore().

draw$setLineDash(lengths) Stroke with dashes from here on: a vector of on and off lengths, c(8, 4) for dashes, c(2, 6) for dots, c() for solid lines again. lineDashOffset slides the pattern along, which animates a marching outline.

Transforms#

A transform moves the paper under the pen. After translate(100, 100), the point you call (0, 0) is drawn at (100, 100); after rotate(45), everything is drawn turned. Transforms accumulate, so the usual pattern is save(), move and turn, draw, restore(), which puts the paper back where it was.

draw$translate(x, y) Move the origin by (x, y).

draw$rotate(degrees) Turn the drawing clockwise around the origin. To turn a shape around its own centre, translate to the centre first, rotate, and draw the shape around (0, 0).

draw$scale(sx, sy) Stretch by sx across and sy down. draw$scale(1, -1) flips y so that maths coordinates draw the right way up.

draw$transform(a, b, c, d, e, f) / draw$setTransform(a, b, c, d, e, f) / draw$resetTransform() The matrix forms: multiply the current transform by one, replace it, or go back to none.

State#

draw$save() / draw$restore() save() remembers every style, the transform and the clip; restore() puts them all back. Calls nest, so a function that changes the pen can begin with draw$save() and end with draw$restore() and leave no trace on whoever called it.

Gradients#

A gradient is a colour that changes from place to place. Make one, give it colour stops, and assign it to fillStyle or strokeStyle as you would a colour:

rOpen
library(Rprimo)

s <- draw$size()
g <- draw$createLinearGradient(0, 0, 0, s$height)   # top to bottom
g$addColorStop(0, "navy")
g$addColorStop(1, "orange")
draw$fillStyle <- g
draw$fillRect(0, 0, s$width, s$height)

draw$createLinearGradient(x0, y0, x1, y1) Along the line from one point to the other; beyond the ends the end colours continue.

draw$createRadialGradient(x0, y0, r0, x1, y1, r1) From a circle out to a larger one, usually with the same centre: a glow or a shaded ball.

draw$createConicGradient(start, x, y) Sweeping around (x, y) like a clock hand, from start degrees: a colour wheel.

g$addColorStop(offset, color) The colour at offset, from 0 at the start to 1 at the end. Add as many as you like, in any order. Gradient coordinates are drawing units on the canvas, not positions within the shape, so a gradient made for the whole page shades a small rectangle with only the slice of it that the rectangle covers.

The objects these return are of class primoGradient, and print as <Rprimo gradient #1>. A gradient is made by the page, never by hand: Gradient() exists only to say so. A gradient belongs to the run that made it, and the page forgets it when the next run starts; should one from an earlier run still be around, assigning it stops with an error, so make it again in the program that uses it.

Text#

draw$write(x, y, s, size = 16, font = "sans") Write s with its top-left corner at (x, y), in fillStyle. size is the height in drawing units. font is "sans", "serif" or "mono", the names in FONTS (see below), or the name of any font family the page has loaded, such as "Georgia". A number or a date is written the way R would print it, and a vector of several strings is written as one line with spaces between. This is the beginner's call: size and face in the call, nothing to set first.

draw$fillText(text, x, y) / draw$strokeText(text, x, y) The canvas's own, in the font style, positioned by textAlign and textBaseline. Note the order: text first, as on the canvas. Use these when you want a bold or italic face (draw$font <- "italic bold 32px serif"), an outline, or text centred on a point:

rOpen
library(Rprimo)

s <- draw$size()
draw$font <- "bold 40px sans-serif"
draw$textAlign <- "center"
draw$textBaseline <- "middle"
draw$fillText("centred", s$width / 2, s$height / 2)

draw$measureText(text) → a number. How wide text would be in the current font, in drawing units. For centring, textAlign is simpler; measureText is for when the width itself matters, such as underlining a word, drawing a box around it, or fitting a label to a bar. On the canvas measureText answers with an object and the width is one of its fields; here it is the width itself.

FONTS A list mapping the three short names to real font families: sans is "sans-serif", serif is "serif", mono is "monospace". It is part of the package, next to draw, so FONTS at the console shows it. It is for reading: FONTS$fancy <- "Georgia" stops with an error, because write() knows only the package's own three names. Pass the family itself instead, font = "Georgia".

Hit tests#

draw$isPointInPath(x, y) → TRUE / FALSE Whether (x, y) lies inside the current path. Build the path, then ask, before the next beginPath().

draw$isPointInStroke(x, y) → TRUE / FALSE Whether (x, y) lies on the outline of the current path, at the current lineWidth.

The point is in plain drawing units, before any transform you have set, which is exactly the form a mouse position comes in. So a button is a path, and "was it clicked" is one call; see Buttons in the examples.

The keyboard#

A game needs to know what the player pressed while it runs. The page collects every press as it happens; the functions below hand them over. Keys have short names: "a", "left", "right", "up", "down", "space", "enter", "escape", "shift". Letters are always lower case, and the modifier keys never come out of key(): Shift+A is the press "a", and pressed("shift") tells A from a.

draw$key() → "left", "a", … or NULL The next key that was pressed, or NULL if nothing was. Presses are queued in order, so a quick double tap is not lost. Holding a key down counts as one press. This is the one to use for turning, jumping or choosing. Because it can be NULL, compare it with identical(k, "escape"), or check !is.null(k) first: if (k == "escape") stops with an error when no key was pressed.

draw$pressed(name) → TRUE / FALSE Whether the key called name is held down right now. For steady movement: if (draw$pressed("right")) x <- x + 3.

draw$keys() → a character vector. Every key pressed since the last call, oldest first; character(0) if none.

draw$keysDown() → a character vector. Every key held down right now, each once.

draw$waitKey() → a name. Waits until a key is pressed and returns it. For "press any key to start".

When a program first asks for a key, the canvas takes the keyboard: the arrows steer instead of scrolling, and Space no longer presses the Run button. Click in the editor to type there again; click the drawing to give the game the keys back.

The mouse#

The same arrangement as the keyboard: the page watches the pointer over the drawing and answers when asked. Positions are in drawing units, the same ones size() reports and shapes are drawn in, so a click can be compared with a shape without any conversion. On a touch screen a finger is the mouse: a finger down is a button held, and a tap is a click.

draw$mouse() → c(x = , y = ) Where the pointer last was over the drawing, as a vector of two named numbers: m <- draw$mouse(), then m[["x"]] and m[["y"]]. Before it has moved over the drawing at all, (0, 0).

draw$mouseDown() → TRUE / FALSE Whether a button is held down right now, or a finger is on the screen. For painting and dragging.

draw$click() → c(x = , y = ) or NULL The next click, or NULL if there was none. A click is counted when the button is let go, at the place it was let go, and clicks are queued in order like key presses, so none is lost between two frames. This is the one to use for choosing and placing.

draw$clicks() → a list of c(x = , y = ) pairs. Every click since the last call, oldest first; an empty list if none.

Up to 64 key presses and 64 clicks wait to be asked for; a program that never asks loses the oldest.

Asking the page a question, whether for keys, the mouse, a measurement or a hit test, needs the same shared memory as readline(), which the Rprimo site has. A copy of the editor served without the two cross-origin headers cannot answer. There the keyboard and the mouse hear nothing (key() is always NULL, mouse() is always (0, 0)), measureText estimates from the font size, the hit tests answer FALSE, waitKey() stops with an error, and colours are taken on trust instead of being checked.

6Examples#

All of these are complete programs. Paste one into the editor and press Run.

A face#

Two pens at work: the head is filled in one colour and outlined in another, and the smile is an arc built as a path and stroked.

rOpen
library(Rprimo)

s <- draw$size()
cx <- s$width / 2
cy <- s$height / 2
r <- min(s$width, s$height) / 3

draw$clear("skyblue")
draw$strokeStyle <- "black"
draw$lineWidth <- 3

draw$fillStyle <- "gold"
draw$fillCircle(cx, cy, r)                        # head
draw$strokeCircle(cx, cy, r)                      # and its outline
draw$fillStyle <- "black"
draw$fillCircle(cx - r / 3, cy - r / 4, r / 8)    # eyes
draw$fillCircle(cx + r / 3, cy - r / 4, r / 8)

draw$beginPath()
draw$arc(cx, cy + r / 6, r / 2, 20, 160)          # smile: lower part of a circle
draw$stroke()

A grid with loops#

seq() plus line is the workhorse of most drawings.

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height
step <- 40

draw$clear("white")
draw$color("lightgray")
for (x in seq(0, w, by = step)) {
  draw$line(x, 0, x, h)
}
for (y in seq(0, h, by = step)) {
  draw$line(0, y, w, y)
}

draw$color("black")
draw$line(0, h / 2, w, h / 2)   # axes
draw$line(w / 2, 0, w / 2, h)

A bar chart#

Data → rectangles, with labels underneath. The data is a named vector: the names are the days, the values the scores. Note how the bar height is subtracted from the baseline because y grows downward, and how fillStyle is switched between the bars and the text, since write paints with it too.

rOpen
library(Rprimo)

scores <- c(Mon = 3, Tue = 7, Wed = 5, Thu = 9, Fri = 6)

s <- draw$size()
w <- s$width
h <- s$height
margin <- 40
base <- h - margin
gap <- (w - 2 * margin) / length(scores)
tallest <- max(scores)

draw$clear("white")
draw$strokeStyle <- "black"
draw$line(margin, base, w - margin, base)

for (i in seq_along(scores)) {
  x <- margin + (i - 1) * gap + gap * 0.15
  barH <- (base - margin) * scores[[i]] / tallest
  draw$fillStyle <- "steelblue"
  draw$fillRect(x, base - barH, gap * 0.7, barH)
  draw$fillStyle <- "black"
  draw$write(x, base + 6, names(scores)[i], size = 14)
  draw$write(x, base - barH - 20, scores[[i]], size = 14)
}

This is the kind of chart barplot(scores) draws for you in the Plots tab; drawing it yourself is how you get one that looks exactly the way you want, or one that moves.

A graph of a function#

A curve is a path of many short segments: moveTo the first point, lineTo the rest, stroke once. R works out all the points at once, as vectors; the loop only hands them to the pen. This maps maths coordinates (x from −2π to 2π, y from −1 to 1) onto the canvas.

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height
draw$clear("white")

draw$color("gray")
draw$line(0, h / 2, w, h / 2)

px <- seq(0, w, by = 2)                 # every second column of the canvas
x <- (px / w) * 4 * pi - 2 * pi         # canvas -> maths
y <- sin(x)
py <- h / 2 - y * (h / 2 - 20)          # maths -> canvas (flip y)

draw$color("crimson")
draw$beginPath()
draw$moveTo(px[1], py[1])
for (i in 2:length(px)) {
  draw$lineTo(px[i], py[i])
}
draw$stroke()
draw$write(10, 10, "y = sin(x)", size = 18, font = "mono")

A regular polygon and a star#

fillPolygon takes a matrix of points, one row per point, and trigonometry produces them, a whole vector at a time. Angles here are turned into radians because cos() and sin() want radians; only draw itself speaks degrees.

rOpen
library(Rprimo)

# n points evenly spaced on a circle, the first one at `start` degrees.
ring <- function(cx, cy, r, n, start = -90) {
  a <- (start + (seq_len(n) - 1) * 360 / n) * pi / 180
  cbind(cx + r * cos(a), cy + r * sin(a))
}

s <- draw$size()
w <- s$width
h <- s$height
draw$clear("midnightblue")
draw$strokeStyle <- "white"

# hexagon on the left
draw$fillStyle <- "seagreen"
draw$fillPolygon(ring(w * 0.3, h / 2, 80, 6))
draw$strokePolygon(ring(w * 0.3, h / 2, 80, 6))

# five-pointed star on the right: ten points, the radius taking turns
# between outer and inner
star <- ring(w * 0.7, h / 2, rep(c(90, 36), 5), 10)
draw$fillStyle <- "gold"
draw$fillPolygon(star)

A pie chart, in degrees#

Each slice is a path: the centre, an arc, and back. Degrees make the arithmetic plain: a share of the total is a share of 360.

rOpen
library(Rprimo)

shares <- c(rent = 40, food = 25, fun = 20, saved = 15)
colours <- c("steelblue", "salmon", "gold", "seagreen")

s <- draw$size()
cx <- s$width / 2
cy <- s$height / 2
r <- min(s$width, s$height) / 3
draw$clear("white")

start <- -90                                  # begin at the top
for (i in seq_along(shares)) {
  end <- start + 360 * shares[[i]] / 100
  draw$beginPath()
  draw$moveTo(cx, cy)
  draw$arc(cx, cy, r, start, end)
  draw$closePath()
  draw$fillStyle <- colours[i]
  draw$fill()
  draw$stroke()
  start <- end
}

A rainbow with HSL colours#

Varying the hue in a loop is the easiest way to get many distinct colours; paste0() builds each colour's text. Each band is a half circle, stroked with a thick pen.

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height
draw$clear("white")
bands <- 12
draw$lineWidth <- h / bands / 2
for (i in 0:(bands - 1)) {
  draw$strokeStyle <- paste0("hsl(", i * 360 / bands, ", 90%, 55%)")
  draw$beginPath()
  draw$arc(w / 2, h, h * (1 - i / bands) - 10, 180, 360)
  draw$stroke()
}

A sunset with gradients#

A linear gradient for the sky, a radial one for the sun. Both are assigned to fillStyle like a colour.

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height

sky <- draw$createLinearGradient(0, 0, 0, h)
sky$addColorStop(0, "midnightblue")
sky$addColorStop(0.6, "orangered")
sky$addColorStop(1, "gold")
draw$fillStyle <- sky
draw$fillRect(0, 0, w, h)

sun <- draw$createRadialGradient(w / 2, h * 0.7, 0, w / 2, h * 0.7, h / 4)
sun$addColorStop(0, "white")
sun$addColorStop(0.3, "yellow")
sun$addColorStop(1, "rgba(255, 200, 0, 0)")   # fades to nothing
draw$fillStyle <- sun
draw$fillCircle(w / 2, h * 0.7, h / 4)

A clock face with transforms#

Instead of computing where each tick goes, turn the paper: translate to the centre, rotate by the hour, and draw every tick at the same place. save() and restore() undo the turn each time.

rOpen
library(Rprimo)

s <- draw$size()
cx <- s$width / 2
cy <- s$height / 2
r <- min(s$width, s$height) / 2.5

draw$clear("white")
draw$strokeCircle(cx, cy, r)
draw$lineWidth <- 4
for (hour in 0:11) {
  draw$save()
  draw$translate(cx, cy)
  draw$rotate(hour * 30)
  draw$line(0, -r, 0, -r + 15)
  draw$restore()
}

draw$save()
draw$translate(cx, cy)
draw$rotate(10 * 30 + 15)                 # half past ten
draw$lineWidth <- 6
draw$line(0, 0, 0, -r * 0.55)             # hour hand
draw$restore()
draw$save()
draw$translate(cx, cy)
draw$rotate(180)
draw$lineWidth <- 3
draw$line(0, 0, 0, -r * 0.85)             # minute hand
draw$restore()

A dashed, clipped drawing#

clip() keeps everything after it inside a path, and setLineDash changes the stroke. Both are undone by restore().

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height
draw$clear("white")

draw$save()
draw$beginPath()
draw$arc(w / 2, h / 2, min(w, h) / 3, 0, 360)
draw$clip()                               # only inside the circle from here on
for (y in seq(0, h, by = 12)) {
  draw$strokeStyle <- paste0("hsl(", y * 360 / h, ", 80%, 60%)")
  draw$line(0, y, w, y)
}
draw$restore()

draw$setLineDash(c(10, 6))
draw$strokeStyle <- "black"
draw$strokeCircle(w / 2, h / 2, min(w, h) / 3)
draw$setLineDash(c())

Text placement#

write anchors at the top-left and takes its size in the call. The canvas's fillText uses the font style and can be centred with textAlign, and measureText says how wide a word will be, so that a box fits it.

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height
draw$clear("#222")
draw$fillStyle <- "white"
draw$strokeStyle <- "gold"

draw$write(10, 10, "top-left, sans 16")
draw$write(10, 34, "serif, 20", size = 20, font = "serif")

draw$font <- "bold 48px monospace"
draw$textAlign <- "center"
draw$textBaseline <- "middle"
draw$fillText("Rprimo", w / 2, h / 2)

width <- draw$measureText("Rprimo")
draw$strokeRect(w / 2 - width / 2 - 10, h / 2 - 34, width + 20, 68)

A game loop#

Draw a frame, sleep a little, ask what was pressed, repeat. That is all a game loop is. Example 14 in the examples menu, Catch the dot, is a whole game; this is its shape. repeat goes round for ever, so the loop needs a way out: here Escape ends it with break, and the Stop button ends any loop.

rOpen
library(Rprimo)

x <- 40
y <- 40
dx <- 2
dy <- 0
TURNS <- list(left = c(-2, 0), right = c(2, 0), up = c(0, -2), down = c(0, 2))

draw$fillStyle <- "limegreen"
repeat {
  k <- draw$key()                     # NULL, or a key pressed since last frame
  if (identical(k, "escape")) break
  if (!is.null(k) && k %in% names(TURNS)) {
    dx <- TURNS[[k]][1]
    dy <- TURNS[[k]][2]
  }
  x <- x + dx
  y <- y + dy
  draw$clear("black")
  draw$fillCircle(x, y, 8)
  Sys.sleep(0.03)                     # shows the frame, then waits
}

Painting with the mouse#

mouse() says where the pointer is and mouseDown() whether it is pressed, so a drag leaves a trail. Nothing is cleared between frames here: the drawing accumulates. Press C to start again and Escape to finish.

rOpen
library(Rprimo)

draw$clear("white")
draw$fillStyle <- "crimson"
repeat {
  m <- draw$mouse()
  if (draw$mouseDown()) {
    draw$fillCircle(m[["x"]], m[["y"]], 6)
  }
  k <- draw$key()
  if (identical(k, "c")) draw$clear("white")
  if (identical(k, "escape")) break
  Sys.sleep(0.02)
}

Buttons: hit tests and the mouse#

A button is a path. Draw it once, and when a click arrives, build the same path again and ask isPointInPath. The click is in drawing units, and so is the path, so nothing needs converting.

rOpen
library(Rprimo)

s <- draw$size()
w <- s$width
h <- s$height
buttons <- c(crimson = 30, seagreen = 100, steelblue = 170)   # colour = y

button <- function(y) {
  draw$beginPath()
  draw$roundRect(30, y, 140, 50, 12)
}

draw$clear("white")
for (colour in names(buttons)) {
  button(buttons[[colour]])
  draw$fillStyle <- colour
  draw$fill()
  draw$fillStyle <- "white"
  draw$fillText(colour, 45, buttons[[colour]] + 17)
}

repeat {
  where <- draw$click()
  if (!is.null(where)) {
    for (colour in names(buttons)) {
      button(buttons[[colour]])
      if (draw$isPointInPath(where[["x"]], where[["y"]])) {
        draw$fillStyle <- colour
        draw$fillCircle(w * 0.7, h / 2, min(w, h) / 5)
      }
    }
  }
  if (identical(draw$key(), "escape")) break
  Sys.sleep(0.03)
}

Recursion: a tree#

Drawings make recursion visible. Record this one and scrub the tape: the tree grows branch by branch.

rOpen
library(Rprimo)

branch <- function(x, y, len, angle, depth) {
  if (depth == 0) return(invisible(NULL))
  x2 <- x + len * cos(angle * pi / 180)
  y2 <- y - len * sin(angle * pi / 180)
  draw$lineWidth <- depth
  draw$line(x, y, x2, y2)
  branch(x2, y2, len * 0.72, angle + 25, depth - 1)
  branch(x2, y2, len * 0.72, angle - 25, depth - 1)
}

s <- draw$size()
draw$clear("white")
draw$strokeStyle <- "saddlebrown"
branch(s$width / 2, s$height - 10, s$height / 4, 90, 9)

The same tree with transforms, and no trigonometry: each branch turns the paper and draws straight up.

rOpen
library(Rprimo)

branch <- function(len, depth) {
  if (depth == 0) return(invisible(NULL))
  draw$lineWidth <- depth
  draw$line(0, 0, 0, -len)
  draw$translate(0, -len)
  for (turn in c(25, -25)) {
    draw$save()
    draw$rotate(turn)
    branch(len * 0.72, depth - 1)
    draw$restore()
  }
}

s <- draw$size()
draw$clear("white")
draw$strokeStyle <- "saddlebrown"
draw$translate(s$width / 2, s$height - 10)
branch(s$height / 4, 9)

7Drawing and the debugger#

Drawings are part of the recording. Switch Record on and press Run, then move along the tape: the canvas is rebuilt to show exactly what had been drawn at that step, so a loop that draws a bar per iteration shows one more bar each time you step forward, and one fewer each time you step back. A stage is taken when ink lands on the canvas: fill, stroke, the rectangle and text calls and everything built on them. Setting a style, moving the pen or adding to a path changes nothing visible, so it makes no stage of its own. Up to sixty stages of a drawing are kept per recording; a program that draws more than that keeps every second stage, then every fourth, so the whole run stays covered at a coarser spacing. A program that sleeps between drawings is animating, and there a stage is one whole frame: what the canvas showed when the sleep began.

This is the most useful thing about draw for learning: put a drawing call inside the code you don't understand and watch when it happens.

8Limits and gotchas#

  • Rprimo only exists inside Rprimo. If you copy a program that uses it into R on your own computer, library(Rprimo) will fail there with "there is no package called 'Rprimo'"; that's expected, not a mistake in your code. To share a drawing program, share the Rprimo link instead. For drawing elsewhere in R, the charts of plot() and friends work everywhere, and for a web page there is the canvas itself: the names are the same ones.
  • There is no turtle. Python's turtle module has no counterpart here, and none is planned in Rprimo. The shortcuts (line, fillCircle, fillPolygon) cover what a first turtle drawing does, and transforms are the turtle's "turn and go forward": see the second tree in the examples.
  • y is upside down compared to graph paper. To plot maths, flip it: py <- h - y, or draw$scale(1, -1) after translating the origin to the bottom.
  • draw uses degrees, sin() and cos() use radians. Convert with * pi / 180 and * 180 / pi.
  • Styles live on draw. draw$fillStyle <- "gold" changes the pen; a bare fillStyle <- "gold" makes an ordinary variable of your own and draws nothing. Keep draw$ in front. A misspelt style is caught: draw$fillStile <- "red" stops with cannot add bindings to a locked environment, R's way of saying draw has no such name.
  • One number per argument, except for shapes. The shape calls take vectors and draw one shape per element (see Many shapes in one call). Everything else takes one number per argument: draw$lineTo(xs, ys) with vectors stops with got 10 values where one was needed, because a path is built one point at a time; loop over the points with for (i in seq_along(xs)), or use strokePolygon. NA, Inf and NaN are refused everywhere. The points of fillPolygon and strokePolygon, the lengths of setLineDash and the radii of roundRect take many numbers by nature.
  • A path is painted, not drawn. moveTo, lineTo and arc add to a path; nothing appears until fill() or stroke(). If a shape is missing, the paint call probably is.
  • Animation is a loop you write. There is no frame callback: draw, Sys.sleep(), repeat. Every Sys.sleep() shows what has been drawn so far, and so does every key and mouse function. A repeat loop runs until it reaches break or you press Stop; a for (frame in 1:400) loop ends by itself, which is what example 14 does. A recording (Record) stops after 5000 steps, so record a short game and run a long one.
  • draw$key() can be NULL. if (k == "left") stops with argument is of length zero on every frame where no key was pressed. Write identical(k, "left"), or test !is.null(k) first; the same goes for draw$click().
  • No images or raw pixels. The canvas's drawImage, getImageData, putImageData and patterns are not mirrored: images would have to cross from the page into R and back, and a pixel at a time is slow across that bridge. Draw with shapes; a fractal is fillRect with a 1×1 size, and with vectors of positions one call draws every cell of a colour.
  • Questions need the page's shared memory. The keyboard, the mouse, measureText and the hit tests wait for an answer, which only a cross-origin-isolated page can give; the Rprimo site is one. Everything that only draws works anywhere.
  • Size varies. Never hard-code 400 × 300; ask draw$size(). Resizing the console changes the canvas only at the next run.
  • Colour typos are errors. That is deliberate: a wrong name would otherwise just draw nothing and leave you puzzled. A font string is not checked in the same way, since checking it would mean waiting for an answer on every word; an unknown face in write() silently falls back to sans-serif.
  • clear ignores the transform and the pens; it always paints the whole area, and does not change any style.
  • Styles persist across calls, and so do transforms. A thick outline or a rotated frame that surprises you later was set earlier; draw$save() before and draw$restore() after is the habit that prevents it. Each run starts afresh, though: the canvas, the styles and the transform are back to the table in The styles every time you press Run.