primopydrawing with draw
draw is the drawing context built into primopy. You write from primopy import draw, call a few of its methods, and the picture appears in the Draw tab of the console. It is made to give you a friendly start into Python: draw something on your first day.
primopy is the module 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 imports and then 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 method and every style on this page is the canvas's, written the Python way, 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 import line and then talks to draw:
from primopy import draw
draw.clear("midnightblue")
draw.fill_style = "gold"
draw.fill_circle(200, 150, 60)
draw.write(20, 20, "Hello!")Every method is called on draw, and a style such as fill_style is set by assignment: draw.fill_style = "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, and 16 is a whole game.
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 may be floats.
draw.fill_circle(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:
from primopy import draw
width, height = draw.size()
draw.stroke_circle(width / 2, height / 2, min(width, 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, with the names spelled the way Python spells them; a page gets it with canvas.getContext("2d"), a program here gets it with from primopy import draw. The rule is mechanical: a name written likeThis in the canvas documentation is written like_this here.
| On the canvas (MDN) | In draw |
|---|---|
ctx.fillRect(x, y, w, h) | draw.fill_rect(x, y, w, h) |
ctx.fillStyle = "gold" | draw.fill_style = "gold" |
ctx.beginPath(), ctx.moveTo(), ctx.lineTo() | draw.begin_path(), draw.move_to(), draw.line_to() |
ctx.arc(x, y, r, 0, Math.PI) | draw.arc(x, y, r, 0, 180) |
ctx.createLinearGradient(...) | draw.create_linear_gradient(...) |
ctx.measureText(s).width | draw.measure_text(s) |
So when you wonder what line_join does, or how bezier_curve_to bends, look up lineJoin or bezierCurveTo 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 primopy; 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.line_width = 4anddraw.fill_style = "gold", exactly likectx.lineWidth = 4on the canvas. There is noset_line_width()function. Reading a style back gives what you last set. - Every call that paints says how. The canvas has no
rect()that draws; it hasfill_rectandstroke_rect, andfill_textandstroke_text, 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:fill_circleandstroke_circle,fill_polygonandstroke_polygon. - Angles are in degrees. The canvas wants radians;
drawtakes degrees everywhere an angle appears (arc,ellipse,rotate,create_conic_gradient) and converts, because90reads better thanmath.pi / 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. A gradient (see Gradients) is accepted wherever a colour is, for fill_style and stroke_style.
If you pass something that isn't a colour, draw raises a ValueError instead of silently drawing nothing:
ValueError: '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: fill_style paints the inside of shapes and the body of text, stroke_style draws outlines and lines. They are set by assignment, and each stays as it is until you change it:
from primopy import draw
draw.fill_style = "gold"
draw.stroke_style = "black"
draw.line_width = 3
draw.fill_circle(100, 100, 40) # gold
draw.stroke_circle(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 |
|---|---|---|
fill_style | "black" | Colour (or gradient) for fill* calls and write |
stroke_style | "black" | Colour (or gradient) for stroke* calls and line |
line_width | 2 | Thickness of lines and outlines, in drawing units |
line_cap | "round" | Line ends: "butt", "round" or "square" |
line_join | "round" | Corners: "round", "bevel" or "miter" |
text_baseline | "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 fill_text and stroke_text |
text_align | "start" | Which part of the text sits on x: "start" (the left, in left-to-right text), "end", "left", "center", "right" |
letter_spacing | "0px" | Extra space between letters, as a CSS length |
miter_limit | 10 | How sharp a "miter" corner may get before it is cut off |
line_dash_offset | 0 | Where along the dash pattern a stroke starts; see set_line_dash |
global_alpha | 1 | Transparency of everything drawn afterwards, 0 to 1 |
global_composite_operation | "source-over" | How new paint mixes with what is there, e.g. "lighter", "multiply" |
shadow_color | "rgba(0, 0, 0, 0)" | The colour of a drop shadow under everything drawn afterwards; transparent, so no shadow, until set |
shadow_blur, shadow_offset_x, shadow_offset_y | 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)" |
image_smoothing_enabled | True | The canvas has it, so draw has it; without images it changes nothing |
Reading a style back, print(draw.line_width), 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.
5Reference#
Setup#
draw.size() → (width, height) The drawing area in drawing units. Call it at the start and derive your positions from it.
draw.clear(colour="white") Fill the whole canvas with one colour. draw.clear() gives a white page; draw.clear(None) 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 fill_style.
draw.color(colour) Set fill_style and stroke_style together.
draw.flush() Shows everything drawn so far, right now. Drawing calls travel to the page in batches; time.sleep(), input(), and every call that needs an answer from the page (the keyboard, the mouse, measure_text, 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 stroke_style.
draw.fill_circle(x, y, r) / draw.stroke_circle(x, y, r) Circle centred on (x, y) with radius r.
draw.fill_ellipse(x, y, rx, ry) / draw.stroke_ellipse(x, y, rx, ry) Ellipse centred on (x, y), rx wide and ry tall (those are half-widths, like a radius).
draw.fill_polygon(points) / draw.stroke_polygon(points) A closed shape through a list of (x, y) pairs; the last point is joined back to the first. Needs at least two points.
And the canvas's own one-call rectangles:
draw.fill_rect(x, y, w, h) / draw.stroke_rect(x, y, w, h) Rectangle with its top-left corner at (x, y).
draw.clear_rect(x, y, w, h) Erase a rectangle to transparent.
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 begin_path(), which is why a shape can be filled and then stroked without being described twice.
from primopy import draw
draw.begin_path()
draw.move_to(50, 150)
draw.line_to(150, 30)
draw.line_to(250, 150)
draw.close_path()
draw.fill_style = "tomato"
draw.fill()
draw.stroke()draw.begin_path() Start a new path, forgetting the old one.
draw.move_to(x, y) Lift the pen and put it down at (x, y): the start of a line.
draw.line_to(x, y) A straight line from the pen to (x, y).
draw.close_path() 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: move_to the centre, arc, close_path, 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.arc_to(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.round_rect(x, y, w, h, radii) Add a rectangle to the path, the second with rounded corners: one radius for all four, or a list of them.
draw.quadratic_curve_to(cpx, cpy, x, y) / draw.bezier_curve_to(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 fill_style or as an outline in stroke_style.
draw.clip() From now on, paint only inside the current path. There is no un-clip; wrap it in save() and restore().
draw.set_line_dash(lengths) Stroke with dashes from here on: a list of on and off lengths, [8, 4] for dashes, [2, 6] for dots, [] for solid lines again. line_dash_offset 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.set_transform(a, b, c, d, e, f) / draw.reset_transform() 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 save() and end with 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 fill_style or stroke_style as you would a colour:
from primopy import draw
w, h = draw.size()
g = draw.create_linear_gradient(0, 0, 0, h) # top to bottom
g.add_color_stop(0, "navy")
g.add_color_stop(1, "orange")
draw.fill_style = g
draw.fill_rect(0, 0, w, h)draw.create_linear_gradient(x0, y0, x1, y1) Along the line from one point to the other; beyond the ends the end colours continue.
draw.create_radial_gradient(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.create_conic_gradient(start, x, y) Sweeping around (x, y) like a clock hand, from start degrees: a colour wheel.
gradient.add_color_stop(offset, colour) 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 Gradient, importable as from primopy import Gradient should you ever need the name. A gradient belongs to the run that made it: keep one in a variable across runs and assigning it raises, so make it again.
Text#
draw.write(x, y, text, size=16, font="sans") Write text with its top-left corner at (x, y), in fill_style. size is the height in drawing units. font is "sans", "serif" or "mono", the keys of FONTS (see below), or the name of any font family the page has loaded. Anything you pass is converted with str(), so numbers are fine. This is the beginner's call: size and face in the call, nothing to set first.
draw.fill_text(text, x, y) / draw.stroke_text(text, x, y) The canvas's own, in the font style, positioned by text_align and text_baseline. 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:
from primopy import draw
w, h = draw.size()
draw.font = "bold 40px sans-serif"
draw.text_align = "center"
draw.text_baseline = "middle"
draw.fill_text("centred", w / 2, h / 2)draw.measure_text(text) → width How wide text would be in the current font, in drawing units. For centring, text_align is simpler; measure_text is for when the width itself matters, such as underlining a word, drawing a box around it, or fitting a label to a bar.
FONTS A dictionary mapping the three short names to real font families. It lives in the module rather than on the context, so it takes its own import: from primopy import draw, FONTS. You can add your own entry: FONTS["fancy"] = "Georgia".
Hit tests#
draw.is_point_in_path(x, y) → True / False Whether (x, y) lies inside the current path. Build the path, then ask, before the next begin_path().
draw.is_point_in_stroke(x, y) → True / False Whether (x, y) lies on the outline of the current path, at the current line_width.
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 None The next key that was pressed, or None 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.
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() → list of names Every key pressed since the last call, oldest first.
draw.keys_down() → set of names Every key held down right now.
draw.wait_key() → 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() → (x, y) Where the pointer last was over the drawing. Before it has moved over the drawing at all, (0, 0).
draw.mouse_down() → True / False Whether a button is held down right now, or a finger is on the screen. For painting and dragging.
draw.click() → (x, y) or None The next click, or None 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() → list of (x, y) pairs Every click since the last call, oldest first.
Asking the page a question, whether for keys, the mouse, a measurement or a hit test, needs the same shared memory as input(), which primopy.com has. A copy of the editor served without the two cross-origin headers cannot answer, and there those calls raise an error; colours are then 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.
from primopy import draw
w, h = draw.size()
cx, cy = w / 2, h / 2
r = min(w, h) / 3
draw.clear("skyblue")
draw.stroke_style = "black"
draw.line_width = 3
draw.fill_style = "gold"
draw.fill_circle(cx, cy, r) # head
draw.stroke_circle(cx, cy, r) # and its outline
draw.fill_style = "black"
draw.fill_circle(cx - r / 3, cy - r / 4, r / 8) # eyes
draw.fill_circle(cx + r / 3, cy - r / 4, r / 8)
draw.begin_path()
draw.arc(cx, cy + r / 6, r / 2, 20, 160) # smile: lower part of a circle
draw.stroke()A grid with loops#
range plus line is the workhorse of most drawings.
from primopy import draw
w, h = draw.size()
step = 40
draw.clear("white")
draw.color("lightgray")
for x in range(0, w, step):
draw.line(x, 0, x, h)
for y in range(0, h, 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. Note how the bar height is subtracted from the baseline because y grows downward, and how fill_style is switched between the bars and the text, since write paints with it too.
from primopy import draw
scores = {"Mon": 3, "Tue": 7, "Wed": 5, "Thu": 9, "Fri": 6}
w, h = draw.size()
margin = 40
base = h - margin
gap = (w - 2 * margin) / len(scores)
tallest = max(scores.values())
draw.clear("white")
draw.stroke_style = "black"
draw.line(margin, base, w - margin, base)
for i, (day, value) in enumerate(scores.items()):
x = margin + i * gap + gap * 0.15
bar_h = (base - margin) * value / tallest
draw.fill_style = "steelblue"
draw.fill_rect(x, base - bar_h, gap * 0.7, bar_h)
draw.fill_style = "black"
draw.write(x, base + 6, day, size=14)
draw.write(x, base - bar_h - 20, value, size=14)A graph of a function#
A curve is a path of many short segments: move_to the first point, line_to the rest, stroke once. This maps maths coordinates (x from −2π to 2π, y from −1 to 1) onto the canvas.
from primopy import draw
import math
w, h = draw.size()
draw.clear("white")
draw.color("gray")
draw.line(0, h / 2, w, h / 2)
draw.color("crimson")
draw.begin_path()
for px in range(0, w + 1, 2):
x = (px / w) * 4 * math.pi - 2 * math.pi # canvas → maths
y = math.sin(x)
py = h / 2 - y * (h / 2 - 20) # maths → canvas (flip y)
if px == 0:
draw.move_to(px, py)
else:
draw.line_to(px, py)
draw.stroke()
draw.write(10, 10, "y = sin(x)", size=18, font="mono")A regular polygon and a star#
fill_polygon takes any list of points; trigonometry produces them. Angles here are in radians because math.cos/math.sin want radians; only draw itself speaks degrees.
from primopy import draw
import math
def ring(cx, cy, r, n, start=-90):
"""n points evenly spaced on a circle, first one at `start` degrees."""
pts = []
for i in range(n):
a = math.radians(start + i * 360 / n)
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
return pts
w, h = draw.size()
draw.clear("midnightblue")
draw.stroke_style = "white"
# hexagon on the left
draw.fill_style = "seagreen"
draw.fill_polygon(ring(w * 0.3, h / 2, 80, 6))
draw.stroke_polygon(ring(w * 0.3, h / 2, 80, 6))
# five-pointed star on the right: alternate outer and inner radius
outer = ring(w * 0.7, h / 2, 90, 5)
inner = ring(w * 0.7, h / 2, 36, 5, start=-90 + 36)
star = [p for pair in zip(outer, inner) for p in pair]
draw.fill_style = "gold"
draw.fill_polygon(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.
from primopy import draw
shares = {"rent": 40, "food": 25, "fun": 20, "saved": 15}
colours = ["steelblue", "salmon", "gold", "seagreen"]
w, h = draw.size()
cx, cy, r = w / 2, h / 2, min(w, h) / 3
draw.clear("white")
start = -90 # begin at the top
for (name, share), colour in zip(shares.items(), colours):
end = start + 360 * share / 100
draw.begin_path()
draw.move_to(cx, cy)
draw.arc(cx, cy, r, start, end)
draw.close_path()
draw.fill_style = colour
draw.fill()
draw.stroke()
start = endA rainbow with HSL colours#
Varying the hue in a loop is the easiest way to get many distinct colours. Each band is a half circle, stroked with a thick pen.
from primopy import draw
w, h = draw.size()
draw.clear("white")
bands = 12
draw.line_width = h / bands / 2
for i in range(bands):
draw.stroke_style = f"hsl({i * 360 / bands}, 90%, 55%)"
draw.begin_path()
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 fill_style like a colour.
from primopy import draw
w, h = draw.size()
sky = draw.create_linear_gradient(0, 0, 0, h)
sky.add_color_stop(0, "midnightblue")
sky.add_color_stop(0.6, "orangered")
sky.add_color_stop(1, "gold")
draw.fill_style = sky
draw.fill_rect(0, 0, w, h)
sun = draw.create_radial_gradient(w / 2, h * 0.7, 0, w / 2, h * 0.7, h / 4)
sun.add_color_stop(0, "white")
sun.add_color_stop(0.3, "yellow")
sun.add_color_stop(1, "rgba(255, 200, 0, 0)") # fades to nothing
draw.fill_style = sun
draw.fill_circle(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.
from primopy import draw
w, h = draw.size()
cx, cy, r = w / 2, h / 2, min(w, h) / 2.5
draw.clear("white")
draw.stroke_circle(cx, cy, r)
draw.line_width = 4
for hour in range(12):
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.line_width = 6
draw.line(0, 0, 0, -r * 0.55) # hour hand
draw.restore()
draw.save()
draw.translate(cx, cy)
draw.rotate(180)
draw.line_width = 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 set_line_dash changes the stroke. Both are undone by restore().
from primopy import draw
w, h = draw.size()
draw.clear("white")
draw.save()
draw.begin_path()
draw.arc(w / 2, h / 2, min(w, h) / 3, 0, 360)
draw.clip() # only inside the circle from here on
for y in range(0, h, 12):
draw.stroke_style = f"hsl({y * 360 / h}, 80%, 60%)"
draw.line(0, y, w, y)
draw.restore()
draw.set_line_dash([10, 6])
draw.stroke_style = "black"
draw.stroke_circle(w / 2, h / 2, min(w, h) / 3)
draw.set_line_dash([])Text placement#
write anchors at the top-left and takes its size in the call. The canvas's fill_text uses the font style and can be centred with text_align, and measure_text says how wide a word will be, so that a box fits it.
from primopy import draw
w, h = draw.size()
draw.clear("#222")
draw.fill_style = "white"
draw.stroke_style = "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.text_align = "center"
draw.text_baseline = "middle"
draw.fill_text("primopy", w / 2, h / 2)
width = draw.measure_text("primopy")
draw.stroke_rect(w / 2 - width / 2 - 10, h / 2 - 34, width + 20, 68)A game loop: Snake#
Draw a frame, sleep a little, ask what was pressed, repeat. That is all a game loop is. Example 16 in the examples menu is the whole game; this is its shape.
from primopy import draw
import time
x, y = 40, 40
dx, dy = 2, 0
TURNS = {"left": (-2, 0), "right": (2, 0), "up": (0, -2), "down": (0, 2)}
draw.fill_style = "limegreen"
while True:
k = draw.key() # None, or a key pressed since last frame
if k in TURNS:
dx, dy = TURNS[k]
if k == "escape":
break
x, y = x + dx, y + dy
draw.clear("black")
draw.fill_circle(x, y, 8)
time.sleep(0.03) # shows the frame, then waitsPainting with the mouse#
mouse() says where the pointer is and mouse_down() whether it is pressed, so a drag leaves a trail. Nothing is cleared between frames here: the drawing accumulates.
from primopy import draw
import time
draw.clear("white")
draw.fill_style = "crimson"
while True:
x, y = draw.mouse()
if draw.mouse_down():
draw.fill_circle(x, y, 6)
k = draw.key()
if k == "c":
draw.clear("white")
if k == "escape":
break
time.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 is_point_in_path. The click is in drawing units, and so is the path, so nothing needs converting.
from primopy import draw
import time
w, h = draw.size()
buttons = {"crimson": 30, "seagreen": 100, "steelblue": 170} # colour: y
def button(y):
draw.begin_path()
draw.round_rect(30, y, 140, 50, 12)
draw.clear("white")
for colour, y in buttons.items():
button(y)
draw.fill_style = colour
draw.fill()
draw.fill_style = "white"
draw.fill_text(colour, 45, y + 17)
while True:
c = draw.click()
if c is not None:
for colour, y in buttons.items():
button(y)
if draw.is_point_in_path(c[0], c[1]):
draw.fill_style = colour
draw.fill_circle(w * 0.7, h / 2, min(w, h) / 5)
if draw.key() == "escape":
break
time.sleep(0.03)Recursion: a tree#
Drawings make recursion visible. Record this one and scrub the tape: the tree grows branch by branch.
from primopy import draw
import math
def branch(x, y, length, angle, depth):
if depth == 0:
return
x2 = x + length * math.cos(math.radians(angle))
y2 = y - length * math.sin(math.radians(angle))
draw.line_width = depth
draw.line(x, y, x2, y2)
branch(x2, y2, length * 0.72, angle + 25, depth - 1)
branch(x2, y2, length * 0.72, angle - 25, depth - 1)
w, h = draw.size()
draw.clear("white")
draw.stroke_style = "saddlebrown"
branch(w / 2, h - 10, h / 4, 90, 9)The same tree with transforms, and no trigonometry: each branch turns the paper and draws straight up.
from primopy import draw
def branch(length, depth):
if depth == 0:
return
draw.line_width = depth
draw.line(0, 0, 0, -length)
draw.translate(0, -length)
for turn in (25, -25):
draw.save()
draw.rotate(turn)
branch(length * 0.72, depth - 1)
draw.restore()
w, h = draw.size()
draw.clear("white")
draw.stroke_style = "saddlebrown"
draw.translate(w / 2, h - 10)
branch(h / 4, 9)7Drawing and the debugger#
Drawings are part of the recording. Press Record & step, 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#
primopyonly exists inside primopy. If you copy a program that uses it into another Python installation,from primopy import drawwill fail there; that's expected, not a mistake in your code. To share a drawing program, share the primopy link instead. For drawing elsewhere, look atturtle(bundled with Python),tkinter, orpygame, and for a web page, the canvas itself: the names are the same ones, in camelCase.- 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,mathuses radians. Convert withmath.radians()/math.degrees().- Styles live on
draw.draw.fill_style = "gold"changes the pen; a barefill_style = "gold"makes an ordinary variable of your own and draws nothing. Keepdraw.in front. - A path is painted, not drawn.
move_to,line_toandarcadd 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,
time.sleep(), repeat. Everysleepshows what has been drawn so far, and so does every key and mouse function. A recording (Record & step) stops after 5000 steps, about 20 seconds of Snake, so record a short game and run a long one. - No images or raw pixels. The canvas's
drawImage,getImageData,putImageDataand patterns are not mirrored: images would have to cross from the page into Python and back, and a pixel at a time is slow across that bridge. Draw with shapes; a fractal isfill_rectwith a 1×1 size, one call per cell, which is fine for a few thousand cells. - Questions need the page's shared memory. The keyboard, the mouse,
measure_textand the hit tests wait for an answer, which only a cross-origin-isolated page can give; primopy.com 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 raise 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 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;
save()before andrestore()after is the habit that prevents it.