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:
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:
- The official docs – It's great, with some superb examples on the main page
- EmberMap's video series – It's paid but it's totally worth it, these guys put out very high quality videos on a regular basis. Also, if you buy my book, you get an extended, 30-day trial.
- ember-animated-tools – An absolutely phenomenal tool if you have to debug or fine tune your animations
- Using ember-animated to re-sort a list – My blog post that shows how to animate a list (the list of chess players) as the items move to their new places
- Adding fly-over animation from list to details – In which I show how to add a cross-route animation with ember-animated
Have fun animating!