Part I: Using JavaScript to set @keyframes in CSS animations (Windows Store apps, IE)
I'm currently exploring the potential of CSS3 for casual game scenarios. For Windows Store apps in particular, CSS3 animations are enticing because you don't have to worry about cross-browser compatibility or APIs that won't work with older versions of Internet Explorer, since everything is IE 10+. Compatibility is one of the main reasons why proponents of Flash for Web apps argue against HTML5/CSS3. But for Windows Store apps, these arguments against HTML5/CSS3 are a non-factor. Instead, if you want to do a game (or just animations) for Windows Store, you need to figure out whether you need CSS3, or HTML5 <canvas>, or maybe the newly supported WebGL—if we're talking JavaScript (we are)—for fancier stuff.
Updated info, 2/19/2014: Check out the completed sample (Windows Store version)
Despite a few hiccups, CSS3 development has been pretty fast–which is an advantage of the technology vs. using the HTML5 <canvas> (note that <canvas> also has a lot of advantages, especially for games with a large number of independent objects). However, I thought I'd talk about a couple of areas where I got tripped up a bit using CSS3. In this post, I'll talk about the need to dynamically manipulate the @keyframes rule using JavaScript.
Basic CSS3 animations are pretty straightforward. You create your DOM elements, declaratively or otherwise, and specify a few animation properties. And then you create a set of one or more @keyframes
rules to handle the actual animations.
Here is some of the CSS code for an HTML element representing a game object. We'll say it's for a DIV element containing an image (<div id="piece"></div>). The animation-name rule specifies the @keyframes "enterPiece" rule.
#piece {
position: absolute;
animation-duration: 1.5s;
animation-fill-mode: forwards;
animation-name: "enterPiece"
}
Here is the CSS code for the @keyframes "enterPiece" rule. @keyframes allows you to specify beginning/end states and anything in between (such as what the animation should look like when 50% complete). In this example, I just set the beginning/end states.
@keyframes enterPiece {
from {
transform: translateY(-150px) scale(1);
opacity: 0;
}
to {
transform: translateY(0px) scale(1);
opacity: 1;
}
}
This is all pretty straightforward. The animation for the game object moves the object from a relative starting position of -150px to 0px, and changes the opacity from 0 to 1 while moving it this distance. (I include a scale value for informational purposes but I won't be using it—I'll be setting that value dynamically.)
Here's code to set some of these animation values in JavaScript: In this code, we set the animation duration, fill mode, and the animation name ("enterPiece", again), which specifies the same @keyframes code in CSS. In this case, we're doing it for a new element that we're about to add to the DOM.
newNode.style.animationDuration = "1.5s";
newNode.style.animationFillMode = "forwards";
newNode.style.animationName = "enterPiece";
As I tested my code, I realized I would need to set different scale values for each piece, due to the use of a 3D perspective. This was a little trickier to do in JavaScript, because animated transform values such as scale, translate, and rotate belong to the harder to access @keyframes rule. I refused to create a bunch of extra @keyframes rules in the CSS**. Instead, after some investigation, I was able to obtain the rule using the following code, which iterates through the stylesheets to get the rule I wanted.
**requestAnimationFrame is another way to go. More on this in a later post.
Note: If you don't need to show these values changing within an animation, you can just include them in CSS code, for example, like this: transform: rotateX(180deg).
var cssRule;
// Returns a reference to the specified CSS rule(s).
function getRule() {
var rule;
var ss = document.styleSheets;
for (var i = 0; i < ss.length; ++i) {
// loop through all the rules!
for (var x = 0; x < ss[i].cssRules.length; ++x) {
rule = ss[i].cssRules[x];
if (rule.name == "enterPiece" && rule.type
== CSSRule.KEYFRAMES_RULE) {
cssRule = rule;
}
}
}
}
To make sure I'm not getting the wrong CSS rule, I check the type, which needs to be CSSRule.KEYFRAMES_RULE, and I check the name. It's best to run this code during app initialization, and cache any rules which you need to modify in JavaScript.
Note: For non-IE browsers, check current recommendations for using the CSSRule.KEYFRAMES_RULE.
Once I had the right rule, I had to change it. The API documentation led me to think that I should use insertRule, but some testing indicated otherwise. Instead, I used appendRule along with deleteRule.
First, I needed to clear out the current rule values. To use deleteRule, you must pass an index to indicate which part of the existing rule to delete. An index of 0 indicates the from portion (that is, the "0%" portion) of the rule. Once I deleted the rule parts, I replaced them using appendRule, and here I passed in my dynamically set scale values.
cssRule.deleteRule("0");
cssRule.deleteRule("1");
cssRule.appendRule("0% { transform: translateY(-150px) "
+ scale +"; opacity: 0; }");
cssRule.appendRule("100% { transform: translateY(0px) "
+ scale + "; opacity: 1; }");
And this worked!
In Part II of this series, I talk about running multiple animations and using requestAnimationFrame. I'll share the full code when it's closer to final state.