1
0

Using div and backgrounds instead of canvas

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-02-02 19:13:09 -08:00
parent 2c9e86931e
commit 27cf10ad48
5 changed files with 166 additions and 439 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -5,3 +5,27 @@
height: 100%;
}
.Tile {
width: 90.75px;
height: 77.5px;
position: absolute;
background-position-y: 0px;
background-size: cover;
transform: translate(-45.375px, -38.75px) rotate(-30deg);
}
.Pip {
width: 50px;
height: 50px;
position: absolute;
background-size: 600% auto; /* pip-numbers is a 6x6 grid of pip images */
transform: translate(-25px, -25px);
}
.Border {
width: 242px;
height: 70.6px;
position: absolute;
transform-origin: 0 0;
background-size: cover;
}

View File

@ -82,48 +82,7 @@ const loadImage = (file, drawFrame) => {
return image;
}
const Tiles = (game, drawFrame) => {
if (!game) {
return;
}
const tiles = game.tiles;
[ "robber", "brick", "wood", "sheep", "stone", "wheat" ].forEach((type) => {
const file = "tiles-" + type + ".png",
image = loadImage(file, drawFrame);
tiles.forEach((tile) => {
if (tile.type === type) {
tile.image = image;
tile.x = 0;
tile.pos = { x: 0, y: 0 };
}
tile.jitter = 0;
});
});
return tiles;
};
const gameSignature = (game) => {
if (!game) {
return "";
}
const signature =
game.borders.map(border => border.file.replace(/borders-(.).*/, "$1")).join('') +
game.pips.map(pip => pip.roll.toString()).join('') +
game.tiles.map(tile => tile.type.charAt(0)).join('');
return signature;
};
const Board = ({ game }) => {
const [signature, setSignature] = useState(gameSignature(game));
const [mouse, setMouse] = useState({x: 0, y: 0, timer: null});
const [pips, setPips] = useState([]);
const [borders, setBorders] = useState([]);
const [tabletop, setTabletop] = useState(null);
const [tiles, setTiles] = useState([]);
const [closest, setClosest] = useState({
info: {},
tile: null,
@ -131,408 +90,134 @@ const Board = ({ game }) => {
tradeToken: null,
settlement: null
});
const [minSize,setMinSize] = useState(0);
const radius = 0.317;
const canvasRef = useRef(null);
const center = { x: 300, y: 350 }, rows = [3, 4, 5, 4, 3];
const drawPips = useCallback((ctx) => {
const image = pips.image, pipSize = 0.06;
const generatePips = () => {
let row = 0, rowCount = 0;
let y = center.y - rows.length * 0.5 * 67,
x = center.x - (rows[row] - 1) * 0.5 * 77.5;
let divs = [], count = 19;
for (let i = 0; i < count; i++) {
let code = (i === (count - 1))
? 'robber'
: String.fromCharCode(65 + i);
divs.push(
<div
key={`pip-${code}`}
className="Pip"
style={{
top: `${y}px`,
left: `${x}px`,
backgroundImage: `url(${assetsPath}/gfx/pip-numbers.png)`,
backgroundPositionX: `${ 100. * (i % 6) / 5.}%`,
backgroundPositionY: `${ 100 * Math.floor(i / 6) / 5. }%`
}}/>
);
function drawTile(tile, angle, radius) {
tile.pos.x = Math.sin(-angle) * radius;
tile.pos.y = Math.cos(-angle) * radius;
const image = tile.image;
ctx.save();
ctx.rotate(angle);
ctx.translate(0., radius);
ctx.rotate(-angle + Math.PI * 1. / 6.);
ctx.drawImage(image,
tile.x * image.width, tile.y * image.height,
image.width, image.height / 4.,
-tileWidth * 0.5, -tileHeight * 0.5,
tileWidth, tileHeight);
ctx.restore();
if (++rowCount === rows[row]) {
row++;
rowCount = 0;
y += 67;
x = center.x - (rows[row] - 1) * 0.5 * 77.5;
} else {
x += 77.5;
}
}
return divs;
};
function drawPip(pip, angle, radius, jitter) {
ctx.save();
ctx.rotate(angle);
ctx.translate(0., radius);
/* Offset into a random direction by a random amount */
ctx.rotate(Math.PI * 2. * jitter);
ctx.translate(0, tileHeight * 0.1 * jitter);
/* Undo random rotation for position, and add random rotation
* for pip placement */
ctx.rotate(-angle - Math.PI * 2. * jitter + jitter * 0.4);
ctx.drawImage(image,
pip.x * image.width, pip.y * image.height,
image.width / 6., image.height / 6.,
-pipSize * 0.5, -pipSize * 0.5,
pipSize, pipSize);
ctx.restore();
}
const generateTiles = () => {
let row = 0, rowCount = 0;
let y = center.y - rows.length * 0.5 * 67,
x = center.x - (rows[row] - 1) * 0.5 * 77.5;
let angle,
rotation = radius,
index = 0, pip; //, roll = dice[0].pips + dice[1].pips;
return [ "robber", "brick", "wood", "sheep", "stone", "wheat" ].map((type) => {
let count;
switch (type) {
case "robber":
count = 1;
break;
case "brick":
count = 3;
break;
case "wood":
count = 4;
break;
case "sheep":
count = 4;
break;
case "stone":
count = 3;
break;
case "wheat":
count = 4;
break;
default:
console.error(`Invalid type: ${type}`);
break;
}
let divs = [];
/* Outer row */
angle = 0;
for (let i = 0; i < 12; i++) {
angle -= Math.PI * 2. / 12.;
pip = pips.pips[index++];
tiles[i].pip = pip;
drawTile(tiles[i], angle, rotation - (i % 2) * 0.04);
drawPip(pip, angle, rotation - (i % 2) * 0.04, tiles[i].jitter);
}
for (let i = 0; i < count; i++) {
divs.push(
<div
key={`${type}-${i}`}
className="Tile"
style={{
top: `${y}px`,
left: `${x}px`,
backgroundImage: `url(${assetsPath}/gfx/tiles-${type}.png)`,
backgroundPositionY: `-${i*77.5}px`
}}/>
);
/* Middle row */
angle = Math.PI * 2. / 12.;
rotation = radius * 0.5;
for (let i = 12; i < 18; i++) {
angle -= Math.PI * 2. / 6.;
pip = pips.pips[index++];
tiles[i].pip = pip;
drawTile(tiles[i], angle, rotation);
drawPip(pip, angle, rotation, tiles[i].jitter);
}
/* Center */
let i = 18;
pip = pips.pips[index++];
tiles[i].pip = pip;
drawTile(tiles[i], 0, 0);
drawPip(pip, 0, 0, tiles[i].jitter);
}, [ tiles, pips]);
const drawBorders = useCallback((ctx) => {
ctx.rotate(Math.PI);
const offset = 0.18;
borders.forEach((border, index) => {
ctx.translate(0, radius);
const image = border.image;
ctx.drawImage(image,
-offset, 0,
0.5, 0.5 * image.height / image.width);
ctx.translate(0, -radius);
ctx.rotate(Math.PI * 2. / 6.);
});
}, [borders]);
const drawFrame = useCallback(() => {
if (!canvasRef || !tabletop || !canvasRef.current) {
return;
}
if (!game) {
console.log("Nothing to render if there is no game!");
return;
}
const canvas = canvasRef.current;
if (canvas.width === 0 || canvas.height === 0) {
console.log("No dimensions to render in");
return;
}
const ctx = canvas.getContext("2d");
// ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.strokeStyle = 'white';
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
/*
* Tabletop tiling:
* Image width: 1080
* Left start: 32
* Right edge: 1010 (1010 - 32 = 978)
*
* If the view is wider than taller, then
*/
const tabletopLeft = 32 * tabletop.width / 1080,
tabletopRight = 1010 * tabletop.width / 1080,
tabletopLeaf = 978 * tabletop.width / 1080;
/* If view is taller than wide, tile the tabletop vertically */
ctx.save();
if (canvas.height > canvas.width) {
const tabletopHeight = canvas.width * tabletop.height / tabletop.width;
for (let top = 0, step = 0; top < canvas.height; top += tabletopHeight, step++) {
if (step % 2) {
ctx.save();
ctx.translate(0, tabletopHeight - 1);
ctx.transform(1, 0, 0, -1, 0, 0);
ctx.drawImage(tabletop,
0, 0,
tabletop.width, tabletop.height,
0, 0, canvas.width, canvas.width * tabletop.height / tabletop.width);
ctx.restore();
if (++rowCount === rows[row]) {
row++;
rowCount = 0;
y += 67;
x = center.x - (rows[row] - 1) * 0.5 * 77.5;
} else {
ctx.drawImage(tabletop,
0, 0,
tabletop.width, tabletop.height,
0, 0,
canvas.width, canvas.width * tabletop.height / tabletop.width);
}
ctx.translate(0, tabletopHeight);
}
} else {
//const tabletopWidth = canvas.height * tabletop.width / tabletop.height;
ctx.drawImage(tabletop,
0, 0,
tabletopRight, tabletop.height,
0, 0,
canvas.height * tabletopRight / tabletop.height, canvas.height);
let left = canvas.height * tabletopRight / tabletop.height;
while (left < canvas.width) {
ctx.drawImage(tabletop,
tabletopLeft, 0,
tabletopLeaf, tabletop.height,
left, 0,
canvas.height * tabletopLeaf / tabletop.height, canvas.height);
left += canvas.height * tabletopLeaf / tabletop.height;
}
}
ctx.restore();
ctx.scale(minSize / hexagonRatio, minSize / hexagonRatio);
ctx.translate(0.5 * hexagonRatio, 0.5 * hexagonRatio);
ctx.lineWidth = 2. / minSize;
/* Board dimensions:
* ________
* /___1__| \
* / / \6\
* /2/ \ \
* / / \/\
* \/\ / /
* \ \ /5/
* \3\______/_/
* \_|__4___/
* 0 0.3 0.6 1
*/
ctx.save();
drawBorders(ctx);
ctx.restore();
ctx.save();
drawPips(ctx);
ctx.restore();
ctx.fillStyle = "rgba(128, 128, 0, 0.125)";
ctx.strokeStyle = "rgba(255, 255, 0, 0.5)";
if (game.state !== 'lobby') {
const roll =
(game.dice[0] ? game.dice[0] : 0) + (game.dice[1] ? game.dice[1] : 0);
if (roll) tiles.forEach((tile) => {
if (tile.pip.roll === roll) {
ctx.save();
ctx.beginPath();
ctx.arc(tile.pos.x, tile.pos.y, tileHeight * 0.5, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
}
});
}
if (closest.tile) {
ctx.save();
Object.assign(ctx, getPlayerColors(game.color));
ctx.translate(closest.tile.pos.x, closest.tile.pos.y);
/* draw circle hovered current tile
ctx.beginPath();
ctx.arc(0, 0, tileHeight * 0.5, 0, Math.PI * 2.);
ctx.stroke();
*/
/* road */
let angle = Math.round(closest.info.angle / (Math.PI / 3.)) * (Math.PI / 3.);
ctx.rotate(angle);
ctx.translate(-tileHeight * 0.5, 0);
ctx.beginPath();
ctx.rect(-roadSize * 0.125, -roadSize * 0.5, roadSize * 0.25, roadSize);
ctx.fill();
ctx.stroke();
ctx.translate(tileHeight * 0.5, 0);
ctx.rotate(-angle);
/* village */
angle = (closest.info.angle - Math.PI / 6.);
angle = Math.round(angle / (Math.PI / 3.)) * (Math.PI / 3.);
angle += Math.PI / 6.;
ctx.rotate(angle);
ctx.translate(-tileWidth * 0.5, 0);
ctx.rotate(-angle);
ctx.beginPath();
ctx.rect(-settlementSize * 0.5, -settlementSize * 0.5, settlementSize, settlementSize);
ctx.fill();
ctx.stroke();
ctx.rotate(angle);
ctx.translate(+tileWidth * 0.5, 0);
ctx.rotate(-angle);
ctx.restore();
}
/* For 0.5 after mouse movement, there is an on
* screen mouse helper. */
if (mouse.timer) {
ctx.strokeStyle = "rgba(0, 255, 255)";
ctx.fillStyle = "rgba(0, 255, 255, 0.25)";
ctx.beginPath();
ctx.arc(mouse.x, mouse.y,
tileHeight * 0.5, 0, Math.PI * 2.);
ctx.stroke();
ctx.fill();
}
ctx.restore();
}, [ game, canvasRef, closest, mouse, minSize, drawBorders, drawPips, tabletop, tiles ]);
const mouseMove = useCallback((event) => {
let x, y;
if (event.changedTouches && event.changedTouches.length > 0) {
x = event.changedTouches[0].clientX;
y = event.changedTouches[0].clientY;
} else {
x = event.clientX;
y = event.clientY;
}
/* Hide the mouse cursor circle after 0.5s */
if (mouse.timer) {
window.clearTimeout(mouse.timer);
}
let timer = window.setTimeout(() => {
mouse.timer = null;
window.requestAnimationFrame(drawFrame);
}, 500);
/* Scale mouse.x and mouse.y relative to board */
setMouse({
x: x / (minSize / hexagonRatio) - 0.5 - tileHeight * 0.5,
y: y / (minSize / hexagonRatio) - 0.5 - tileHeight * 0.5,
timer: timer
});
let tmp = null;
tiles.forEach((tile) => {
const dX = tile.pos.x - mouse.x,
dY = tile.pos.y - mouse.y;
const distance = Math.sqrt(dX * dX + dY * dY);
if (distance > tileHeight * 0.75) {
return;
}
if (!tmp || tmp.distance > distance) {
tmp = {
tile: tile,
distance: distance,
angle: (distance !== 0.0) ? Math.atan2(dY, dX) : 0
x += 77.5;
}
}
return divs;
});
};
if (!tmp) {
closest.tile = null;
closest.info.distance = -1;
closest.road = null;
closest.angle = 0;
closest.settlement = null;
closest.tradeToken = null;
} else {
closest.info.distance = closest.distance;
closest.info.angle = closest.angle;
const generateBorders = () => {
const divs = [],
radius = 77.5 * 2;
const sides = 6;
for (let side = 0; side < sides; side++) {
let x = center.x + Math.sin(Math.PI - side / sides * 2. * Math.PI) * radius,
y = -33.5 + center.y + Math.cos(Math.PI - side / sides * 2. * Math.PI) * radius;
let prev = (side == 0) ? 6 : side;
const file = `borders-${side+1}.${prev}.png`;
divs.push(<div
key={`border-${side}`}
className="Border"
style={{
top: `${y}px`,
left: `${x}px`,
transform: `rotate(${side*(360/sides)}deg) translate(89px, 0) scale(-1, -1)`,
backgroundImage: `url(${assetsPath}/gfx/${file} )`
}}>{side}</div>);
}
return divs;
};
setClosest(closest);
window.requestAnimationFrame(drawFrame);
}, [ drawFrame, closest, setClosest, setMouse, minSize, mouse, tiles ]);
const updateDimensions = useCallback(() => {
if (canvasRef.current.updateSizeTimer) {
clearTimeout(canvasRef.current.updateSizeTimer);
}
canvasRef.current.updateSizeTimer = setTimeout(() => {
const width = canvasRef.current.offsetWidth,
height = canvasRef.current.offsetHeight;
if (width !== canvasRef.current.width ||
height !== canvasRef.current.height) {
canvasRef.current.setAttribute("width", width);
canvasRef.current.setAttribute("height", height);
setMinSize(Math.min(height, width));
}
canvasRef.current.updateSizeTimer = 0;
}, 250);
window.requestAnimationFrame(drawFrame);
}, [ drawFrame, setMinSize ]);
const updateGame = useCallback((game) => {
if (!game || game.state === "invalid") {
return;
}
setSignature(gameSignature(game));
setTiles(Tiles(game, drawFrame));
setPips({
image: loadImage('pip-numbers.png', drawFrame),
pips: game.pips
});
setTabletop(loadImage('tabletop.png', drawFrame));
setBorders(game.borders.map((border) => {
return {
image: loadImage(border.file, drawFrame)
};
}));
}, [drawFrame, setTiles, setPips, setTabletop, setBorders, setSignature]);
useEffect(() => {
if (!canvasRef.current) {
return;
}
if (signature !== gameSignature(game)) {
updateGame(game);
}
const canvas = canvasRef.current;
canvas.addEventListener('mousemove', mouseMove);
canvas.addEventListener('touchmove', mouseMove);
window.addEventListener('resize', updateDimensions);
updateDimensions();
return () => {
canvas.removeEventListener('mousemove', mouseMove);
canvas.removeEventListener('touchmove', mouseMove);
window.removeEventListener('resize', updateDimensions);
};
}, [ mouseMove, updateDimensions, updateGame, signature, game ]);
const tiles = generateTiles(),
pips = generatePips(),
borders = generateBorders();
return (
<canvas className="Board"
ref={canvasRef}>
</canvas>
<div className="Board" style={{backgroundImage: `url(${assetsPath}/gfx/tabletop.png)`}}>
{ borders }
{ tiles }
{ pips }
</div>
);
};

View File

@ -411,7 +411,8 @@ class Table extends React.Component {
wheat: 0,
game: null,
message: "",
error: ""
error: "",
signature: ""
};
this.componentDidMount = this.componentDidMount.bind(this);
this.updateDimensions = this.updateDimensions.bind(this);
@ -426,6 +427,7 @@ class Table extends React.Component {
this.setPlayerName = this.setPlayerName.bind(this);
this.setSelected = this.setSelected.bind(this);
this.updateMessage = this.updateMessage.bind(this);
this.gameSignature = this.gameSignature.bind(this);
this.mouse = { x: 0, y: 0 };
this.radius = 0.317;
@ -470,7 +472,7 @@ class Table extends React.Component {
const error = (game.status !== 'success') ? game.status : undefined;
this.updateGame(game);
this.updateMessage();
this.setState({ game: game, error: error });
this.setState({ error: error });
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -507,7 +509,7 @@ class Table extends React.Component {
this.updateGame(game);
this.updateMessage();
this.setState({ game: game, error: message});
this.setState({ error: message});
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -538,7 +540,7 @@ class Table extends React.Component {
console.log (`Table shuffled!`);
this.updateGame(game);
this.updateMessage();
this.setState({ game: game, error: "Table shuffled!" });
this.setState({ error: "Table shuffled!" });
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -573,7 +575,7 @@ class Table extends React.Component {
}
this.updateGame(game);
this.updateMessage();
this.setState({ game: { ...this.state.game, dice: game.dice }, error: error } );
this.setState({ /*game: { ...this.state.game, dice: game.dice },*/ error: error } );
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -614,7 +616,7 @@ class Table extends React.Component {
this.updateGame(game);
this.updateMessage();
this.setState({ game: game, error: error });
this.setState({ error: error });
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -655,7 +657,7 @@ class Table extends React.Component {
return res.json();
}).then((game) => {
this.updateGame(game);
this.setState({ game: game, error: "" });
this.setState({ error: "" });
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -687,7 +689,7 @@ class Table extends React.Component {
console.log (`Game state set to ${game.state}!`);
this.updateGame(game);
this.updateMessage();
this.setState({ game: { ...this.state.game, state: game.state }, error: `Game state now ${game.state}.` });
this.setState({ /*game: { ...this.state.game, state: game.state }, */error: `Game state now ${game.state}.` });
}).catch((error) => {
console.error(error);
this.setState({error: error.message});
@ -780,10 +782,26 @@ class Table extends React.Component {
}, 250);
}
gameSignature(game) {
if (!game) {
return "";
}
const signature =
game.borders.map(border => border.file.replace(/borders-(.).*/, "$1")).join('') +
game.pips.map(pip => pip.roll.toString()).join('') +
game.tiles.map(tile => tile.type.charAt(0)).join('');
return signature;
};
updateGame(game) {
if (this.state.signature !== this.gameSignature(game)) {
this.setState({ signature: this.gameSignature(game), game: game });
}
// console.log("Update Game", game);
this.game = game;
this.setState({ game: game });
}
updateMessage() {
@ -901,7 +919,7 @@ class Table extends React.Component {
this.updateGame(game);
this.updateMessage();
this.setState({ game: game, error: "" });
this.setState({ error: "" });
}).catch((error) => {
console.error(error);
this.setState({error: error.message});