Saturday, April 05, 2008

On Apple's CSS animation proposal

Apple recently published new proposals for CSS transitions and animations. Having spent some time reviewing approaches to animation on the web, I conclude that their animation proposal has serious shortcomings, and identify a better approach.

Animation - controlling the evolution of styles like position, colour, size, fonts, and layout - has always been a crucial gap for the open web. That's why it's been a key selling point for plug-ins such as Flash, albeit in a proprietary way that doesn't integrate well with the rest of the page. Animation is a good thing and should be brought to the web asap.

For more than ten years now, the W3C's answer to animation has been SMIL. But SMIL has a fundamental problem - it can only animate XML. On the web, you don't want to animate content - you want to animate style. Style is not stored as XML, or even as markup - it's stored as CSS. Finally, Apple has overcome the inertia with CSS animation. Now there is a chance to shape the way it works - hopefully this review will play a role!

There are two fundamentally different ways to evolve style - transitions and animations. In transitions, you don't know in advance what the before and after styles are, you just want to control how quickly the transition takes place for each style property. For example, you could say that background-color always takes two seconds to change, rather than being instantaneous as normal. In animations, you control both the before and after styles, plus the path between them.

Transitions

Apple's model for transitions is clear and straightforward - you simply apply a transition rule to the relevant CSS properties - for example, perhaps there is a delay of two seconds whenever the div's opacity changes:
div {
   transition-property: opacity;
   transition-duration: 2s;
   transition-timing-function: linear;
}
Transitions enable a huge number of simple effects, from context menus that slide out on mouse over to page sections that fade out when closed.

I like this model because it's simple and orthogonal to all the other styles (you can't set the actual property values, only their timing), yet gives them even more power. Also, the new transition styles follow the proper cascading rules as they are applied through the DOM.

Apple's Animations

Apple takes a very similar approach with animations. Using the same opacity example:
div {
   animation-name: div-opacity;
   animation-duration: 2s;
   animation-iteration-count: 1;
}

@keyframes 'div-opacity' {
  from {
    opacity: 0%;
    animation-timing-function: linear;
  }
  to {
    opacity: 100%;
  }
}
Unlike transitions, animations set the exact values over time of the opacity style, using keyframes. This is where the problems arise.

The first issue is orthogonality. Keyframes provide a new way to set the div's opacity, away from its normal position (under the div selector). This adds unnecessary confusion to parsing and understanding the CSS document - there are now two ways to set a style. It also requires several new CSSOM interfaces to control keyframes via script.

As a result, keyframes have a much bigger issue - they don't cascade. Cascading is one of the most important characteristics of stylesheets - it's the C in CSS. Cascading sets a series of priorities for when to apply style rules, based on the DOM and where they are applied. Because keyframes are a totally separate part of CSS, cascading can't work its magic.

For example, what happens if opacity was set in both the div selector and the keyframe? You could set an arbitrary rule to give one location priority, but it would be just that - arbitrary. And how does opacity apply to any elements inside the div? Apple have proposed that keyframes don't cascade. But this removes much of the power of CSS.

A better approach to animation

There's a better approach to animation that respects both the orthogonality and cascading principles. I also think it's simpler - it certainly requires fewer lines of code. See an example below that does exactly the same as the animation example above:
div {
   opacity: calc(t / 2s * 100%);
}
There are two key elements to the solution:
  • The CSS3 calc function, which enables simple mathematics like multiplication and division.
  • The new standard variable t, which measures elapsed time in seconds, starting at t=0 when the style is first applied to the element
In the example above, opacity would start at t=0 with a value of (0s / 2s) * 100%, which is 0%. After exactly two seconds, opacity would have the value (2s / 2s) * 100%, which is 100%.

Notice that since t is measured in seconds, we need to divide by a time unit (in this case 2s) in order to get the units right. I've also multiplied by 100% to return a percentage unit accepted by opacity.

The benefits of this inline approach are that it maintains both orthogonality and cascading rules - in fact, animated styles cascade in exactly the same way as static ones. It's also easier (and much shorter) to read, and requires no additional CSSOM interfaces.

To provide the complete picture you need additional animation functions, plus a few discretionary parameters. Following Apple's approach, I recommend the following:

  • animate-ease(time, iterationcount=1, direction=normal)
  • animate-linear(time, iterationcount=1, direction=normal)
  • animate-ease-in(time, iterationcount=1, direction=normal)
  • animate-ease-out(time, iterationcount=1, direction=normal)
  • animate-ease-in-out(time, iterationcount=1, direction=normal)
In addition, the following functions provide more control
  • animate-step(time) which is the step function, returning zero when time<0s id="dh-l">
  • animate-keyframes(time0 value0, time1 value1, time2 value2, ...), which returns a curve smoothly connecting the points via a bezier function.This negates the need for a separate cubic bezier function.
For example, the following styles apply the same effect as above, but eased-in and stepped after 1s rather than linear:
div.ease-in {
   opacity: calc(animate-ease(t / 2s) * 100%);
}
div.step {
   opacity: calc(animate-step(t / 1s - 1s) * 100%);
}
The following style iterates linearly every second four times in a row, alternating directions each time:
div.iterate {
   opacity: calc(animate-linear(t / 1s, 4, alternate) * 100%);
}
The following style illustrates more complicated animations, by moving an image downwards with uniform acceleration:
div.gravity {
   top: calc(t*t / (1s*1s) * 1px);
}

Synchronisation

Under this model, separate animations are implicitly synchronised. For example, consider the following animation:
div.projectile {
   top: calc(t*t / (1s*1s) * 1px);
   left: calc(t / 1s * 1px)
}
The instant a div element is given the projectile class, both animations will be set to t=0, and hence will be syncronised together.

On the other hand, the following animations will not be automatically synchronised:

<style>
div.moveright {
   left: calc(t / 1s * 1px);
}
div.movedown {
   top: calc(t / 1s * 1px);
}
</style>
<script>
var div1 = document.getElementsByTagName("div")[0];
div1.className = div1.className + " moveright";
div1.className = div1.className + " movedown";
</script>
The classes have been set at slightly different times, one after the other, and therefore the animations will begin at slightly different times as well.

Conclusion

Animation has the potential to turbo-charge style on the web. It's important that it's done in a way that enables the full power of CSS, including the principles of orthogonality and cascading. Apple's transitions model meets these principles, but their animations model does not, so I have proposed a replacement.

1 comment:

andrewfedoniouk said...

In my engine I have implemented so called CSSS! that is CSS Script - event/procedural extension to the static CSS declarations:
http://www.terrainformatica.com/htmlayout/csss!.whtm

Animations are timed sequences that are, indeed, better defined by function means.

E.g. in CSSS! your idea will expressed as:

#my-animated-element
{
top: 0;
active-on!: self.start-animation(0.5s);
animation-step!: self::top = ( animation-time() * animation-time() ) / (1s * 1s) * 1px,
return 0.05s;
}