Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rotate image with animation and read rotation afterwards

I'm implementing a wheel of fortune with a CAKeyframeAnimation and try to read the result after the animation has stopped. But here I do not get deterministic results. Is it not possible to read the rotation after the animation has stopped?

Here my animation code:

let anim = CAKeyframeAnimation(keyPath: "transform.rotation.z")
        anim.duration = max(strength / 2, 1.0)
        anim.isCumulative = true
        anim.values = [NSNumber(value: Float(p)), Float(circleRotationOffset)]
        anim.keyTimes = [NSNumber(value: Float(0)),NSNumber(value: Float(1.0))]
        anim.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
        anim.isRemovedOnCompletion = false
        anim.fillMode = kCAFillModeForwards
        anim.delegate = self
        wheelImage.layer.removeAllAnimations()
        wheelImage.layer.add(anim, forKey: "rotate")

And here how I read the rotation:

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {

        readImageOrientation()
    }


    func readImageOrientation(){

        let radians:Double = atan2( Double(wheelImage.transform.b), Double(wheelImage.transform.a))
        let degrees:CGFloat = CGFloat(radians) * (CGFloat(180) / CGFloat(M_PI))

        sectionForDegrees(degree: degrees)
    }

For the sake of completeness here my complete class.

class WOFView: UIView, CAAnimationDelegate {

@IBOutlet weak var wheelImage: UIImageView!
private var history = [Dictionary<String, Any>]()
private var rotation: CGFloat = 0
private var startAngle: CGFloat = 0
private var circleRotationOffset: CGFloat = 0


override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)

    if let touchPoint = touches.first?.location(in: self){

        if startAngle == 0{
            startAngle = atan2(self.frame.width - touchPoint.y, self.frame.height - touchPoint.x)
        }
        rotation = startAngle
        if !touch(touches.first!, isInLeftHalfOf: wheelImage) {
            rotation = -rotation
        }
        history.removeAll()
    }
}


override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesMoved(touches, with: event)

    guard let touchPoint = touches.first?.location(in: self) else {
        return
    }

    let dic = ["time" : NSNumber(value: CFAbsoluteTimeGetCurrent()),
               "point": NSValue(cgPoint: touchPoint),
               "rotation": NSNumber(value: Float(circleRotationOffset + rotation))]

    history.insert(dic, at: 0)
    if history.count == 3{
        history.removeLast()
    }

    rotation = atan2(self.frame.width - touchPoint.y, self.frame.height - touchPoint.x) - startAngle
    if !touch(touches.first!, isInLeftHalfOf: wheelImage) {
        rotation = -rotation
    }
    wheelImage.transform = CGAffineTransform(rotationAngle: circleRotationOffset + rotation)
    print("offset: \(circleRotationOffset)")
    readImageOrientation()
}


override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)

    guard let touchPoint = touches.first?.location(in: self) else {
        return
    }

    guard let lastObject = history.last else{
        return
    }

    guard let pointValue = lastObject["point"] as? CGPoint else{
        return
    }

    guard let timeValue = lastObject["time"] as? NSNumber else {
        return
    }

    guard let rotationValue = lastObject["rotation"] as? NSNumber else {
        return
    }

    let timeDif = CFAbsoluteTimeGetCurrent() - (timeValue.doubleValue)
    circleRotationOffset = circleRotationOffset + rotation
    let lastRotation = rotationValue.floatValue

    let dist = sqrt(((pointValue.x - touchPoint.x) * (pointValue.x - touchPoint.x)) +
        ((pointValue.y - touchPoint.y) * (pointValue.y - touchPoint.y)))

    let strength = max(Double(min(1.0, dist / 80.0)) * (timeDif / 0.25) * M_PI * 2, 0.3) * 30

    let p = circleRotationOffset
    let dif = circleRotationOffset - CGFloat(lastRotation)
    var inc = dif > 0

    if dif > 3 || dif < -3{
        inc = !inc
    }


    if (inc){
        circleRotationOffset += CGFloat(strength)
    }else{
        circleRotationOffset -= CGFloat(strength)
    }

    let anim = CAKeyframeAnimation(keyPath: "transform.rotation.z")
    anim.duration = max(strength / 2, 1.0)
    anim.isCumulative = true
    anim.values = [NSNumber(value: Float(p)), Float(circleRotationOffset)]
    anim.keyTimes = [NSNumber(value: Float(0)),NSNumber(value: Float(1.0))]
    anim.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
    anim.isRemovedOnCompletion = false
    anim.fillMode = kCAFillModeForwards
    anim.delegate = self
    wheelImage.layer.removeAllAnimations()
    wheelImage.layer.add(anim, forKey: "rotate")
}


func touch(_ touch:UITouch, isInLeftHalfOf view: UIView) -> Bool {
    let positionInView = touch.location(in: view)
    return positionInView.x < view.frame.midX
}

func touch(_ touch:UITouch, isInUpperHalfOf view: UIView) -> Bool {
    let positionInView = touch.location(in: view)
    return positionInView.y < view.frame.midY
}


func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {

    readImageOrientation()
}


func readImageOrientation(){

    let radians:Double = acos(Double(wheelImage.transform.a))
    let degrees:CGFloat = CGFloat(radians) * (CGFloat(180) / CGFloat(M_PI))

    sectionForDegrees(degree: degrees)
}

func sectionForDegrees(degree: CGFloat){

    var result = "not defined"

    switch degree{
    case 0 ... 90:
        result = "3 \(degree)"
    case 90.1...180:
        result = "2 \(degree)"
    case 181.1...270:
        result = "1 \(degree)"
    case 270.1...360:
        result = "4 \(degree)"

    default:
        result = "not defined: \(degree)"
    }

    print(result)
}

}

screenshot of the xib file

like image 544
netshark1000 Avatar asked Oct 18 '22 19:10

netshark1000


2 Answers

According to this Objective-C answer there are (at least) four ways to get the rotation:

let view = UIImageView()
view.transform = CGAffineTransform(rotationAngle: 0.02);

let x = view.value(forKeyPath: "layer.transform.rotation.z")
let a = acos(view.transform.a)
let b = asin(view.transform.b)
let c = atan2(view.transform.b, view.transform.a)

print(a)
print(b)
print(c)
print(x!)

Prints:

0.0200000000000011
0.02
0.02
0.02
like image 186
shallowThought Avatar answered Oct 21 '22 08:10

shallowThought


Original Answer (general thoughts on transformations)

I think you should use acos() instead of atan2(), based on how a three dimensional rotation matrix looks:

rotation matrix

In this Rz matrix we can see that transform.a and transform.b will be given as cosine theta. Ref CGAffineTransform documentation:

https://developer.apple.com/reference/coregraphics/cgaffinetransform

Without testing, I'm pretty sure a simple acos(Double(wheelImage.transform.a)) would be enough to give you your theta rotation. EDIT: This was bad advice, replacing atan2 with acos means you have to check if your answer is a positive or a negative cosine value, and is completely pointless. Another thing to remember is that the transform will also change if you apply any scaling to your wheel.

If I'm wrong, you can always use the strength value to calculate how long you are going to allow the wheel to spin and find the angle from that. This would be require you to learn how the kCAMediaTimingFunctionEaseOut works exactly, so I would instead suggest you to change the animation to first calculate an angle (that you save as a parameter) then apply this directly to the layer.transform instead of using the duration as the decisive factor.


Updated Answer

After seeing your complete code, I found out that you had only interpreted the angle wrong, here is a rewrite of your sectionForDegrees function that will give the correct section:

func sectionForDegrees(degree: CGFloat){

    var result = "not defined"

    switch degree{
    case 0.1 ... 90:
        result = "1 \(degree)"
    case 90.1...180:
        result = "4 \(degree)"
    case -90.1...0:
        result = "2 \(degree)"
    case -180...(-90):
        result = "3 \(degree)"

    default:
        result = "not defined: \(degree)"
    }

    print(result)
}
like image 22
Simen91 Avatar answered Oct 21 '22 07:10

Simen91