Context Transforms

Episode #286 | 10 minutes | published on July 14, 2017 | Uses swift-3.0, Xcode-8.3
Subscribers Only
Transforms allow you to draw things rotated, moved, or scaled differently than you specified. You can use this technique to reuse drawing operations where they only differ by some small factor (like drawing lines on a graph), to tilt things like text, or to correct issues where the drawn element is upside down or sideways. In this episode, Sam shares a helpful technique of drawing a grid with a highlighted "origin square" to make it obvious what the transforms are doing.

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))
blog comments powered by Disqus