Swift - Rotate Gesture and Rotation Increments of 90 Degrees

Swift - Rotate gesture and rotation increments of 90 degrees

I achieved this by implementing it as follows:

@IBAction func handleRotation(_ recognizer: UIRotationGestureRecognizer) {
if let recognizerView = recognizer.view {
recognizerView.transform = recognizerView.transform.rotated(by: recognizer.rotation)
recognizer.rotation = 0

let radians:Double = atan2( Double(recognizerView.transform.b), Double(recognizerView.transform.a))
let degrees = radians * Double((180 / Float.pi))

if recognizer.state == .ended || recognizer.state == .cancelled {
var degreeToAnimate:CGFloat = 0

switch degrees {
case -45...45:
print("the default value 0, no need to any assign...")
case 46...135:
degreeToAnimate = CGFloat(M_PI_2)
case 136...180, -180 ... -136:
degreeToAnimate = CGFloat(M_PI)
case -135 ... -46:
degreeToAnimate = CGFloat(-M_PI_2)
default:
print("!")
}

UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 1.0, options: .curveEaseIn, animations: {
recognizerView.transform = CGAffineTransform(rotationAngle: degreeToAnimate)
}, completion: { _ in
recognizer.rotation = 0
})
}
}
}

Note that I added the UIRotationGestureRecognizer on the desired view from the Interface Builder, that's why the function is an @IBAction.

Output:

Sample Image

SceneKit - rotate SCNNode with pan gesture based on direction

After a LOT of research and few hours of head-banging the wall, i finally found a working solution.

My working code:

func handlePan(sender: UIPanGestureRecognizer){

// get pan direction
let velocity: CGPoint = sender.velocity(in: sender.view!)
if self.panDirection == nil {
self.panDirection = GameHelper.getPanDirection(velocity: velocity)
}

// if selected brick
if self.selectedBrickNode != nil {

if sender.state == UIGestureRecognizerState.began{
lastAngle = 0.0 // reset last angle
}

let translation = sender.translation(in: sender.view!)
let anglePan = (self.panDirection == "horizontal") ? GameHelper.deg2rad(deg: Float(translation.x)) : GameHelper.deg2rad(deg: Float(translation.y))

let x:Float = (self.panDirection == "vertical" ) ? 1 : 0.0
let y:Float = (self.panDirection == "horizontal" ) ? 1 : 0.0

// calculate the angle change from last call
let fraction = anglePan - lastAngle
lastAngle = anglePan

// perform rotation by difference to last angle
selectedBrickNode.transform = SCNMatrix4Mult(selectedBrickNode.transform,SCNMatrix4MakeRotation(fraction, x, y, 0.0))


if(sender.state == UIGestureRecognizerState.ended) {

// calculate angle to snap to 90 degree increments
let finalRotation = GameHelper.rad2deg(rad:anglePan)
let diff = finalRotation.truncatingRemainder(dividingBy: 90.0)
var finalDiff = Float(0.0)

switch diff {
case 45..<90 :
finalDiff = 90 - diff
case 0..<45 :
finalDiff = -diff
case -45..<0 :
finalDiff = abs(diff)
case -90 ..< -45 :
finalDiff = -90 - diff
default:
print("do nothing")
}

// final transform to apply snap to closest 90deg increment
let snapAngle = GameHelper.deg2rad(deg: finalDiff)
selectedBrickNode.transform = SCNMatrix4Mult(selectedBrickNode.transform, SCNMatrix4MakeRotation(snapAngle, x, y, 0.0))

// remove highlight from node and deselect
self.selectedBrickNode?.geometry?.materials = [hlp.defaultMaterial]
self.selectedBrickNode = nil
}
}


}

and GameHelper.swift

class GameHelper {

static func rad2deg( rad:Float ) -> Float {
return rad * (Float) (180.0 / Double.pi)
}

static func deg2rad( deg:Float ) -> Float{
return deg * (Float)(Double.pi / 180)
}

static func getPanDirection(velocity: CGPoint) -> String {
var panDirection:String = ""
if ( velocity.x > 0 && velocity.x > abs(velocity.y) || velocity.x < 0 && abs(velocity.x) > abs(velocity.y) ){
panDirection = "horizontal"
}

if ( velocity.y < 0 && abs(velocity.y) > abs(velocity.x) || velocity.y > 0 && velocity.y > abs(velocity.x)) {
panDirection = "vertical"
}


return panDirection
}

}

Scenekit - multiple rotation of SCNNode and direction of axes

After further research i realised i chose completely wrong approach. The way to achieve what i want was to rotate the node using the axes of the rootNode - this way i don't need to worry about local axes of my node.

EDIT: updated code based on Xartec's suggestions

let rotation = SCNMatrix4MakeRotation(angle,  x, y, Float(0))
let newTransform = SCNMatrix4Mult(bricksNode.transform, rotation)
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = bricksNode.transform
animation.toValue = scnScene.rootNode.convertTransform(newTransform,from: nil)
animation.duration = 0.5
bricksNode.addAnimation(animation, forKey: nil)

How to Rotate a UIImage 90 degrees?

What about something like:

static inline double radians (double degrees) {return degrees * M_PI/180;}
UIImage* rotate(UIImage* src, UIImageOrientation orientation)
{
UIGraphicsBeginImageContext(src.size);

CGContextRef context = UIGraphicsGetCurrentContext();

if (orientation == UIImageOrientationRight) {
CGContextRotateCTM (context, radians(90));
} else if (orientation == UIImageOrientationLeft) {
CGContextRotateCTM (context, radians(-90));
} else if (orientation == UIImageOrientationDown) {
// NOTHING
} else if (orientation == UIImageOrientationUp) {
CGContextRotateCTM (context, radians(90));
}

[src drawAtPoint:CGPointMake(0, 0)];

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

Code for a dedicated rotation button

There's a project on GitHub called KTOneFingerRotationGestureRecognizer that is a custom rotation gesture recognizer that uses a single finger to do rotations. You touch down on an object and spin it as if it had an axle in it's center. It might work for you, depending on how small your shapes are. You might need to make your views a little larger the the images they contain (add some padding, in other words.)

How to animate to rotate counter-clockwise

Just rotate it by whatever angle you did for clockwise, but multiply it by negative one

func rotateLeft() {
UIView.animate(withDuration: 0.5, animations: {
self.profileImageView.transform = CGAffineTransform(rotationAngle: ((180.0 * CGFloat(M_PI)) / 180.0) * -1)
self.profileImageView.transform = CGAffineTransform(rotationAngle: ((0.0 * CGFloat(M_PI)) / 360.0) * -1)
self.view.layoutIfNeeded()
})
}

I should also mention that when you say (180.0 * CGFloat(π)) / 180 - You're just saying π, because 180/180 is one.

It looks to me like you're trying to do two different rotations at once? I'm a little confused by that. One thing I've found that really helps is to track the view's axis orientation if you're doing multiple rotations both clockwise and counter-clockwise. Do something like create a property called viewAngle

var viewAngle: CGFloat = 0    // Right-side up to start
let π = CGFloat.pi // Swift allows special characters hold alt+p to use this, it allows for cleaner code

func rotate(by angle: CGFloat) {

for i in 0 ..< 4 {
UIView.animate(withDuration: 0.125, delay: 0.125 * Double(i), options: .curveLinear, animations: {

self.viewAngle += angle/4
self.rotateView.transform = CGAffineTransform(rotationAngle: self.viewAngle)
self.view.layoutIfNeeded()
}, completion: nil)
}

}

If you want to rotate right pass in a positive angle (i.e. π, π/2, π/4, 2*π), or if left pass in a negative angle.

Rotate right by 180 degrees:

rotate(by: π)

Rotate left by 180 degrees:

rotate(by: -π)

EDIT: As you were mentioning, which I didn't see until I tried for myself, when you plug in π or 2π whether it's negative or positive, it just rotates clockwise. To get around this I had to make it rotate by -π/4 increments. So to do a full-circle rotate counter clockwise I did a loop of -π/4 animations if that makes sense. It works, but I feel like there should be an easier way to do this. I would appreciate anyone else chiming in.

Bendable arm rotation in SpriteKit - rotate at point

You want to use an SKPhysicsJointPin

Here are the available Sprite-Kit joints :https://developer.apple.com/reference/spritekit/skphysicsjoint

Sample Image

You add or remove joints using the physics world. When you create a joint, the points that connect the joint are always specified in the scene’s coordinate system. This may require you to first convert from node coordinates to scene coordinates before creating a joint.

To use a physics joint in your game, follow these steps:

  1. Create two physics bodies.
  2. Attach the physics bodies to a pair of SKNode objects in the scene.
  3. Create a joint object using one of the subclasses listed in the
    diagram above. If necessary, configure the joint object’s properties
    to define how the joint should operate.
  4. Retrieve the scene’s SKPhysicsWorld object.
  5. Call the physics world’s add(_:) method.

The following code pins two nodes, upperArm and lowerArm, together using a 'elbow' implemented via an , SKPhysicsJointPin joint. We calculate the 'elbow' position by taking the middle of the area where upperArm and lowerArm overlap - this is pretty crude and should be improved.

let elbowArea = lowerArm.frame.intersection(upperArm.frame)

let elbowPosition = CGPoint(x: elbowArea.midX, y: elbowArea.midY)

let elbowJoint = SKPhysicsJointPin.joint(withBodyA: upperArm.physicsBody!,
bodyB: lowerArm.physicsBody!,
anchor: elbowPosition)
scene.physicsWorld.add(elbowJoint)

You should also set rotation limits on the pin joint (elbow) via the joint's properties:

var shouldEnableLimits: Bool A Boolean value that indicates whether
the pin joint’s rotation is limited to a specific range of values.

var lowerAngleLimit: CGFloat The smallest angle allowed for the pin
joint, in radians.

var upperAngleLimit: CGFloat The largest angle allowed for the pin
joint, in radians.

Rotate an UIView with CGAffineTransformMakeRotation

When you add the transform to myView you are not applying a transform to it's existing transformed state, but rather overwriting the transform. This means that when you set transform = CGAffineTransformMakeRotation(M_PI/2) for the second time, you are not actually changing the transform at all.

EDIT:
As Rob pointed out, there is no need to use the property rotationInRadians when the rotation value is already stored in the transform. Instead you can simply modify the existing transform like this:

[UIView animateWithDuration:0.2 animations:^{
_myView.transform = CGAffineTransformRotate(_myView.transform, M_PI/2);
}];

Below is the not ideal solution that I previously presented.

You will need a variable that keeps track of your rotation. You will increment this variable by your desired amount each time you want to rotate, then create a new transform based on the new value of the variable.

Make a property to do this:

@property (nonatomic, assign) CGFloat rotationInRadians;

Initialize it to 0 in your init method. Then modify your rotateView method to increment rotationInRadians and apply the new transform.

- (IBAction)rotateView:(id)sender {
self.rotationInRadians += M_PI/2;
[UIView animateWithDuration:0.2 animations:^{
_myView.transform = CGAffineTransformMakeRotation(self.rotationInRadians);
}];
}

Converting Radians to Degrees using UIRotationGestureRecognizer?

Your main issue is that you are resetting the gesture's rotation property. You should not do that. From the documentation:

The rotation value is a single value that varies over time. It is not the delta value from the last time that the rotation was reported. Apply the rotation value to the state of the view when the gesture is first recognized—do not concatenate the value each time the handler is called.

So remove the sender.rotation = 0 line.

As a result of that, you need to fix how you apply the transform to the image view.

Replace:

imageView.transform = imageView.transform.rotated(by: sender.rotation)

with:

imageView.transform = CGAffineTransform(rotatationAngle: sender.rotation)

That applies the full rotation instead of trying to increment the current rotation.



Related Topics



Leave a reply



Submit