Canvas, size, and export
You draw a sketch once, but it is seen at more than one size. There is a preview window that fits your screen, and there is a fixed-resolution export. If you write the sketch relative to the canvas, it holds up at either size. This page covers scale, the canvasSize export presets, and the preview window.
Example
override func draw() {
background(.black)
// The same composition at any canvas size.
fill(.white)
drawCircle(width / 2, height / 2, 300 * scale)
}
Contents
- Resolution independence
- scale
- shortSide / longSide
- Export size
- The preview window
- The performance panel
- Retina and pixel density
- Normalized coordinates: uv
Resolution independence
Coordinates are in logical points. The origin is the top-left corner and y increases downward, the same as in p5, Processing, and OPENRNDR. Inside draw(), width and height are the canvas size in those points.
To keep a sketch looking the same at every size, write it relative to the canvas instead of in fixed pixels. Two tools cover that:
scalegrows and shrinks with the canvas, so a size multiplied by it keeps its proportion at any canvas size. Pick the size you would want on a canvas of about 1000 points, then multiply: a12 * scaledot, a375 * scaleradius.width/heightfractions suit layout, such aswidth * 0.8for a centered block,height / 8for a wave's amplitude, andshortSide* 0.125for an inset. For positions,uv(u, v)states the same fractions as a single point, so you writeuv(0.5, 0.75)instead ofVector2(width * 0.5, height * 0.75).
A bare drawCircle(400, 400, 150) ties the sketch to one canvas size, because the same call lands somewhere else once the canvas changes. Use scale and fractions instead.
scale
scale: Double
A read-only factor that grows and shrinks with the canvas. Multiply your sizes by it so a sketch holds its proportions at any size. This is not the scale(_:) transform in Drawing, which scales the coordinate system.
drawCircle(width / 2, height / 2, 120 * scale)
strokeWeight(2 * scale)
shortSide / longSide
shortSide: Double
longSide: Double
These are the two canvas edges by length. shortSide is min(width, height), and longSide is the other one. The short side decides how big something can be and still fit whichever way the canvas turns, so shortSide * 0.4 sizes a centerpiece the same on a square, a wide, and a tall canvas.
drawCircle(center: center, radius: shortSide * 0.4)
let margin = shortSide * 0.08
scale above is the same quantity divided by 1000, and you use it to hold a size's proportion. Use shortSide when you want to state the fraction directly.
Export size
canvasSize is the resolution a sketch renders and exports at, in whole pixels, because a canvas is an integer grid. Its type is CanvasSize, and it defaults to Sketch.defaultSize, which is .square1080, a 1:1 square of 1080×1080. (cgSize hands the same size to anything that speaks Core Graphics.) Override it on a subclass with one of the named presets below, as in override var canvasSize: CanvasSize { .uhd4K }:
| Constant | Pixels | Aspect | Good for |
|---|---|---|---|
.square1080 | 1080 × 1080 | 1:1 | the default, for square social and feed posts |
.square1440 | 1440 × 1440 | 1:1 | a larger square |
.square2160 | 2160 × 2160 | 1:1 | a 4K-class square master |
.hd720 | 1280 × 720 | 16:9 | 720p / HD |
.fhd1080 | 1920 × 1080 | 16:9 | 1080p / Full HD |
.qhd1440 | 2560 × 1440 | 16:9 | 1440p / QHD |
.uhd4K | 3840 × 2160 | 16:9 | 4K / UHD, the usual "4K video" deliverable |
.dci4K | 4096 × 2160 | ~17:9 | cinema 4K (DCI), for film delivery |
.vertical1080 | 1080 × 1920 | 9:16 | full-screen vertical: stories, reels, TikTok, Shorts |
.portrait1080 | 1080 × 1350 | 4:5 | the portrait feed crop (for example Instagram) |
.photo1080 | 1080 × 1440 | 3:4 | the photographic frame stood up tall, and the shape of a feed grid |
.usLetter | 612 × 792 | 8.5:11 | US Letter paper (8.5×11 in), for PDF export |
.usLegal | 612 × 1008 | 8.5:14 | US Legal paper (8.5×14 in), for PDF export |
.a3 | 842 × 1191 | 1:√2 | ISO A3 paper (297×420 mm), for PDF export |
.a4 | 595 × 842 | 1:√2 | ISO A4 paper (210×297 mm), for PDF export |
.a5 | 420 × 595 | 1:√2 | ISO A5 paper (148×210 mm), for PDF export |
The paper presets are sized in PDF points, 72 per inch, and they come portrait like the physical sheet. The PDF export maps one canvas pixel to one point, so a sketch on .a4 exports as a true A4 page. The vector geometry then prints sharp at any resolution. When the raster export needs print resolution too, add .dpi(_:). Writing .a4.dpi(300) renders and --exports at 2479×3508 pixels, which is 300 dots per inch. With that in place, --export-pdf still writes the page at exactly A4, and the pixel geometry is scaled back onto it. It works on any size, and the size it is called on is taken as the 72-dpi page. Calling .dpi(72) changes nothing.
Use .portrait and .landscape to flip any preset's orientation, so .uhd4K.portrait is 2160×3840 and .a4.landscape is 842×595. The flips compose with .dpi(_:) in either order. Headless --export always renders at canvasSize, so a sketch produces the same pixels on any machine. The --export-sequence <dir> --frames N flags render a deterministic numbered PNG sequence at a fixed timestep, so it is reproducible and assembles into a smooth video. See Sketch ▸ Running a sketch.
For a custom size, override canvasSize with .square(_) for a square or .size(_, _) for any rectangle:
final class MySketch: Sketch {
override var canvasSize: CanvasSize { .square(1000) } // a 1000×1000 canvas
// override var canvasSize: CanvasSize { .size(1000, 600) } // or any rectangle
override func draw() {
background(.white)
drawCircle(width / 2, height / 2, 300)
}
}
The preview window
The on-screen window does not have to match canvasSize, because a 1080² or 4K sketch would overflow a laptop screen. windowMode sets the window size relative to canvasSize:
.autois the default. It opens at 1:1 when the screen has room for the fullcanvasSize. Otherwise it steps down to the largest clean fraction that fits (¾, ½, …), so the window always fits. A 1080² sketch opens at 1080 on a large or external display, and at ¾ (810pt) on a 14"/16" laptop..fixed(_)pins an explicit fraction ofcanvasSizeand ignores the screen, so.fixed(0.5)is always half size and.fixed(1)is always 1:1..resizableopens a freely resizable window at the auto-fit size, and the canvas follows it live. Use it for sketches made for the screen rather than for a fixed export. Draw withscale,width, andheight, and the sketch adapts as you drag the window.
override var windowMode: WindowMode { .fixed(0.5) } // always half of canvasSize
.auto and .fixed lock the window, and .resizable does not. Resizing applies to the standalone swift run window. The examples gallery and the live host show the sketch at the auto-fit size with a collapsible sidebar.
In either mode, a sketch written with scale composes the same at the preview size and at the export size. What you see while you work therefore matches the exported frame, so the export stays dependable for video and for Instagram.
The performance panel
When a sketch feels slow, choose View ▸ Show Inspector (⌘/). That opens a floating panel beside the sketch window. It reports the frame rate, the CPU time spent building a frame, the clock, and the canvas size. It also counts the geometry the frame emitted: vertices, SDF shapes, point-cloud splats, and GPU particles. Below those, it shows a slider for each of the sketch's @Param parameters. Choose the same command again to hide the panel.
The panel is a separate utility window, not drawing inside the canvas, so it never appears in an exported frame. That is because --export renders only the canvas. The CPU time is the cost of building a frame on the draw thread. It is the first number to climb when a sketch gets heavy. It reads far lower in a release build (swift run -c release) than in the default debug build.
The live host (OllinLive) shows the same readout in its own inspector sidebar, so there the readout is built into the window.
Retina and pixel density
The preview is crisp on Retina displays and there is nothing to switch on, because it renders at the screen's native pixel density. Export renders directly at canvasSize, so a 1080² export is exactly 1080×1080 pixels.
Normalized coordinates: uv
func uv(_ u: Double, _ v: Double) -> Vector2
This returns the canvas point at normalized coordinates. uv(0, 0) is the top-left corner, uv(1, 1) the bottom-right, and uv(0.5, 0.5) the center. A layout stated as proportions never reads width or height, and it uses the same 0…1 top-left space that per-pixel shader code sees. Values outside 0…1 land off the canvas in proportion, so nothing clamps.
drawCircle(center: uv(0.5, 0.25), radius: 40 * scale) // top-center
drawPolygon([uv(0, 0.66), uv(1, 0.66), uv(1, 1), uv(0, 1)]) // the lower third
The same mapping exists on any Rectangle as point(u:v:), and uv(of:) is its inverse. Reading bounds.uv(of: Vector2(mouseX, mouseY)) gives you the mouse position as canvas fractions. The Basic/NormalizedCoordinates example composes a whole scene this way.