SyntaxHighlighter

Friday, March 11, 2016

Card Game Project - Components

There are two components that are custom to this game, the <game-card> and the <card-stack>, which is a collection of <game-card>s.


Cards



The simpler of the two will be the game card. Like your standard playing card, it need a front and a back. The back will have a logo (the Polymer logo), the same on all cards. The front will have text that reflects some rank (in this game, it's basically 1-6 plus 3 special card types).

.game-card {
position: relative;
transition: transform .5s linear 0s;
backface-visibility: hidden;
}
.game-card .front {
transform: rotateY( 180deg);
position: absolute;
}
.game-card.front.flipped {
transform: rotateY( 0deg);
}
.game-card .back {
transform: rotateY( 0deg);
}
.game-card.back.flipped {
transform: rotateY( -180deg);
}
<div class="game-card">
<div id="cardFront" class$="front {{isFlipped(show)}}" style$="width: {{cardWidth}}px; height: {{cardHeight}}px" on-click="flip">
<div class="rank left">{{rank}}</div>
<div class="rank right">{{rank}}</div>
</div>
<div id="cardBack" class$="back {{isFlipped(show)}}" style$="width: {{cardWidth}}px; height: {{cardHeight}}px" on-click="flip"></div>
</div>
The first trick is to not show both sides of the card at the same time. I took out the other styling attributes for the css, and just wanted to show the affect of a relatively positioned outer <div> (so it's still affected by flexbox correctly), and two absolutely positioned inner <div>, with classes front and back. The outer div defines the css transition, and that the back of a flipped card can be seen (backface-visibility: hidden;). The front of the card is flipped over with  transform: rotateY( 180deg), and the front and back are both rotated 180 degrees in the same direction to get the affect.

That flipped class is applied with Polymer's special attribute data bindingclass$="front {{isFlipped(show)}}", which binds the calculation of the class to a function in game-card's script called "isFlipped", that is evaluated every time the value of the attribute "show" changes (internally to game-card, or applied to the element from elsewhere). When the class appears or disappears, the css animation is applied.

For now, an "on-click" event is present to flip the cards as needed.


Stacks



Cards in this game are played from stacks (or piles). Two things I wanted to do for stacks was

  1. Proportion the cards dimensions
  2. Fan the cards so I could see them all from top to bottom.


My first step was to make cards that had a respectable shape! Normal playing cards are 64mm x 89mm so we want to keep that proportion. Plus, in the card stacks I want to "fan out" a stack of up to 5 card and still fit in the width of the <card-stack> element (which is always resized with flexbox). A little calculation is needed, and the solution I've used was this (polymer properties removed):

Polymer({
is: 'card-stack',
behaviors: [Polymer.IronResizableBehavior],
listeners: {
"iron-resize": "resized"
},
resized: function() {
var height = this.offsetHeight - 2;
var stackWidth = this.offsetWidth - 2;
if (height > 0) {
var calcWidth = height * 64 / 89;
if (calcWidth < stackWidth / 2) {
//cards will fit when fanned out, 1/4 of width showing for up to 4 cards, then full 5th card
this.cardHeight = height - 2;
this.cardWidth = calcWidth - 2;
} else {
//calc width and height to fit inside parent box
this.cardHeight = stackWidth / 2 * 89 / 64 - 2;
this.cardWidth = stackWidth / 2 - 2;
}
console.log('For stack ' + stackWidth + 'x' + height + ', setting cards to ' + this.cardWidth + 'x' + this.cardHeight);
}
}
});

With Polymer, getting the size of elements (or their parents) is tricky if not done at the right time where all the values are 0. Here I used the IronResizeableBehavior to catch an iron-resize event, and if the height is > 0 (meaning it actually rendered), performed some calculations to preserve the aspect ratio. 

Fanning the cards needs to happen from inside the <card-stack>, but outside of <game-card>, so the aspects and behaviors of the cards are independent of the styles applied. Using Polymer's <dom-repeat> template, we can list the cards in an array, and with CSS just stack them on top of each other. This style was very calculated, so this was done with a function bound to three attributes of the <card-stack> (so will be recalculated each time).

<template is="dom-repeat" items="{{data}}">
<game-card style$="{{calcMargin(reverse, index, cardWidth)}}" class="card-in-stack" rank="{{item}}" card-width="[[cardWidth]]" card-height="[[cardHeight]]">
</game-card>
</template>
<style include="shared-styles">
.card-in-stack {
position: absolute;
}
</style>
calcMargin: function(reverse, index, cardWidth) {
//console.log('Calculating the margin for reverse ' + reverse + ' index ' + index + ' width ' + cardWidth);
//Margin is 1/4 of a card width for every index, based on fan direction
var margin = 'margin-' + (reverse ? 'right' : 'left') + ': ' + Math.floor(index * (cardWidth / 4)) + 'px;';
//z-index is based on index, so stack is correct
var zidx = 'z-index: ' + (index + 2) + ';';
//position for absolute edge of card, based on fan direction
var position = (reverse ? 'right' : 'left') + ': 0;';
return margin + zidx + position;
},


Tracking


This step is wrapped up, so I created a branch. https://github.com/chimmelb/polycardwar/tree/step-components

The resizing of cards (and where to bind values in the layout) took about 3 hours, the card flip was about 2, and the fanning elements was about 1. Measuring time is funny, because it really was about a day between meetings, lunch, the other projects that needs attention, etc . . . cramming 6 hours of good work into 9 hours of my day : )

No comments:

Post a Comment