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)`and`context.translate`It works the same way because it all ends up there`devicePath = newPath * CTM`This step
2. but`path.apply(transform)`and`context.translate`It’s not exactly equivalent
• After the context changes, the new path must be based on`newCTM`Let’s map the points
3. So we can think of it this way
• use`UIKit`When the group API draws something, it fixes the canvas (i.e. the CTM of the context) and draws the path arbitrarily
• use`CGContext`“, 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 see`UIKit is centered in the upper left corner`and`CoreGraphics(Quartz) centrepoint is in the lower left corner`This 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 still`CoreGraphics`In the work. It’s just that the UIKit framework modifies CTM to make us feel like`Upper 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 avoid`UIKit`and`CoreGraphics`Mix. A classic example of this is in the`UIKit`To get the context, use`CGContextDrawImage`The 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