diff --git a/examples/react-web/config/webpack.config.js b/examples/react-web/config/webpack.config.js
index 78934ecc3..e4fd2b849 100644
--- a/examples/react-web/config/webpack.config.js
+++ b/examples/react-web/config/webpack.config.js
@@ -322,6 +322,15 @@ module.exports = function(webpackEnv) {
name: 'static/media/[name].[hash:8].[ext]',
},
},
+ {
+ test: [/\.bin$/],
+ // exclude hash in bin to enable gltf loading
+ loader: require.resolve('url-loader'),
+ options: {
+ limit: 10000,
+ name: 'static/media/[name].[ext]',
+ },
+ },
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
diff --git a/examples/react-web/src/ui/chess3d/board3d.js b/examples/react-web/src/ui/chess3d/board3d.js
new file mode 100644
index 000000000..476469bf3
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/board3d.js
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2018 The boardgame.io Authors.
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import Chess from 'chess.js';
+import Checkerboard from './checkerboard3d';
+import { Token } from 'boardgame.io/ui';
+import bishopObj from './pieces/bishop.gltf';
+import './pieces/bishop.bin';
+import kingObj from './pieces/king.gltf';
+import './pieces/king.bin';
+import knightObj from './pieces/knight.gltf';
+import './pieces/knight.bin';
+import pawnObj from './pieces/pawn.gltf';
+import './pieces/pawn.bin';
+import queenObj from './pieces/queen.gltf';
+import './pieces/queen.bin';
+import rookObj from './pieces/rook.gltf';
+import './pieces/rook.bin';
+var THREE = (window.THREE = require('../../../../../node_modules/three'));
+require('../../../../../node_modules/three/examples/js/loaders/GLTFLoader');
+
+const COL_NAMES = 'abcdefgh';
+const SELECTED_COLOR = 'green';
+const MOVABLE_COLOR = 'palegreen';
+
+class Board extends React.Component {
+ static propTypes = {
+ G: PropTypes.any.isRequired,
+ ctx: PropTypes.any.isRequired,
+ moves: PropTypes.any.isRequired,
+ playerID: PropTypes.string,
+ isActive: PropTypes.bool,
+ isMultiplayer: PropTypes.bool,
+ isConnected: PropTypes.bool,
+ };
+
+ constructor(props) {
+ super(props);
+ this.chess = new Chess();
+ this.loader = new THREE.GLTFLoader();
+ }
+
+ _handleMesh = (out, type) => {
+ out = out.scene.children[0];
+ let temp = {};
+ temp[type] = out;
+ // get the relative size of he piece
+ const bbox = new THREE.Box3().setFromObject(out);
+ let meshSize = new THREE.Vector3();
+ bbox.getSize(meshSize);
+ temp[type].realSize = meshSize.x;
+ this.setState(temp);
+ };
+
+ state = {
+ selected: '',
+ mesh: {},
+ loading: true,
+ };
+
+ componentDidMount() {
+ // loading 3d objects
+ this.loader.load(bishopObj, out => this._handleMesh(out, 'bishop'));
+ this.loader.load(kingObj, out => this._handleMesh(out, 'king'));
+ this.loader.load(knightObj, out => this._handleMesh(out, 'knight'));
+ this.loader.load(pawnObj, out => this._handleMesh(out, 'pawn'));
+ this.loader.load(queenObj, out => this._handleMesh(out, 'queen'));
+ this.loader.load(rookObj, out => this._handleMesh(out, 'rook'));
+ }
+
+ // eslint-disable-next-line react/no-deprecated
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.G.pgn) {
+ this.chess.load_pgn(nextProps.G.pgn);
+ this.setState({ selected: '' });
+ }
+ }
+
+ render() {
+ let disconnected = null;
+ if (this.props.isMultiplayer && !this.props.isConnected) {
+ disconnected =
Disconnected!
;
+ }
+ return (
+
+
+ {this._getPieces()}
+
+ {this._getStatus()}
+ {disconnected}
+
+ );
+ }
+
+ click = ({ square }) => {
+ if (!this.props.isActive) {
+ return;
+ }
+
+ if (!this.state.selected && this._isSelectable(square)) {
+ this.setState({ selected: square });
+ } else if (this.state.selected) {
+ let moves = this._getMoves();
+ let move = moves.find(
+ move => move.from == this.state.selected && move.to == square
+ );
+ if (move) {
+ this.props.moves.move(move.san);
+ } else {
+ this.setState({ selected: '' });
+ }
+ }
+ };
+
+ _getHighlightedSquares() {
+ let result = {};
+ if (this.state.selected) {
+ result[this.state.selected] = SELECTED_COLOR;
+ }
+ for (let move of this._getMoves()) {
+ result[move.to] = MOVABLE_COLOR;
+ }
+ return result;
+ }
+
+ _getLargest() {
+ let max = -Infinity;
+ for (const p of 'bknpqr') {
+ let piece = this._pieceShortToLong(p);
+ max = this.state[piece].realSize > max ? this.state[piece].realSize : max;
+ }
+ this._maxRealSize = max;
+ }
+
+ _getSize(type) {
+ // if not finished loading return null
+ for (const p of 'bknpqr') {
+ let piece = this._pieceShortToLong(p);
+ if (!this.state[piece]) return null;
+ }
+ // otherwise return size ratio to max size
+ let piece = this._pieceShortToLong(type);
+ if (!this._maxRealSize) this._getLargest();
+ return this.state[piece].realSize / this._maxRealSize;
+ }
+
+ _pieceShortToLong(type) {
+ switch (type) {
+ case 'b':
+ return 'bishop';
+ case 'k':
+ return 'king';
+ case 'n':
+ return 'knight';
+ case 'p':
+ return 'pawn';
+ case 'q':
+ return 'queen';
+ case 'r':
+ return 'rook';
+ }
+ }
+
+ _getPieces() {
+ let result = [];
+ for (let y = 1; y <= 8; y++) {
+ for (let x = 0; x < 8; x++) {
+ let square = COL_NAMES[x] + y;
+ let p = this.chess.get(square);
+ if (p) {
+ result.push(
+
+ );
+ }
+ }
+ }
+ return result;
+ }
+
+ _getPieceByTypeAndColor(type, color) {
+ let piece = this._pieceShortToLong(type);
+ let mesh;
+ mesh = this.state[piece];
+ if (mesh) {
+ let ret = mesh.clone();
+ let blackMat = new THREE.MeshLambertMaterial({ color: 0x555555 });
+ let whiteMat = new THREE.MeshLambertMaterial({ color: 0xeeeeee });
+ // rotate the piece and assign color.
+ if (color == 'b') {
+ if (piece == 'bishop') {
+ // the bishop piece has different orientation with other pieces
+ ret.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), Math.PI);
+ }
+ ret.traverse(obj => {
+ if (obj.material) {
+ obj.material = blackMat;
+ }
+ });
+ } else {
+ if (piece != 'bishop') {
+ ret.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), Math.PI);
+ }
+ ret.traverse(obj => {
+ if (obj.material) {
+ obj.material = whiteMat;
+ }
+ });
+ }
+ return ret;
+ } else return null;
+ }
+
+ _getStatus() {
+ let message = null;
+ if (this.chess.in_check()) {
+ message = 'CHECK';
+ }
+ if (this.props.ctx.winner) {
+ switch (this.props.ctx.winner) {
+ case 'b':
+ message = 'Black won!';
+ break;
+ case 'w':
+ message = 'White won!';
+ break;
+ case 'd':
+ message = 'Draw!';
+ break;
+ }
+ }
+ if (message) {
+ return (
+
+ {message}
+
+ );
+ }
+ }
+
+ _getInitialCell(square) {
+ let history = this.chess.history({ verbose: true });
+ let lastSeen = square;
+ for (let i = history.length - 1; i >= 0; i--) {
+ let move = history[i];
+ if (lastSeen == move.to) {
+ lastSeen = move.from;
+ }
+ }
+ return lastSeen;
+ }
+
+ _isSelectable(square) {
+ let piece = this.chess.get(square);
+ return (
+ piece &&
+ piece.color === this._getCurrentPlayer() &&
+ this.chess.moves({ square }).length > 0
+ );
+ }
+
+ _getCurrentPlayer() {
+ if (this.props.ctx.currentPlayer == 0) {
+ return 'w';
+ } else {
+ return 'b';
+ }
+ }
+
+ _getMoves() {
+ if (!this.state.selected) {
+ return [];
+ }
+ return this.chess.moves({
+ verbose: true,
+ square: this.state.selected,
+ });
+ }
+}
+
+export default Board;
diff --git a/examples/react-web/src/ui/chess3d/checkerboard3d.js b/examples/react-web/src/ui/chess3d/checkerboard3d.js
new file mode 100644
index 000000000..7ff95a7ed
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/checkerboard3d.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2018 The boardgame.io Authors.
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Grid } from 'boardgame.io/ui';
+import { UI } from 'boardgame.io/ui';
+
+/**
+ * Checkerboard
+ *
+ * Component that will show a configurable checker board for games like
+ * chess, checkers and others. The vertical columns of squares are labeled
+ * with letters from a to z, while the rows are labeled with numbers, starting
+ * with 1.
+ *
+ * Props:
+ * rows - How many rows to show up, 8 by default.
+ * cols - How many columns to show up, 8 by default. Maximum is 26.
+ * onClick - On Click Callback, (row, col) of the square passed as argument.
+ * primaryColor - Primary color, #d18b47 by default.
+ * secondaryColor - Secondary color, #ffce9e by default.
+ * colorMap - Object of object having cell names as key and colors as values.
+ * Ex: { 'c5': 'red' } colors cells c5 with red.
+ *
+ * Usage:
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+class Checkerboard extends React.Component {
+ static propTypes = {
+ rows: PropTypes.number,
+ cols: PropTypes.number,
+ onClick: PropTypes.func,
+ primaryColor: PropTypes.string,
+ secondaryColor: PropTypes.string,
+ highlightedSquares: PropTypes.object,
+ style: PropTypes.object,
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.element),
+ PropTypes.element,
+ ]),
+ };
+
+ static defaultProps = {
+ rows: 8,
+ cols: 8,
+ onClick: () => {},
+ primaryColor: '#d18b47',
+ secondaryColor: '#ffce9e',
+ highlightedSquares: {},
+ style: {},
+ };
+
+ onClick = ({ x, y }) => {
+ this.props.onClick({ square: this._cartesianToAlgebraic(x, y) });
+ };
+
+ render() {
+ // Convert the square="" prop to x and y.
+ const tokens = React.Children.map(this.props.children, child => {
+ const square = child.props.square;
+ const { x, y } = this._algebraicToCartesian(square);
+ const onClick = ({ x, y }) => {
+ child.props.onClick({ square: this._cartesianToAlgebraic(x, y) });
+ };
+ return React.cloneElement(child, { x, y, onClick });
+ });
+
+ // Build colorMap with checkerboard pattern.
+ let colorMap = {};
+ for (let x = 0; x < this.props.cols; x++) {
+ for (let y = 0; y < this.props.rows; y++) {
+ const key = `${x},${y}`;
+ let color = this.props.secondaryColor;
+ if ((x + y) % 2 == 0) {
+ color = this.props.primaryColor;
+ }
+ colorMap[key] = color;
+ }
+ }
+
+ // Add highlighted squares.
+ for (const square in this.props.highlightedSquares) {
+ const { x, y } = this._algebraicToCartesian(square);
+ const key = `${x},${y}`;
+ colorMap[key] = this.props.highlightedSquares[square];
+ }
+
+ return (
+
+
+ {tokens}
+
+
+ );
+ }
+
+ _algebraicToCartesian(square) {
+ let regexp = /([A-Za-z])(\d+)/g;
+ let match = regexp.exec(square);
+ if (match == null) {
+ throw 'Invalid square provided: ' + square;
+ }
+ let colSymbol = match[1].toLowerCase();
+ let col = colSymbol.charCodeAt(0) - 'a'.charCodeAt(0);
+ let row = parseInt(match[2]);
+ return { x: col, y: this.props.rows - row };
+ }
+
+ _cartesianToAlgebraic(x, y) {
+ let colSymbol = String.fromCharCode(x + 'a'.charCodeAt(0));
+ return colSymbol + (this.props.rows - y);
+ }
+}
+
+export default Checkerboard;
diff --git a/examples/react-web/src/ui/chess3d/game.js b/examples/react-web/src/ui/chess3d/game.js
new file mode 100644
index 000000000..c26b9b5ea
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/game.js
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018 The boardgame.io Authors
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import { Game } from 'boardgame.io/core';
+import Chess from 'chess.js';
+
+// Helper to instantiate chess.js correctly on
+// both browser and Node.
+function Load(pgn) {
+ let chess = null;
+ if (Chess.Chess) {
+ chess = new Chess.Chess();
+ } else {
+ chess = new Chess();
+ }
+ chess.load_pgn(pgn);
+ return chess;
+}
+
+const ChessGame = Game({
+ name: 'chess',
+
+ setup: () => ({ pgn: '' }),
+
+ moves: {
+ move(G, ctx, san) {
+ const chess = Load(G.pgn);
+ if (
+ (chess.turn() == 'w' && ctx.currentPlayer == '1') ||
+ (chess.turn() == 'b' && ctx.currentPlayer == '0')
+ ) {
+ return { ...G };
+ }
+ chess.move(san);
+ return { pgn: chess.pgn() };
+ },
+ },
+
+ flow: {
+ movesPerTurn: 1,
+
+ endGameIf: G => {
+ const chess = Load(G.pgn);
+ if (chess.game_over()) {
+ if (
+ chess.in_draw() ||
+ chess.in_threefold_repetition() ||
+ chess.insufficient_material() ||
+ chess.in_stalemate()
+ ) {
+ return 'd';
+ }
+ if (chess.in_checkmate()) {
+ if (chess.turn() == 'w') {
+ return 'b';
+ } else {
+ return 'w';
+ }
+ }
+ }
+ },
+ },
+});
+
+export default ChessGame;
diff --git a/examples/react-web/src/ui/chess3d/pieces/CREDITS b/examples/react-web/src/ui/chess3d/pieces/CREDITS
new file mode 100644
index 000000000..75510f096
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/CREDITS
@@ -0,0 +1,5 @@
+Chess piece artwork made by marcelo.medeirossilva:
+
+https://sketchfab.com/marcelo.medeirossilva
+
+The artwork is under CC Attribution-NonCommercial-ShareAlike license.
\ No newline at end of file
diff --git a/examples/react-web/src/ui/chess3d/pieces/bishop.bin b/examples/react-web/src/ui/chess3d/pieces/bishop.bin
new file mode 100644
index 000000000..da3adbc04
Binary files /dev/null and b/examples/react-web/src/ui/chess3d/pieces/bishop.bin differ
diff --git a/examples/react-web/src/ui/chess3d/pieces/bishop.gltf b/examples/react-web/src/ui/chess3d/pieces/bishop.gltf
new file mode 100644
index 000000000..9e3a174b9
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/bishop.gltf
@@ -0,0 +1,211 @@
+{
+ "accessors": [
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 833,
+ "max": [
+ 1.9688500165939331,
+ 1.9627100229263306,
+ 5.044950008392334
+ ],
+ "min": [
+ -1.9566400051116943,
+ -1.9627900123596191,
+ -2.8324899673461914
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 9996,
+ "componentType": 5126,
+ "count": 833,
+ "max": [
+ 1,
+ 0.95240366458892822,
+ 1
+ ],
+ "min": [
+ -1,
+ -0.95240366458892822,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 0,
+ "componentType": 5125,
+ "count": 1347,
+ "max": [
+ 832
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ }
+ ],
+ "asset": {
+ "extras": {
+ "author": "marcelo.medeirossilva (https://sketchfab.com/marcelo.medeirossilva)",
+ "license": "CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)",
+ "source": "https://sketchfab.com/models/75681488e5fe457280813781cf3d15c1",
+ "title": "Low Poly Chess - Bishop"
+ },
+ "generator": "Sketchfab-3.18.7",
+ "version": "2.0"
+ },
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 5388,
+ "byteOffset": 0,
+ "name": "floatBufferViews",
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteLength": 19992,
+ "byteOffset": 5388,
+ "byteStride": 12,
+ "name": "floatBufferViews",
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 25380,
+ "uri": "bishop.bin"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "name": "Root",
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 0.41025152440000001,
+ 0.41025152440000001,
+ 0.41025152440000001,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 0.59999999999999998
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Chess_Bishop_0",
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 0
+ },
+ "indices": 2,
+ "material": 0,
+ "mode": 4
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "name": "RootNode (gltf orientation matrix)",
+ "rotation": [
+ -0.70710678118654746,
+ -0,
+ -0,
+ 0.70710678118654757
+ ]
+ },
+ {
+ "children": [
+ 2
+ ],
+ "name": "RootNode (model correction matrix)"
+ },
+ {
+ "children": [
+ 3,
+ 5
+ ],
+ "name": "Root"
+ },
+ {
+ "children": [
+ 4
+ ],
+ "matrix": [
+ -0.29086000000000006,
+ 0.95517000000000007,
+ -0.05519000000000001,
+ 0,
+ -0.77110000000000001,
+ -0.19988000000000003,
+ 0.60452000000000006,
+ 0,
+ 0.56639000000000006,
+ 0.21839000000000003,
+ 0.7946700000000001,
+ 0,
+ 4.0762500000000008,
+ 1.00545,
+ 5.9038600000000008,
+ 1
+ ],
+ "name": "Lamp"
+ },
+ {
+ "name": "Lamp"
+ },
+ {
+ "children": [
+ 6
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ -0.0061000000000000021,
+ 4.0000000000000017e-05,
+ 2.8324899999999995,
+ 1
+ ],
+ "name": "Chess_Bishop"
+ },
+ {
+ "mesh": 0,
+ "name": "Chess_Bishop_0"
+ }
+ ],
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "OSG_Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ]
+}
+
diff --git a/examples/react-web/src/ui/chess3d/pieces/king.bin b/examples/react-web/src/ui/chess3d/pieces/king.bin
new file mode 100644
index 000000000..53c0077a9
Binary files /dev/null and b/examples/react-web/src/ui/chess3d/pieces/king.bin differ
diff --git a/examples/react-web/src/ui/chess3d/pieces/king.gltf b/examples/react-web/src/ui/chess3d/pieces/king.gltf
new file mode 100644
index 000000000..b45f4fb25
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/king.gltf
@@ -0,0 +1,211 @@
+{
+ "accessors": [
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 1111,
+ "max": [
+ 2.20809006690979,
+ 2.20809006690979,
+ 6.3334498405456543
+ ],
+ "min": [
+ -2.20809006690979,
+ -2.20809006690979,
+ -3.4714999198913574
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 13332,
+ "componentType": 5126,
+ "count": 1111,
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 0,
+ "componentType": 5125,
+ "count": 1890,
+ "max": [
+ 1110
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ }
+ ],
+ "asset": {
+ "extras": {
+ "author": "marcelo.medeirossilva (https://sketchfab.com/marcelo.medeirossilva)",
+ "license": "CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)",
+ "source": "https://sketchfab.com/models/640c64de51ba48c4aa341b803e243d36",
+ "title": "Low Poly Chess - King"
+ },
+ "generator": "Sketchfab-3.18.7",
+ "version": "2.0"
+ },
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 7560,
+ "byteOffset": 0,
+ "name": "floatBufferViews",
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteLength": 26664,
+ "byteOffset": 7560,
+ "byteStride": 12,
+ "name": "floatBufferViews",
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 34224,
+ "uri": "king.bin"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "name": "Root",
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 0.59999999999999998
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Chess_King_0",
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 0
+ },
+ "indices": 2,
+ "material": 0,
+ "mode": 4
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "name": "RootNode (gltf orientation matrix)",
+ "rotation": [
+ -0.70710678118654746,
+ -0,
+ -0,
+ 0.70710678118654757
+ ]
+ },
+ {
+ "children": [
+ 2
+ ],
+ "name": "RootNode (model correction matrix)"
+ },
+ {
+ "children": [
+ 3,
+ 5
+ ],
+ "name": "Root"
+ },
+ {
+ "children": [
+ 4
+ ],
+ "matrix": [
+ -0.29086000000000006,
+ 0.95517000000000007,
+ -0.05519000000000001,
+ 0,
+ -0.77110000000000001,
+ -0.19988000000000003,
+ 0.60452000000000006,
+ 0,
+ 0.56639000000000006,
+ 0.21839000000000003,
+ 0.7946700000000001,
+ 0,
+ 4.0762500000000008,
+ 1.00545,
+ 5.9038600000000008,
+ 1
+ ],
+ "name": "Lamp"
+ },
+ {
+ "name": "Lamp"
+ },
+ {
+ "children": [
+ 6
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ -0,
+ 3.4714999999999998,
+ 1
+ ],
+ "name": "Chess_King"
+ },
+ {
+ "mesh": 0,
+ "name": "Chess_King_0"
+ }
+ ],
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "OSG_Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ]
+}
+
diff --git a/examples/react-web/src/ui/chess3d/pieces/knight.bin b/examples/react-web/src/ui/chess3d/pieces/knight.bin
new file mode 100644
index 000000000..7591d2dec
Binary files /dev/null and b/examples/react-web/src/ui/chess3d/pieces/knight.bin differ
diff --git a/examples/react-web/src/ui/chess3d/pieces/knight.gltf b/examples/react-web/src/ui/chess3d/pieces/knight.gltf
new file mode 100644
index 000000000..2eac38e22
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/knight.gltf
@@ -0,0 +1,211 @@
+{
+ "accessors": [
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 1457,
+ "max": [
+ 1.9627499580383301,
+ 1.8802800178527832,
+ 4.1786699295043945
+ ],
+ "min": [
+ -1.9627499580383301,
+ -2.0452098846435547,
+ -2.522550106048584
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 17484,
+ "componentType": 5126,
+ "count": 1457,
+ "max": [
+ 1,
+ 0.98960000276565552,
+ 0.99914759397506714
+ ],
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 0,
+ "componentType": 5125,
+ "count": 2196,
+ "max": [
+ 1456
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ }
+ ],
+ "asset": {
+ "extras": {
+ "author": "marcelo.medeirossilva (https://sketchfab.com/marcelo.medeirossilva)",
+ "license": "CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)",
+ "source": "https://sketchfab.com/models/112534cb4cbb47588c2cf566441f37fc",
+ "title": "Low Poly Chess - Knight"
+ },
+ "generator": "Sketchfab-3.18.7",
+ "version": "2.0"
+ },
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 8784,
+ "byteOffset": 0,
+ "name": "floatBufferViews",
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteLength": 34968,
+ "byteOffset": 8784,
+ "byteStride": 12,
+ "name": "floatBufferViews",
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 43752,
+ "uri": "knight.bin"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "name": "Root",
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 0.59999999999999998
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Chess_Knight_0",
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 0
+ },
+ "indices": 2,
+ "material": 0,
+ "mode": 4
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "name": "RootNode (gltf orientation matrix)",
+ "rotation": [
+ -0.70710678118654746,
+ -0,
+ -0,
+ 0.70710678118654757
+ ]
+ },
+ {
+ "children": [
+ 2
+ ],
+ "name": "RootNode (model correction matrix)"
+ },
+ {
+ "children": [
+ 3,
+ 5
+ ],
+ "name": "Root"
+ },
+ {
+ "children": [
+ 4
+ ],
+ "matrix": [
+ -0.29086000000000006,
+ 0.95517000000000007,
+ -0.05519000000000001,
+ 0,
+ -0.77110000000000001,
+ -0.19988000000000003,
+ 0.60452000000000006,
+ 0,
+ 0.56639000000000006,
+ 0.21839000000000003,
+ 0.7946700000000001,
+ 0,
+ 4.0762500000000008,
+ 1.00545,
+ 5.9038600000000008,
+ 1
+ ],
+ "name": "Lamp"
+ },
+ {
+ "name": "Lamp"
+ },
+ {
+ "children": [
+ 6
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0.082470000000000016,
+ 2.5225499999999998,
+ 1
+ ],
+ "name": "Chess_Knight"
+ },
+ {
+ "mesh": 0,
+ "name": "Chess_Knight_0"
+ }
+ ],
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "OSG_Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ]
+}
+
diff --git a/examples/react-web/src/ui/chess3d/pieces/pawn.bin b/examples/react-web/src/ui/chess3d/pieces/pawn.bin
new file mode 100644
index 000000000..389ff4686
Binary files /dev/null and b/examples/react-web/src/ui/chess3d/pieces/pawn.bin differ
diff --git a/examples/react-web/src/ui/chess3d/pieces/pawn.gltf b/examples/react-web/src/ui/chess3d/pieces/pawn.gltf
new file mode 100644
index 000000000..9a76726d6
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/pawn.gltf
@@ -0,0 +1,211 @@
+{
+ "accessors": [
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 448,
+ "max": [
+ 1.472059965133667,
+ 1.472059965133667,
+ 2.8698999881744385
+ ],
+ "min": [
+ -1.472059965133667,
+ -1.472059965133667,
+ -1.7796900272369385
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 5376,
+ "componentType": 5126,
+ "count": 448,
+ "max": [
+ 0.92387974262237549,
+ 0.92387974262237549,
+ 0.94439965486526489
+ ],
+ "min": [
+ -0.92387974262237549,
+ -0.92387974262237549,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 0,
+ "componentType": 5125,
+ "count": 666,
+ "max": [
+ 447
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ }
+ ],
+ "asset": {
+ "extras": {
+ "author": "marcelo.medeirossilva (https://sketchfab.com/marcelo.medeirossilva)",
+ "license": "CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)",
+ "source": "https://sketchfab.com/models/3443007498c54b5b9a31a08697a3b1b3",
+ "title": "Low Poly Chess - Pawn"
+ },
+ "generator": "Sketchfab-3.18.7",
+ "version": "2.0"
+ },
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 2664,
+ "byteOffset": 0,
+ "name": "floatBufferViews",
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteLength": 10752,
+ "byteOffset": 2664,
+ "byteStride": 12,
+ "name": "floatBufferViews",
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 13416,
+ "uri": "pawn.bin"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "name": "Root",
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 0.59999999999999998
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Chess_Pawn_0",
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 0
+ },
+ "indices": 2,
+ "material": 0,
+ "mode": 4
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "name": "RootNode (gltf orientation matrix)",
+ "rotation": [
+ -0.70710678118654746,
+ -0,
+ -0,
+ 0.70710678118654757
+ ]
+ },
+ {
+ "children": [
+ 2
+ ],
+ "name": "RootNode (model correction matrix)"
+ },
+ {
+ "children": [
+ 3,
+ 5
+ ],
+ "name": "Root"
+ },
+ {
+ "children": [
+ 4
+ ],
+ "matrix": [
+ -0.29086000000000006,
+ 0.95517000000000007,
+ -0.05519000000000001,
+ 0,
+ -0.77110000000000001,
+ -0.19988000000000003,
+ 0.60452000000000006,
+ 0,
+ 0.56639000000000006,
+ 0.21839000000000003,
+ 0.7946700000000001,
+ 0,
+ 4.0762500000000008,
+ 1.00545,
+ 5.9038600000000008,
+ 1
+ ],
+ "name": "Lamp"
+ },
+ {
+ "name": "Lamp"
+ },
+ {
+ "children": [
+ 6
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ -0,
+ 1.77969,
+ 1
+ ],
+ "name": "Chess_Pawn"
+ },
+ {
+ "mesh": 0,
+ "name": "Chess_Pawn_0"
+ }
+ ],
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "OSG_Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ]
+}
+
diff --git a/examples/react-web/src/ui/chess3d/pieces/queen.bin b/examples/react-web/src/ui/chess3d/pieces/queen.bin
new file mode 100644
index 000000000..1a72e3d29
Binary files /dev/null and b/examples/react-web/src/ui/chess3d/pieces/queen.bin differ
diff --git a/examples/react-web/src/ui/chess3d/pieces/queen.gltf b/examples/react-web/src/ui/chess3d/pieces/queen.gltf
new file mode 100644
index 000000000..93a67f370
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/queen.gltf
@@ -0,0 +1,211 @@
+{
+ "accessors": [
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 960,
+ "max": [
+ 2.20809006690979,
+ 2.20809006690979,
+ 5.3958601951599121
+ ],
+ "min": [
+ -2.20809006690979,
+ -2.20809006690979,
+ -3.1525800228118896
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 11520,
+ "componentType": 5126,
+ "count": 960,
+ "max": [
+ 0.92388087511062622,
+ 0.92388087511062622,
+ 0.97531545162200928
+ ],
+ "min": [
+ -0.92388087511062622,
+ -0.92388087511062622,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 0,
+ "componentType": 5125,
+ "count": 1386,
+ "max": [
+ 959
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ }
+ ],
+ "asset": {
+ "extras": {
+ "author": "marcelo.medeirossilva (https://sketchfab.com/marcelo.medeirossilva)",
+ "license": "CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)",
+ "source": "https://sketchfab.com/models/ab958c61eb2a405aa7a7b0cec91c79b0",
+ "title": "Low Poly Chess - Queen"
+ },
+ "generator": "Sketchfab-3.18.7",
+ "version": "2.0"
+ },
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 5544,
+ "byteOffset": 0,
+ "name": "floatBufferViews",
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteLength": 23040,
+ "byteOffset": 5544,
+ "byteStride": 12,
+ "name": "floatBufferViews",
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 28584,
+ "uri": "queen.bin"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "name": "Root",
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 0.59999999999999998
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Chess_Queen_0",
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 0
+ },
+ "indices": 2,
+ "material": 0,
+ "mode": 4
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "name": "RootNode (gltf orientation matrix)",
+ "rotation": [
+ -0.70710678118654746,
+ -0,
+ -0,
+ 0.70710678118654757
+ ]
+ },
+ {
+ "children": [
+ 2
+ ],
+ "name": "RootNode (model correction matrix)"
+ },
+ {
+ "children": [
+ 3,
+ 5
+ ],
+ "name": "Root"
+ },
+ {
+ "children": [
+ 4
+ ],
+ "matrix": [
+ -0.29086000000000006,
+ 0.95517000000000007,
+ -0.05519000000000001,
+ 0,
+ -0.77110000000000001,
+ -0.19988000000000003,
+ 0.60452000000000006,
+ 0,
+ 0.56639000000000006,
+ 0.21839000000000003,
+ 0.7946700000000001,
+ 0,
+ 4.0762500000000008,
+ 1.00545,
+ 5.9038600000000008,
+ 1
+ ],
+ "name": "Lamp"
+ },
+ {
+ "name": "Lamp"
+ },
+ {
+ "children": [
+ 6
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ -0,
+ 3.1525799999999999,
+ 1
+ ],
+ "name": "Chess_Queen"
+ },
+ {
+ "mesh": 0,
+ "name": "Chess_Queen_0"
+ }
+ ],
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "OSG_Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ]
+}
+
diff --git a/examples/react-web/src/ui/chess3d/pieces/rook.bin b/examples/react-web/src/ui/chess3d/pieces/rook.bin
new file mode 100644
index 000000000..305aee51e
Binary files /dev/null and b/examples/react-web/src/ui/chess3d/pieces/rook.bin differ
diff --git a/examples/react-web/src/ui/chess3d/pieces/rook.gltf b/examples/react-web/src/ui/chess3d/pieces/rook.gltf
new file mode 100644
index 000000000..166b75c07
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/pieces/rook.gltf
@@ -0,0 +1,211 @@
+{
+ "accessors": [
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 464,
+ "max": [
+ 1.9627499580383301,
+ 1.9627499580383301,
+ 3.0701000690460205
+ ],
+ "min": [
+ -1.9627499580383301,
+ -1.9627499580383301,
+ -2.7254500389099121
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 5568,
+ "componentType": 5126,
+ "count": 464,
+ "max": [
+ 1,
+ 1,
+ 1
+ ],
+ "min": [
+ -1,
+ -1,
+ -1
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 0,
+ "componentType": 5125,
+ "count": 708,
+ "max": [
+ 463
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ }
+ ],
+ "asset": {
+ "extras": {
+ "author": "marcelo.medeirossilva (https://sketchfab.com/marcelo.medeirossilva)",
+ "license": "CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)",
+ "source": "https://sketchfab.com/models/cbd416e785f64648bff3675fd45b3594",
+ "title": "Low Poly Chess - Rook"
+ },
+ "generator": "Sketchfab-3.18.7",
+ "version": "2.0"
+ },
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 2832,
+ "byteOffset": 0,
+ "name": "floatBufferViews",
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteLength": 11136,
+ "byteOffset": 2832,
+ "byteStride": 12,
+ "name": "floatBufferViews",
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 13968,
+ "uri": "rook.bin"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "emissiveFactor": [
+ 0,
+ 0,
+ 0
+ ],
+ "name": "Root",
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "metallicFactor": 0,
+ "roughnessFactor": 0.59999999999999998
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Chess_Rook_0",
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 0
+ },
+ "indices": 2,
+ "material": 0,
+ "mode": 4
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 1
+ ],
+ "name": "RootNode (gltf orientation matrix)",
+ "rotation": [
+ -0.70710678118654746,
+ -0,
+ -0,
+ 0.70710678118654757
+ ]
+ },
+ {
+ "children": [
+ 2
+ ],
+ "name": "RootNode (model correction matrix)"
+ },
+ {
+ "children": [
+ 3,
+ 5
+ ],
+ "name": "Root"
+ },
+ {
+ "children": [
+ 4
+ ],
+ "matrix": [
+ -0.29086000000000006,
+ 0.95517000000000007,
+ -0.05519000000000001,
+ 0,
+ -0.77110000000000001,
+ -0.19988000000000003,
+ 0.60452000000000006,
+ 0,
+ 0.56639000000000006,
+ 0.21839000000000003,
+ 0.7946700000000001,
+ 0,
+ 4.0762500000000008,
+ 1.00545,
+ 5.9038600000000008,
+ 1
+ ],
+ "name": "Lamp"
+ },
+ {
+ "name": "Lamp"
+ },
+ {
+ "children": [
+ 6
+ ],
+ "matrix": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ -0,
+ 2.7254499999999999,
+ 1
+ ],
+ "name": "Chess_Rook"
+ },
+ {
+ "mesh": 0,
+ "name": "Chess_Rook_0"
+ }
+ ],
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "OSG_Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ]
+}
+
diff --git a/examples/react-web/src/ui/chess3d/singleplayer3d.js b/examples/react-web/src/ui/chess3d/singleplayer3d.js
new file mode 100644
index 000000000..7abbbcd8f
--- /dev/null
+++ b/examples/react-web/src/ui/chess3d/singleplayer3d.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018 The boardgame.io Authors.
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import React from 'react';
+import { Client } from 'boardgame.io/react';
+import ChessGame from './game';
+import ChessBoard from './board3d';
+
+const App = Client({
+ game: ChessGame,
+ board: ChessBoard,
+});
+
+const Singleplayer = () => (
+
+);
+
+export default Singleplayer;
diff --git a/examples/react-web/src/ui/drag-n-drop.js b/examples/react-web/src/ui/drag-n-drop.js
index 3ac0467a9..c46e6fcce 100644
--- a/examples/react-web/src/ui/drag-n-drop.js
+++ b/examples/react-web/src/ui/drag-n-drop.js
@@ -37,22 +37,28 @@ class Board extends React.Component {
render() {
return (
-
+
Drag the card into the deck
+
+
+ {this.state.deck.map(c => (
+
+ ))}
+
-
- {this.state.deck.map(c => (
-
- ))}
-
-
- {this.state.free && (
-
- )}
-
+ {this.state.free && (
+
+ )}
+
+
);
}
}
diff --git a/examples/react-web/src/ui/index.js b/examples/react-web/src/ui/index.js
index ae69639d1..3d65016ca 100644
--- a/examples/react-web/src/ui/index.js
+++ b/examples/react-web/src/ui/index.js
@@ -7,13 +7,25 @@
*/
import DragDrop from './drag-n-drop';
+import Test3D from './test3d';
+import Singleplayer3d from './chess3d/singleplayer3d';
const routes = [
{
- path: '/ui',
+ path: '/ui/2d',
text: 'Drag and Drop',
component: DragDrop,
},
+ {
+ path: '/ui/3d',
+ text: '3D Grid',
+ component: Test3D,
+ },
+ {
+ path: '/ui/chess3d',
+ text: 'Chess 3D',
+ component: Singleplayer3d,
+ },
];
export default { routes };
diff --git a/examples/react-web/src/ui/test3d.js b/examples/react-web/src/ui/test3d.js
new file mode 100644
index 000000000..009138ac6
--- /dev/null
+++ b/examples/react-web/src/ui/test3d.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018 The boardgame.io Authors.
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import React from 'react';
+import { Client } from 'boardgame.io/react';
+import { Game } from 'boardgame.io/core';
+import { UI, Grid, Token } from 'boardgame.io/ui';
+import bishop from './chess3d/pieces/bishop.gltf';
+import knight from './chess3d/pieces/knight.gltf';
+import './chess3d/pieces/bishop.bin';
+import './chess3d/pieces/knight.bin';
+// example and source use different modules, so direct to source modules here to make THREE.js global.
+var THREE = (window.THREE = require('../../../../node_modules/three'));
+require('../../../../node_modules/three/examples/js/loaders/GLTFLoader');
+
+class Board extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ deck: [],
+ free: true,
+ loading: true,
+ };
+ }
+
+ onClick = ({ x, y }) => {
+ console.log(x + ' ' + y);
+ };
+
+ componentDidMount() {
+ this.loader = new THREE.GLTFLoader();
+ this.loader.load(bishop, out => {
+ out = out.scene.children[0];
+ this.setState({
+ bishop: out,
+ });
+ });
+
+ this.loader.load(knight, out => {
+ out = out.scene.children[0];
+ this.setState({
+ knight: out,
+ });
+ });
+ }
+
+ render() {
+ return (
+
+
Showing chess pieces on the grid
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const App = Client({
+ game: Game({}),
+ board: Board,
+ debug: false,
+});
+
+const Singleplayer = () => (
+
+);
+
+export default Singleplayer;
diff --git a/src/ui/3d/deck.js b/src/ui/3d/deck.js
index 188114c4f..489bcbb91 100644
--- a/src/ui/3d/deck.js
+++ b/src/ui/3d/deck.js
@@ -48,7 +48,6 @@ export class DeckImpl extends React.Component {
onEvent = e => {
if (e.type == 'drop') {
- console.log(e.what[0]);
e.what[0].position.x = -2;
e.what[0].position.z = 0;
e.what[0].position.y += 20 * 0.02;
diff --git a/src/ui/3d/grid.js b/src/ui/3d/grid.js
index c163a1ced..75778beb1 100644
--- a/src/ui/3d/grid.js
+++ b/src/ui/3d/grid.js
@@ -7,17 +7,184 @@
*/
import React from 'react';
+import PropTypes from 'prop-types';
+import UIContext from '../ui-context';
+import * as THREE from 'three';
+
+/**
+ * Grid
+ *
+ * Component that will show children on a cartesian regular grid.
+ *
+ * Props:
+ * rows - Number of rows (height) of the grid.
+ * cols - Number of columns (width) of the grid.
+ * cellSize - Size of a square.
+ * thichness - Thichness of a square.
+ * padding - Padding between squares.
+ * colorMap - A map from 'x,y' => color.
+ * onClick - (x, y) => {}
+ * Called when a square is clicked.
+ * onMouseOver - (x, y) => {}
+ * Called when a square is mouse over.
+ * onMouseOut - (x, y) => {}
+ * Called when a square is mouse out.
+ *
+ * Usage:
+ *
+ *
+ *
+ *
+ */
+export const Grid = props => (
+
+ {context => }
+
+);
+
+class GridImpl extends React.Component {
+ static propTypes = {
+ rows: PropTypes.number.isRequired,
+ cols: PropTypes.number.isRequired,
+ cellSize: PropTypes.number,
+ thickness: PropTypes.number,
+ padding: PropTypes.number,
+ colorMap: PropTypes.object,
+ onClick: PropTypes.func,
+ onMouseOver: PropTypes.func,
+ onMouseOut: PropTypes.func,
+ context: PropTypes.any,
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.element),
+ PropTypes.element,
+ ]),
+ };
+ static defaultProps = {
+ colorMap: {},
+ cellSize: 1,
+ padding: 0.1,
+ thickness: 0.1,
+ };
+
+ constructor(props) {
+ super(props);
+ this.boardGroup = new THREE.Group();
+ this.tokenGroup = new THREE.Group();
+ this.boardGroup.add(this.tokenGroup);
+ // translate the board to center on (0,0,0)
+ this.boardGroup.translateX(
+ (-(this.props.padding + this.props.cellSize) * (this.props.cols - 1)) / 2
+ );
+ this.boardGroup.translateZ(
+ (-(this.props.padding + this.props.cellSize) * (this.props.rows - 1)) / 2
+ );
+ }
+
+ _getCellColor(x, y) {
+ const key = `${x},${y}`;
+ let color = '#777777';
+ if (key in this.props.colorMap) {
+ color = this.props.colorMap[key];
+ }
+ return color;
+ }
+
+ componentWillUnmount() {
+ this.context.remove(this.boardGroup);
+ }
-// Not yet implemented.
-export class Grid extends React.Component {
render() {
+ this.context = this.props.context;
+ this.context.add(this.boardGroup);
+
+ // when rerendering, render a new squareGroup
+ this.boardGroup.remove(this.squareGroup);
+ this.squareGroup = new THREE.Group();
+ this.boardGroup.add(this.squareGroup);
+
+ // add square base
+ for (let x = 0; x < this.props.cols; x++) {
+ for (let y = 0; y < this.props.rows; y++) {
+ const squareProps = {
+ x: x,
+ y: y,
+ size: this.props.cellSize,
+ color: this._getCellColor(x, y),
+ padding: this.props.padding,
+ thickness: this.props.thickness,
+ };
+
+ const square = new Square(squareProps);
+ this.squareGroup.add(square);
+
+ const onEvent = e => {
+ if (e.type == 'click') {
+ if (this.props.onClick) this.props.onClick({ x: x, y: y });
+ } else if (e.type == 'mouseOver') {
+ if (this.props.onMouseOver) this.props.onMouseOver({ x: x, y: y });
+ } else if (e.type == 'mouseOut') {
+ if (this.props.onMouseOut) this.props.onMouseOut({ x: x, y: y });
+ }
+ };
+
+ this.context.regCall(square, onEvent);
+ }
+ }
+
+ // set tokens
+ const tokens = React.Children.map(this.props.children, child => {
+ return React.cloneElement(child, {
+ three: true,
+ boardSize: this.props.cellSize,
+ parent: this.tokenGroup,
+ padding: this.props.padding,
+ lift: this.props.thickness,
+ });
+ });
+
+ if (tokens) {
+ return tokens;
+ }
+
return null;
}
}
-// Not yet implemented.
-export class Square extends React.Component {
- render() {
- return null;
+/**
+ * Square
+ *
+ * Component that renders a square inside a Grid.
+ *
+ * Props
+ * x - X coordinate on grid coordinates.
+ * y - Y coordinate on grid coordinates.
+ * size - Square size.
+ * color - Color of the square
+ * thichness - Thichness of a square.
+ * padding - Padding between squares.
+ *
+ * Not meant to be used by the end user directly (use Token).
+ * Also not exposed in the NPM.
+ */
+export class Square extends THREE.Mesh {
+ constructor(props) {
+ super();
+ this.userData = {
+ responsive: true,
+ draggable: false,
+ ...props,
+ };
+ props = this.userData;
+ this.geometry = new THREE.BoxBufferGeometry(
+ props.size,
+ props.thickness,
+ props.size
+ );
+ this.material = new THREE.MeshLambertMaterial({ color: props.color });
+
+ this.receiveShadow = true;
+ this.translateX(this.userData.x * (props.size + props.padding));
+ this.translateZ(this.userData.y * (props.size + props.padding));
+ this.translateY(this.userData.thickness / 2);
}
}
diff --git a/src/ui/3d/grid.test.js b/src/ui/3d/grid.test.js
new file mode 100644
index 000000000..47ebeadf8
--- /dev/null
+++ b/src/ui/3d/grid.test.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018 The boardgame.io Authors
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import React from 'react';
+import { Grid } from './grid';
+import { UI } from './ui';
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import * as THREE from 'three';
+
+THREE.WebGLRenderer = jest.fn(function() {
+ this.shadowMap = {};
+ this.setSize = () => {
+ return null;
+ };
+ this.domElement = document.createElement('canvas');
+ this.render = () => {
+ return null;
+ };
+});
+Enzyme.configure({ adapter: new Adapter() });
+
+class MockChild extends React.Component {
+ render() {
+ return ;
+ }
+}
+
+test('render correctly', () => {
+ const grid = Enzyme.mount(
+
+
+
+
+
+ );
+ expect(grid.html()).toContain('rect');
+ expect(grid.html()).toContain('bgio-canvas');
+ const gridIns = grid.find('GridImpl').instance();
+ expect(gridIns.squareGroup.children).toHaveLength(12);
+});
+
+// test callback function
+test('click handler', () => {
+ {
+ const onClick = jest.fn();
+ const onMouseOver = jest.fn();
+ const onMouseOut = jest.fn();
+ const grid = Enzyme.mount(
+
+
+
+
+
+ );
+ const uiIns = grid.instance();
+ const gridIns = grid.find('GridImpl').instance();
+ const id = gridIns.squareGroup.children[0].id;
+ uiIns.callbacks_[id]({
+ type: 'mouseOver',
+ });
+ uiIns.callbacks_[id]({
+ type: 'click',
+ });
+ uiIns.callbacks_[id]({
+ type: 'mouseOut',
+ });
+ expect(onClick).toHaveBeenCalled();
+ expect(onMouseOut).toHaveBeenCalled();
+ expect(onMouseOver).toHaveBeenCalled();
+ }
+
+ // No crash when onClick is not provided.
+ const grid = Enzyme.mount(
+
+
+
+
+
+ );
+ const uiIns = grid.instance();
+ const gridIns = grid.find('GridImpl').instance();
+ const id = gridIns.squareGroup.children[0].id;
+ uiIns.callbacks_[id]({
+ type: 'mouseOver',
+ });
+ uiIns.callbacks_[id]({
+ type: 'click',
+ });
+ uiIns.callbacks_[id]({
+ type: 'mouseOut',
+ });
+});
+
+test('colorMap', () => {
+ const colorMap = { '0,0': '#d18b47' };
+ const grid = Enzyme.mount(
+
+
+
+ );
+ let gridIns = grid.find('GridImpl').instance();
+ expect(gridIns.squareGroup.children[0].material.color.getHexString()).toBe(
+ 'd18b47'
+ );
+});
diff --git a/src/ui/3d/loading.css b/src/ui/3d/loading.css
new file mode 100644
index 000000000..c45942346
--- /dev/null
+++ b/src/ui/3d/loading.css
@@ -0,0 +1,17 @@
+.loader {
+ border: 16px solid #f3f3f3; /* Light grey */
+ border-top: 16px solid #3498db; /* Blue */
+ border-radius: 50%;
+ width: 80px;
+ height: 80px;
+ animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/ui/3d/token.js b/src/ui/3d/token.js
index ab0690d48..a1d5b2c9d 100644
--- a/src/ui/3d/token.js
+++ b/src/ui/3d/token.js
@@ -7,10 +7,156 @@
*/
import React from 'react';
+import PropTypes from 'prop-types';
+import UIContext from '../ui-context';
+import * as THREE from 'three';
+
+/**
+ * Token
+ *
+ * Component that represents a board game piece (or token).
+ * Can be used by itself or with one of the grid systems
+ * provided (Grid or HexGrid).
+ *
+ * A token renders as a 3D Mesh. IF no mesh prop is passed.
+ * It will render a white box on the grid.
+ *
+ * Props:
+ * x - X coordinate on grid / hex grid.
+ * y - Y coordinate on grid / hex grid.
+ * z - Z coordinate on hex grid.
+ * onClick - Called when the token is clicked.
+ * onMouseOver - Called when the token is mouse over.
+ * onMouseOut - Called when the token is mouse out.
+ *
+ * Usage:
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+export const Token = props => (
+
+ {context => }
+
+);
+
+class TokenImpl extends React.Component {
+ static propTypes = {
+ x: PropTypes.number,
+ y: PropTypes.number,
+ z: PropTypes.number,
+ mesh: PropTypes.any,
+ padding: PropTypes.number,
+ size: PropTypes.number,
+ lift: PropTypes.number,
+ boardSize: PropTypes.number,
+ parent: PropTypes.instanceOf(THREE.Object3D),
+ context: PropTypes.object,
+ animate: PropTypes.bool,
+ onClick: PropTypes.func,
+ onMouseOver: PropTypes.func,
+ onMouseOut: PropTypes.func,
+ children: PropTypes.element,
+ animationDuration: PropTypes.number,
+ };
+
+ static defaultProps = {
+ animationDuration: 750,
+ size: 1,
+ padding: 0.1,
+ lift: 0.1,
+ };
+
+ constructor(props) {
+ super();
+
+ if (!props.size) {
+ this.size = props.boardSize;
+ } else {
+ this.size = props.size;
+ }
+
+ if (props.parent) {
+ this.parent = props.parent;
+ } else {
+ this.parent = props.context;
+ }
+ }
+
+ _attachMesh = mesh => {
+ const size = this.size;
+ let meshSize = new THREE.Vector3();
+ let meshCenter = new THREE.Vector3();
+ const bbox = new THREE.Box3().setFromObject(mesh);
+ bbox.getSize(meshSize);
+ bbox.getCenter(meshCenter);
+ // determine the scale factor
+ let scale = meshSize.z < meshSize.x ? meshSize.x : meshSize.z;
+ scale = size / scale;
+ mesh.scale.set(scale, scale, scale);
+ // set the mesh to the ground
+ if (this.props.boardSize && this.props.lift && this.props.padding) {
+ mesh.position.x =
+ this.props.x * (this.props.boardSize + this.props.padding);
+ mesh.position.z =
+ this.props.y * (this.props.boardSize + this.props.padding);
+ mesh.position.y = -bbox.min.y + this.props.lift;
+ } else {
+ mesh.position.x = this.props.x;
+ mesh.position.z = this.props.y;
+ mesh.position.y = -bbox.min.y;
+ }
+ this.parent.add(mesh);
+ // register the event
+ const onEvent = e => {
+ if (e.type == 'click') {
+ this.props.onClick({ x: this.props.x, y: this.props.y });
+ } else if (e.type == 'mouseOver') {
+ this.props.onMouseOver({ x: this.props.x, y: this.props.y });
+ } else if (e.type == 'mouseOut') {
+ this.props.onMouseOut({ x: this.props.x, y: this.props.y });
+ }
+ };
+ this.props.context.regCall(mesh, onEvent);
+ };
+
+ componentWillUnmount() {
+ this.parent.remove(this.prevMesh);
+ }
-// Not yet implemented.
-export class Token extends React.Component {
render() {
+ let mesh = this.props.mesh;
+
+ if (this.prevMesh && this.prevMesh === mesh) return null;
+
+ if (!mesh) {
+ mesh = new THREE.Mesh(
+ new THREE.BoxBufferGeometry(1, 1 * 0.3, 1),
+ new THREE.MeshLambertMaterial({ color: '#eeeeee' })
+ );
+ this._attachMesh(mesh);
+ } else if (mesh.isObject3D) {
+ this._attachMesh(mesh);
+ } else {
+ console.error('Your input to tokens should be an three js 3d object');
+ }
+ this.parent.remove(this.prevMesh);
+ this.prevMesh = mesh;
+
return null;
}
}
diff --git a/src/ui/3d/token.test.js b/src/ui/3d/token.test.js
new file mode 100644
index 000000000..a670a73d8
--- /dev/null
+++ b/src/ui/3d/token.test.js
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018 The boardgame.io Authors
+ *
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import React from 'react';
+import { Grid } from './grid';
+import { UI } from './ui';
+import { Token } from './token';
+import Enzyme from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import * as THREE from 'three';
+
+// mock renderer function
+THREE.WebGLRenderer = jest.fn(function() {
+ this.shadowMap = {};
+ this.setSize = () => {
+ return null;
+ };
+ this.domElement = document.createElement('canvas');
+ this.render = () => {
+ return null;
+ };
+});
+Enzyme.configure({ adapter: new Adapter() });
+
+test('click handler', () => {
+ const onClick = jest.fn();
+ const onMouseOver = jest.fn();
+ const onMouseOut = jest.fn();
+ const token = Enzyme.mount(
+
+
+
+
+
+ );
+
+ const uiIns = token.instance();
+ const tokenIns = token.find('TokenImpl').instance();
+
+ const id = tokenIns.prevMesh.id;
+ uiIns.callbacks_[id]({
+ type: 'mouseOver',
+ });
+ uiIns.callbacks_[id]({
+ type: 'click',
+ });
+ uiIns.callbacks_[id]({
+ type: 'mouseOut',
+ });
+ expect(onClick).toHaveBeenCalled();
+ expect(onMouseOut).toHaveBeenCalled();
+ expect(onMouseOver).toHaveBeenCalled();
+});
+
+test('correct x and y', () => {
+ const grid = Enzyme.mount(
+
+
+
+
+
+ );
+ const tokenIns = grid.find('TokenImpl').instance();
+ const x =
+ tokenIns.props.x * (tokenIns.props.boardSize + tokenIns.props.padding);
+ const y =
+ tokenIns.props.y * (tokenIns.props.boardSize + tokenIns.props.padding);
+ expect(tokenIns.prevMesh.position.x).toBe(x);
+ expect(tokenIns.prevMesh.position.z).toBe(y);
+
+ // No crash when componentWillUnmount
+ {
+ grid
+ .find('TokenImpl')
+ .instance()
+ .componentWillUnmount();
+ }
+});
diff --git a/src/ui/3d/ui.js b/src/ui/3d/ui.js
index acd4d5d0c..9520939cb 100644
--- a/src/ui/3d/ui.js
+++ b/src/ui/3d/ui.js
@@ -11,19 +11,31 @@ import PropTypes from 'prop-types';
import UIContext from '../ui-context';
import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
+import './loading.css';
/**
* Root element of the React/threejs based 3D UI framework.
*/
export class UI extends React.Component {
static propTypes = {
+ width: PropTypes.number,
+ height: PropTypes.number,
children: PropTypes.any,
onMouseEvent: PropTypes.func,
};
+ static defaultProps = {
+ width: 1024,
+ height: 768,
+ };
+
constructor(props) {
super(props);
+ this.state = {
+ isLoading: false,
+ };
+
/**
* Set of callbacks that children of this element pass via context.subscribeToMouseEvents
* in order to receive mouse events that pertain to the objects that they manage.
@@ -47,13 +59,13 @@ export class UI extends React.Component {
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
- this.renderer.setSize(window.innerWidth, window.innerHeight);
+ this.renderer.setSize(this.props.width, this.props.height);
// Set up camera.
this.camera = new THREE.PerspectiveCamera(
45,
- window.innerWidth / window.innerHeight,
+ this.props.width / this.props.height,
0.1,
1000
);
@@ -94,6 +106,40 @@ export class UI extends React.Component {
this.childGroup = new THREE.Group();
this.scene.add(this.childGroup);
+
+ // set up loading screen
+
+ this.loader = ;
+ THREE.DefaultLoadingManager.onStart = () => {
+ this.setState({ isLoading: true });
+ this.ref_.current.removeChild(this.renderer.domElement);
+ console.log('Started loading file');
+ };
+ THREE.DefaultLoadingManager.onLoad = () => {
+ this.setState({ isLoading: false });
+ this.ref_.current.appendChild(this.renderer.domElement);
+ console.log('Loading Complete!');
+ };
+
+ THREE.DefaultLoadingManager.onProgress = function(
+ url,
+ itemsLoaded,
+ itemsTotal
+ ) {
+ console.log(
+ 'Loading file: ' +
+ url +
+ '\nLoaded ' +
+ itemsLoaded +
+ ' of ' +
+ itemsTotal +
+ ' files.'
+ );
+ };
+
+ THREE.DefaultLoadingManager.onError = function(url) {
+ console.log('There was an error loading: ' + url);
+ };
}
setupMouseEvents() {
@@ -139,20 +185,24 @@ export class UI extends React.Component {
true
);
}
-
if (this.props.onMouseEvent) {
this.props.onMouseEvent(e, objects);
}
- objects.forEach(obj => {
+ // only intersect the nearest object.
+ let obj = objects[0];
+ if (obj) {
e.point = obj.point;
- if (obj.object.id in this.callbacks_) {
- this.callbacks_[obj.object.id](e);
- }
- if (obj.object.parent.id in this.callbacks_) {
- this.callbacks_[obj.object.parent.id](e);
+ let current = this.childGroup.getObjectById(obj.object.id);
+ // check parents until we hit a callback or hit the top level.
+ while (current && current.parent && current.id != this.childGroup.id) {
+ if (current.id in this.callbacks_) {
+ this.callbacks_[current.id](e);
+ break;
+ }
+ current = current.parent;
}
- });
+ }
};
const onMouseDown = e => {
@@ -224,8 +274,8 @@ export class UI extends React.Component {
t = t.parentNode;
}
- mouse.x = (x / window.innerWidth) * 2 - 1;
- mouse.y = -(y / window.innerHeight) * 2 + 1;
+ mouse.x = (x / this.props.width) * 2 - 1;
+ mouse.y = -(y / this.props.height) * 2 + 1;
dispatchMouseCallbacks(e);
@@ -287,6 +337,12 @@ export class UI extends React.Component {
}
};
+ registerCallback = (obj, callback) => {
+ if (obj && callback) {
+ this.callbacks_[obj.id] = callback;
+ }
+ };
+
getContext = () => {
return {
three: true,
@@ -294,21 +350,31 @@ export class UI extends React.Component {
remove: obj => this.scene.remove(obj),
scene: this.scene,
camera: this.camera,
+ regCall: this.registerCallback,
};
};
- componentDidMount() {
+ _initCanvas() {
this.renderer.domElement.id = 'bgio-canvas';
this.ref_.current.appendChild(this.renderer.domElement);
this.setupMouseEvents();
this.animate();
}
+ componentDidMount() {
+ this._initCanvas();
+ }
+
render() {
+ const children = React.Children.map(this.props.children, child => {
+ return React.cloneElement(child, {
+ three: true,
+ });
+ });
return (
- {this.props.children}
+ {this.state.isLoading ? this.loader : children}
);