Codetoy — Python API Documentation

Codetoy runs Python via Pyodide in the browser. Your script is executed once, then the runtime calls your exported functions every frame and on input events.

The sample program below is a complete working sketch — a good starting point:

from codetoy.canvas import fill, rect, circle
from codetoy.screen import width, height, centerX, centerY
from codetoy.time import deltaTime
from codetoy._input import mouseX, mouseY
import math

t = 0

def update():
    global t
    t += deltaTime()

    fill(20, 20, 30)
    rect(0, 0, width(), height())

    r = 60 + math.sin(t * 2) * 20
    fill(100, 180, 255)
    circle(centerX(), centerY(), r)

    fill(255, 200, 50)
    circle(mouseX(), mouseY(), 12)

How it works

When you run your script, the runtime:

  1. Executes the entire file top-to-bottom (module-level code runs once — good for setup, world generation, class definitions, etc.)
  2. Looks for functions with specific names in your global scope (update, keyDown, mouseDown, etc.) and calls them at the appropriate times.

You never register callbacks. Just define the function at the top level with the right name and the runtime will find it.


Exported functions

These are the names the runtime looks for. Define any you need; omit the ones you don't.

def update() -> None

Called every frame. Put all drawing and game logic here.

from codetoy.canvas import fill, rect
from codetoy.screen import width, height

def update():
    fill(30, 30, 40)
    rect(0, 0, width(), height())

def resize(w: float, h: float) -> None

Called whenever the canvas is resized. Use this to cache width() / height() instead of calling them every frame.

from codetoy.screen import width, height

cx = width() / 2
cy = height() / 2

def resize(w, h):
    global cx, cy
    cx = w / 2
    cy = h / 2

def keyDown(key: str) -> None

Called once when a key is pressed. key matches the MDN KeyboardEvent.key values: "a", "A", "ArrowLeft", " " (space), "Enter", etc.

Use this for actions that should fire once per press (jumping, toggling, slot selection). For movement that should continue while held, use isKeyDown() inside update() instead.

jumping = False

def keyDown(key):
    global jumping
    if key == " ":
        jumping = True
    if key == "r":
        reset_game()

def keyUp(key: str) -> None

Called once when a key is released.

def keyUp(key):
    if key == "Shift":
        stop_sprinting()

def mouseDown(button: int) -> None

Called once when a mouse button is pressed. button: 0 = left, 1 = middle, 2 = right.

Use this for actions that should fire on click (not while held). For held-button logic, use isMouseDown() inside update().

just_clicked = False

def mouseDown(button):
    global just_clicked
    if button == 0:
        just_clicked = True

def mouseUp(button: int) -> None

Called once when a mouse button is released.

def mouseUp(button):
    if button == 0:
        finish_drag()

def touchStart(id: int, x: float, y: float) -> None

Called when a finger touches the screen. id is a unique identifier for that touch point.

def touchStart(id, x, y):
    spawn_particle(x, y)

def touchEnd(id: int) -> None

Called when a finger is lifted.

def touchEnd(id):
    end_swipe(id)

def touchMove(id: int, x: float, y: float) -> None

Called when a finger moves on the screen.

def touchMove(id, x, y):
    drag_to(id, x, y)

Imports

codetoy.canvas

from codetoy.canvas import fill, rect, circle  # named imports
import codetoy.canvas as canvas                # namespace import

codetoy.screen

from codetoy.screen import width, height, centerX, centerY

codetoy.time

from codetoy.time import deltaTime, elapsedTime

codetoy._input

from codetoy._input import mouseX, mouseY, isMouseDown, isKeyDown

API Reference

codetoy.canvas

Color state

fill(r, g, b, a=1.0)

Sets the fill color for subsequent drawing. r, g, b are 0–255; a is 0.0–1.0.

fill(255, 100, 0)        # opaque orange
fill(0, 150, 255, 0.5)  # semi-transparent blue

stroke(r, g, b, a=1.0)

Sets the stroke (outline) color.

stroke(255, 255, 255)

noFill() / noStroke()

Disables fill or stroke for subsequent shapes.

noStroke()
noFill()

fillStyle(color: str) / strokeStyle(color: str)

Sets fill/stroke using any CSS color string.

fillStyle("#ff6600")
fillStyle("hsl(200, 80%, 60%)")
strokeStyle("rgba(255,0,0,0.5)")

Line state

lineWidth(width: float)

Sets the line/stroke thickness in pixels.

lineWidth(3)

lineJoin(join: str)

"round", "bevel", or "miter".

lineCap(cap: str)

"butt", "round", or "square".

lineMiterLimit(limit: float)


Shape drawing

rect(x, y, w, h, r=0)

Draws a rectangle. r is the corner radius.

fill(0, 150, 255)
rect(100, 100, 200, 150)       # sharp corners
rect(100, 100, 200, 150, 12)   # rounded corners

circle(x, y, radius)

Draws a circle at (x, y).

fill(255, 80, 80)
circle(400, 300, 50)

ellipse(x, y, w, h)

Draws an ellipse. w and h are the horizontal and vertical radii.

ellipse(centerX(), centerY(), 100, 40)

line(x1, y1, x2, y2)

Draws a line segment.

stroke(255, 255, 0)
lineWidth(2)
line(0, 0, 400, 300)

triangle(x1, y1, x2, y2, x3, y3)

Draws a filled triangle.

fill(100, 200, 100)
triangle(200, 50, 100, 200, 300, 200)

polygon(points: list)

Draws a filled polygon from a flat list of [x1, y1, x2, y2, ...] coordinates.

fill(180, 100, 255)
polygon([100, 50,  200, 50,  250, 150,  50, 150])

Note: Pass a plain Python list. The runtime converts it to JS automatically.


Text

font(family: str, size: float, weight: str = "normal")

Sets the font before drawing text. Call this before every text() call if the font may have changed.

font("Arial", 24, "bold")
font("Courier", 14)

text(content: str, x: float, y: float)

Draws text at (x, y) using the current font and fill color.

fill(255, 255, 255)
font("Arial", 18)
text("Score: 42", 10, 30)

measureText(content: str) -> float

Returns the pixel width of a string in the current font. Useful for centering.

font("Arial", 20, "bold")
label = "Game Over"
text(label, centerX() - measureText(label) / 2, centerY())

Transform

translate(x, y) / rotate(radians) / scale(x, y)

Standard 2D canvas transforms applied to subsequent drawing.

import math
translate(centerX(), centerY())
rotate(math.pi / 4)
rect(-25, -25, 50, 50)  # drawn at center, rotated 45°
resetTransform()

resetTransform()

Resets to the identity transform.

push() / pop()

Save and restore the full canvas state (transform + color + line settings).

push()
translate(100, 100)
rotate(0.5)
fill(255, 0, 0)
rect(0, 0, 40, 40)
pop()
# back to previous state

reset()

Clears the entire screen and resets all canvas state.


Advanced path drawing

beginPath()
moveTo(100, 100)
lineTo(200, 100)
bezierCurveTo(250, 100, 250, 200, 200, 200)
lineTo(100, 200)
closePath()
fill(80, 160, 255)
fillPath()
stroke(255, 255, 255)
strokePath()
  • beginPath() — starts a new path
  • moveTo(x, y) — moves the pen without drawing
  • lineTo(x, y) — adds a straight line segment
  • bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) — cubic bezier
  • quadraticCurveTo(cpx, cpy, x, y) — quadratic bezier
  • closePath() — closes the path back to the start
  • fillPath() — fills the current path
  • strokePath() — strokes the current path

codetoy.screen

width() -> float / height() -> float

Current canvas dimensions in pixels.

centerX() -> float / centerY() -> float

Equivalent to width() / 2 and height() / 2.

frameCount() -> int

Total frames rendered since the program started.

fps() -> float

Current frames per second.

from codetoy.screen import fps, frameCount
from codetoy.canvas import fill, font, text

def update():
    fill(255, 255, 255)
    font("Arial", 12)
    text(f"FPS: {fps():.0f}  Frame: {frameCount()}", 8, 20)

codetoy.time

deltaTime() -> float

Seconds elapsed since the last frame. Always use this to make animations frame-rate independent.

from codetoy.time import deltaTime

x = 0.0
speed = 150.0  # pixels per second

def update():
    global x
    x += speed * deltaTime()

elapsedTime() -> float

Total seconds since the program started.

from codetoy.time import elapsedTime
import math

def update():
    t = elapsedTime()
    y = centerY() + math.sin(t * 3) * 80

codetoy._input

Mouse

mouseX() -> float / mouseY() -> float

Current mouse position in canvas coordinates.

isMouseDown(button: int) -> bool

Returns True while a mouse button is held. 0 = left, 1 = middle, 2 = right.

Use this inside update() for continuous held-button behavior (e.g. hold to paint). Use mouseDown() for one-shot click detection.

from codetoy._input import isMouseDown, mouseX, mouseY
from codetoy.canvas import fill, circle

def update():
    if isMouseDown(0):
        fill(255, 100, 0)
        circle(mouseX(), mouseY(), 20)

Keyboard

isKeyDown(key: str) -> bool

Returns True while a key is held. Call inside update() for continuous movement.

from codetoy._input import isKeyDown

def update():
    global player_x
    if isKeyDown("ArrowLeft") or isKeyDown("a"):
        player_x -= 200 * deltaTime()
    if isKeyDown("ArrowRight") or isKeyDown("d"):
        player_x += 200 * deltaTime()

Touch

isTouchDown(id: int = -1) -> bool

Returns True if any touch is active (no argument), or if a specific touch id is active.

touchCount() -> int

Number of active touch points.

touchX(id: int) -> float / touchY(id: int) -> float

Position of a touch point by its id. Returns -1 if the id is not active.


Patterns and gotchas

Global state

Python does not automatically write to globals from inside functions. Always declare global for any module-level variable you modify:

score = 0

def update():
    global score   # required — otherwise this creates a local variable
    score += 1

One-shot vs. continuous input

Situation Use
Jump on keypress def keyDown(key)
Move while key held isKeyDown(key) in update()
Click to place block def mouseDown(button) + a flag
Paint while mouse held isMouseDown(0) in update()
# One-shot click detection
clicked = False

def mouseDown(button):
    global clicked
    if button == 0:
        clicked = True

def update():
    global clicked
    if clicked:
        place_block()
        clicked = False

Frame-rate independence

Never move things by a fixed amount per frame. Always multiply by deltaTime():

# Bad — speed depends on frame rate
x += 5

# Good — 300 pixels per second regardless of FPS
x += 300 * deltaTime()

Module-level setup

Code outside any function runs exactly once when your script loads. Use it for world generation, initial state, and class definitions:

from codetoy.canvas import fill, rect
from codetoy.screen import width, height
import random

# Runs once
tiles = [[random.randint(0, 3) for _ in range(20)] for _ in range(15)]
player_x = 100.0
player_y = 100.0

def update():
    # Runs every frame
    draw_tiles()
    draw_player()

Example programs

Bouncing ball

from codetoy.canvas import fill, rect, circle
from codetoy.screen import width, height
from codetoy.time import deltaTime

x, y   = 200.0, 150.0
vx, vy = 180.0, 220.0

def update():
    global x, y, vx, vy

    dt = deltaTime()
    x += vx * dt
    y += vy * dt

    if x < 20 or x > width()  - 20: vx = -vx
    if y < 20 or y > height() - 20: vy = -vy

    fill(20, 20, 30)
    rect(0, 0, width(), height())

    fill(100, 200, 255)
    circle(x, y, 20)

Keyboard movement

from codetoy.canvas import fill, rect
from codetoy.screen import width, height, centerX, centerY
from codetoy.time import deltaTime
from codetoy._input import isKeyDown

px, py = centerX(), centerY()

def update():
    global px, py
    spd = 200 * deltaTime()

    if isKeyDown("ArrowLeft")  or isKeyDown("a"): px -= spd
    if isKeyDown("ArrowRight") or isKeyDown("d"): px += spd
    if isKeyDown("ArrowUp")    or isKeyDown("w"): py -= spd
    if isKeyDown("ArrowDown")  or isKeyDown("s"): py += spd

    fill(20, 20, 30)
    rect(0, 0, width(), height())

    fill(100, 220, 120)
    rect(px - 16, py - 16, 32, 32, 4)

Click to spawn

from codetoy.canvas import fill, rect, circle
from codetoy.screen import width, height
import random

dots = []
spawn = False

def mouseDown(button):
    global spawn
    if button == 0:
        spawn = True

def update():
    global spawn
    from codetoy._input import mouseX, mouseY

    if spawn:
        dots.append((mouseX(), mouseY(),
                     random.randint(6, 20),
                     (random.randint(100,255), random.randint(100,255), random.randint(100,255))))
        spawn = False

    fill(15, 15, 25)
    rect(0, 0, width(), height())

    for x, y, r, (dr, dg, db) in dots:
        fill(dr, dg, db)
        circle(x, y, r)

Animated sine wave

from codetoy.canvas import fill, rect, circle, stroke, lineWidth, beginPath, moveTo, lineTo, strokePath, noFill
from codetoy.screen import width, height
from codetoy.time import elapsedTime
import math

def update():
    t = elapsedTime()

    fill(15, 15, 30)
    rect(0, 0, width(), height())

    noFill()
    stroke(100, 200, 255)
    lineWidth(2)

    beginPath()
    steps = int(width())
    for i in range(steps + 1):
        x = float(i)
        y = height() / 2 + math.sin(x * 0.03 + t * 3) * 80
        if i == 0:
            moveTo(x, y)
        else:
            lineTo(x, y)
    strokePath()

Multi-touch paint

from codetoy.canvas import fill, rect, circle
from codetoy.screen import width, height
from codetoy._input import touchCount, touchX, touchY, isTouchDown
import random

touches = {}   # id -> color

def touchStart(id, x, y):
    touches[id] = (random.randint(100,255), random.randint(100,255), random.randint(100,255))

def touchEnd(id):
    touches.pop(id, None)

def update():
    fill(20, 20, 30, 0.15)  # trail fade
    rect(0, 0, width(), height())

    for id, (r, g, b) in list(touches.items()):
        if isTouchDown(id):
            fill(r, g, b)
            circle(touchX(id), touchY(id), 24)

Want to learn more?

Join the discord and chat with the community