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:
- First calculate the position of the image in the applied Local coordinate system
- 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
- CGContext.[translate | scale | rotate]
- CGContext.concatenate(transform)
- CGAffineTransform class
- CGAffineTransform.[translate | scale | rotate]
- CGAffineTransform.concatenating(transform)
- In UIKit
- 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
path.apply(transform)
andcontext.translate
It works the same way because it all ends up theredevicePath = newPath * CTM
This step- but
path.apply(transform)
andcontext.translate
It’s not exactly equivalent- After the context changes, the new path must be based on
newCTM
Let’s map the points
- After the context changes, the new path must be based on
- 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
- use
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
- We often see
UIKit is centered in the upper left corner
andCoreGraphics(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 - 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 likeUpper left corner of origin
- 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
- In real development, try to avoid
UIKit
andCoreGraphics
Mix. A classic example of this is in theUIKit
To get the context, useCGContextDrawImage
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