Issue with scaling when UIView is rotated upside down - ios

I'm trying to rotate and scale UIView which has UIImageView inside as subView with one finger using UIPanGestureRecognizer. both rotation and scaling works fine in regular situations but when view is rotated upside down then it works in opposite direction instead of scaling up it scales down and vice versa, also when rotated at corner points it kind of flickers, don't work properly. results are shown in these gifs :
issue
another issue
here is how i'm trying to achieve this :
#objc func ScaleGest(gesture: UIPanGestureRecognizer) {
guard let gestureSuperView = gesture.view?.superview else { return }
let center = frameScaleBut.center
let touchLocation = gesture.location(in: self.view)
if gesture.state == UIPanGestureRecognizer.State.began {
self.initialDistance = distance(center, touchLocation)
} else if gesture.state == UIPanGestureRecognizer.State.changed {
let dist = distance(center, touchLocation) - initialDistance
let smallScale: CGFloat = editingMode == .sticker ? 90 : 190
let nextScale: CGSize = CGSize(width: gestureSuperView.bounds.width + dist, height: gestureSuperView.bounds.height + dist)
if (nextScale.width >= (self.view.frame.width * 0.8)) || nextScale.width <= smallScale {
return
}
gestureSuperView.bounds.size = CGSize(width: gestureSuperView.bounds.width + dist, height: gestureSuperView.bounds.height + dist)
}
}
i get distance with this method from this answer https://stackoverflow.com/a/1906659/20306199
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(sqrt(xDist * xDist + yDist * yDist))
}
this works fine when object has no rotation, but i think when object is rotated upside down it still works with same coordinates but in reality it should those values in opposite manner, can anyone please help me to understand or achieve this, thanks.

The problem is not in distance(_:_:) function, but in the center parameter. Your center parameter is determined in small images coordinate space, but gestureLocation is determined in view’s coordinate space. You can use convert(_:to:) method to do that. You can read about it here.
UPD:
Actually you don't need to calculate distance between frameScaleBut.center and gesture.location(in: self.view). Why? Because self.initialDistance almost always will be around 0. So let dist = distance(center, touchLocation) - initialDistance will always be positive. Instead calculate distance between gestureSuperView.center and gesture.location(in: self.view). gestureSuperView is already in view coordinate space.

Related

Zoom with one finger on mapbox map also seems to change the center location?

Below is my method for zooming. I am trying to implement it, so that when the user uses one finger to zoom (move finger up or down in specified region), it should zoom straight down or up just like normal (see snapchat as an example).
The problem is the code I have written changes latitude at the same time, which is unintended.
Why is this happening and how can I change it?
#objc func panGesture(_ sender: UIPanGestureRecognizer) {
print("recognized?")
// note that 'view' here is the overall video preview
let velocity = sender.velocity(in: mapView) //view
//print(sender.translation(in: view), "<-- waht is val?")
// print(sender.setTranslation(CGPoint(x: 16, y: 495), in: view), "<-- mmmmm")
if velocity.y >= 2 || velocity.y <= 2 {
let minimumZoomFactor: CGFloat = 1.0
let maximumZoomFactor: CGFloat = 17.0 // artificially set a max useable zoom of 14x (maybe shuld be icncrease?)
// clamp a zoom factor between minimumZoom and maximumZoom
func clampZoomFactor(_ factor: CGFloat) -> CGFloat {
return min(max(factor, minimumZoomFactor), maximumZoomFactor)
}
func update(scale factor: CGFloat) {
mapView.zoomLevel = Double(exactly: factor)! //maaybe setZoomLevel(... and animate it bit by bit?
}
var lastVal = 0
//BELOW IS SENDER.STATE THINGS!!!
switch sender.state {
case .began:
originalZoomLevel = mapView.zoomLevel//device.videoZoomFactor
//print(originalZoomLevel, "<-- what is initialZoom11111111???")
case .changed:
// distance in points for the full zoom range (e.g. min to max), could be view.frame.height
let fullRangeDistancePoints: CGFloat = 300.0 //dont know fi this is right??
// extract current distance travelled, from gesture start
let currentYTranslation: CGFloat = sender.translation(in: view).y
// calculate a normalized zoom factor between [-1,1], where up is positive (ie zooming in)
let normalizedZoomFactor = -1 * max(-1,min(1,currentYTranslation / fullRangeDistancePoints))
// calculate effective zoom scale to use
let newZoomFactor = clampZoomFactor(CGFloat(originalZoomLevel) + normalizedZoomFactor /** (maximumZoomFactor - minimumZoomFactor)*/)
print(originalZoomLevel, "<-- what is initialZoom???")
print(newZoomFactor, "<-- what is newZoomFactor???")
// update device's zoom factor'
update(scale: newZoomFactor)
print(lastVal - Int(mapView.centerCoordinate.latitude), " : The change is here")
lastVal = Int(mapView.centerCoordinate.latitude)
print(mapView.centerCoordinate, " : Cenetr courdenate in .changed")
case .ended, .cancelled:
print(originalZoomLevel, "<-- what is this???")
break
default:
break
}
}
}
The problem in your code is in your func update(scale ...)
Replace this code:
mapView.zoomLevel = Double(exactly: factor)!
With this:
mapView.setCenter(centerCoordOrig, zoomLevel: Double(exactly: factor)!, animated: false)
By the way, create a new variable outside for the centerCoordOrig.

Place anchor point at the centre of the screen while doing gestures

I have a view with an image which responds to pinch, rotation and pan gestures. I want that pinching and rotation of the image would be done with respect to the anchor point placed in the middle of the screen, exactly as it is done using Xcode iPhone simulator by pressing the options key. How can I place the anchor point in the middle of the screen if the centre of the image might be scaled and panned to a different location?
Here's my scale and rotate gesture functions:
#IBAction func pinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
// Move the achor point of the view's layer to the touch point
// so that scaling the view and the layer becames simpler.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Scale the view by the current scale factor.
if(gestureRecognizer.state == .began) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = gestureRecognizer.scale
}
if (gestureRecognizer.state == .began || gestureRecognizer.state == .changed) {
let currentScale = gestureRecognizer.view!.layer.value(forKeyPath:"transform.scale")! as! CGFloat
// Constants to adjust the max/min values of zoom
let kMaxScale:CGFloat = 15.0
let kMinScale:CGFloat = 1.0
var newScale = 1 - (lastScale - gestureRecognizer.scale)
newScale = min(newScale, kMaxScale / currentScale)
newScale = max(newScale, kMinScale / currentScale)
let transform = (gestureRecognizer.view?.transform)!.scaledBy(x: newScale, y: newScale);
gestureRecognizer.view?.transform = transform
lastScale = gestureRecognizer.scale // Store the previous scale factor for the next pinch gesture call
scale = currentScale // Save current scale for later use
}
}
#IBAction func rotationGesture(_ gestureRecognizer: UIRotationGestureRecognizer) {
// Move the achor point of the view's layer to the center of the
// user's two fingers. This creates a more natural looking rotation.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Apply the rotation to the view's transform.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
gestureRecognizer.view?.transform = (gestureRecognizer.view?.transform.rotated(by: gestureRecognizer.rotation))!
// Set the rotation to 0 to avoid compouding the
// rotation in the view's transform.
angle += gestureRecognizer.rotation // Save rotation angle for later use
gestureRecognizer.rotation = 0.0
}
}
func adjustAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .began {
let view = gestureRecognizer.view
let locationInView = gestureRecognizer.location(in: view)
let locationInSuperview = gestureRecognizer.location(in: view?.superview)
// Move the anchor point to the the touch point and change the position of the view
view?.layer.anchorPoint = CGPoint(x: (locationInView.x / (view?.bounds.size.width)!), y: (locationInView.y / (view?.bounds.size.height)!))
view?.center = locationInSuperview
}
}
EDIT
I see that people aren't eager to get into this. Let me help by sharing some progress I've made in the past few days.
Firstly, I wrote a function centerAnchorPoint which correctly places the anchor point of an image to the centre of the screen regardless of where that anchor point was previously. However the image must not be scaled or rotated for it to work.
func setAnchorPoint(_ anchorPoint: CGPoint, forView view: UIView) {
var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y)
newPoint = newPoint.applying(view.transform)
oldPoint = oldPoint.applying(view.transform)
var position = view.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
view.layer.position = position
view.layer.anchorPoint = anchorPoint
}
func centerAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .ended {
view?.layer.anchorPoint = CGPoint(x: (photo.bounds.midX / (view?.bounds.size.width)!), y: (photo.bounds.midY / (view?.bounds.size.height)!))
}
}
func centerAnchorPoint() {
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Find new anchor point
let newAnchorPoint = CGPoint(x: (imageCenter.x+2*distanceX)/(UIScreen.main.bounds.size.width), y: (imageCenter.y+2*distanceY)/(UIScreen.main.bounds.size.height))
//photo.layer.anchorPoint = newAnchorPoint
self.setAnchorPoint(newAnchorPoint, forView: photo)
let dotPath = UIBezierPath(ovalIn: CGRect(x: photo.layer.position.x-2.5, y: photo.layer.position.y-2.5, width: 5, height: 5))
layer.path = dotPath.cgPath
}
Function setAchorPoint is used here to set anchor point to a new position without moving an image.
Then I updated panGesture function by inserting this at the end of it:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
EDIT 2
Ok, so I'll try to simply explain the code above.
What I am doing is:
Finding the distance between the center of the photo and the center of the screen
Apply this formula to find the new position of anchor point:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/screenWidth
Then do the same for y position.
Set this point as a new anchor point without moving the photo using setAnchorPoint function
As I said this works great if the image is not scaled. If it is, then the anchor point does not stay at the center.
Strangely enough distanceX or distanceY doesn't exactly depend on scale value, so something like this doesn't quite work:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/(scaleValue*screenWidth)
EDIT 3
I figured out the scaling. It appears that the correct scale factor has to be:
scaleFactor = photo.frame.size.width/photo.layer.bounds.size.width
I used this instead of scaleValue and it worked splendidly.
So panning and scaling are done. The only thing left is rotation, but it appears that it's the hardest.
First thing I thought is to apply rotation matrix to increments in X and Y directions, like this:
let incrementX = (distanceX)/(screenWidth)
let incrementY = (distanceY)/(screenHeight)
// Applying rotation matrix
let incX = incrementX*cos(angle)+incrementY*sin(angle)
let incY = -incrementX*sin(angle)+incrementY*cos(angle)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incX, y: 0.5+incY)
However this doesn't work.
Since the question is mostly answered in the edits, I don't want to repeat myself too much.
Broadly what I changed from the code posted in the original question:
Deleted calls to adjustAnchorPoint function in pinch and rotation gesture functions.
Placed this piece of code in pan gesture function, so that the anchor point would update its position after panning the photo:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
Updated centerAnchorPoint function to work for rotation.
A fully working centerAnchorPoint function (rotation included):
func centerAnchorPoint() {
// Scale factor
photo.transform = photo.transform.rotated(by: -angle)
let curScale = photo.frame.size.width / photo.layer.bounds.size.width
photo.transform = photo.transform.rotated(by: angle)
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Apply rotational matrix to the distances
let distX = distanceX*cos(angle)+distanceY*sin(angle)
let distY = -distanceX*sin(angle)+distanceY*cos(angle)
let incrementX = (distX)/(curScale*UIScreen.main.bounds.size.width)
let incrementY = (distY)/(curScale*UIScreen.main.bounds.size.height)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incrementX, y: 0.5+incrementY)
self.setAnchorPoint(newAnchorPoint, forView: photo)
}
The key things to notice here is that the rotation matrix has to be applied to distanceX and distanceY. The scale factor is also updated to remain the same throughout the rotation.

Get distance from the edge in swift 3

I've got a moveable image using UIPanGestureRecognizer and i need to make the image more transparent the closer it get's to the edge of the screen.
Below is the code I'm using to move and rotate the image the further it get's away from the center.
Adding the UIPanGestureRecognizer to the UIView the image is in:
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(self.detectPan))
moveImage.cancelsTouchesInView = false
MainImageView.addGestureRecognizer(moveImage)
The function that get's called when the UIPanGestureRecognizer starts.
func detectPan(gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began || gesture.state == UIGestureRecognizerState.changed {
let translation = gesture.translation(in: self.view)
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y)
gesture.setTranslation(CGPoint(x: 0,y: 0), in: self.view)
let newValue = CGFloat(((gesture.view!.center.x + translation.x) - (self.view.bounds.width * 0.50)) / 500)
MainImageView.transform = MainImageView.transform.rotated(by: -lastValue)
MainImageView.transform = MainImageView.transform.rotated(by: newValue)
lastValue = newValue
}
}
I'm assuming that " further it get's away from the center" is relative to the center of the view's parent (or if it's relative to something else, you can adjust my example accordingly).
Since you seem to only be moving the image horizontally, the distance should be a simple first degree calculation.
Something like:
let distanceToCenter = abs(gesture.view!.center.x - gesture.view!.parent!.center.x)
If you were moving both vertically and horizontally, you would need a second degree calculation to get the distance:
let deltaX = gesture.view!.center.x - gesture.view!.parent!.center.x
let deltaY = gesture.view!.center.y - gesture.view!.parent!.center.y
let distanceToCenter = sqrt(deltaX*deltaX + deltaY*deltaY)
You could then compute the alpha for fading out as follows:
let alpha = 1 - 2 * distanceToCenter / gesture.view!.parent!.bounds.size.width
Note that Swift may need some type casting for these calculations (at least until it goes to math school and figures out number theory)

Making a "Joy stick" swift

What I have been trying to do is create a "Joy stick" that moves a player around. Here is what I have so far:
import UIKit
import SpriteKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var isTracking = false
var firstTrackingLocation = CGPoint.zero
var trackingVelocity = CGPoint.zero
var trackingDistance : CGFloat = 0.0
var previousTime : NSTimeInterval = 0.0
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/level.scn")!
let scnView = self.view as! SCNView
scnView.delegate = self
scnView.scene = scene
scnView.showsStatistics = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking == false {
for touch in touches {
isTracking = true
let location = touch.locationInView(self.view)
firstTrackingLocation = location
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking {
trackingVelocity = touches.first!.locationInView(self.view)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
isTracking = false
trackingVelocity = CGPoint.zero
}
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
if isTracking == true {
let scnView = self.view as! SCNView
let character = scnView.scene!.rootNode.childNodeWithName("person", recursively: true)
let deltaTime = time - previousTime
let pointsPerSecond: CGFloat = 1.0 * CGFloat(deltaTime)
var xResult:CGFloat = 0.0
var yResult:CGFloat = 0.0
let point = firstTrackingLocation
let endPoint = trackingVelocity
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
if direction.x > direction.y {
let movePerSecond = pointsPerSecond/direction.x
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
} else {
let movePerSecond = pointsPerSecond/direction.y
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
}
character!.position = SCNVector3(CGFloat(character!.position.x) + (xResult), CGFloat(character!.position.y), CGFloat(character!.position.z) + (yResult))
let camera = scnView.scene?.rootNode.childNodeWithName("camera", recursively: true)
camera?.position = SCNVector3(CGFloat(camera!.position.x) + (xResult), CGFloat(camera!.position.y), CGFloat(camera!.position.z) + (yResult))
}
previousTime = time
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Landscape
}
}
Now this works except if you drag your finger to the other side of the phone the character moves 10 times faster then it would if you barely moved your finger. So what I would like to have, is a Joy stick that moves the character the same speed if you drag a little bit or to the other side of the screen. And I would also like if you changed direction at the other side of the screen that the character would move the other way. So, my guess is that there needs to be a lastPoint saved then when the touchesMoved gets called that somehow we calculate a direction from lastPoint to the currentPoint and then move the character in renderer. I understand that most of this code is probably rubbish, but thanks in advance.
Your joystick should be a value from 0 to 1, you need to determine the radius of your joystick, then calculate distance of (point touched to center of control) and the angle of the control with arc tan.
Now we need to ensure we never go past maxRadius, so if our distance is > maxRadius, we just set it to max radius, then we divide this value by our maxRadius to get out distance ratio.
Then we just take the cos and sin of our angle, and multiply it by our distance ratio, and get the x and y ratio values. (Should be between 0 and 1)
Finally, take this x and y value, and multiply it to the speed at which your object should be moving at.
let maxRadius = 100 //Your allowable radius
let xDist = (p2.x - p1.x)
let yDist = (p2.y - p1.y)
let distance = (sqrt((xDist * xDist) + (yDist * yDist))
let angle = atan2(yDist , xDist )
let controlDistanceRatio = (distance > maxRadius) ? 1 : distance / maxRadius
let controllerX = cos(angle) * controlDistanceRatio
let controllerY = sin(angle) * controlDistanceRatio
This seems like a good case to use a custom UIGestureRecognizer. See Apple API Reference.
In this particular case you would create a continuous gesture. The resultant CGVector would be calculated from the center (origin) point of your joystick on screen. The recognizer would fail if the joystick node isn't selected and end if unselected (deselected). The resultant CGVector will be updated while the gesture's state is moved.
Now the tricky part to figure out would be moving the node image in such a way that allows the user to have the feeling of a joystick. For this you may need to update the node texture and make slight adjustments to the node position to give the appearance of moving a node around.
See if this helps you: Single Rotation Gesture Recognizer
Let me know if this points you in the right direction.
Your stated problem is the character moves at a variable rate depending on the how far from the start point the user drags their finger. The crucial point in the code seems to be this
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
The difference between endPoint and point is variable and so you are getting a variable magnitude in your direction. To simplify, you could just put in a constant value like
let direction = CGPoint(x: 10, y: 10)
That gets the character moving at a constant speed when the user presses the joystick, but the character is always moving the same direction.
So somehow you've got to bracket in the variable directional values. The first thing that comes to mind is using min and max
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
direction.x = min(direction.x, 10)
direction.x = max(direction.x, -10)
direction.y = min(direction.y, 10)
direction.y = max(direction.y, -10)
That seems like it would keep the magnitude of the direction values between -10 and 10. That doesn't make the speed completely constant, and it allows faster travel along diagonal lines than travel parallel to the x or y axis, but maybe it is closer to what you want.

Create path to move player from current position to new position through a "turn circle"

I've been wracking my brain for a couple of days trying to come up with a way to move a player from there current position to new position using Swift and SpriteKit. Sounds relatively easy.
Now, I know I can use a CGPath and a SKAction to move the player along a path, but what I need to know is how to create the path for the player to move along.
I need the player to move through a predetermined radius as it turns towards the new point first as it moves, let me demonstrate...
So, the red circle is the player and their current orientation, the large circle is the turn radius and the red crosses are possible points where the player wants to move to (obviously you'd only have one at any point in time, but the idea is demonstrate the difference in movement between one possible point and another)
Also, the player could move left or right depending in which ever path is shortest to the target point.
What I tried (sorry the list is kind of short)...
Basically, I know the current position/orientation of the player; I know the radius of the turn circle and I know the point I want to move to. I need to calculate the arc through which the player will need to initially move through to orientate themselves to the new point (tacking on a CGPathAddLineToPoint to the end of the arc should be trivial)
Other then spending copious amounts of time reading through the docs, Googling, reading blog posts and tutorials, I also tried looping through a series of angles from a start angle through a given iteration level (+/-0.5 degrees for example) and calculating the angle between the current point and next point on the circle and comparing that to the angle of the current point to the target point and basically selecting the angle with the lowest difference/delta ...
So, the two red circles represent two points on the circle, the blue line represents the angle between them, the green line represents the angle from the first point to the target point.
Let's just say, that while that might possibly work, I'm kind of horrified at the idea and hope that it might be possible to come up with a better/faster solution.
I'm not sure if something like CGPathAddArcToPoint would help, as it would create an arc from my players current position to the target point, rather then allow the player to move through a turning circle.
Once the player leaves the turning circle, I'm not particular fussed if the move in a straight line or not (ie they could curve slightly to the target point), but I'm currently focused on trying to calculate the required arc needed to get the player started.
Sorry, my maths is pretty poor, so, please, be nice
The code "currently" looks something like (a complete mess)
func pointTowards(point thePoint: CGPoint) {
// Need to calculate the direction of the turn
//let angle = atan2(thePoint.y - self.position.y, thePoint.x - self.position.x) - CGFloat(180.0.toRadians());
let angle = angleBetween(startPoint: self.position, endPoint: thePoint) - CGFloat(180.0.toRadians())
if (self.zRotation < 0) {
// self.zRotation
// self.zRotation = self.zRotation + M_PI * 2;
}
let rotateTo: SKAction = SKAction.rotateToAngle(angle, duration: 1, shortestUnitArc: true)
rotateTo.timingMode = SKActionTimingMode.EaseInEaseOut
self.runAction(rotateTo)
let offset = CGPoint(x: rotorBlur.position.x, y: rotorBlur.position.y + (rotorBlur.size.width / 2))
let radius = rotorBlur.size.width / 2.0
var points: [AnglesAndPoints] = self.pointsOnCircleOf(
radius: radius,
offset: offset);
let centerPoint = CGPoint(x: offset.x + radius, y: offset.y + radius)
var minAngle = CGFloat.max
var minDelta = CGFloat.max
for var p: Int = 1; p < points.count; p++ {
let p1 = points[p - 1].point
let p2 = points[p].point
let point = angleBetween(startPoint: p1, endPoint: p2) - CGFloat(180.0.toRadians())
let target = angleBetween(startPoint: p1, endPoint: thePoint) - CGFloat(180.0.toRadians())
let delta = target - point
if delta < minDelta {
minDelta = delta
minAngle = points[p - 1].angle
}
}
println("projected: \(minAngle); delta = \(minDelta)")
if let pathNode = pathNode {
pathNode.removeFromParent()
}
//points = self.pointsOnCircleOf(
// radius: rotorBlur.size.width / 2.0,
// offset: CGPoint(x: 0, y: rotorBlur.size.width / 2));
let path = CGPathCreateMutable()
CGPathAddArc(
path,
nil,
0,
rotorBlur.size.width / 2,
rotorBlur.size.width / 2,
CGFloat(-180.0.toRadians()),
minAngle,
true)
pathNode = SKShapeNode()
pathNode?.path = path
pathNode?.lineWidth = 1.0
pathNode?.strokeColor = .lightGrayColor()
addChild(pathNode!)
}
func pointsOnCircleOf(radius r : CGFloat, offset os: CGPoint) -> [AnglesAndPoints] {
var points: [AnglesAndPoints] = []
let numPoints = 360.0 * 2.0
let delta = 360.0 / numPoints
for var degrees: Double = 0; degrees < numPoints; degrees += delta {
var point: CGPoint = pointOnCircle(angle: CGFloat(degrees.toRadians()), radius: r)
point = CGPoint(x: point.x + os.x, y: point.y + os.y)
points.append(AnglesAndPoints(angle: CGFloat(degrees.toRadians()), point: point))
}
return points
}
func pointOnCircle(angle radians:CGFloat, radius theRadius:CGFloat) -> CGPoint {
return CGPointMake((cos(radians) * theRadius),
(sin(radians) * theRadius));
}
func angleBetween(startPoint p1: CGPoint, endPoint p2: CGPoint) -> CGFloat {
return atan2(p2.y - p1.y, p2.x - p1.x) //- CGFloat(180.0.toRadians());
}
Basically, I went about pre-calculating the points on a circle of a given radius with a given offset, which is just horrible and if I had the time right now, would re-work it so that the point was dynamically created (or I could cache the values some how and simply translate them), but as I said, this was such a horrible idea I really wanted to find a different way and abandon this approach
I'm pretty sure that the current code doesn't take into the players current orientation and it should be supplying a start angle and direction (counter/clockwise) in which to iterate, but I've gotten to the point I'd like to see if their is simply a better solution then this before trying to fix any more issues with it
Funny, I actually have motion in my game almost exactly as you described except that instead of always going clock-wise when on the right side and counter-clock when on the left, it will pick the closer path.
So I grabbed some of the code and modified it sightly to fit your description. It will move left when the target point is to the left of the player, else it will move right. You can also set the speed of the node, as well as the radius and position of the "orbit."
My implementation however does not use SKActions and paths to move. Everything is done dynamically in real-time which allows for collisions with the moving objects and greater motion control. However if you absolutely need to use paths with SKActions let me know and I'll try to come up with a solution. Essentially what it comes down to is finding the arc to the tangent points (which the code already does to an extent).
The physics calculations come from my two answerers here, and here.
The way the implementation works is that it first determines the final destination point, as well as the angular distance to the best tangent point using a secondary circle to find the tangent points. Then using centripetal motion, the node moves along the path to the tangent point and then switches to linear motion to finish moving to the end destination.
Below is the code for the GameScene:
import SpriteKit
enum MotionState { case None, Linear, Centripetal }
class GameScene: SKScene {
var node: SKShapeNode!
var circle: SKShapeNode!
var angularDistance: CGFloat = 0
var maxAngularDistance: CGFloat = 0
let dt: CGFloat = 1.0/60.0 //Delta Time
var centripetalPoint = CGPoint() //Point to orbit.
let centripetalRadius: CGFloat = 60 //Radius of orbit.
var motionState: MotionState = .None
var invert: CGFloat = 1
var travelPoint: CGPoint = CGPoint() //The point to travel to.
let travelSpeed:CGFloat = 200 //The speed at which to travel.
override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
circle = SKShapeNode(circleOfRadius: centripetalRadius)
circle.strokeColor = SKColor.redColor()
circle.hidden = true
self.addChild(circle)
}
func moveToPoint(point: CGPoint) {
travelPoint = point
motionState = .Centripetal
//Assume clockwise when point is to the right. Else counter-clockwise
if point.x > node.position.x {
invert = -1
//Assume orbit point is always one x radius right from node's position.
centripetalPoint = CGPoint(x: node.position.x + centripetalRadius, y: node.position.y)
angularDistance = CGFloat(M_PI)
} else {
invert = 1
//Assume orbit point is always one x radius left from node's position.
centripetalPoint = CGPoint(x: node.position.x - centripetalRadius, y: node.position.y)
angularDistance = 0
}
}
final func calculateCentripetalVelocity() {
let normal = CGVector(dx:centripetalPoint.x + CGFloat(cos(self.angularDistance))*centripetalRadius,dy:centripetalPoint.y + CGFloat(sin(self.angularDistance))*centripetalRadius);
let period = (CGFloat(M_PI)*2.0)*centripetalRadius/(travelSpeed*invert)
self.angularDistance += (CGFloat(M_PI)*2.0)/period*dt;
if (self.angularDistance>CGFloat(M_PI)*2)
{
self.angularDistance = 0
}
if (self.angularDistance < 0) {
self.angularDistance = CGFloat(M_PI)*2
}
node.physicsBody!.velocity = CGVector(dx:(normal.dx-node.position.x)/dt ,dy:(normal.dy-node.position.y)/dt)
//Here we check if we are at the tangent angle. Assume 4 degree threshold for error.
if abs(maxAngularDistance-angularDistance) < CGFloat(4*M_PI/180) {
motionState = .Linear
}
}
final func calculateLinearVelocity() {
let disp = CGVector(dx: travelPoint.x-node.position.x, dy: travelPoint.y-node.position.y)
let angle = atan2(disp.dy, disp.dx)
node.physicsBody!.velocity = CGVector(dx: cos(angle)*travelSpeed, dy: sin(angle)*travelSpeed)
//Here we check if we are at the travel point. Assume 15 point threshold for error.
if sqrt(disp.dx*disp.dx+disp.dy*disp.dy) < 15 {
//We made it to the final position! Code that happens after reaching the point should go here.
motionState = .None
println("Node finished moving to point!")
}
}
override func update(currentTime: NSTimeInterval) {
if motionState == .Centripetal {
calculateCentripetalVelocity()
} else if motionState == .Linear {
calculateLinearVelocity()
}
}
func calculateMaxAngularDistanceOfBestTangent() {
let disp = CGVector(dx: centripetalPoint.x - travelPoint.x, dy: centripetalPoint.y - travelPoint.y)
let specialCirclePos = CGPoint(x: (travelPoint.x+centripetalPoint.x)/2.0, y: (travelPoint.y+centripetalPoint.y)/2.0)
let specialCircleRadius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)/2.0
let tangentPair = getPairPointsFromCircleOnCircle(centripetalPoint, radiusA: centripetalRadius, pointB: specialCirclePos, radiusB: specialCircleRadius)
let tangentAngle1 = (atan2(tangentPair.0.y - centripetalPoint.y,tangentPair.0.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
let tangentAngle2 = (atan2(tangentPair.1.y - centripetalPoint.y,tangentPair.1.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
if invert == -1 {
maxAngularDistance = tangentAngle2
} else {
maxAngularDistance = tangentAngle1
}
}
//Not mine, modified algorithm from https://stackoverflow.com/q/3349125/2158465
func getPairPointsFromCircleOnCircle(pointA: CGPoint, radiusA: CGFloat, pointB: CGPoint, radiusB: CGFloat) -> (CGPoint,CGPoint) {
let dX = (pointA.x - pointB.x)*(pointA.x - pointB.x)
let dY = (pointA.y - pointB.y)*(pointA.y - pointB.y)
let d = sqrt(dX+dY)
let a = (radiusA*radiusA - radiusB*radiusB + d*d)/(2.0*d);
let h = sqrt(radiusA*radiusA - a*a);
let pointCSub = CGPoint(x:pointB.x-pointA.x,y:pointB.y-pointA.y)
let pointCScale = CGPoint(x: pointCSub.x*(a/d), y: pointCSub.y*(a/d))
let pointC = CGPoint(x: pointCScale.x+pointA.x, y: pointCScale.y+pointA.y)
let x3 = pointC.x + h*(pointB.y - pointA.y)/d;
let y3 = pointC.y - h*(pointB.x - pointA.x)/d;
let x4 = pointC.x - h*(pointB.y - pointA.y)/d;
let y4 = pointC.y + h*(pointB.x - pointA.x)/d;
return (CGPoint(x:x3, y:y3), CGPoint(x:x4, y:y4));
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touchPos = (touches.first! as! UITouch).locationInNode(self)
node = SKShapeNode(circleOfRadius: 10)
node.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
self.addChild(node)
moveToPoint(touchPos)
calculateMaxAngularDistanceOfBestTangent() //Expensive!
circle.hidden = false
circle.position = centripetalPoint
}
}
Note that the circle you see is another node I added to the scene to make the motion more visible; you can easily just remove it. When debugging you might also find it useful to add nodes at the tangent points. The tangentPair tuple inside the calculateMaxAngularDistanceOfBestTangent function contains the two tangent points.
Additionally note that finding the tangent points/angles is expensive but it only happens each time you need to move to a new point. If however you game requires constantly moving to a new point, using this algorithm repeatedly on many nodes can be costly (always profile before assuming this though). Another way to check when to move from centripetal motion to linear motion is to check if the velocity vector is approaching the end position as shown below. This is less accurate but allows you to remove the calculateMaxAngularDistanceOfBestTangent function entirely.
let velAngle = atan2(node.physicsBody!.velocity.dy,node.physicsBody!.velocity.dx)
let disp = CGVector(dx: travelPoint.x-node.position.x, dy: travelPoint.y-node.position.y)
let dispAngle = atan2(disp.dy,disp.dx)
//Here we check if we are at the tangent angle. Assume 4 degree threshold for error.
if velAngle != 0 && abs(velAngle - dispAngle) < CGFloat(4*M_PI/180) {
motionState = .Linear
}
Lastly let me know if you need to use paths with SKActions, regardless I think I will update this last part showing how this is done (unless someone beats me to it! And as I mentioned earlier the code I posted does this to an extent.) I don't have time to right now but hopefully I get a chance to soon! I hope something mentioned in this answer helps you. Good luck with your game.
Update including SKActions
The code below shows getting the same exact effect except this time using SKActions to animate a CGPath to the tangent angle then to the final destination point. It is much simpler as there is no longer a manual calculation of centripetal and linear motion, however because it is an animation you lose the dynamic real-time motion control that the solution above provides.
class GameScene: SKScene {
var centripetalPoint = CGPoint() //Point to orbit.
let centripetalRadius: CGFloat = 60 //Radius of orbit.
var travelPoint: CGPoint = CGPoint() //The point to travel to.
var travelDuration: NSTimeInterval = 1.0 //The duration of action.
var node: SKShapeNode!
var circle: SKShapeNode!
override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
circle = SKShapeNode(circleOfRadius: centripetalRadius)
circle.strokeColor = SKColor.redColor()
circle.hidden = true
self.addChild(circle)
}
//Not mine, modified algorithm from https://stackoverflow.com/q/3349125/2158465
func getPairPointsFromCircleOnCircle(pointA: CGPoint, radiusA: CGFloat, pointB: CGPoint, radiusB: CGFloat) -> (CGPoint,CGPoint) {
let dX = (pointA.x - pointB.x)*(pointA.x - pointB.x)
let dY = (pointA.y - pointB.y)*(pointA.y - pointB.y)
let d = sqrt(dX+dY)
let a = (radiusA*radiusA - radiusB*radiusB + d*d)/(2.0*d);
let h = sqrt(radiusA*radiusA - a*a);
let pointCSub = CGPoint(x:pointB.x-pointA.x,y:pointB.y-pointA.y)
let pointCScale = CGPoint(x: pointCSub.x*(a/d), y: pointCSub.y*(a/d))
let pointC = CGPoint(x: pointCScale.x+pointA.x, y: pointCScale.y+pointA.y)
let x3 = pointC.x + h*(pointB.y - pointA.y)/d;
let y3 = pointC.y - h*(pointB.x - pointA.x)/d;
let x4 = pointC.x - h*(pointB.y - pointA.y)/d;
let y4 = pointC.y + h*(pointB.x - pointA.x)/d;
return (CGPoint(x:x3, y:y3), CGPoint(x:x4, y:y4));
}
func moveToPoint(point: CGPoint) {
travelPoint = point
//Assume clockwise when point is to the right. Else counter-clockwise
if point.x > node.position.x {
centripetalPoint = CGPoint(x: node.position.x + centripetalRadius, y: node.position.y)
} else {
centripetalPoint = CGPoint(x: node.position.x - centripetalRadius, y: node.position.y)
}
let disp = CGVector(dx: centripetalPoint.x - travelPoint.x, dy: centripetalPoint.y - travelPoint.y)
let specialCirclePos = CGPoint(x: (travelPoint.x+centripetalPoint.x)/2.0, y: (travelPoint.y+centripetalPoint.y)/2.0)
let specialCircleRadius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)/2.0
let tangentPair = getPairPointsFromCircleOnCircle(centripetalPoint, radiusA: centripetalRadius, pointB: specialCirclePos, radiusB: specialCircleRadius)
let tangentAngle1 = (atan2(tangentPair.0.y - centripetalPoint.y,tangentPair.0.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
let tangentAngle2 = (atan2(tangentPair.1.y - centripetalPoint.y,tangentPair.1.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, node.position.x, node.position.y)
if travelPoint.x > node.position.x {
CGPathAddArc(path, nil, node.position.x+centripetalRadius, node.position.y, centripetalRadius, CGFloat(M_PI), tangentAngle2, true)
} else {
CGPathAddArc(path, nil, node.position.x-centripetalRadius, node.position.y, centripetalRadius, 0, tangentAngle1, false)
}
CGPathAddLineToPoint(path, nil, travelPoint.x, travelPoint.y)
let action = SKAction.followPath(path, asOffset: false, orientToPath: false, duration: travelDuration)
node.runAction(action)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touchPos = (touches.first! as! UITouch).locationInNode(self)
node = SKShapeNode(circleOfRadius: 10)
node.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
self.addChild(node)
moveToPoint(touchPos)
circle.hidden = false
circle.position = centripetalPoint
}
}

Resources