Core Concepts /Drawing

Core Concepts

Drawing

Coordinates, clearing, colors, shapes, text, transforms and paths. Everything you need to put pixels on screen.

Codetoy draws in immediate mode: nothing on screen is an object you keep around. Each frame you describe the whole picture, top to bottom, back to front. The API mirrors the HTML canvas 2D context, simplified for games.

#Coordinates

The canvas is measured in pixels with the origin at the top-left corner. X grows to the right and Y grows downward.

Text
(0, 0) ──────────────► x
  │
  │        • (centerX, centerY)
  │
  ▼ y                 (width, height)

The canvas fills the preview and can change size. Read screen.width()Screen.Widthwidth(), screen.height()Screen.Heightheight(), screen.centerX()Screen.CenterXcenterX() and screen.centerY()Screen.CenterYcenterY() every frame instead of hard-coding sizes, so your program works on phones, embeds and Discord.

#Clearing the screen

The canvas keeps whatever was drawn last frame. Start every frame by painting a full-screen rectangle, or call reset()Canvas.Reset()reset(), which clears the canvas and resets colors, line styles and transforms.

export function update(): void {
  fill(15, 15, 25)                             // background color
  rect(0, 0, screen.width(), screen.height())  // paint over last frame
  // ...draw this frame
}
Screen.OnUpdate(() => {
    Canvas.Fill(15, 15, 25);                         // background color
    Canvas.Rect(0, 0, Screen.Width, Screen.Height);  // paint over last frame
    // ...draw this frame
});
def update():
    fill(15, 15, 25)                # background color
    rect(0, 0, width(), height())   # paint over last frame
    # ...draw this frame

Trails for free

Paint the background with some transparency, for example fill()Canvas.Fill()fill() with alpha 0.15, and moving shapes leave fading trails.

#Colors

Drawing uses two colors: fill for the inside of shapes and text, and stroke for outlines and lines. Both stay set until you change them.

  • fill()Canvas.Fill()fill() and stroke()Canvas.Stroke()stroke() take red, green and blue from 0 to 255, plus an optional alpha from 0.0 to 1.0.
  • fillStyle()Canvas.FillStyle()fillStyle() and strokeStyle()Canvas.StrokeStyle()strokeStyle() accept any CSS color string: "#ff6600", "tomato", "hsl(200 80% 60%)".
  • noFill()Canvas.NoFill()noFill() and noStroke()Canvas.NoStroke()noStroke() turn either one off.

Fill and stroke are both on when a program starts, and the default stroke is a thin black outline. Call noStroke()Canvas.NoStroke()noStroke() once at the top of export function update()Screen.OnUpdate()def update() for flat shapes. Setting a color with fill()Canvas.Fill()fill() or stroke()Canvas.Stroke()stroke() turns that one back on.

fill(255, 100, 0)          // opaque orange
fill(0, 150, 255, 0.5)     // half-transparent blue
fillStyle("hsl(140 70% 55%)")

stroke(255, 255, 255)
lineWidth(3)
noFill()                   // outline only
circle(200, 200, 50)
Canvas.Fill(255, 100, 0);          // opaque orange
Canvas.Fill(0, 150, 255, 0.5);     // half-transparent blue
Canvas.FillStyle("hsl(140 70% 55%)");

Canvas.Stroke(255, 255, 255);
Canvas.LineWidth(3);
Canvas.NoFill();                   // outline only
Canvas.Circle(200, 200, 50);
fill(255, 100, 0)          # opaque orange
fill(0, 150, 255, 0.5)     # half-transparent blue
fillStyle("hsl(140 70% 55%)")

stroke(255, 255, 255)
lineWidth(3)
noFill()                   # outline only
circle(200, 200, 50)

#Shapes

Shape Function Notes
Rectangle rect()Canvas.Rect()rect() x, y is the top-left corner. An optional fifth argument rounds the corners.
Circle circle()Canvas.Circle()circle() x, y is the center.
Ellipse ellipse()Canvas.Ellipse()ellipse() x, y is the center. w, h are the horizontal and vertical radii.
Line line()Canvas.Line()line() Drawn with the stroke color and line width.
Triangle triangle()Canvas.Triangle()triangle() Three corner points.
Polygon polygon()Canvas.Polygon()polygon() A flat list of points: [x1, y1, x2, y2, …].
Arc arc()not in C#arc() Part of a circle, from a start angle to an end angle, in radians.

Every signature is in the Canvas API.

#Text

Set a font with font()Canvas.Font()font(), then draw with text()Canvas.Text()text(). Text uses the fill color, and x, y is the top-left corner of the text. measureText()Canvas.MeasureText()measureText() returns a string's width in the current font, which is how you center text:

font("Arial", 32, "bold")
const label = "Game Over"
fill(255, 255, 255)
text(label, screen.centerX() - measureText(label) / 2, screen.centerY())
Canvas.Font("Arial", 32, "bold");
string label = "Game Over";
Canvas.Fill(255, 255, 255);
Canvas.Text(label, Screen.CenterX - Canvas.MeasureText(label) / 2, Screen.CenterY);
font("Arial", 32, "bold")
label = "Game Over"
fill(255, 255, 255)
text(label, centerX() - measureText(label) / 2, centerY())

#Transforms

translate()Canvas.Translate()translate(), rotate()Canvas.Rotate()rotate() and scale()Canvas.Scale()scale() move the coordinate system for everything drawn after them. Wrap them in push()Canvas.Push()push() and pop()Canvas.Pop()pop() so they only affect what's in between. push()Canvas.Push()push() saves the colors, line settings and transform, and pop()Canvas.Pop()pop() restores them. It doesn't save whether noFill()Canvas.NoFill()noFill() or noStroke()Canvas.NoStroke()noStroke() is on.

push()
translate(screen.centerX(), screen.centerY())  // origin is now the center
rotate(elapsedTime())                          // spin over time (radians)
fill(255, 200, 60)
rect(-40, -40, 80, 80)                         // a square centered on the origin
pop()                                          // back to normal
double angle = 0;

Screen.OnUpdate(() => {
    angle += Screen.DeltaTime;

    Canvas.Push();
    Canvas.Translate(Screen.CenterX, Screen.CenterY);  // origin is now the center
    Canvas.Rotate(angle);                              // spin over time (radians)
    Canvas.Fill(255, 200, 60);
    Canvas.Rect(-40, -40, 80, 80);                     // a square centered on the origin
    Canvas.Pop();                                      // back to normal
});
push()
translate(centerX(), centerY())   # origin is now the center
rotate(elapsedTime())             # spin over time (radians)
fill(255, 200, 60)
rect(-40, -40, 80, 80)            # a square centered on the origin
pop()                             # back to normal

Transforms combine in order. Translate first, then rotate, to spin something around its own center. resetTransform()Canvas.ResetTransform()resetTransform() drops all transforms without touching colors.

#Custom paths

For curves and arbitrary shapes, build a path and then fill or stroke it:

beginPath()
moveTo(100, 200)
quadraticCurveTo(200, 60, 300, 200)   // control point, end point
bezierCurveTo(330, 260, 70, 260, 100, 200)
closePath()

fill(80, 160, 255)
fillPath()
stroke(255, 255, 255)
strokePath()
Canvas.BeginPath();
Canvas.MoveTo(100, 200);
Canvas.QuadraticCurveTo(200, 60, 300, 200);   // control point, end point
Canvas.BezierCurveTo(330, 260, 70, 260, 100, 200);
Canvas.ClosePath();

Canvas.Fill(80, 160, 255);
Canvas.FillPath();
Canvas.Stroke(255, 255, 255);
Canvas.StrokePath();
beginPath()
moveTo(100, 200)
quadraticCurveTo(200, 60, 300, 200)   # control point, end point
bezierCurveTo(330, 260, 70, 260, 100, 200)
closePath()

fill(80, 160, 255)
fillPath()
stroke(255, 255, 255)
strokePath()

#Performance tips

  • A few hundred to a thousand shapes per frame is comfortable. Watch screen.fps()Screen.FPSfps() if things slow down.
  • Avoid creating new arrays or lists every frame. Reuse them.
  • Static backgrounds still need repainting every frame, so keep them simple.
Esc