Deepen understanding instead of memorizing

When learning Core Graphics, I didn’t quite understand the realization principle of Graphics transformation. Transform is also widely used in iOS Animation framework Core Animation. In this paper, I will explain the mathematical principle of CGAffineTransform. Try to provide an easy way to understand and use the CGAffineTransform API

Local Coordinate System in Mac App

When drawing content to the screen of a device (Mac or iPhone), there is a coordinate system. This coordinate system has an origin. The simplest and most direct drawing process is that each content to be drawn has its own coordinates and dimensions and is drawn according to the origin.

But there’s a problem, like on the Mac, you can have multiple applications running at the same time, you can have multiple Windows, and everything is drawn according to the origin of the screen and it makes it very complicated.

So Cocoa introduced the concept of local coordinate System, which means that the device Screen, the application Window, and the View all have their own coordinate system. The content of each level is drawn according to its origin, the content of View is drawn according to its origin, the content of Window is drawn according to its origin, and so on. After the content of the first level is drawn, the content is mapped to the coordinate system of the next level. As shown in the figure:

Think about iOS apps

The iPhone screen can only display one App at a time, but it works in a similar way to Mac apps. Think of the display of iPhone content as a Window in the Mac App

When you use Core Graphics, CGContext represents a local coordinate system. It also displays on the iPhone:

  1. First calculate the position of the image in the applied Local coordinate system
  2. Map to the coordinate system of the device’s screen

How do you map between coordinate systems

The above equation is equivalent to:

The formula says that (x, y) is a point in one coordinate system, and by multiplying the matrices, we get a point in another coordinate system (x’, y’).

The multiplied matrix corresponds to the CGAffineTransform in Core Graphics, where (x, y) and (x’, y’) correspond to points in different CGContext coordinates.

Get a feel for what transform looks like

let context = UIGraphicsGetCurrentContext()!
print(context.ctm) //CGAffineTransform(a: 2.0, b: 0.0, c: -0.0, d: -2.0, tx: 0.0, ty: 1000.0)
Copy the code

The different values of CGAffineTransform can achieve “pan”, “scale”, and “rotate” transformations.

Let’s start with the CGAffineTransform API to see how we can better understand the transformation process.

More on context.ctm later

Three groups of API

There are three groups of apis related to CGAffineTransform in iOS:

  • CGContext class
    1. CGContext.[translate | scale | rotate]
    2. CGContext.concatenate(transform)
  • CGAffineTransform class
    1. CGAffineTransform.[translate | scale | rotate]
    2. CGAffineTransform.concatenating(transform)
  • In UIKit
    1. UIBezierPath.apply(transform)

The apply method in UIKit also calls the CGAffineTransform method at the bottom, but UIKit is often used in normal development, so it is also mentioned here

UIBezierPath.apply(transform)

  • UIBezierPath represents the object to draw, which is a set of points in a coordinate system
  • The transform parameter is the 3 by 3 matrix in the previous formula

Uibezierpath. apply(transform) causes the coordinates of UIBezierPath to change.

let context = UIGraphicsGetCurrentContext()
let size = CGSize(width: 20, height: 20)
letPath = UIBezierPath(ovalIn: CGRect(Origin: CGPoint. Zero, size: size))// a circular patternprint(path.bounds) //(0.0, 0.0, 20.0, 20.0)
let//t1: CGAffineTransform(a: 1.0, B: 0.0, C: 0.0, D: 1.0, tx: 20.0, ty: 20.0) path. The apply (t1)print(path.bounds) //(20.0, 20.0, 20.0, 20.0)
Copy the code

This code is relatively easy, and you can think of it as the path circle, moving from (0, 0) to (20, 20).

The underlying mathematical implementation of path.apply(T1) is the formula from the previous section, which can also be written as newPath = path * transform

We’ll come back to that, but let’s look at another set of apis

CGContext class

UIGraphicsBeginImageContext(CGSize(width: 500, height: 500))
let context = UIGraphicsGetCurrentContext()
let size = CGSize(width: 80, height: 80)
let path = UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: size))
print(path.bounds) //(0.0, 0.0, 80.0, 80.0) uicolor.white.setfill () path.fill()Copy the code

context.translateBy(x: 100, y: 100)
print(path. Bounds) / / (0.0, 0.0, 80.0, 80.0) path. The fill ()Copy the code

As a result, the position of path relative to the upper-left origin becomes (100, 100), but the path bounds remain the same. Intuitively, it looks like the coordinate system has changed due to context.translate.

Context. translate and path.translate work the same. Why?

Context.translateby (x: 100, y: 100) {context.translateby (x: 100, y: 100)}

/* Translate the current graphics state's transformation matrix (the CTM) by `(tx, ty)'*/ @available(iOS 2.0, *) public func translateBy(x tx: CGFloat, y ty: CGFloat)Copy the code

CTM(Current Transform matrix) is the coordinate matrix corresponding to the current context:

print("Before context transform :\(context.ctm)")
context.translateBy(x: 100, y: 100)
print("Context transformed :\(context.ctm)"// CGAffineTransform(a: 1.0, b: 0.0, C: -0.0, d: -1.0, tx: 0.0, ty: // CGAffineTransform(a: 1.0, b: 0.0, C: -0.0, D: -1.0, tx: 100.0, ty: 400.0)Copy the code

NewCTM = transform * CTM (in this case transform is the matrix of translateBy(x: 100, y: 100)

Now, if you think about it further, what does this CTM do?

CTM is the matrix required to map the application page to the hardware device screen: devicePath = Path * newCTM

Note: According to Apple’s official explanation, CTM should be a matrix that maps the application page to the View coordinate system, not a matrix that maps the pixels on the device’s screen. Because mapping from the View coordinate system to specific physical pixels requires scaling. I don’t know what a view coordinate system is, but it doesn’t affect what we’re doing here, right

DevicePath = (path * transform * CTM) devicePath = (path * transform) * CTM DevicePath = newPath * CTM

The key to the

  1. path.apply(transform)andcontext.translateIt works the same way because it all ends up theredevicePath = newPath * CTMThis step
  2. butpath.apply(transform)andcontext.translateIt’s not exactly equivalent
    • After the context changes, the new path must be based onnewCTMLet’s map the points
  3. So we can think of it this way
    • useUIKitWhen the group API draws something, it fixes the canvas (i.e. the CTM of the context) and draws the path arbitrarily
    • useCGContext“, move, rotate, scale the canvas first, then the new drawing content will be based on the new coordinate system, and the previous drawing content will also be affected

Cgcontext.concatenate (transform) is similar, but receives different parameters

CGAffineTransform class

As can be seen from the above two sections, CGAffineTransform provides the data structure of the specific transformation during the change process. Note in this section that the order is important when the transform is superimposed.

  • CGAffineTransform.concatenating(transform)

    /* Concatenate `t2' to `t1' and return the result:
        t'= T1 * T2 */ @available(iOS 2.0, *) public func concatenating(_ t2: CGAffineTransform) -> CGAffineTransformCopy the code

    There’s nothing wrong with that. T is t1 times T2

  • CGAffineTransform.[translate | scale | rotate]

    /* Translate `t' by `(tx, ty)' and return the result:
         t'= [1 0 0 1 tx ty] * t */ @available(iOS 2.0, *) public func translatedBy(x tx: CGFloat, y ty: CGFloat) -> CGAffineTransformCopy the code

    If t = t1.translatedBy(x: 1, y: 1) then t = CGAffineTransform(translationX: 1, y: 1) * T1 The order is reversed.

conclusion

  1. We often seeUIKit is centered in the upper left cornerandCoreGraphics(Quartz) centrepoint is in the lower left cornerThis statement, in fact, is ultimately through the matrix multiplication mentioned above to achieve the mapping of the final point
  2. When UIKit draws something, the bottom layer is stillCoreGraphicsIn the work. It’s just that the UIKit framework modifies CTM to make us feel likeUpper left corner of origin
  3. There are two options for drawing content: one is to use CGPath objects to draw content directly into the context; The other way is you can change the context and draw, or you can change the frame of the canvas and draw at the same time. And this way it corresponds to the CONTEXT’s API. This approach is suitable for drawing complex custom content
  4. In real development, try to avoidUIKitandCoreGraphicsMix. A classic example of this is in theUIKitTo get the context, useCGContextDrawImageThe picture is in the right position, but the content is mirrored in the y direction

reference

  • Drawing and Printing Guide for iOS
  • Quartz 2D Programming Guide
  • Coordinate Systems and Transforms
  • Core Graphics Tutorial: Curves and Layers