Advanced Transitions, Targeting Individual Transition Edges

(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