I'm adding a new feature for a drawing app library and I'm implementing a pinch to resize feature for it. For example I have a Line shape created by connecting 2 CGPoint
s. I want to increase the size of the shape by using UIPinchGestureRecognizer
since it has the scale
property. So far what I've done is this:
// The startPoint is the point of the first finger of the gesture, and the endPoint is the second finger.
// The scale value is from the UIPinchGestureRecognizer class
func handlePinch(startPoint: CGPoint, endPoint: CGPoint, scale: CGFloat) {
...
var castedShape = SomeShapeWithTwoPoints...
let transform = CGAffineTransform(scaleX: scale, y: scale)
castedShape.a = castedShape.a.applying(transform)
castedShape.b = castedShape.b.applying(transform)
...
}
But unfortunately, it doesn't behave as I expect it to (I would say the calculated points are too big and it positioned the points outside of the view). Since I'm not sure where did I do wrong, My question is how do I get the updated CGPoint
s based on the scale value of the pinch gesture the correct way?
EDIT:
For example, I have the first line before the gesture was done. It has 2 points A and B. When I do the gesture, I want the line to be longer while keeping the position like this. In order to achieve this, I want to calculate the A and B points based on the scale of the pinch gesture (I just need the new points after the gesture was done). Is there any way to do this?
The problem you're running into when scaling with a CGAffineTransform
is that the transform is relative to 0,0
.
So, if you have points at:
a 2.0
scaling will result in:
Which is, obviously, not what you're going for -- you want the distance between the points to grow (or shrink) based on the scale factor.
There are several ways to do this...
Let's use some math.
We'll start with two points:
As we know, any two points can be used to define a line segment:
Thanks to Pythagoras, we can easily get the length of the line -- and easily find the center-point on that line:
Now, what else can we do with a line and its center-point? We can define a circle:
and the circle's radius:
Now, if we want to "scale the points by 2.0", we can scale the radius to define a new circle:
and, with just a little more math, we can get the angle of the line and then "find the point on the circle":
and we've moved the two points away from each other, by the scale factor, keeping them on the original line.
Here's a quick example...
We'll start with some "convenience" CGPoint
funcs:
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
func angleToPoint(otherPoint: CGPoint) -> CGFloat {
let originX = otherPoint.x - self.x
let originY = otherPoint.y - self.y
return atan2(originY, originX)
}
func distanceToPoint(otherPoint: CGPoint) -> CGFloat {
return sqrt(pow((otherPoint.x - x), 2) + pow((otherPoint.y - y), 2))
}
func centerBetween(otherPoint: CGPoint) -> CGPoint {
let xDiff: CGFloat = otherPoint.x - x
let yDiff: CGFloat = otherPoint.y - y
return .init(x: x + xDiff * 0.5, y: y + yDiff * 0.5)
}
func scaleOnLineTo(otherPoint: CGPoint, scale: CGFloat) -> (CGPoint, CGPoint) {
let p1: CGPoint = self
let p2: CGPoint = otherPoint
// get center point between p1,p2
let center: CGPoint = p1.centerBetween(otherPoint: p2)
// get radius (distance between center,p1)
let radius: CGFloat = center.distanceToPoint(otherPoint: p1)
// angle (radians) from center to p1
let a1: CGFloat = center.angleToPoint(otherPoint: p1)
// angle (radians) from center to p2
let a2: CGFloat = center.angleToPoint(otherPoint: p2)
// new p1 is point on circle with radius * scale
let scaledP1: CGPoint = CGPoint.pointOnCircle(center: center, radius: radius * scale, angle: a1)
// new p2 is point on circle with radius * scale
let scaledP2: CGPoint = CGPoint.pointOnCircle(center: center, radius: radius * scale, angle: a2)
return (scaledP1, scaledP2)
}
}
a simple "round" view:
class PointView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.width * 0.5
}
}
and a demonstration:
class PinchExampleVC: UIViewController {
let vP1 = PointView()
let vP2 = PointView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
vP1.backgroundColor = .red
vP2.backgroundColor = .blue
vP1.frame.size = .init(width: 20.0, height: 20.0)
vP2.frame.size = vP1.frame.size
view.addSubview(vP1)
view.addSubview(vP2)
let pg = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
view.addGestureRecognizer(pg)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
vP1.center = .init(x: view.bounds.midX + 40.0, y: view.bounds.midY - 80.0)
vP2.center = .init(x: view.bounds.midX - 40.0, y: view.bounds.midY + 60.0)
}
@objc func handlePinch(_ gestureRecognizer : UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let (xp1, xp2) = vP1.center.scaleOnLineTo(otherPoint: vP2.center, scale: gestureRecognizer.scale)
vP1.center = xp1
vP2.center = xp2
gestureRecognizer.scale = 1.0
}
}
}
Looks like this:
As I said, there are several ways to do this. Depending on what else you might need to do -- for example, if you need to scale multiple points, a bezier path, etc -- you'd need to investigate transforms a bit more.
But, for your specific question, this should do the job.