Card Animations

Using web components, Elm, and CSS transitions

A hand with cards being randomly placed to simulate user interaction

I wanted to make an online game with cards moving realistically between players' hands and the table-top.

In the past I had developed game demonstrations that were meant to augment textual rules. Those used jQuery animations, which is impossible to replicate today because the Web Police would confiscate my license to dev.

Many Web Components

To keep it modern my first approach was to create a web component representing each zone in the game:

A hand of three pilatch cards with the ten-of-rock placed on the table

Each web component was responsible for positioning its children, which was great for abstraction and separation of concerns. For instance to make a <pilatch-card> flip, I just toggle its up attribute, and a <player-hand> would space out the cards it contained evenly.

Unfortunately this fails because the concept of moving a card between zones didn't apply in virtual DOM. Elm's renderer would destroy the card element that was in the player's hand, and make a similar one elsewhere. The result was more teleportation than animation.

I had solved this same problem in the past with my jQuery game-rules demos by noting which zone a card was moving to, calculating its position, animating the card to that new position, then finally appending the card element to its new parent. Were I to do this in Elm, I would need to use ports to do that destination-position calculation, or somehow track the placement of every zone in Elm.

Neither of those options were appealing.

One relative table-top

So my next attempt featured a single <div class="table-top"> containing each card without additional nesting. Cards fly about the table thanks to CSS transitions by adding or removing HTML attributes. The CSS is solely responsible for knowing the position of cards on the table, thus dividing the layout concern from the logic and view-model.

A simplified snippet of table-top CSS

Now a card in hand could move to the table by adding an attribute to it, such as placed.

A hand of five pilatch cards, with the One of Scissors transitioning to the table

In the example above, adding that placed attribute is what triggers the animation to move the One of Scissors card.

Once that's done, however, it would be naïve to think I could leave the DOM out of order. That card isn't really in a player's hand any more, so it shouldn't be mixed in with the other cards in hand. Specifically, it should lose its hand attribute and move below the list of cards that still do have hand attributes, while each of their hand-indicies are decremented:

A hand of four cards, and the One of Scissors placed on the table

If this process weren't broken into two steps, then to accurately render a player's cards, my model would also have to track the placed card's previous hand-index. This same concept would apply for each pair of zones that cards move between, necessitating the tracking of more than just game state, but also the history of different card zones.

Sounds like a nightmare. Therefore I swap positions of cards in the DOM.

But undesired animations are triggered!

If you look at the two code snippets above, you might understand how a browser would interpret this phenomenon. The fifth <pilatch-card> seems to morph from the Five of Scissors to the One of Scissors. At the same time the first card in hand gives up its top/left positions associated with being placed, and the fifth card is suddenly placed, causing an animation that looks like the last card in hand is being plunked down.

Or visually:

A loop that keeps placing the leftmost card on the table

Watch the rank number on the rightmost card closely. See how sometimes it switches, then goes to the table? That's the unwanted CSS transition.

What's going on here?

Virtual DOM

Even in frameworks requiring developers to create immutable data structures from pure functions, what happens in browsers doesn't follow such constraints.

Performance reasons, I suppose.

It would be more costly to cull each HTML element and instantialize very similar ones, than to reuse the existing elements and modify them. That's what's going on here. The browser is reusing HTML elements in a way the developer did not intend.

React does something similar, and thankfully it's more obvious.

Unlike jQuery, which encourages developers to work with references to long-lived HTML elements and mutate them by fiat, the concept of virtual DOM takes a data-driven approach. Through a virtual DOM architecture you change the state of your program's model, and expect something to happen on screen the next time the renderer runs its view function. As developers we're giving up low-level control over the DOM in favor of productivity gains.

Here's an example of two different data structures that would be fed to a virtual DOM renderer: frameOne is before placing the One of Scissors on the table, and frameTwo is after.

Whether it's JSX or Elm, we're not writing HTML directly

How is a virtual-DOM-diffing algorithm to know that, between frames, an HTML element which was the first child of its parent is now the same one as the fifth child, but with different attributes? How could it know the developer's intent? How fast could that be?

Clairvoyance probably ain't fast.

Solution

It seems obvious in retrospect. Don't animate when rearranging the cards in the DOM.

Thank you, no-transition attribute

With the above CSS, cards will transition unless we specifically tell them not to. That's exactly what happens during the rearrange step by adding the no-transition attribute to each card.

How to only have animations when we want them

Another option

While experimenting with React I stumbled upon a lazy approach. When moving a card from one zone to another, put it in both zones at the same time! Have a look here.

If you're interested in the nitty gritty, read on.

Iterative Development

In each demonstration you can step through the animations by clicking the NEXT button that appears in the upper left corner.

The goal is the same in each: to place the five of paper face-down, rearrange the remaining cards in hand, then return the card to hand.

See the bumpy road to success.

Technologies

I took a swing at a few technologies before settling on Elm.