The Rubber Ducks demo: CSS3/Webkit animations + touch events in MobileSafari
November 15, 2010
I’ve previously posted about CSS3 transitions. I now decided to make a more complete demo, while learning about a more advanced CSS animation technique, animation with keyframes.
See the Rubber Ducks demo. It probably only works on MobileSafari (iPad or iPhone).
If you don’t have MobileSafari, here’s what it looks like.
You put your finger on one of the green bars, and drag it to another green bar, to transfer rubber ducks from one box to another. As the transfer is in progress, you see the rubber ducks fly around with a neat animation.
You can use two fingers to rotate and zoom the boxes. Just put two fingers on a box and rotate/pinch. Just make sure that at least one finger is on the white/background part of the box. (This can be improved.)
As I say in the readme, the demo uses the following techniques:
- Touch events for starting/stopping actions
- Gesture events for rotating/scaling elements
- Delayed activation of an action to let the user drag through intermediate elements to reach the desired target element
- A simple state machine to manage the app state
- Detecting/remapping the desired target element from a touch location with elementFromPoint DOM API
- Webkit transitions with simple CSS rules
- Webkit animation with CSS keyframes
- Dynamic CSS animation keyframe insertion
- Programmatically inserting DOM elements and applying CSS animations to them at runtime
Some of these things are basic, and some of these are new to me and I had not done them before. Overall, I’m happy with the small amount of code there is to enable it all, it’s quite compact and for the most part feels nice. It gives you a lot of “bang per line of code,” much more than if you were to hand-roll all this or even build the same stuff in a more complex language in a native app.
RaphaelJS and performance
With Webkit, a lot of the legwork seems to be hardware-accelerated on devices, and this demo performs very well. It’s not without limits, though, and I did run into performance problems while poking a few things. For example, when you first put finger on a box, you’ll see a green border subtle glow/flashing, which I currently implemented with border style. I tried a similar one with box-shadow, and it worked fine on narrower boxes, but killed performance on wider boxes, as the devices don’t seem to be fast enough to keep recalculating dynamic box-shadows without visible slowdown.
Box-shadow aside, you’ll see that the performance is great in MobileSafari even when doing more complex operations, e.g all the ducks flying around on the screen and fading out at the end of their animation, which yields a fairly complex compositing operation.
The one thing that is in RaphaelJS, that is NOT there in Webkit/CSS as far as I can tell, is path-based animation. In RaphaelJS, I can construct all sorts of crazy Bezier paths and animate objects across them. In Webkit, I think I can only set points/locations, and all transitions are linear from pointA to pointB in a straight line, with no opportunity for curves. You can see in the demo that although the animation is pretty neat, it would be smoother with a curve.
Dynamic keyframe creation
This was one of my pain points when building the demo. I had to resort to a technique that I found on StackOverflow where you basically just keep inserting a CSS rule for each new animation. This works, but it’s not nice. The thread links to a Webkit commit where they demonstrate a technique to dynamically modify a keyframe animation without adding new CSS rules, but it’s more verbose than I’m interested in, I determined that the new rule insertion works better for me for now.
elementFromPoint and touch event model
Touch events work differently from mouse events. There is no mouseenter/mouseleave, for example—all touch events are passed to the original target. What I ended up doing was to bind a touchmove event to the parent container and track targets myself with elementFromPoint, which is a key API to power this kind of demo. I even don’t know how I would do it if elementFromPoint didn’t exist.
(Interestingly, jQuery event binding does not maintain the “touches” properties of touch events, so I have to use DOM addEventListener API instead of jQuery to bind the touch events.)
As ppk notes:
elementFromPoint() returns the topmost element located at the requested coordinates. / In a future version it should return an array of elements located at the coordinates, in (reversed?) order of z-index. Thus we’d get all the eligible elements, and it would be up to us to choose between them. Pretty please, browser vendors?
Since it’s probably not great to change an existing API, just add a new one like elementsFromPoint (note “s”), which returns an array.