Time Warp in Animation

David Rönnqvist did a better job at describing CAMediaTiming recently: Controlling Animation Timing.


CAMediaTiming Protocol offers a small set of eight properties. But it is sufficient to do all kinds of time warps, if you know what exactly every property means and how to use it.

The most used and simple one is duration:

duration
Specifies the basic duration of the animation, in seconds.

Nothing needs further explanation here.

The duration you specify may differ from the real duration you experience, depending on the parent time space or speed:

speed
Specifies how time is mapped to receiver’s time space from the parent time space.

So an animation with duration of 1 second and speed of 1 is the same as an animation with duration of 2 seconds and speed of 2, when mapped to real time space.

The two repeating properties — repeatCount and repeatDuration — are as clear as they are named. So is autoreverses.

Finally, we are talking about the three interesting but not intuitive properties:

beginTime

beginTime
Specifies the begin time of the receiver in relation to its parent object, if applicable.

If an animation is in an animation group, beginTime is the offset from the beginning of its parent object — the animation group. So if beginTime of the animation is 5, it begins 5 seconds after the animation group begins.

If an animation is added directly to a layer, beginTime is still the offset from the beginning of its parent object — the layer. But since the beginning of a layer is in the past1, I can not simply set beginTime to 5 to delay the animation 5 seconds, because 5 seconds after the beginning of a layer is probably still a past time. What I usually really want is a delay relative to when the animation is added to the layer — denoted by addTime. So

animation.beginTime = addTime + delay;

In order to get addTime, I can use CACurrentMediaTime and convertTime:fromLayer::

addTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

If beginTime of the layer itself is to be set, addTime must be calculated after it is set because beginTime shifts the time space of the layer.

These things can be chained up:

CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval currentTimeInSuperLayer = [superLayer convertTime:currentTime fromLayer:nil];
layer.beginTime = currentTimeInSuperLayer + 2;
CFTimeInterval currentTimeInLayer = [layer convertTime:currentTimeInSuperLayer fromLayer:superLayer];
CFTimeInterval addTime = currentTimeInLayer;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.beginTime = addTime + 1;
group.animations = [NSArray arrayWithObject:anim];
group.duration = 2;
anim.beginTime = 0.5;
[layer addAnimation:group forKey:nil];

timeOffset

timeOffset
Specifies an additional time offset in active local time.

It’s not clear what it means from the words. But with a very simple example you’ll get it.

Supposing a 3-second animation with states s0, s1, s2, s3, wherein sn denotes the state at second n. The normal state sequence without timeOffset is:
s0s1s2s3

With timeOffset set to 1, the state sequence becomes: s1s2s3s0

With timeOffset set to 2:
s2s3s0s1

Shift and wrap around. That’s it.

fillMode

fillMode is really badly documented. I can figure out kCAFillModeRemoved and kCAFillModeForwards but have no idea what kCAFillModeBackwards and kCAFillModeBoth are about.

Fortunately, they turn out very straightforward if you see them in practice.

Supposing an animation in an animation group configured as follow:

anim.beginTime = 1;
anim.duration = 3;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObject:anim];
group.duration = 5;

Denote initial state of anim as si and final state as sf. Hence in group’s time space:
si = s1
sf = s4

Normally, there is no s0 or s5, because anim is not in effect during [0, 1) or (4, 5]. And it is what kCAFillModeRemoved means.

With kCAFillModeForwards, the state sequence of anim extends forward to the end of its parent time space, that is:
s1s2s3s4s5

But what should s5 be? I believe you can guess it.
s5 = sf = s4

After anim is completed, it remains. It is what filling forward means. Quite intuitive, if put clearly, isn’t it?

kCAFillModeBackwards is the mirror of kCAFillModeForwards, which we can easily tell from their names. Actually, it is.

If fillMode of anim is set to kCAFillModeBackwards, the state sequence extends backward to the beginning of its parent time space, that is:
s0s1s2s3s4
with
s0 = si = s1

Before anim begins, it appears. It is, indeed, filling backward.

At this time, kCAFillModeBoth is a nothing mysterious at all. It is just kCAFillModeForwards + kCAFillModeBackwards, filling both way:
s0s1s2s3s4s5
with
s0 = si = s1
s5 = sf = s4

To do you a further favor, I re-document fillMode below, in the way it should have been documented in the first place:

fillMode
Determines the receiver’s presentation during its inactive duration.

Discussion
The possible values are described in “Fill Modes”. The default is kCAFillModeRemoved.

Fill Modes
These constants determine how the timed object behaves in its inactive duration. They are used with the fillMode property.

NSString * const kCAFillModeRemoved;
NSString * const kCAFillModeForwards;
NSString * const kCAFillModeBackwards;
NSString * const kCAFillModeBoth;
NSString * const kCAFillModeFrozen;

Constants
kCAFillModeRemoved
The receiver does not appear until it begins and is removed from the presentation when it is completed.

kCAFillModeForwards
The receiver does not appear until it begins but remains visible in its final state when it is completed.

kCAFillModeBackwards
The receiver appears in its initial state before it begins but is removed from the presentation when it is completed.

kCAFillModeBoth
The receiver appears in its initial state before it begins and remains visible in its final state when it is completed.

That’s all. I think I’ve filled your mind quite full. You can get the sample code on github.


  1. I guess it is the time when it is added to a layer tree