CSSで石を回転させる方法
これから作る石のHTMLは次のようになっています。
<div class="stone-face-1"></div>
<div class="stone-face-2" style="background-color: white;"></div>
石のdiv要素の中に、石の色を表すdiv要素が2つ入っています。この class="stone-face-1"
や class="stone-face-2"
がついているものを石面1、石面2とよぶことにします。
初期状態では石面1が裏側、石面2が表側となっています。初期状態では裏側は見えないので色を付けていません。
この石を裏返すと次のようになります。
<div class="stone" style="transform: rotateY(180deg);">
<div class="stone-face-1" style="background-color: black;"></div>
<div class="stone-face-2" style="background-color: white;"></div>
まず、裏側となっていた石面1に background-color: black
をつけることで黒色にしています。
そのあと、石のdiv要素に transform: rotateY(180deg)
をつけることで180度回転させます。
こうすることで表側が石面1となるので、石が黒色に変化しました。
この石をもう1回、裏返すと次のようになります。
<div class="stone" style="transform: rotateY(360deg);">
<div class="stone-face-1" style="background-color: black;"></div>
<div class="stone-face-2" style="background-color: orange;"></div>
裏側となっていた石面2に background-color: orange
をつけることでオレンジ色にしています。
さっきまで transform: rotateY(180deg)
だった要素をさらに180度回転させるため、 transform: rotateY(360deg)
をつけます。
こうすることで今度は表側が石面2となるので、石がオレンジ色に変わりました。
このようにしてCSSで石を回転させています。
石のCSSを追加する
styles.cssに次のものを書きくわえます。
transform-style: preserve-3d;
box-shadow: 1px 2px 2px 0px rgba(0, 0, 0, 0.4);
backface-visibility: hidden;
background: radial-gradient(circle closest-side at 20% 20%, rgba(256, 256, 256, 0.3) 0%, transparent 100%);
transform: rotateY(180deg);
stone
に transform-style: preserve-3d
をつけて、その子要素である stone-face
に backface-visibility: hidden
をつけています。
こうすることで、 stone
に transform: rotateY(180deg)
をつけるたとき、表と裏が回転しているようにみえます。
石となるStoneクラスの作成
Stoneクラスには次のようなプロパティやメソッドがあります。
名前 | 説明 |
---|
constructor(color) | コンストラクターでColorオブジェクトを受け取る |
color | Colorオブジェクト |
flippedCount | 裏返した回数 |
element | 石となるdiv要素 |
face1Element | 石面1となるdiv要素 |
face2Element | 石面2となるdiv要素 |
createElements() | 上記のdiv要素たちを作成する |
flip(nextColor) | 裏側に新しい色を付けて、石を裏返す |
remove() | この石のdiv要素を除去する |
Stone.jsに次のように書き込みます。
this.element = document.createElement('div');
this.element.classList.add('stone');
this.element.style.transitionDuration = `${FLIP_DURATION_TIME}ms`;
// 石面1のdiv要素を作成する。初期状態ではこれが裏側となる
this.face1Element = document.createElement('div');
this.face1Element.classList.add('stone-face-1');
// 石面2のdiv要素を作成する。初期状態ではこれが表側となるため色付けしておく。
this.face2Element = document.createElement('div');
this.face2Element.classList.add('stone-face-2');
this.face2Element.style.backgroundColor = this.color.colorCode;
this.element.append(this.face1Element, this.face2Element);
// 現在、裏側となっている石面のdiv要素を取得する
// 裏返した回数が偶数回ならばface1Elementが裏側となっており、
// 奇数回であればface2Elementが裏側となっている
const backFaceElement = this.flippedCount % 2 === 0 ? this.face1Element : this.face2Element;
backFaceElement.style.backgroundColor = nextColor.colorCode;
this.element.style.transform = `rotateY(${this.flippedCount * 180}deg)`;
// 石のdiv要素を除去して、この色の石の数を減らす
マス目となるSquareクラスの作成
Squareクラスには次のようなプロパティやメソッドがあります。
名前 | 説明 |
---|
constructor(rowIndex, columnIndex) | コンストラクターで行番号と列番号を受け取る |
rowIndex | このマス目の行番号 |
columnIndex | このマス目の列番号 |
stone | Stoneオブジェクト |
sandwichStones | 現在のプレイヤーがこのマスに石を置いたら挟むことができる石の配列 |
isCorner | 角のマスが否か |
element | マス目となるdiv要素 |
setNewStone(color) | 引数で受け取ったColorオブジェクト受け取り、その色の石を置く |
setSandwichStones() | 挟める石の配列をセットする |
isSelectable() | このマス目に石を置くことができるか判別する |
select() | このマス目に石を置いたあと、ゲームを進行する |
click() | このマス目をクリックしたときの処理 |
removeStone() | このマス目に石が置いてあれば、それを除去する |
Square.jsに次のように書き込みます。
constructor(rowIndex, columnIndex) {
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
// 行番号と列番号をIDに持つdiv要素がすでに作成されているのでそれを取得する
this.element = document.getElementById(`index-${rowIndex}-${columnIndex}`);
this.element.classList.add('square');
this.element.addEventListener('click', this.click.bind(this));
this.sandwichStones = [];
// 角のマス目か否かを行番号と列番号から判別する
this.isCorner = rowIndex === 0 && columnIndex === 0
|| rowIndex === 0 && columnIndex === initialBoard[0].length - 1
|| rowIndex === initialBoard.length - 1 && columnIndex === 0
|| rowIndex === initialBoard.length - 1 && columnIndex === initialBoard[0].length - 1;
state.emptySquareCount++;
this.stone = new Stone(color);
this.element.append(this.stone.element);
state.emptySquareCount--;
// このマス目に石が置いていない、かつ挟める石がある場合は石を置ける
return this.stone === null && this.sandwichStones.length !== 0;
this.setNewStone(state.currentPlayer.color);
state.sandwichedStones = this.sandwichStones;
// 現在のプレイヤーがヒトで、かつ石を置くことができるマス目であれば石を置く
// こうすることで無関係なマス目をクリックしても何も起きないようにする
if (state.currentPlayer.isHuman() && this.isSelectable()) {
setSandwichStones (sandwichStones) {
this.sandwichStones = sandwichStones;
// 現在のプレイヤーがヒトの場合、マス目の上に矢印を表示したりけしたりする
if (state.currentPlayer.isHuman()) {
if (this.isSelectable()) {
this.element.classList.add('selectable');
this.element.classList.remove('selectable');
state.emptySquareCount++;
コンストラクター内で this.element.addEventListener('click', this.click.bind(this));
とすることで、このマス目をクリックしたらclickメソッドが実行されるようになります。
.bind(this)
をつけないと、clickメソッド内の this
はクリックしたdiv要素となるため、実際にクリックしたらエラーが生じます。
.bind(this)
をつけることによりclickメソッド内の this
がSquareインスタンスとなり正常に動作するようになります。
マス目のCSSの追加
次のようにstyles.cssに書きくわえます。
/* マス目の背景色に少しグラデーションをかける */
background: radial-gradient(circle farthest-side at 100% 100%, forestgreen 0%, darkgreen 100%);
transform: rotateY(0deg);
transform: rotateY(180deg);
animation-duration: 700ms;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-in-out;
これでマス目のclass属性に selectable
を追加すると赤い矢印が飛び跳ねるアニメーションが追加されます。
どの石を挟めるのかをチェックする方法
たとえば次のような盤面のとき、 行:1 列:0
のマス目に白い石を置くとしたらどの石を挟めるでしょうか?
上下左右、斜めの八方向をそれぞれチェックしていきます。それぞれチェックし始めるときは、挟める石を入れておく配列 sandwichStones
を用意しておきます。
左上方向のチェック
まず、左上からチェックしていきます。左上のマス目に移動するには、行番号に-1を、列番号に-1をに加えていきます。そうすると左上のマス目は 行:0 列:-1
となりますが、そこにマス目はないので挟める石も存在しません。
上方向のチェック
次に、真上をチェックしていきます。行番号に-1を、列番号に0を加えた 行:0 列:0
の場所にはマス目はありますが石が置いてありません。よって挟める石もありません。
右上方向のチェック
右上をチェックしていきます。行番号に-1を、列番号に1を加えた 行:0 列:1
のマス目には黒い石が置いてあります。自分以外の石にに出合ったら、その石を sandwichStones
に入れておきます。
さらに行番号に-1を、列番号に1を加えた 行:-1 列:2
の場所にはマス目がないので挟める石もありません。
左方向のチェック
左上と同じくマス目がないので挟める石も存在しません。
右方向のチェック
行番号に0を、列番号に1を加えた 行:1 列:1
の場所には黒い石が置いてるので sandwichStones
に入れておきます。
さらに行番号に0を、列番号に1を加えた 行:1 列:2
のマス目には白い石があります。自分と同じ色の石に出合った場合、 sandwichStones
に入っている石が挟める石となります。
左下方向のチェック
左上と同じくマス目はないので挟める石も存在しません。
下方向のチェック
行番号に1を、列番号に0を加えた 行:2 列:0
のマス目には白い石が置いあります。自分と同じ色の石に出合った場合、 sandwichStones
に入っている石が挟める石となりますが、このチェックでは sandwichStones
は空のままなので挟める石はないということになります。
右下方向のチェック
行番号に1を、列番号に1を加えた 行:2 列:1
のマス目には黒い石が置いてるので sandwichStones
を用意して入れておきます。
さらに行番号に1を、列番号に1を加えた 行:3 列:2
のマス目には石がないので挟める石もありません。
このマス目に石を置いたとしたら、右方向のチェックをしたときの sandwichStones
に入っている石が挟める石となります。
Boardクラスの作成
BoardクラスはSquareオブジェクトを管理して、石が置けるマス目のチェックなどを行います。
Boardクラスには次のようなプロパティやメソッドがあります。
名前 | 説明 |
---|
directions | 八方向の移動方向を入れた配列 |
squares | Squareオブジェクトの二次元配列 |
calcSquareElementSize() | マス目の1辺の長さを計算する |
createBaseElements() | マス目の基となるdiv要素たちを作成する |
createSquares() | Squareオブジェクトの二次元配列を作成する |
setInitialStones() | 初期配置の石を置く |
getStone(rowIndex, columnIndex) | 指定されたマス目の石を取得する |
getSandwichStones(rowIndex, columnIndex, direction) | 指定されたマス目に石を置くと挟むことができる石たちを取得する(一方向) |
getAllSandwichStones(rowIndex, columnIndex) | 指定されたマス目に石を置くと挟むことができる石たちを取得する(八方向) |
checkSelectableSquares() | 石を置くことができるマス目をチェックする |
resetSelectableSquares() | 石を置くことができるマス目をリセットする |
reset() | 盤面にあるすべての石を除去したあと、初期配置の石を置く |
盤面をパソコンやスマホで表示するとき、画面ぴったりに表示させたいのでマス目1辺の長さを計算します。
しかし、あまりにもマス目が小さいとクリックしずらくなってしまうため、マス目1辺の最小サイズは30pxということにしています。
Board.jsに次のように書き込みます。
this.createBaseElements();
calcSquareElementSize () {
// 画面内に盤面が収まるように、マス目の縦幅と横幅の一片の長さを計算する
const widthSize = window.innerWidth / initialBoard[0].length;
const heightSize = window.innerHeight / initialBoard.length;
// マス目は正方形にするため、これらのうち小さいほうを一片の長さとする
const size = Math.min(widthSize, heightSize);
// マス目が小さすぎるとクリックしずらくなってしまうため、1辺の最小サイズは30pxとする
return Math.max(size, 30);
const squareSize = this.calcSquareElementSize();
// 親要素はdisplay:inline-gridとなっている
const boardElement = document.getElementById('board');
boardElement.style.gridTemplateRows = `repeat(${initialBoard.length}, ${squareSize}px)`;
boardElement.style.gridTemplateColumns = `repeat(${initialBoard[0].length}, ${squareSize}px)`;
// gridElementに直接div要素を入れていくとその都度、ブラウザが再描画を行うため負荷がかかってしまう
const fragment = document.createDocumentFragment();
forEachNest(initialBoard, (contentType, rowIndex, columnIndex) => {
const divElement = document.createElement('div');
divElement.id = `index-${rowIndex}-${columnIndex}`;
fragment.append(divElement);
// fragmentを使えばブラウザが再描画を行うのはこの一回のみですむ
boardElement.append(fragment);
this.squares = initialBoard.map((row, rowIndex) => {
return row.map((contentType, columnIndex) => {
// 何もない空間にはnullを、それ以外はSquareオブジェクトを生成する
return contentType === -2 ? null : new Square(rowIndex, columnIndex);
forEachNest(initialBoard, (contentType, rowIndex, columnIndex) => {
// 数字が0以上の場合は石が置いてあることを意味する
const color = state.colors[contentType];
this.squares[rowIndex][columnIndex].setNewStone(color);
getStone (rowIndex, columnIndex) {
// 指定された場所の石を取得する。マス目が無い空間、または石が無い場合はnullが返される
return this.squares[rowIndex] && this.squares[rowIndex][columnIndex]?.stone || null;
getSandwichStones (rowIndex, columnIndex, direction) {
let currentRowIndex = rowIndex;
let currentColumnIndex = columnIndex;
const sandwichStones = [];
currentRowIndex += direction.row;
currentColumnIndex += direction.column;
const stone = this.getStone(currentRowIndex, currentColumnIndex);
// もし石がない場合は挟むことのできる石がないため空の配列を返す
// もし自分の色と同じ石が存在していたら、これまでsandwichStonesに
// 入れた石は挟むことができるのでsandwichStonesを返す
if (stone.color === state.currentPlayer.color) {
// 自分以外の石が存在しているため配列にその石を追加する
sandwichStones.push(stone);
getAllSandwichStones (rowIndex, columnIndex) {
const allSandwichStones = [];
this.directions.forEach((direction) => {
const sandwichStones = this.getSandwichStones(rowIndex, columnIndex, direction);
allSandwichStones.push(...sandwichStones);
return allSandwichStones;
checkSelectableSquares () {
// 石を置くことができるマス目をこの配列に入れていく
const selectableSquares = [];
forEachNest(this.squares, (square, rowIndex, columnIndex) => {
// マス目が存在していて、かつ石が置いていない場合
if (square && square.stone === null) {
const sandwichStones = this.getAllSandwichStones(rowIndex, columnIndex);
if (sandwichStones.length !== 0) {
// このマス目(Squareオブジェクト)にsandwichStonesをセットする
square.setSandwichStones(sandwichStones);
selectableSquares.push(square);
state.selectableSquares = selectableSquares;
resetSelectableSquares () {
state.selectableSquares.forEach((square) => {
square.setSandwichStones([]);
state.selectableSquares = [];
forEachNest(this.squares, (square, rowIndex, columnIndex) => {
if (square && square.stone) {
盤面のCSSを追加する
styles.cssに次のものを書きくわえます。