15

In Cocoa/Touch, CAMediaTimingFunction represents four control points that specify a cubic bezier curve of a timing function. For an application I am writing I would like to be able to extract the result of said bezier curve at an arbitrary time t (0 -> 1). What is confusing me is that when I look up how to do this, the result is supposed to be a point as well, not a scalar:

B(t) = (1 - t) ^ 3 * P0 + 3 * (1 - t) ^ 2 * t * P1 + 3 * (1 - t) * t ^ 2 * P2 + t ^ 2 * P3

However, Apple's implementation results in a scalar value (you can see on this graph they plot x(t) vs t: http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Animation_Types_Timing/Articles/Timing.html#//apple_ref/doc/uid/TP40006670-SW1 )

So does Apple simply ignore the y coordinate of the result and only deal with the x? This seems strange because then you wouldn't need to pass in control points but rather control scalars as the y's wouldn't influence the result at all.

  • I think I could probably answer your question better if you could expand on the context of the question. Are you using CAMediaTimingFunction to animate something? If so, is it a keyframe animation? – jin Apr 11 '12 at 02:18

4 Answers4

13

CoreAnimation's CAMediaTimingFunction does what you want but doesn't expose getting 'y' for a given 'x' for versatile (animation) use but rather just feeds the solved values opaquely to the animation system under the hood.

I needed it myself so built a class with the interface and capabilities exactly like CAMediaTimingFunction but with the needed -valueForX: method; usage example:

RSTimingFunction *heavyEaseInTimingFunction = [RSTimingFunction timingFunctionWithControlPoint1:CGPointMake(0.8, 0.0) controlPoint2:CGPointMake(1.0, 1.0)];
CGFloat visualProgress = [heavyEaseInTimingFunction valueForX:progress];

You can create ease-in, ease-out, ease-in-ease-out or really any curves that can be described with a cubic Bézier curve. The implementation math is based on WebCore (WebKit), which is presumably what CoreAnimation is using under the hood too.

Enjoy, Raphael

Raphael Schaad
  • 1,659
  • 1
  • 17
  • 17
  • +1 Nice work :) - do you have any idea how the physics based iOS 7 animations work under the hood? (`animate...usingSpringWithDamping:..`) – Robert Apr 06 '14 at 18:22
  • 1
    They use a physics engine -- shared with SpriteKit --, which for these particular calculations likely uses the popular RK4 Integrator. The RK4 Integrator is explained really well here http://gafferongames.com/game-physics/integration-basics/ with a great code sample in C++. – Raphael Schaad Apr 17 '14 at 03:21
  • 1
    Not sure what you mean, @Andy -- https://gist.github.com/raphaelschaad/6739676 works well on any current version of iOS. – Raphael Schaad Aug 08 '14 at 05:10
  • @RaphaelSchaad I think I got crash on iOS 7 when I tested this. – pronebird Aug 08 '14 at 08:23
  • @Andy Do you have a stack trace so I could investigate? – Raphael Schaad Aug 18 '14 at 22:23
  • @RaphaelSchaad oh sorry man, it's been few months, I don't think I have it anymore. I may give it a shot again though. Btw, why don't you publish a cocoa pod, seems like useful thing! – pronebird Aug 18 '14 at 22:42
  • @RaphaelSchaad Core Animation actually is not using a physics engine. The spring animations are just using `CASpringAnimation`, which is a simple spring solver. UIKit Dynamics, SpriteKit, and SceneKit use actual real-time physics engines, but Core Animation just computes all the keyframe values in advance. – CIFilter Apr 01 '17 at 19:26
  • Shameless plug for what I did in 2012: https://github.com/gnustep/libs-quartzcore/blob/16950e2f8f0b55cb97b6916e724adf093103bc92/Source/CAMediaTimingFunction.m (I'll include this in my answer below too) – Ivan Vučica Jan 22 '20 at 20:21
6

It's unfortunate, but Core Animation doesn't expose its internal computational model for its animation timing. However, what has worked really well for me is to use Core Animation to do the work!

  1. Create a CALayer to serve as an evaluator
  2. Set its frame to ((0.0, 0.0), (1.0, 1.0))
  3. Set isHidden to true
  4. Set speed to 0.0
  5. Add this layer to some container layer
  6. When you want to evaluate any CAMediaTimingFunction, create a reference animation:

    let basicAnimation = CABasicAnimation(keyPath: "bounds.origin.x")
    basicAnimation.duration = 1.0
    basicAnimation.timingFunction = timingFunction
    basicAnimation.fromValue = 0.0
    basicAnimation.toValue = containerLayer.bounds.width
    
    referenceLayer.add(basicAnimation, forKey: "evaluatorAnimation")
    
  7. Set the reference layer's timeOffset to whatever normalized input value (i.e., between 0.0 and 1.0) you want to evaluate:

    referenceLayer.timeOffset = 0.3    // 30% into the animation
    
  8. Ask for the reference layer's presentation layer, and get its current bounds origin x value:

    if let presentationLayer = referenceLayer.presentation() as CALayer? {
        let evaluatedValue = presentationLayer.bounds.origin.x / containerLayer.bounds.width
    }
    

Basically, you're using Core Animation to run an animation for an invisible layer. But the layer's speed is 0.0, so it won't progress the animation at all. Using timeOffset, we can manually adjust the current position of the animation then get its presentation layer's x position. This represents the current perceived value of that property as driven by the animation.

It's a little unconventional, but there's nothing hacky about it. It's as faithful a representation of the output value of a CAMediaTimingFunction as you can get because Core Animation is actually using it.

The only thing to be aware of is that presentation layers are close approximations of the values presented on screen. Core Animation makes no guarantees as to their accuracy, but in all my years of using Core Animation, I've never seen it be inaccurate. Still, if your application requires absolute accuracy, it's possible this technique might not be the best.

CIFilter
  • 8,647
  • 4
  • 46
  • 66
  • Simple but clever. Love it. – mattsven Dec 24 '18 at 14:15
  • 1
    One issue with this approach: the presentation layer is updated asynchronously, so depending on when you stop/start asking for values from it, you may get unexpected results. – mattsven Dec 24 '18 at 17:04
  • Yes definitely. This solution isn’t perfect, and you are relying on a bunch of different asynchronous communication across processes (your app and the render server). So you might get weird results in some instances. – CIFilter Feb 11 '19 at 02:17
5

Note: I am not an expert on CoreAnimation. This is just my understanding from reading the docs you have linked.

Apple is mixing coordinate systems here, which is creating some confusion.

x(t) in the example plots represents a scalar progression along some path, not a physical coordinate.

The control points used in CAMediaTimingFunction are used to describe this progression, not a geometric points. To add to the confusion, x in the control points actually maps to t on the plots and y in the control points to x(t) on the plots.

To take the plot for kCAMediaTimingFunctionEaseInEaseOut as an example, this plot would be described roughly by control points (0,0), (0.5,0), (0.5,1), (1,1).

jin
  • 735
  • 3
  • 10
  • You are indeed correct that the problem was that t means x and x(t) means y. The question remains as to how to find y given some x however (right now it seems to involve solving the cubic equation which is a pretty difficult task). – Francisco Ryan Tolmasky I Apr 11 '12 at 03:55
  • 2
    @Fransisco You marked this as the answer, but as you state in the comment, the real question is still to get y for a given x. Did you ever solve this? Cheers. – epologee May 16 '12 at 21:38
  • 1
    @jin: Actually, I just used the CAMediaTimingFunction method getControlPointAtIndex to interrogate the control points of a kCAMediaTimingFunctionEaseInEaseOut timing function, and the control points are (.42, 0) and (.58, 1). (The first and last points are always 0,0 and 1,1) – Duncan C Mar 15 '14 at 20:45
1

Best hint would probably be UnitBezier.h in WebKit's source code. In fact, I guess Apple uses the same code inside Core Animation.

More lengthy, they use calculate parameter t (which has nothing to do with time) given a value of x which actually contains the time value. Then they use Newton-Raphson method (additional explanation with examples). Since it can fail, they iterate a bit more to using bisection ("divide and conquer"). See this in solveCurveX() method.

After getting parameter t (which is necessary given that cubic Beziers are parametrically defined), they simply use it to calculate y. See this in solve() method.

By the way, a big fan of Cappuccino, and still hoping for a release of Atlas :-)


Update January 2020: I did this back in 2012 in my implementation of the Core Animation APIs. Feel free to refer to it.

Ivan Vučica
  • 9,529
  • 9
  • 60
  • 111