Search code examples
iosswiftuikitcore-graphicsdrawrect

iOS Core Graphic, how to calculate CGAffineTransform(scaleX offset?


While I am learning Core Graphic by ray wenderlich,

one step is to transform UIBezierPath, var transform = CGAffineTransform(scaleX: 0.8, y: 0.8),

I do not know why the step after is right,which is transform = transform.translatedBy(x: 15, y: 30)?

I don't know how the x and y position is calculated out.

By printing the UIBezierPath currentPoint print(medallionPath.currentPoint), I thought the width should be (x1 - x2) * 0.5, the height should be y1 - y2, I really don't know why it is

(x: 15, y: 30)

The whole code is following , tested in Playground

let size = CGSize(width: 120, height: 200)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!

//Gold colors
let darkGoldColor = UIColor(red: 0.6, green: 0.5, blue: 0.15, alpha: 1.0)
let midGoldColor = UIColor(red: 0.86, green: 0.73, blue: 0.3, alpha: 1.0)

let medallionPath = UIBezierPath(ovalIn: CGRect(x: 8, y: 72, width: 100, height: 100))

print(medallionPath.currentPoint)
//  (108.0, 122.0)
print(medallionPath.bounds)
//  (8.0, 72.0, 100.0, 100.0)
context.saveGState()
medallionPath.addClip()

darkGoldColor.setFill()
medallionPath.fill()

context.restoreGState()
// question
var transform = CGAffineTransform(scaleX: 0.8, y: 0.8)

// transform = transform.translatedBy(x: 15, y: 30)
medallionPath.lineWidth = 2.0

//apply the transform to the path
medallionPath.apply(transform)
print(medallionPath.currentPoint)
//  (86.4, 97.6)
print(medallionPath.bounds)
//  (6.4, 57.6, 80.0, 80.0)
medallionPath.stroke()


//This code must always be at the end of the playground
let image = UIGraphicsGetImageFromCurrentImageContext()!

UIGraphicsEndImageContext()

Solution

  • transform = transform.translatedBy(x: 15, y: 30)
    

    is a translation. It shifts the entire path to the right by 15 and down by 30. Your question is where did the magic numbers 15 and 30 come from.


    The medallion drawing has a circle with dimensions 100 x 100 starting at position (8, 72) as established by this line of code:

    let medallionPath = UIBezierPath(ovalIn: CGRect(x: 8, y: 72, width: 100, height: 100))
    

    The code then adds an inner ring by scaling the original path by 0.8, so it will be an 80 x 80 circle. So that the center of this smaller ring lines up with the center of the bigger circle, it will need to be offset an additional 10 in both the horizontal and vertical directions. (The smaller circle is 20 smaller horizontally and vertically, so shifting it by 1/2 of 20 gets it to align properly). The goal then is to have it positioned at (18, 82) with width: 80, height: 80.

    So, we need to apply a translation (a shift) in the X and Y directions, such that when the path is scaled we end up with a path anchored at (18, 82). The tricky bit is that the scaling gets applied to the shift values, so that has to be accounted for as well.

    So, we are starting with an X position of 8, and we want to apply some translation value dx so that when the result is scaled by 0.8 we end up with the value 18:

    (8 + dx) * 0.8 = 18
    

    solving for dx:

    dx = (18 / 0.8) - 8
    dx = 14.5
    

    Similarly for Y, we are starting with a Y position of 72 and want to apply a translation dy such that when it is scaled by 0.8 we end up with 82:

    (72 + dy) * 0.8 = 82
    

    solving for dy:

    dy = (82 / 0.8) - 72
    dy = 30.5
    

    So, the mathematically correct translation is (14.5, 30.5):

    transform = transform.translatedBy(x: 14.5, y: 30.5)
    

    Ray Wenderlich rounded those to (15, 30) for some reason known only to them (because the round numbers look better in the code perhaps?). It's possible that they didn't bother to do the math and just tried values until it looked right.