A
good-looking application must provide user with visual
feedback. Users must always know that an order (a click,
a tap or whatever) is well received and understood by the application and animations
are a great tool to do so.
The
new HTML 5 specification (to be honest, I should say "the new CSS 3
specification") introduces a great tool to handle simple animations:
the transitions.
According
to "CSS Transitions Module Level 3" specification on W3C site,
CSS3 Transitions allows property changes in CSS values to
occur smoothly over a specified duration.
The
aim of this article will be to first describe the concept of transitions and
then to see how CSS3 Transitions works and how we can handle browsers that
don’t support the feature:
- CSS3 Transitions
- Putting it all together
- Transitions without CSS3 Transitions
- Conclusion
- Going further
In
addition, I suggest you to read the "Introduction
to CSS3 Animations" (by David Rousset) which is an
excellent companion for this article.
To see
how CSS3 Transitions can be used, I developed is a sample of a game which uses
CSS3 Transitions to animate cells of a puzzle (and which will fallback to
JavaScript if your browser doesn’t support CSS3 Transitions):
CSS3 Transitions
Introduction
At the
beginning, the W3C CSS workgroup resisted adding transitions to CSS arguing
that transitions are not really style properties. But eventually designers and
developers managed to convince them that transitions is about dynamic styles
and can take place in a CSS file.
According
to the W3C site, CSS3 Transitions are able to animate the following types of
properties: (click here to show them)
- color:
interpolated via red, green, blue and alpha components (treating each as a
number, see below)
- length:
interpolated as real numbers.
- percentage:
interpolated as real numbers.
- integer:
interpolated via discrete steps (whole numbers). The interpolation happens in
real number space and is converted to an integer using floor().
- number:
interpolated as real (floating point) numbers.
- transform
list: see CSS Transforms specification: http://www.w3.org/TR/css3-2d-transforms/
- rectangle:
interpolated via the x, y, width and height components (treating each as a number).
- visibility:
interpolated via a discrete step. The interpolation happens in real number
space between 0 and 1, where 1 is "visible" and all other values are
"hidden".
- shadow:
interpolated via the color, x, y and blur components (treating them as color
and numbers where appropriate). In the case where there are lists of shadows,
the shorter list is padded at the end with shadows whose color is transparent
and all lengths (x, y, blur) are 0.
- gradient:
interpolated via the positions and colors of each stop. They must have the same
type (radial or linear) and same number of stops in order to be animated.
- paint
server (SVG): interpolation is only supported between: gradient to
gradient and color to color. They then work as above.
- space-separated
list of above: If the lists have the same number of items, each item in the
list is interpolated using the rules above. Otherwise, no interpolation.
- a
shorthand property: If all the parts of a shorthand can be
animated, then interpolation is performed as if each property was individually
specified.
And
the following properties must be supported for transitions:(click here to show
them)
- background-color
(color)
- background-image
(only gradients)
- background-position
(percentage and length)
- border-bottom-color
(color)
- border-bottom-width
(length)
- border-color
(color)
- border-left-color
(color)
- border-left-width
(length)
- border-right-color
(color)
- border-right-width
(length)
- border-spacing
(length)
- border-top-color
(color)
- border-top-width
(length)
- border-width
(length)
- bottom
(length and percentage)
- color
(color)
- crop
(rectangle)
- font-size
(length and percentage)
- font-weight
(number)
- grid-*
(various)
- height
(length and percentage)
- left
(length and percentage)
- letter-spacing
(length)
- line-height
(number, length and percentage)
- margin-bottom
(length)
- margin-left
(length)
- margin-right
(length)
- margin-top
(length)
- max-height
(length and percentage)
- max-width
(length and percentage)
- min-height
(length and percentage)
- min-width
(length and percentage)
- opacity
(number)
- outline-color
(color)
- outline-offset
(integer)
- outline-width
(length)
- padding-bottom
(length)
- padding-left
(length)
- padding-right
(length)
- padding-top
(length)
- right
(length and percentage)
- text-indent
(length and percentage)
- text-shadow
(shadow)
- top
(length and percentage)
- vertical-align
(keywords, length and percentage)
- visibility
(visibility)
- width
(length and percentage)
- word-spacing
(length and percentage)
- z-index
(integer)
- zoom
(number)
SVG
The
properties of SVG objects are animatable when they are defined as animatable:true
in the SVG specification: http://www.w3.org/TR/SVG/struct.html.
Declarations
To
declare a transition in a CSS file, you just have to write the following code:
69.transition-property: all;
70.transition-duration: 0.5s;
71.transition-timing-function: ease;
72.transition-delay: 0s;
This
declaration defines that any update on any property will be done in 0.5s (and
not immediately so).
You
can also define your translations on a per property basis:
73.transition-property: opacity left top;
74.transition-duration: 0.5s 0.8s 0.1s;
75.transition-timing-function: ease linear ease;
76.transition-delay: 0s 0s 1s;
And
finally you can use the shorthand property "transition" to define all
you need in a single line:
77.transition: all 0.5s ease 0s;
In
this shorthand version you can precise as many properties as you want separated
by a comma:
78.transition: opacity 0.5s ease 0s, left 0.8s linear 0s;
The
transitions will be triggered when a property of the target object is updated.
The update can be done with JavaScript or using CSS3 by assign new
class to a tag.
For
example, using IE10 if you have the following CSS3 declaration:
79.-ms-transition-property: opacity left top;
80.-ms-transition-duration: 0.5s 0.8s 0.5s;
81.-ms-transition-timing-function: ease linear ease;
When
you update the opacity of your tag, the current value will be animated to the
new value over 0.5s with a ease timing function (which give a smooth
animation).
Non Linear
Transitions
The
"transition-timing-function" line defines that the transition will not be
linear but will use a timing function to produce a non linear animation.
Basically,
CSS3 transitions will use cubic bezier
curve to smooth the transition by computing different speed over its
duration.
The
following functions are supported:
- linear:
Constant speed
- cubic-bezier:
Speed will be computed according to a cubic bezier curve define by two control
points: P0 et P1 (so you will have to define 4 values here: P0x,P0y and P1x,
P1y.
- ease:
Speed will be computed with cubic-bezier(0.25, 0.1, 0.25, 1)
- ease-in:
Speed will be computed with cubic-bezier(0.42, 0, 1, 1)
- ease-inout:
Speed will be computed with cubic-bezier(0.42, 0, 0.58, 1)
- ease-out:
Speed will be computed with cubic-bezier(0, 0, 0.58, 1)
Here
is a simulation tool (using SVG of course) to show the impact of each timing function:
<p>Your
browser does not support iframes.</p> Click here to show the demo : <a
href="http://www.catuhe.com/msdn/transitions/easingfunctions.htm">http://www.catuhe.com/msdn/transitions/easingfunctions.htm</a>
This
simulator is written with pure JavaScript code to facilitate the
understanding of the function:
88.TRANSITIONSHELPER.computeCubicBezierCurveInterpolation = function (t, x1, y1, x2, y2) {
89.90.var f0 = 1 - 3 * x2 + 3 * x1;
91.var f1 = 3 * x2 - 6 * x1;
92.var f2 = 3 * x1;
93.
94.var refinedT = t;
95.for (var i = 0; i < 5; i++) {
96.var refinedT2 = refinedT * refinedT;
97.var refinedT3 = refinedT2 * refinedT;
98.
99.var x = f0 * refinedT3 + f1 * refinedT2 + f2 * refinedT;
100. var slope = 1.0 / (3.0 * f0 * refinedT2 + 2.0 * f1 * refinedT + f2);
101. refinedT -= (x - t) * slope;
102. refinedT = Math.min(1, Math.max(0, refinedT));
103. }
104.
105. 106. return 3 * Math.pow(1 - refinedT, 2) * refinedT * y1 +
107. 3 * (1 - refinedT) * Math.pow(refinedT, 2) * y2 +
108. Math.pow(refinedT, 3);
109. };
This
code is the implementation of the cubic bezier based on this definition
and you can find the source of the simulator here.
Delay
The
"transition-delay" line defines the delay between an update of a property and
the start of the transition
Events
An event
is raised at the end of a transition: "TransitionEnd". According to your
browser the correct name will be:
- Chrome
& Safari: webkitTransitionEnd
- Firefox:
mozTransitionEnd
- Opera:
oTransitionEnd
- Internet
Explorer: MSTransitionEnd
The
event will give you the following information:
- propertyName:
Name of the animated property
- elapsedTime:
The amount of time the transition has been running, in seconds
Here
is an usage sample for IE10:
116.
block.addEventListener("MSTransitionEnd",
onTransitionEvent);
More about CSS3 transitions
I can
mainly propose two reasons why CSS3 transitions are really useful:
- Hardware
acceleration: CSS3 Transitions are directly handled on the GPU (where
available) and produce smoother results. And it is really important on mobile
devices where computing power is really limited
- Better
separation between code and design: For me, the developer
must not be aware of animations or anything related to design. In the same way
the designer/artist must not be aware of JavaScript. That’s why CSS3
Transitions are really interesting as designers can describe all the
transitions in the CSS without needing developers
Support and fallback
Since
PP3, IE10 (which you can download with Windows "8" Developer Preview here)
supports CSS3 Transitions:
This report was produced by http://caniuse.com/#search=CSS3
transitions.
Of
course, as the specification is not finished (working draft), you must
use vendor’s prefixes such as –ms-, –moz-, –webkit-, –o-.
We can
obviously see that we need to provide a transparent solution in order to
address all kind of browsers. The best way will be to develop an API that can
detect the support of CSS3 transitions. If the browser doesn’t support the
feature, we will fallback to some JavaScript code.
It is
important to support a fallback method if you rely on transitions for websites
functionalities. If you don’t want to do that, you should consider using
transitions only for design enhancements. In this case, the site will still
work but only supported browsers will deliver the full experience. We speak
here of "progressive enhancements" as the more powerfull the browser is,
the more features he gets.
Transitions without CSS3 Transitions
So to
be able to support a fallback to CSS3 Transitions, we will develop a small
toolkit to provide transitions by code.
First
of all, we will create a container object for our namespace:
119. var TRANSITIONSHELPER = TRANSITIONSHELPER || {};
120.
121. TRANSITIONSHELPER.tickIntervalID = 0;
122.
123. TRANSITIONSHELPER.easingFunctions = {
124. linear:0,
125. ease:1,
126. easein:2,
127. easeout:3,
128. easeinout:4,
129. custom:5
130. };
131.
132. TRANSITIONSHELPER.currentTransitions = [];
To
support the same level of easing functions, we must declare an "enum" with all
required fields.
The
toolkit is based on a function which is called every 17ms (to achieve
animations at 60 fps). The function will enumerate through a collection of
active transitions. For each transition the code will evaluate the next value
given the current value and the target value.
We
will need some handy functions to extract value of properties and units
used:
133. TRANSITIONSHELPER.extractValue = function (string) {
134. try {
135. var result = parseFloat(string);
136.
137. if (isNaN(result)) {
138. return 0;
139. }
140.
141. return result;
142. } catch (e) {
143. return 0;
144. }
145. };
146.
147. TRANSITIONSHELPER.extractUnit = function (string) {
148.
149. 150. if (string == "") {
151. return "px";
152. }
153.
154. var value = TRANSITIONSHELPER.extractValue(string);
155. var unit = string.replace(value, "");
156.
157. return unit;
158. };
The
main function will process active transitions and will call the cubic bezier
function to evaluate current values:
159. TRANSITIONSHELPER.tick = function () {
160. 161. for (var index = 0; index < TRANSITIONSHELPER.currentTransitions.length; index++) {
162. var transition = TRANSITIONSHELPER.currentTransitions[index];
163.
164. 165. var currentDate = (new Date).getTime();
166. var diff = currentDate - transition.startDate;
167.
168. var step = diff / transition.duration;
169. var offset = 1;
170.
171. 172. switch (transition.ease) {
173. case TRANSITIONSHELPER.easingFunctions.linear:
174. offset = TRANSITIONSHELPER.computeCubicBezierCurveInterpolation(step, 0, 0, 1.0, 1.0);
175. break;
176. case TRANSITIONSHELPER.easingFunctions.ease:
177. offset = TRANSITIONSHELPER.computeCubicBezierCurveInterpolation(step, 0.25, 0.1, 0.25, 1.0);
178. break;
179. case TRANSITIONSHELPER.easingFunctions.easein:
180. offset = TRANSITIONSHELPER.computeCubicBezierCurveInterpolation(step, 0.42, 0, 1.0, 1.0);
181. break;
182. case TRANSITIONSHELPER.easingFunctions.easeout:
183. offset = TRANSITIONSHELPER.computeCubicBezierCurveInterpolation(step, 0, 0, 0.58, 1.0);
184. break;
185. case TRANSITIONSHELPER.easingFunctions.easeinout:
186. offset = TRANSITIONSHELPER.computeCubicBezierCurveInterpolation(step,0.42, 0, 0.58, 1.0);
187. break;
188. case TRANSITIONSHELPER.easingFunctions.custom:
189. offset = TRANSITIONSHELPER.computeCubicBezierCurveInterpolation(step, transition.customEaseP1X, transition.customEaseP1Y, transition.customEaseP2X, transition.customEaseP2Y);
190. break;
191. }
192.
193. offset *= (transition.finalValue - transition.originalValue);
194.
195. var unit = TRANSITIONSHELPER.extractUnit(transition.target.style[transition.property]);
196. var currentValue = transition.originalValue + offset;
197.
198. transition.currentDate = currentDate;
199.
200. 201. if (currentDate >= transition.startDate + transition.duration) {
202. currentValue = transition.finalValue; 203. TRANSITIONSHELPER.currentTransitions.splice(index, 1); 204. index--;
205.
206. 207. if (transition.onCompletion) {
208. transition.onCompletion({propertyName:transition.property,elapsedTime:transition.duration});
209. }
210. }
211.
212. 213. transition.target.style[transition.property] = currentValue + unit;
214. }
215. };
The
current version of the toolkit only supports numeric values but if you want to
animate complex values (such as color) you just have to decompose them to
simple values.
Registering
a transition in the system will be done using the following code:
216. TRANSITIONSHELPER.transition = function (target, property, newValue, duration, ease, customEaseP1X, customEaseP1Y,
customEaseP2X, customEaseP2Y, onCompletion) {
217.
218. 219. var transition = {
220. target: target,
221. property: property,
222. finalValue: newValue,
223. originalValue: TRANSITIONSHELPER.extractValue(target.style[property]),
224. duration: duration,
225. startDate: (new Date).getTime(),
226. currentDate: (new Date).getTime(),
227. ease:ease,
228. customEaseP1X:customEaseP1X,
229. customEaseP2X:customEaseP2X,
230. customEaseP1Y: customEaseP1Y,
231. customEaseP2Y: customEaseP2Y,
232. onCompletion: onCompletion
233. };
234.
235. 236. if (TRANSITIONSHELPER.tickIntervalID == 0) {
237. TRANSITIONSHELPER.tickIntervalID = setInterval(TRANSITIONSHELPER.tick, 17);
238. }
239.
240. 241. for (var index = 0; index < TRANSITIONSHELPER.currentTransitions.length; index++) {
242. var temp = TRANSITIONSHELPER.currentTransitions[index];
243.
244. if (temp.target === transition.target && temp.property === transition.property) {
245. TRANSITIONSHELPER.currentTransitions.splice(index, 1);
246. index--;
247. }
248. }
249.
250. 251. if (transition.originalValue != transition.finalValue) {
252. TRANSITIONSHELPER.currentTransitions.push(transition);
253. }
254. };
The "tick"
function is launched when the first transition is activated.
Finally
you just have to use modernizr
to define if CSS3 Transitions is supported by the current browser. If not, you
can fallback to our toolkit.
The
code for the TransitionsHelper can be downloaded here: http://www.catuhe.com/msdn/transitions/transitionshelper.js
For
example, in my puzzle game, the following code is used to animate the cells:
255. if (!PUZZLE.isTransitionsSupported) {
256. TRANSITIONSHELPER.transition(block.div, "top", block.x * totalSize + offset, 500, TRANSITIONSHELPER.easingFunctions.ease);
257. TRANSITIONSHELPER.transition(block.div, "left", block.y * totalSize + offset, 500, TRANSITIONSHELPER.easingFunctions.ease);
258. }
259. else {
260. block.div.style.top = (block.x * totalSize + offset) + "px";
261. block.div.style.left = (block.y * totalSize + offset) + "px";
262. }
We can
note that I could use another way to animate my cells when CSS3
transitions are supported: I could have defined a collection of CSS3 classes
with predefined left and top values (one for each cell) to affect them to right
cells.
Some
frameworks and toolkits already exist to support software transitions:
By the
way, you can also use the old good animate() method of jQuery.
Conclusion
As we
saw, CSS3 Transitions is a really easy way to add animations to your
project. You can produce a more reactive application just by using some
transitions when you want to change values.
By the
way, there are two solutions if you want to implement a JavaScript fallback:
- You
can do all in the JavaScript side and if you detect the support of CSS3
transitions, you will inject CSS3 declarations in the page.
- Or
you can use standard way (using true CSS3 declarations in the CSS files) and
just detect the need of fallback in JavaScript. For me, it is the better option
as the fallback must be an option and not the main subject. In a near future,
all browsers will support CSS3 Transitions and in this case you will just have
to remove your fallback code. Furthermore, it is a better way to let all the
CSS under the control of the creative team and not in the code part.
Going further