Coordinates, camera and autoscale¶
Where things land on the screen: Create's coordinate system, the three spaces a position can be in, the camera, and how the size a program is written for maps onto a real window.
The screen¶
These are not Processing's coordinates. The origin is the centre of the screen and y points up:
top()
▲ +y
│
│
left() ───────(0,0)──────▶ right()
│ +x
│
bottom()
So x ∈ [-w/2, w/2] and y ∈ [-h/2, h/2]; rotate turns counter-clockwise; gravity is negative
y; and canvas.left() and canvas.bottom() are negative. Lay things out against the edge
methods (left(), right(), bottom(), top()) rather than by arithmetic on width and
height:
from create import *
@fieldwise_init
struct Corners(Program):
@staticmethod
def create(mut context: Context, mut canvas: Canvas) raises -> Corners:
return Corners()
def update(mut self, mut context: Context, mut canvas: Canvas) raises:
canvas.background(Color.WHITE)
canvas.fill(Color.BLUE)
canvas.circle((canvas.left() + 30, canvas.top() - 30), 20) # top left
canvas.circle((canvas.right() - 30, canvas.bottom() + 30), 20) # bottom right
canvas.rectangle((0, 0), 200, 100) # centred on the origin
def main() raises:
run[Corners]("Corners")
canvas.width() and canvas.height() are Float64 units: under EXTEND (below) the window's
pixels divide out to a fraction. canvas.bounds() is the screen as a Rectangle, for
snapshot, clip or contains.
Every shape is positioned by its centre, Rectangle.position included, as the rectangle above
shows. Glyphs and images are not flipped by the y-up axis, but they turn, shear and mirror with
canvas.transform like every render call.
Screen, world and local space¶
A position is always in one of three spaces:
| Space | What it is |
|---|---|
| Screen space | Origin-centred, y-up, camera-independent. canvas.left()…top(), context.mouse.position and canvas.color_at live here, and it is what canvas.to_local takes and to_screen gives |
| World space | What render calls use once a Camera is set; identical to screen space without one. Only the camera converts into it: camera.to_world/to_screen |
| Local space | The current transform's frame, where a render call's coordinates are read. canvas.to_local(screen)/to_screen(local) convert through the transform and the camera together |
The camera¶
A Camera has a position (the world point at the centre of the screen) and a zoom.
canvas.camera(camera) maps world space onto screen space for every later render call and nested
transform, and is reset every frame. canvas.overlay() suspends it for HUD content:
# A fragment of an `update` whose program keeps a `camera: Camera` field.
canvas.camera(self.camera)
canvas.image(self.player.image, self.player.position) # world space
with canvas.overlay():
canvas.text("Score: " + String(self.score), (0, canvas.top() - 20)) # screen space
context.mouse.position is in screen space. To pick what is under the mouse,
canvas.to_local(context.mouse.position) gives the position in the frame a shape is drawn in,
camera and transform included, and camera.to_world(context.mouse.position) gives the world
position.
Design size and autoscale¶
The design size is the width and height passed to run (or set later with
context.design_size(w, h)): the space the program is authored in, not a window size. Fullscreen
and maximized windows scale the design onto the display. How it lands on a window of another size
is the autoscale mode, run(..., autoscale=) or context.autoscale(mode) later:
| Mode | Effect |
|---|---|
FIT (default) |
Scales the design uniformly to fit, centred; the leftover is letterbox bars. canvas.width()/height() never change |
EXTEND |
The same scale, no bars: the leftover becomes extra world, so the reported size grows with the window. Anchor layout to the edges |
OFF |
No scaling: coordinates are the window's own pixels |
Both design_size and autoscale decide frame 1's mapping only as run arguments, because the
viewport is derived before create runs. Font size, stroke thickness and image size scale with
the design, by canvas.scale(). See examples/window.
The run's settings¶
Context's settings (its "dials") belong to the run, not the drawing: design_size,
autoscale, title, window_mode, resizable, cursor, lock_mouse, text_input,
quit_on_escape and max_frame_rate. Like the canvas setters, they are methods, each read back
with no arguments:
# A fragment, inside `create`.
context.autoscale(Autoscale.EXTEND)
context.title("Level 1")
var mode = context.autoscale() # Autoscale.EXTEND
Context's only fields are the readings: time, keyboard and mouse. The gamepads are
context.gamepad(player), a copy, since there can be several.
Every run argument but backend has a setter (a dial here, or canvas.antialiasing), and the
argument is only the starting value. Not every dial is a run argument: cursor,
quit_on_escape and max_frame_rate are set in create.
The cursor. context.cursor(Cursor.CROSSHAIR) is Processing's cursor(CROSS), and
Cursor.HIDDEN its noCursor(); any other value shows the cursor again. Each Cursor is the
platform's own system cursor: ARROW (the default), CROSSHAIR, HAND, TEXT, WAIT,
PROGRESS, MOVE, NOT_ALLOWED, and four resize ones, RESIZE_HORIZONTAL, RESIZE_VERTICAL
and the diagonals named by slope, RESIZE_DIAGONAL_RISING (/) and RESIZE_DIAGONAL_FALLING.
A cursor is sent to the window only when it changes, so setting it every frame from a hover test
is free. Image cursors are not supported.
When a change applies¶
A frame-wide setting applies to the frame it is set in: the canvas ones (see
Drawing), and title, resizable, cursor, lock_mouse and
text_input, which the windowed loops send before presenting.
What decides the frame's geometry waits for the next frame: design_size, autoscale and
window_mode, which is switched after presenting (some compositors report the new size a frame
later). A getter reports the value as set.
max_frame_rate(), quit() and rumble() are read after the frame body returns. Headless runs
ignore the window dials, like rumble.