5

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

netshark1000
  • 7,245
  • 9
  • 59
  • 116
  • Possible duplicate of [iphone sdk CGAffineTransform getting the angle of rotation of an object](http://stackoverflow.com/questions/2051811/iphone-sdk-cgaffinetransform-getting-the-angle-of-rotation-of-an-object) – shallowThought Jan 13 '17 at 12:37

2 Answers2

2

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
Community
  • 1
  • 1
shallowThought
  • 19,212
  • 9
  • 65
  • 112
1

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)
}
Simen91
  • 73
  • 7
  • I added my complete class to give you more context. How would you apply the rotation including the strength? Would you also use acos to compute the rotation? – netshark1000 Jan 13 '17 at 12:47
  • Could you provide a picture of your wheel? I can't seem to understand where you are calculating your `startAngle` from – Simen91 Jan 13 '17 at 13:55
  • Sure. Currently I'm using this for development and get a new one from the designer later: http://orig15.deviantart.net/8144/f/2013/200/3/7/wheel_of_fortune_deluxe_edition__2006__by_wheelgenius-d6dx9l4.png – netshark1000 Jan 13 '17 at 13:59
  • I'm sorry, I mean how you've set up your ViewController, a screenshot of the simulator would do – Simen91 Jan 13 '17 at 14:00
  • I added a screenshot if the xib-file. I then add a UIView to my controller of type WOF-control. – netshark1000 Jan 13 '17 at 14:05
  • Ok, I must correct my original post, because the advice to switch `atan2` with `acos`was bad. That removes results in 3rd and 4th quadrant, and was a blip from my side. However, your code seems to be working fine for me, the problem was just that you had interpreted the angles wrong in your switch/case! I'll update my answer with the fix – Simen91 Jan 13 '17 at 14:49
  • Thanks for your help! I'm currently trying to figure it out, because the results are not deterministic. Do you have a completely working example? Currently I'm following the approach to tell the animation where it should stop by create a random number. anim.values = [NSNumber(value: Float(p)), NSNumber(value:randomNumber)] – netshark1000 Jan 13 '17 at 15:48
  • What do you mean the results are not deterministic? English is not my native language, so I might me misunderstanding what you need help for. Didn't my updated switch case give you the correct section? – Simen91 Jan 13 '17 at 15:53
  • Sometimes it is correct and sometimes not. I haven't figured out what is causing this. Is it always correct on your side? – netshark1000 Jan 13 '17 at 17:43