Animate chess pieces with ember-animated

Animate chess pieces with ember-animated

I've used ember-animated in the Sac Sac Mate app in several places: to animate the list being resorted and to move the player's photo from the list to the details page (which is a cross-route animation, now that I think of it).

Possibly the most obvious animation has been missing from the app up until recently, though. As one steps through the game, the experience would be vastly improved, I thought, if the pieces actually moved, just like in an OTB (over-the-board) game, instead of appearing at their new location.

Declarative rendering for the win

Fortunately, the chess board is rendered declaratively so knowing the awesomeness of ember-animated, my assumption had been that this wouldn't be too hard – and I was proven right, as you'll see.

The chess board is represented by the ChessBoard component, which shows each square on the board by rendering BoardSquare components:

{{!-- app/components/chess-board.hbs --}}
<div class="w-400px" ...attributes>
  {{#each this.reversedRanks as |rank|}}
    <div class="flex flex-row relative">
      {{#each this.files as |file index|}}
        {{!-- (...) -- }}
          <BoardSquare
            @rank={{rank}}
            @file={{file}}
            @numericFile={{index}}
            @isLightSquare={{lightSquare}}
            @piece={{get @pieces square}}
            @isCurrentMove={{get @lastMoveSquares square}}
          />
      {{/each}}
      {{!-- (...) -- }}
    </div>
  {{/each}}
</div>
{{!-- app/components/board-square.hbs --}}
<div   class={{concat
    "w-50px h-50px "
    (if @isCurrentMove "bg-yellow" (if @isLightSquare "light-square" "dark-square"))
  }}
  ...attributes
>
  {{#if @piece}}
    <img
      src="/images/pieces/wikipedia/{{@piece}}.png"
      alt={{@piece}}
    >
  {{/if}}
</div>

Adding the animation

It's the pieces (the images) we want to animate so we have to wrap them in an animator. The one we have to use here is animated-value since we're animating a single value between two places. animated-value takes a positional argument, a "predicate" that identifies that value. In our case, we can use @piece as it can serve as an identifier ("wP" stands for "white pawn", "bN" for black knight, etc.).

<div
  class={{concat
    "w-50px h-50px "
    (if @isCurrentMove "bg-yellow" (if @isLightSquare "light-square" "dark-square"))
  }}
  ...attributes
>
  {{#if @piece}}
    <AnimatedContainer>
      {{#animated-value @piece}}
        <img
          src="/images/pieces/wikipedia/{{@piece}}.png"
          alt={{@piece}}
        >
      {{/animated-value}}
    </AnimatedContainer>
  {{/if}}
</div>

I also wrapped the animator in AnimatedContainer so that the space is reserved for it between animations and we don't get weird artifacts while the element is traveling from its source to its destination.

Next, we have to define the animation and use it in the animated-value we just defined.

I decided to add this to the game-play service that all of the logic and state related to stepping through a game. This is debatable as I could've added it to either the BoardSquare component or the ChessBoard. Both of these components are pretty slim, however (`BoardSquare` doesn't even have a backing JS class), and I wanted to keep it that way.

// app/services/game-play.js
// (...)
import move from 'ember-animated/motions/move';

export default class GamePlayService extends Service {
  // (...)
  // eslint-disable-next-line require-yield
  * pieceTransition({ receivedSprites }) {
    receivedSprites.forEach(sprite => {
      sprite.applyStyles({ 'z-index': 1 });
      move(sprite, { duration: 100 })
    });
  }
}

The default move transition probably does the job, so let's pass it in:

{{!-- app/components/chess-board.hbs --}}
<div class="w-400px" ...attributes>
  {{#each this.reversedRanks as |rank|}}
    <div class="flex flex-row relative">
      {{#each this.files as |file index|}}
        {{!-- (...) -- }}
          <BoardSquare
            @rank={{rank}}
            @file={{file}}
            @numericFile={{index}}
            @isLightSquare={{lightSquare}}
            @piece={{get @pieces square}}
            @isCurrentMove={{get @lastMoveSquares square}}
            @transition={{this.gamePlay.pieceTransition}}
          />
      {{/each}}
      {{!-- (...) -- }}
    </div>
  {{/each}}
</div>

And then use it (literally "use" it) in BoardSquare:

<div
  class={{concat
    "w-50px h-50px "
    (if @isCurrentMove "bg-yellow" (if @isLightSquare "light-square" "dark-square"))
  }}
  ...attributes
>
  {{#if @piece}}
    <AnimatedContainer>
      {{#animated-value @piece use=@transition}}
        <img
          src="/images/pieces/wikipedia/{{@piece}}.png"
          alt={{@piece}}
        >
      {{/animated-value}}
    </AnimatedContainer>
  {{/if}}
</div>

The pieces have now started to move:

Queen Sacs are the best

The animated gif makes the animation somewhat choppy, so you can check out this game here or other games here.

Conclusion

ember-animated is awesome and you should check it out. I'm very far from being an expert in animation and yet it took me less than an hour to add this to the app – even including my forgetting to use AnimatedContainer and having to adjust tests due to the added markup.

Here are some more resources to get started with (or get better at) ember-animated:

Have fun animating!