
This video is only available to subscribers. Start a subscription today to get access to this and 484 other videos.
Context Transforms
This episode is part of a series: Dive Into Core Graphics.
1. Intro 2 min |
2. Basic Shapes 7 min |
3. Paths 17 min |
4. Colors 9 min |
5. Gradients 11 min |
6. Clipping Paths 6 min |
7. Context Transforms 10 min |
8. Images 7 min |
9. Text 9 min |
10. Offscreen Rendering 12 min |
11. Custom CALayer 13 min |
12. Pie Progress View 7 min |
13. Watermarking Photos 6 min |
14. Working in AppKit 8 min |
Links
Drawing a Grid to Help Visualize Transformations
This utility function is a helpful way of visualizing where your drawing is happening. Not the use of the saveGState
and restoreGState
to keep this function self contained (in other words, no drawing state will leak out of this method and cause other operations to draw incorrectly).
func drawGrid(_ context: CGContext) {
context.saveGState()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let color = CGColor(colorSpace: colorSpace, components: [0, 0, 0, 0.2])!
context.setStrokeColor(color)
context.setLineWidth(2)
// Stroke the border
context.stroke(bounds)
// Draw a line every 20px
let increment: CGFloat = 20
for x in 1..<Int(bounds.height / increment) {
// Vertical line
context.move(to: CGPoint(x: CGFloat(x) * increment, y: 0))
context.addLine(to: CGPoint(x: CGFloat(x) * increment, y: bounds.height))
for y in 1..<Int(bounds.width / increment) {
// Horizontal line
context.move(to: CGPoint(x: 0, y: CGFloat(y) * increment))
context.addLine(to: CGPoint(x: bounds.width, y: CGFloat(y) * increment))
}
}
// Stroke grid
context.strokePath()
// Draw top left red square
context.setFillColor(CGColor(colorSpace: colorSpace, components: [1, 0, 0, 0.5])!)
context.fill(CGRect(x: 0, y: 0, width: increment, height: increment))
context.restoreGState()
}
Zooming Out
Zooming out can help you figure out issues where your drawing is happening outside the frame. This is a common situation you might find yourself in when drawing with transforms. For instance if you rotate around the origin 180°, your entire drawing will happen outside the visible rect.
When performing transformations like zoom, it is important to remember that it happens at the anchor point, or origin of the context. In order to zoom out from the center, we'll first have to translate so that the anchor point is at the center of the view:
context.translateBy(x: bounds.midX, y: bounds.midY)
Then we can apply our scale transformation:
context.scaleBy(x: 0.5, y: 0.5)
And then we just need to jump back to where we were:
context.translateBy(x: -bounds.midX, y: -bounds.midY)
Flipping Horizontally
This is a common thing you might do when showing an image of someone through the camera. Since most people are accustomed to looking at themselves in a mirror, you can simulate this effect by horizontally flipping the image.
Flip horizontally
context.translateBy(x: bounds.midX, y: bounds.midY)
context.scaleBy(x: -1, y: 1)
context.translateBy(x: -bounds.midX, y: -bounds.midY)
drawGrid(context)
Whenever you are dealing with scale transforms, be careful to use 1
(not 0
) to represent a no-op, because otherwise that will result in drawing nothing!
You might also use this technique to create reflections of objects by drawing them again, scaled -1
in the y dimension in combination with a gradient overlay.
Rotating
If we want to rotate 45° we first have to convert that to radians. In radians, 2π is equal to the entire circle, so 2π = 360. to convert this to radians:
// 45° = 2π * (45/360)
// or (simplified)...
// 45° = π / 180
It helps to think of thinks in radians, so if 2π is the whole circle, π is half the circle, π/2 is 1/4 circle, and π/4 is 1/8th the circle (or 45°).
Again we use the trick to translate to the center so we rotate around the center, then back again after the transformation:
// Rotate -45°
context.translateBy(x: bounds.midX, y: bounds.midY)
context.rotate(by: .pi / -4)
context.translateBy(x: -bounds.midX, y: -bounds.midY)
drawGrid(context)
We can then draw things and they will all be rotated:
// Draw blue square
context.setFillColor(CGColor(colorSpace: colorSpace, components: [0, 0, 1, 1])!)
context.fill(CGRect(x: 60, y: 60, width: 200, height: 200))
This ends up looking like a diamond since we are rotated.
It is important to rotate back (or use the graphics state) to go back to how we were:
// Rotate 45° (back to regular)
context.translateBy(x: bounds.midX, y: bounds.midY)
context.rotate(by: .pi / 4)
context.translateBy(x: -bounds.midX, y: -bounds.midY)
Now we can draw other things that aren’t rotated.
// Draw green square
context.setFillColor(CGColor(colorSpace: colorSpace, components: [0, 1, 0, 0.5])!)
context.fill(CGRect(x: 60, y: 60, width: 200, height: 200))
// Draw yellow square
context.setFillColor(CGColor(colorSpace: colorSpace, components: [1, 1, 0, 0.5])!)
context.fill(CGRect(x: 300, y: 300, width: 20, height: 20))