Skip to content

Commit dbe15f3

Browse files
committed
adding multiplayer demo
1 parent a208404 commit dbe15f3

File tree

8 files changed

+786
-4
lines changed

8 files changed

+786
-4
lines changed

demo.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { generatePuzzle } from './index.js';
2+
23
const container = document.getElementById("result");
34
const canvas = document.getElementById("original");
45
const defaultPuzzle = document.querySelector('#default');
@@ -45,7 +46,6 @@ function clear() {
4546
}
4647

4748
function random(x, y, width, height, containerWidth, containerHeight) {
48-
console.log(y, height, (Math.random() * (containerHeight - height)));
4949
return {
5050
x: x + (Math.random() * (containerWidth - width)),
5151
y: y + (Math.random() * (containerHeight - height)),

multiplayer-demo/client/index.css

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*.unselectable {
2+
-moz-user-select: -moz-none;
3+
-khtml-user-select: none;
4+
-webkit-user-select: none;
5+
6+
/*
7+
Introduced in IE 10.
8+
See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/
9+
*/
10+
-ms-user-select: none;
11+
user-select: none;
12+
}

multiplayer-demo/client/index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<title>Jigsaw Puzzle Demo</title>
4+
<link rel="stylesheet" href="index.css">
5+
</head>
6+
<body>
7+
<div id="playground" style="background-color: grey; display: inline-block;">
8+
<canvas id="original"></canvas>
9+
<div id="result"></div>
10+
</div>
11+
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
12+
<script src="../../index.js" defer type="module"></script>
13+
<script src="util.js" defer type="module"></script>
14+
<script src="index.js" defer type="module"></script>
15+
</body>
16+
</html>

multiplayer-demo/client/index.js

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { loadImage, random } from './util.js';
2+
import { generatePuzzle } from '../../index.js';
3+
4+
const socket = io('http://localhost:3000');
5+
6+
socket.on('gameState', handleGameState);
7+
8+
class GameState {
9+
puzzle;
10+
11+
constructor(puzzle) {
12+
this.puzzle = puzzle;
13+
}
14+
}
15+
16+
class ServerPuzzleState {
17+
row;
18+
col;
19+
x;
20+
y;
21+
imageSrc;
22+
isActive;
23+
24+
constructor({
25+
row, col, x, y, imageSrc, isActive
26+
}) {
27+
this.row = row;
28+
this.col = col;
29+
this.x = x;
30+
this.y = y;
31+
this.imageSrc = imageSrc;
32+
this.isActive = isActive;
33+
}
34+
}
35+
36+
const canvas = document.getElementById('original');
37+
const container = document.getElementById("result");
38+
let clientPuzzle;
39+
let isDown;
40+
let metadata;
41+
42+
init().then(puzzle => {
43+
clientPuzzle = puzzle;
44+
45+
const serverPuzzle = clientPuzzle.map(p =>
46+
new ServerPuzzleState({
47+
x: p.style.left.replace('px', ''),
48+
y: p.style.top.replace('px', ''),
49+
isActive: false,
50+
})
51+
);
52+
const serverGameState = new GameState(serverPuzzle);
53+
console.log(serverGameState);
54+
socket.emit('start', serverGameState);
55+
});
56+
57+
// Initialize the client puzzle and the game state
58+
async function init() {
59+
60+
const img = await loadImage("https://images.unsplash.com/photo-1478827217976-7214a0556393?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1500&q=80");
61+
62+
canvas.width = img.width;
63+
canvas.height = img.height;
64+
const context = canvas.getContext('2d');
65+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
66+
67+
// Creating the puzzle pieces
68+
const puzzle = await generatePuzzle(img, 25, 25);
69+
const puzzlePieces = puzzle.map(p => {
70+
const img = new Image();
71+
img.src = p.imageSrc;
72+
img.width = p.width;
73+
img.height = p.height;
74+
img.className = "unselectable";
75+
return img;
76+
});
77+
78+
// shuffle(puzzlePieces);
79+
const fragment = document.createDocumentFragment();
80+
puzzlePieces.forEach(piece => {
81+
piece.classList.add("puzzle-piece")
82+
fragment.appendChild(piece);
83+
});
84+
85+
container.appendChild(fragment);
86+
87+
puzzlePieces.forEach((piece, idx) => {
88+
piece.dataset.index = idx;
89+
piece.style.position = "absolute";
90+
const { x, y } = random(canvas.offsetLeft, canvas.offsetTop, piece.width, piece.height, canvas.width, canvas.height);
91+
92+
piece.style.left = `${x}px`;
93+
piece.style.top = `${y}px`;
94+
95+
piece.addEventListener('dragstart', (event) => {
96+
return false;
97+
});
98+
99+
['mousedown', 'touchstart'].forEach(listener => piece.addEventListener(listener, (event) => {
100+
const x = event.clientX || event.touches[0].pageX;
101+
const y = event.clientY || event.touches[0].pageY;
102+
console.log('touchstart');
103+
if (isDown) {
104+
return;
105+
}
106+
107+
isDown = true;
108+
metadata = {
109+
idx,
110+
offset: [
111+
piece.offsetLeft - x,
112+
piece.offsetTop - y
113+
]
114+
}
115+
}, { passive: false}));
116+
});
117+
118+
['mouseup', 'touchend'].forEach(listener => container.addEventListener(listener, function() {
119+
console.log('touchend');
120+
isDown = false;
121+
metadata = undefined;
122+
}, { passive: false }));
123+
124+
['mousemove', 'touchmove'].forEach(listener => container.addEventListener(listener, (event) => {
125+
event.preventDefault();
126+
console.log(isDown);
127+
if (isDown) {
128+
const piece = puzzlePieces[metadata.idx];
129+
const x = event.clientX || event.touches[0].pageX;
130+
const y = event.clientY || event.touches[0].pageY;
131+
const newX = x + metadata.offset[0];
132+
const newY = y + metadata.offset[1];
133+
134+
piece.style.left = `${newX}px`;
135+
piece.style.top = `${newY}px`;
136+
socket.emit('move', { idx: metadata.idx, x: newX, y: newY });
137+
}
138+
}, { passive: false }));
139+
140+
return puzzlePieces;
141+
}
142+
143+
// Render the game given the game state, match the updated server game state with the client game state
144+
function render(gameState) {
145+
const serverPuzzle = gameState.puzzle;
146+
147+
for (let i = 0; i < clientPuzzle.length; i++) {
148+
const sp = serverPuzzle[i];
149+
const cp = clientPuzzle[i];
150+
151+
if (metadata?.idx === i) {
152+
return;
153+
}
154+
155+
if (cp.style.left !== `${sp.x}px`) {
156+
cp.style.left = `${sp.x}px`;
157+
}
158+
159+
if (cp.style.top !== `${sp.y}px`) {
160+
cp.style.top = `${sp.y}px`;
161+
}
162+
}
163+
}
164+
165+
function handleGameState(gameState) {
166+
const state = JSON.parse(gameState);
167+
168+
requestAnimationFrame(() => render(state));
169+
}

multiplayer-demo/client/util.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function random(x, y, width, height, containerWidth, containerHeight) {
2+
return {
3+
x: x + (Math.random() * (containerWidth - width)),
4+
y: y + (Math.random() * (containerHeight - height)),
5+
}
6+
}
7+
8+
export function shuffle(arr) {
9+
for (let i = arr.length - 1; i >= 0; i--) {
10+
const idx = Math.floor(Math.random() * i);
11+
[arr[idx], arr[i]] = [arr[i], arr[idx]];
12+
}
13+
}
14+
15+
export async function loadImage(src) {
16+
const image = new Image();
17+
const loadPromise = new Promise(resolve => {
18+
image.onload = function() {
19+
resolve(this);
20+
}
21+
image.setAttribute('crossorigin', 'anonymous');
22+
image.src = src;
23+
});
24+
25+
return await loadPromise;
26+
}

multiplayer-demo/server/server.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const io = require('socket.io')(3000, {
2+
cors: {
3+
origin: ["http://localhost:8080"]
4+
}
5+
});
6+
7+
let state = {};
8+
let payload = {};
9+
10+
io.on('connection', client => {
11+
console.log('connected')
12+
client.on('start', handleStart);
13+
client.on('move', handleMove);
14+
15+
16+
function handleStart(gameState) {
17+
state = gameState;
18+
startGameInterval(client);
19+
}
20+
21+
function handleMove({ idx, x, y}) {
22+
console.log(...arguments);
23+
state.puzzle[idx].x = x;
24+
state.puzzle[idx].y = y;
25+
}
26+
});
27+
28+
function startGameInterval(client) {
29+
const intervalId = setInterval(() => {
30+
client.emit('gameState', JSON.stringify(state));
31+
}, 1000 / 60);
32+
}
33+

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jigsaw-canvas",
3-
"version": "0.0.1",
3+
"version": "0.1.0",
44
"description": "A jigsaw piece generator for use with canvas",
55
"main": "index.js",
66
"repository": "https://github.com/ygongdev/jigsaw-canvas",
@@ -11,5 +11,11 @@
1111
},
1212
"devDependencies": {
1313
"release-it": "^14.10.0"
14+
},
15+
"dependencies": {
16+
"express": "^4.17.1",
17+
"http": "^0.0.1-security",
18+
"socket.io": "^4.1.3",
19+
"socket.io-client": "^4.1.3"
1420
}
1521
}

0 commit comments

Comments
 (0)