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#
1Getting started#
Nothing to install. A drawing program starts with one library() line and then talks to draw:
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:
draw$size()
draw$fillStyle <- "tomato"
draw$fillRect(20, 20, 80, 50)
draw$fillStyleR'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:
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).width | draw$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 <- 4anddraw$fillStyle <- "gold", exactly likectx.lineWidth = 4on the canvas. There is nosetLineWidth()function. Reading a style back gives what you last set. - Every call that paints says how. The canvas has no
rect()that draws; it hasfillRectandstrokeRect, andfillTextandstrokeText, and a generalfill()andstroke()for a path you built. Fill means solid, in the fill colour; stroke means outline, in the stroke colour, at the line width.drawkeeps that rule for its own additions too:fillCircleandstrokeCircle,fillPolygonandstrokePolygon. - Angles are in degrees. The canvas wants radians;
drawtakes degrees everywhere an angle appears (arc,ellipse,rotate,createConicGradient) and converts, because90reads better thanpi / 2on 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:
| Form | Examples |
|---|---|
| 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:
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 widedraw$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.
| Style | Starts at | What it does |
|---|---|---|
fillStyle | "black" | Colour (or gradient) for fill* calls and write |
strokeStyle | "black" | Colour (or gradient) for stroke* calls and line |
lineWidth | 2 | Thickness 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 |
miterLimit | 10 | How sharp a "miter" corner may get before it is cut off |
lineDashOffset | 0 | Where along the dash pattern a stroke starts; see setLineDash |
globalAlpha | 1 | Transparency 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, shadowOffsetY | 0 | How soft the shadow is, and how far it sits to the right and down |
filter | "none" | A CSS filter, e.g. "blur(4px)" |
imageSmoothingEnabled | TRUE | The 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.
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.
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:
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
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#
Rprimoonly 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 ofplot()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
turtlemodule has no counterpart here, and none is planned inRprimo. 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, ordraw$scale(1, -1)after translating the origin to the bottom. drawuses degrees,sin()andcos()use radians. Convert with* pi / 180and* 180 / pi.- Styles live on
draw.draw$fillStyle <- "gold"changes the pen; a barefillStyle <- "gold"makes an ordinary variable of your own and draws nothing. Keepdraw$in front. A misspelt style is caught:draw$fillStile <- "red"stops with cannot add bindings to a locked environment, R's way of sayingdrawhas 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 withfor (i in seq_along(xs)), or usestrokePolygon.NA,InfandNaNare refused everywhere. The points offillPolygonandstrokePolygon, the lengths ofsetLineDashand the radii ofroundRecttake many numbers by nature. - A path is painted, not drawn.
moveTo,lineToandarcadd to a path; nothing appears untilfill()orstroke(). 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. EverySys.sleep()shows what has been drawn so far, and so does every key and mouse function. Arepeatloop runs until it reachesbreakor you press Stop; afor (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 beNULL.if (k == "left")stops with argument is of length zero on every frame where no key was pressed. Writeidentical(k, "left"), or test!is.null(k)first; the same goes fordraw$click().- No images or raw pixels. The canvas's
drawImage,getImageData,putImageDataand 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 isfillRectwith 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,
measureTextand 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; askdraw$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. clearignores 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 anddraw$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.