- From: Tab Atkins Jr. <jackalmage@gmail.com>
- Date: Fri, 1 Apr 2011 15:46:27 -0700
- To: www-style list <www-style@w3.org>
(This is probably separate from the other Advanced Animations thread, though I've only lightly skimmed that thread so far. It took a supreme act of will not to start writing this while I was on vacation, once I saw that thread.) (Also, apologies in advance for this being quite a long email. I'll use headings and whatnot so it's nicely structured and easy to skip around in.) I and others on the Chrome team have been thinking about the limitations of current Transitions/Animations, and how to remove some of the restrictions to get a more general powerful animation system comparable to what app developers are often used to in other platforms. The Problem =========== All of us at Chrome, when we first started using T/A, thought of Transitions as something simple which applies when an element *leaves* a state, and Animations as something complex which applies when an element *enters* a state. This confused us terribly, but luckily it was just a result of our mental model being very wrong. Unfortunately, it's easy to construct this incorrect mental model, especially when you try to actually *use* Animations for doing complex state transitions. The correct mental model is that the 'transition' property sets up a very simple animation (just a fade from the start value to the end value of a single property) that fires when an element moves between two states, where a "state" is a particular value of a given property. As long as 'transition' is specified for an element, any state-change in the specified properties will fire the simple animation. It's the state change which is important for the triggering; 'transition' itself should be specified on the element at all times if you want easily-predictable behavior. The 'animation' property, on the other hand, simply runs an animation constantly while an element is in a state, where a "state" is tied to the value of the 'animation' property. It can *appear* that the animation is fired by the element entering some state, but that's an illusion; there is no state-changing behavior to speak of, so trying to use 'animation' to do complex state transitions is a direct route to confusion and pain. The Current Model ================= Allow me to simply restate the current model that underlies the T/A specs, so it's clear what parts I'm modifying/extending in my proposal. First, there are animations. These are defined in two ways: 1. Keyframe animations. These can affect multiple properties at the same time, but the property values that they're animating are defined at creation time; you can't alter them on the fly based on current CSS values. 2. Transitions. These are anonymou animations that simply animate a single property from a starting value to an ending value. Then, there are ways to play the animations. Again, these are defined in two ways: 1. Keyframe animations are played constantly as long as they're specified in the 'animation' property for an element. (If an animation has a finite iteration count, it still "plays" forever, it just doesn't do anything once its iterations are done.) 2. The 'transition' property sets up transition animations on an element, which play whenever the property they're defined over changes its value. Use-Cases ========= (This section will be expanded as time goes on, but I have a few to put down off the top of my head right now.) 1. The jQuery slideIn/slideOut animations. The mental model here is that these are fancy ways of transitioning to/from "display:none". Right now, you have to do some complex and fragile hacking of classes and states to make this more-or-less work with the 'animation' keyword; you can't simple say "any time this element goes to display:none, play the slideOut animation for it". 2. Hovering/activating buttons and other complex widget-type things. There are four distinct interaction states here (normal, over, down, and out) with 7 distinct edges that users can traverse to switch between states (normal<=>over, over<=>down, down<=>out, out=>normal). In apps that pay a *lot* of attention to these things, many or all of these edges may have subtly distinct animations to maximally capture or avoid attention, as desired. For example, going from normal=>over may play over .2s, but going back from over=>normal may play in half the time. These animations may be complex (requiring keyframes) or trivial (just needing transitions), but you can't key any of them off of a single property, as no single property uniquely describes the state in general. The Proposal ============ I propose an extension to the T/A specs that allows authors to attach arbitrary keyframe-defined animations to arbitrary edges in the property-value graph, such that it's simple to have distinct complex animations run for every distinct pair of starting and ending values of a particular property. This proposal is in several, largely distinct, pieces. Relative Keyframes ------------------ Right now, keyframes animations are absolute, in that you must fill in all values at the time of defining the keyframe. This is very limiting - it means you can't create a general "pulse" animation that makes an element get slightly wider and narrower, for example; instead, you have to create individual pulse animations for every box-size you want to animate, and you have to remember to change the animation if the size of the box is changed. If the box size is dynamic, you're screwed. To fix this, I propose adding three new functions that are only valid inside of keyframe rules - from(), to(), and prev(). For keyframe animations set on a transition (see below for details on this), the from() and to() functions in a property value resolve to the starting and ending values of that property, respectively. For keyframe animations played via the 'animation' property, both functions resolve to the current value of the property. The prev() function is used to make dynamic stepping easier - if the property that prev() is used in was set in an earlier keyframe, prev() resolves to that value; otherwise, it's identical to from(). For example, we can use this to define an animation over 'opacity' that is identical to the anonymous animations created by using 'transition' over opacity: @keyframes transition-opacity { 0% { opacity: from(); } 100% { opacity: to(); } } We can also use this in conjunction with calc() to make the general "pulse" animation mentioned previously: @keyframes general-pulse { 0% { width: from(); } 25% { width: calc( from() + 5% ); } 50% { width: from(); } 75% { width: calc( from() - 5% ); } 100% { width: from(); } } Setting Animations on Transition Edges -------------------------------------- Right now, you can only set trivial animations on transition edges, and they must be the same animation on *all* edges of a particular property-value graph. Again, this is limiting, for reasons stated above in the button use-case. To fix this, I propose a new @-rule that allows an author to specify an animation on a particular transition-edge for an element. The syntax is as follows: @transition [selector] { over: [property name]; from: [property value]; to: [property value]; animation: [animation value]; } This can be used, for example, to solve the slideIn/slideOut case: @transition .menu-item { over: display; from: block; to: none; animation: slideOut .2s; } @transition .menu-item { over: display; from: none; to: block; animations: slideIn .2s; } (Where slideIn and slideOut are keyframe animations that specify roughly what the current equivalent jQuery animations do manually, probably using the relative-value functions.) This sets up two transition on all ".menu-item" elements: one for when the 'display' value changes from 'block' to 'none', and one for the reverse. Any other transition between 'display' values will take effect immediately, as normal, and other elements won't have transitions at all (unless they're specified elsewhere). (In case of conflict, @transitions are cascaded the same way as a declaration block, according to their selector.) The 'from' and 'to' property can be omitted, or specified with a (currently undecided) magic value, to indicate that they should apply all values. For example, one could omit the 'block' values from the previous example to make the slideIn/slideOut animations apply any time an element is changed to/from display:none, no matter what the source/dest display value is. The 'transition' property can now be viewed as a magical way of specifying an @transition rule. Something like "foo { transition: opacity .2s; }" could instead be written as: @transition foo { over: opacity; animation: transition-opacity .2s; } (We could also imagine an animation-generating function that generates these trivial transitions for us, so that rather than having to define a "transition-opacity" animation I could simply say "animation: transition(opacity) .2s" and get the same effect automatically. I'm not sure if this case is worth simplifying, but it might be.) (Omitting the 'from' or 'to' changes the cascading behavior slightly - assuming specificity is the same otherwise, a rule that omits 'from' or 'to' loses to one that does. This lets you set up "default" transitions that omit one or both of the endpoints, and then specify more specific transitions between particular states.) (The interaction of the 'transition' property and @transitions specified on the same element should be pretty simple - 'transition' is treated like it creates an @transition like the above, but it always loses conflicts with explicit @transition rules.) (This proposal allows you to create a sparse graph of transitions. If you define a transition from A=>B, and from B=>C, what happens when the user does something that just changes the value from A to C directly? Should you ignore it, because there's no direct edge, or should you pathfind across the graph and select the shortest series of animations that will get you there? How do you define shortest - lowest number of animations, or lowest total duration?) Running an Animation on Arbitrary State Changes ----------------------------------------------- Even with the previous addition, we still can't easily solve the Button use-case, because there isn't, in general, a single property which represents the four states with unique values, such that we can key some @transition rules off of it. We can possibly hack this in by using some property that we very barely alter (like changing the text color from #000000 to #000001 to represent going from normal=>hover), but that's nasty. To fix this, I propose that we add a set of user-extensible properties to CSS that can take arbitrary values. These will have absolutely no effect on any type of rendering; they are to be used solely for keying transitions off of. (They are roughly analogous to the user-extensible set of data-* attributes in HTML5, intended for storing private app-specific data in a well-known location.) They would be used like so: button { state-interaction: normal; } button:hover { state-interaction: over; } button:hover:active { state-interaction: down; } button:active { state-interaction: out; } @transition button { over: state-interaction; from: normal; to: over; ... } ...6 more transitions... Any property starting with the "state-" prefix is guaranteed to be ignored for rendering purposes, but recognized as a valid property by the browser. The user can use anything after the prefix (subject to the normal property-name restrictions). state-* properties never inherit, and their computed and used values are always the same as their specified values. I'm not sure if we want/need any restrictions on their value; we can probably get by fine on saying that their value must be a single arbitrary keyword, but maybe it's useful to be able to specify more, like a keyword+int combo? ### An Aside About This Approach ### Internally, there was a lot of debate over what the proper state-carrying mechanism was. A lot of people thought that Selectors was the appropriate way to do this, but I feel strongly that this is wrong. An element can match multiple selectors at once - does this mean it's in multiple states at once? For example, the most natural way to define the four button states is as I have done above. If we want to be precise, though, we need to define it instead as: button:not(:hover):not(:active) { ... } button:hover:not(:active) { ... } button:hover:active { ... } button:not(:hover):active { ... } That's much more confusing and more difficult to write; in more general cases when your state axis could be defined by 3, 4, or 8 classes (rather than just two independent states), your selectors could get enormous and unwieldy! Even if the author pays attention and does this right, there's no way to *verify* that it's correct, and that you'll never have overlapping states. Using a property, though, does give us this guarantee - a property can only have a single value at a given time. It also lets us write simple selectors and hand value resolution over to the normal cascade, like I do with the simple button selectors. Finally, this makes arbitrary state changes work identically to normal property changes, meaning that we can avoid having to cart around two different mental models of how to define transitions work. Unaddressed Use-Cases ===================== There are several use-cases I haven't addressed in this email. Some have already gotten some thought from us, but I'd like to see how the WG feels about the simpler parts of the proposal first. Some are simply hard problems, and we don't yet know how to solve them. Control Over Mid-Animation Interruption --------------------------------------- If an animation is interrupted midway through, there are a lot of options for what to do. Which one is appropriate can even vary based on what the new endpoint is. For example, if you're currently animating from A to B, and then the user does something to make the value change back to A, you may want to simply reverse everything that has been done so far. This may not always be true, though - say you have a graphic of a train on a loop of track, where the train starts at the top and moves down to the bottom of the loop when you hover. If the user just quickly hovers and unhovers, you may want the train to continue forward and finish the loop back, rather than reversing quickly. When you interrupt an A=>B transition by moving back to A, the reasonable options seem to be (a) reverse the transition, or (b) continue the transition to B, then pathfind a new transition from B=>A. When you interrupt an A=>B transition by moving to C, the reasonable options seem to be (a) continue the transition to B, then pathfind a new transition from B=>C, or (b) automatically generate a new transition from the interruption point to C, subbing in some default animation values. On a related topic, when you interrupt a transition and change to a new strategy, you may want a sense of "momentum", where it takes a moment to switch from its previous direction to the new. This is basically just a transition over transitions! Transitions Covering Multiple Elements -------------------------------------- For example, I've seen game UIs before where the active menu item has a little bobbing light thing that flits around when you change the active item. I have *no* idea how to solve this. We've put a few hours of thought into it, but haven't yet come up with something I'm too happy with. I can present it later, though, for brainstorming purposes. Scripted Control Over Transitions --------------------------------- Dean Jackson is doing some work on this already in Webkit. I'm *greatly* interested in this subject, and I need to compile my list of requirements; I may just wait until Dean proposes some of the stuff he's experimenting with. In any case, not touching this right now. ~TJ
Received on Friday, 1 April 2011 22:47:20 UTC