Pyctures: Python 2D and 3D Graphics

Pyctures ("Python pictures") is for making 2D and 3D graphics and games in Python. It is for any age and any level, a first program or a thousandth, and it is designed to bring the joy back into programming: you type a few lines and see something move. A Python file in the code editor can show a preview beside the code, and the file draws on it: shapes, text, pictures, moving sprites, games, charts and 3D scenes.

A few plain functions draw your first shapes. Behind them, Python gets the full set of JavaScript graphics functions: the web's 2D canvas for 2D, and three.js for 3D, with the same names. So any JavaScript canvas or three.js example can be brought across with a few small changes (how).

Pyctures is also a free, open-source library for your own Python: see the Pyctures page, where you can edit a scene and run it in your browser.

Its JavaScript sibling is pictures.js, which draws the same way from a plain .js file: see JavaScript graphics and the pictures.js page.

It works the way an HTML file does. You write code on one side and see the result on the other. Press Run and the preview draws; change the code and press Run again.

def draw():
    background("midnightblue")
    circle(width / 2, height / 2, 80, "gold")

That is a whole program. There is nothing to import and nothing to set up: draw() is called every frame. background paints the whole preview and circle(x, y, radius, colour) draws a circle. width and height are the size of the preview, so the circle sits in the middle. For everything else there is ctx, the web's own 2D drawing surface (Drawing in 2D).

The next three steps

Change that program a little at a time. Each of these is a whole program too.

Make it move

x = 0

def update(dt):
    global x
    x += 200 * dt            # 200 points every second
    if x > width:            # off the right edge: start again on the left
        x = 0

def draw():
    background("midnightblue")
    circle(x, height / 2, 40, "gold")

update(dt) is called before every draw(). dt is the time since the last frame, in seconds: about a sixtieth of a second on most screens, and less on a faster one. It is there because screens do not all draw at the same rate, and a busy device can drop frames. A program that moved the circle a fixed amount every frame would run faster on one screen than on another. Multiplying by dt evens that out: 200 * dt is 200 points a second everywhere, however many frames that second has. global x lets the function change the x made outside it. Change the 200 and see it change.

Make it answer you

x, y = 200, 150

def update(dt):
    global x, y
    if "ArrowLeft" in keys: x -= 300 * dt
    if "ArrowRight" in keys: x += 300 * dt
    if mouse.down:           # a mouse button, or a finger
        x, y = mouse.x, mouse.y

def draw():
    background("midnightblue")
    circle(x, y, 40, "gold")

"ArrowLeft" in keys is true while that key is held, and mouse is the pointer, or a finger on a touch screen. Click the preview first, so it has the keyboard.

A bit more: a small game

basket = Sprite("🧺", 200, 250, size=64)
stars = [Sprite("⭐", random(400), random(300), size=36) for i in range(5)]
score = 0

def update(dt):
    global score
    if mouse.inside:                 # the pointer is over the picture
        basket.x = mouse.x
    basket.y = height - 50
    for star in stars:
        star.y += 200 * dt
        caught = star.touches(basket)
        if caught:
            score += 1
        if caught or star.y > height:    # caught or missed: back to the top
            star.x = random(width)
            star.y = 0

def draw():
    background("midnightblue")
    for star in stars:
        star.draw()
    basket.draw()
    text(f"Score {score}", 12, 30, "white", 20)

A Sprite is a picture that moves, here an emoji, and touches says when two of them meet. Move the mouse, or drag a finger, to catch the stars.

The tutorials carry on from here: your first drawing, a bouncing ball, a simple game and Space Rocks. The plain functions (background, circle, rect, line, text) cover a first picture; for everything else there is ctx, the web's own 2D drawing surface (Drawing in 2D).

The quickest start: ask for one

Type a short request in Ask, such as 2d python game, 3d game, 3d scene in python or python animation. A ready-made Python file opens beside its preview and starts running:

Ask forYou get
a 2D Python gameStar Catcher: move a rocket, catch stars, dodge rocks, with a score and lives
a 3D gameCube Dodge: roll a ball left and right to dodge oncoming blocks
a 3D scene in Python3D scene: a lit scene with shadows that you can orbit and zoom
a Python animationBouncing balls: balls with trails; tap to add more

It is an ordinary Python file, so you can read it, change it and press Run again.

Make a Python file

  1. From the new-document menu (+), choose Code, then Python.
  2. Give the file a name ending in .py and create it.
  3. A new, empty Python file opens split: the code on the left and an empty preview on the right that says Run to draw here.
  4. Type or paste a program, then press Run in the footer.

The first run in a session loads Python, which can take a few seconds. After that, runs start straight away.

The views

Two buttons in the editor's footer choose what you see, the same pair an HTML file has:

ButtonWhat you see
PreviewThe preview only, filling the editor
SplitThe code and the preview side by side
Both offPlain Python: only the code, and Run prints to the output panel as usual

A new, empty Python file opens split, and so does a file that draws (one that uses ctx, circle(...), THREE, Sprite and so on). Plain Python opens as code only. Once you pick a view yourself, it is remembered for that file on this device.

You do not have to remember to turn it on. If you press Run with the preview off and the file uses the preview (ctx, THREE, Sprite, await frame() and so on), the editor switches to split and runs it there.

Run and Stop

  • Run runs the file in the preview. While the program is running, the same button is Stop.
  • A program that only draws once finishes by itself. A program with a draw() function, an update(dt) function or a loop keeps running until you press Stop.
  • Every Run starts from a clean preview.
  • Anything your program prints with print() appears under the preview, and so do errors. copies all of it, to paste into a chat or a message; clears it.
  • A file that draws starts by itself when its tab comes on screen, and waits while you are on another tab. About half a second after you stop typing, it runs again with the new code. To make a file wait for Run, put # pyctures: manual near the top: see Starting and stopping.
  • Frame rate shows how many frames per second the program is drawing, as a small number in the top-right corner of the preview. It is off until you turn it on, and the choice applies to every Python file.
  • Debugging turns breakpoints on and off. See Debugging.

Record a video

Record Video in the footer saves the preview as an MP4 video, every frame exactly one frame after the one before it: see Recording videos, which is the same for a Pyctures file. A file that keeps running records until you stop it.

A scene that makes sound is recorded with its sound in the same file, exactly in step with the pictures: see A video with its sound. The words spoken by say() are not in the video: your device speaks them straight to its speaker.

On a phone

A phone has room for one pane at a time.

  1. A file that draws opens on the preview. For any other file, tap in the footer to turn the preview on.
  2. A Code / Preview switcher appears. Use it to move between the code and the preview.
  3. Run shows the preview. Tapping an error's line takes you to the code.

Touch works as the mouse does: a finger on the preview is mouse.down, and dragging moves mouse.x and mouse.y. For more than one finger, read touches and pinch.

Where your Python runs

You never choose this; Circuitry picks for each run:

  • On iPhone and iPad, the Python built into Circuitry runs the file, inside the app, when the libraries the file imports are included with it.
  • In the desktop app, Circuit runs the file with the same built-in Python, installed the first time you run Python. If you picked your own Python in Circuit's Python setup, that one is used.
  • Otherwise (on the web, or when a file imports a library the built-in Python doesn't include), a Python that runs inside the page runs it. It works on any device, with nothing to install, and loads libraries the first time a file imports them.

The program and the preview behave the same in each. The one difference is debugging: a run can stop at a breakpoint only on iPhone and iPad, with the built-in Python. Elsewhere the run tells you so and goes straight through.

Using the module

Everything is ready to use without an import. The names come from a module called pyctures, so if your editor or your habits want an import, this works too:

from pyctures import *

def draw():
    ctx.fill_style = "teal"
    ctx.fill_rect(20, 20, 200, 120)

What's in this section