summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2024-08-22 23:36:26 -0400
committerpommicket <pommicket@gmail.com>2024-08-22 23:36:26 -0400
commita685c63f83bc12ac36d587c7c31ebf5dd170d5b4 (patch)
tree2c21b4378db748d2ce0a61f1660bd5d5fe52b831
parentae028a28f7a40b527512e362d9f059ff427493f4 (diff)
adjustable piece size, etc.
-rw-r--r--game.html1
-rw-r--r--game.js248
-rw-r--r--server/src/main.rs3
-rw-r--r--style.css13
4 files changed, 181 insertions, 84 deletions
diff --git a/game.html b/game.html
index 2784661..50bee57 100644
--- a/game.html
+++ b/game.html
@@ -14,6 +14,7 @@
<a href="index.html">← Back</a>
<a id="image-link" style="visibility: hidden;">🖼️ Link to image</a>
<a id="join-link" style="display: none;">🔗 Invite others to join</a>
+ <button style="width:3em;overflow:hidden;" id="piece-size-minus">🔍−</button> <button style="width:3em;overflow:hidden;" id="piece-size-plus">🔍+</button>
</div>
<div id="play-area">
</div>
diff --git a/game.js b/game.js
index a26dc27..4a90b48 100644
--- a/game.js
+++ b/game.js
@@ -70,6 +70,24 @@ window.addEventListener('load', function () {
y: scr.y / playArea.clientHeight,
};
}
+ function deriveConnectedPiecePositions() {
+ for (const piece of pieces) {
+ piece.deriveConnectedPiecePositions();
+ }
+ }
+ function setPieceSize(w, h) {
+ pieceWidth = w;
+ pieceHeight = h;
+ nibSize = Math.min(pieceWidth / 4, pieceHeight / 4);
+ for (const piece of pieces)
+ piece.updatePieceSize();
+ deriveConnectedPiecePositions();
+ document.body.style.setProperty('--piece-width', (pieceWidth) + 'px');
+ document.body.style.setProperty('--piece-height', (pieceHeight) + 'px');
+ document.body.style.setProperty('--nib-size', (nibSize) + 'px');
+ document.body.style.setProperty('--image-width', (pieceWidth * puzzleWidth) + 'px');
+ document.body.style.setProperty('--image-height', (pieceHeight * puzzleHeight) + 'px');
+ }
function random() {
// https://en.wikipedia.org/wiki/Linear_congruential_generator
// this uses the "Microsoft Visual/Quick C/C++" constants because
@@ -102,28 +120,23 @@ window.addEventListener('load', function () {
}
console.assert(false);
}
- function connectPieces(piece1, piece2) {
+ function connectPieces(piece1, piece2, interactive) {
if (piece1.connectedComponent === piece2.connectedComponent) return false;
- if (piece1.connectedComponent.length < piece2.connectedComponent.length) {
+ if (interactive && piece1.connectedComponent.length < piece2.connectedComponent.length) {
// always connect the smaller component to the larger component
- return connectPieces(piece2, piece1);
+ return connectPieces(piece2, piece1, interactive);
}
piece1.connectedComponent.push(...piece2.connectedComponent);
const maxZIndex = Math.max(...piece1.connectedComponent.map((x) => parseInt(x.element.style.zIndex)));
+ for (const piece of piece2.connectedComponent) {
+ piece.connectedComponent = piece1.connectedComponent;
+ }
for (const piece of piece1.connectedComponent) {
// update z-index to max in connected component
piece.element.style.zIndex = maxZIndex;
}
- let piece1Col = piece1.col();
- let piece1Row = piece1.row();
- for (const piece of piece2.connectedComponent) {
- piece.connectedComponent = piece1.connectedComponent;
- const row = piece.row();
- const col = piece.col();
- piece.x = (col - piece1Col) * pieceWidth / playArea.clientWidth + piece1.x;
- piece.y = (row - piece1Row) * pieceHeight / playArea.clientHeight + piece1.y;
- piece.updatePosition();
- }
+ if (interactive)
+ deriveConnectedPiecePositions();
if (!solved && piece1.connectedComponent.length === puzzleWidth * puzzleHeight) {
solveAudio.play();
solved = true;
@@ -162,13 +175,13 @@ window.addEventListener('load', function () {
}
randomize() {
const bendiness = 0.5;
- this.dx11 = Math.floor((random() * 2 - 1) * nibSize * bendiness);
- this.dy11 = Math.floor((random() * 2 - 1) * nibSize * bendiness);
- this.dx12 = Math.floor((random() * 2 - 1) * nibSize * bendiness);
+ this.dx11 = (random() * 2 - 1) * bendiness;
+ this.dy11 = (random() * 2 - 1) * bendiness;
+ this.dx12 = (random() * 2 - 1) * bendiness;
// this ensures base of nib is flat
- this.dy12 = nibSize;
- this.dx22 = Math.floor((random() * 2 - 1) * nibSize * bendiness);
- this.dy22 = Math.floor((random() * 2 - 1) * nibSize * bendiness);
+ this.dy12 = 1;
+ this.dx22 = (random() * 2 - 1) * bendiness;
+ this.dy22 = (random() * 2 - 1) * bendiness;
return this;
}
static random(orientation) {
@@ -176,19 +189,19 @@ window.addEventListener('load', function () {
}
path() {
let xMul = this.orientation === BOTTOM_IN || this.orientation === LEFT_IN
- || this.orientation === BOTTOM_OUT || this.orientation === LEFT_OUT ? -1 : 1;
+ || this.orientation === BOTTOM_OUT || this.orientation === LEFT_OUT ? -nibSize : nibSize;
let yMul = this.orientation === RIGHT_IN || this.orientation === BOTTOM_IN
- || this.orientation === TOP_OUT || this.orientation === LEFT_OUT ? -1 : 1;
+ || this.orientation === TOP_OUT || this.orientation === LEFT_OUT ? -nibSize : nibSize;
let dx11 = this.dx11 * xMul;
- let dy11 = (nibSize / 2 + this.dy11) * yMul;
+ let dy11 = (1 / 2 + this.dy11) * yMul;
let dx12 = this.dx12 * xMul;
let dy12 = this.dy12 * yMul;
- let dx22 = (nibSize / 2 + this.dx22) * xMul;
- let dy22 = (-nibSize / 2 + this.dy22) * yMul;
- let dx1 = (nibSize / 2) * xMul;
- let dy1 = nibSize * yMul;
- let dx2 = (nibSize / 2) * xMul;
- let dy2 = -nibSize * yMul;
+ let dx22 = (1 / 2 + this.dx22) * xMul;
+ let dy22 = (-1 / 2 + this.dy22) * yMul;
+ let dx1 = (1 / 2) * xMul;
+ let dy1 = yMul;
+ let dx2 = (1 / 2) * xMul;
+ let dy2 = -yMul;
if (this.orientation === LEFT_IN
|| this.orientation === RIGHT_IN
|| this.orientation === LEFT_OUT
@@ -213,40 +226,11 @@ window.addEventListener('load', function () {
nibTypes;
connectedComponent;
needsServerUpdate;
- getClipPath() {
- const nibTypes = this.nibTypes;
- let shoulderWidth = (pieceWidth - nibSize) / 2;
- let shoulderHeight = (pieceHeight - nibSize) / 2;
- let clipPath = [];
- clipPath.push(`M${nibSize} ${nibSize}`);
- clipPath.push(`l${shoulderWidth} 0`);
- if (nibTypes[0]) {
- clipPath.push(nibTypes[0].path());
- }
- clipPath.push(`L${pieceWidth + nibSize} ${nibSize}`);
- clipPath.push(`l0 ${shoulderHeight}`);
- if (nibTypes[1]) {
- clipPath.push(nibTypes[1].path());
- }
- clipPath.push(`L${pieceWidth + nibSize} ${pieceHeight + nibSize}`);
- clipPath.push(`l-${shoulderWidth} 0`);
- if (nibTypes[2]) {
- clipPath.push(nibTypes[2].path());
- }
- clipPath.push(`L${nibSize} ${pieceHeight + nibSize}`);
- clipPath.push(`l0 -${shoulderHeight}`);
- if (nibTypes[3]) {
- clipPath.push(nibTypes[3].path());
- }
- clipPath.push(`L${nibSize} ${nibSize}`);
- return clipPath.join(' ');
- }
- constructor(id, u, v, x, y, nibTypes) {
+ constructor(id, x, y, nibTypes) {
this.id = id;
this.x = x;
this.y = y;
- this.u = u;
- this.v = v;
+ this.nibTypes = nibTypes;
this.needsServerUpdate = false;
this.connectedComponent = [this];
const element = this.element = document.createElement('div');
@@ -261,22 +245,24 @@ window.addEventListener('load', function () {
this.style.cursor = 'none';
});
element.style.zIndex = 0; // default zIndex
- this.updateUV();
this.updatePosition();
- this.nibTypes = nibTypes;
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.element.appendChild(svg);
+ this.updatePieceSize();
+ // disable animation during initialization
+ this.setAnimate(false);
+ playArea.appendChild(element);
+ }
+ updatePieceSize() {
+ const svg = this.element.querySelector('svg');
const clipPath = this.getClipPath();
this.element.style.clipPath = `path("${clipPath}")`;
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('width', pieceWidth + 2 * nibSize);
svg.setAttribute('height', pieceHeight + 2 * nibSize);
svg.setAttribute('viewBox', `0 0 ${pieceWidth + 2 * nibSize} ${pieceHeight + 2 * nibSize}`);
svg.innerHTML = `<path d="${clipPath}" stroke-width="1" stroke="black" fill="none" />`;
- this.element.appendChild(svg);
- playArea.appendChild(element);
- }
- updateUV() {
- this.element.style.backgroundPositionX = (nibSize - this.u) + 'px';
- this.element.style.backgroundPositionY = (nibSize - this.v) + 'px';
+ this.element.style.backgroundPositionX = (nibSize - this.col() * pieceWidth) + 'px';
+ this.element.style.backgroundPositionY = (nibSize - this.row() * pieceHeight) + 'px';
}
col() {
return this.id % puzzleWidth;
@@ -294,12 +280,61 @@ window.addEventListener('load', function () {
left: pos.x, top: pos.y, right: pos.x + pieceWidth + 2 * nibSize, bottom: pos.y + pieceHeight + 2 * nibSize
});
}
+ deriveConnectedPiecePositions() {
+ if (this === this.connectedComponent[0]) {
+ const myRow = this.row();
+ const myCol = this.col();
+ for (const piece of this.connectedComponent) {
+ if (piece === this) continue;
+ const row = piece.row();
+ const col = piece.col();
+ piece.x = (col - myCol) * pieceWidth / playArea.clientWidth + this.x;
+ piece.y = (row - myRow) * pieceHeight / playArea.clientHeight + this.y;
+ piece.updatePosition();
+ }
+ }
+ }
+ setAnimate(enabled) {
+ if (enabled) {
+ this.element.classList.remove('no-animation');
+ } else {
+ this.element.classList.add('no-animation');
+ }
+ }
+ getClipPath() {
+ const nibTypes = this.nibTypes;
+ let shoulderWidth = (pieceWidth - nibSize) / 2;
+ let shoulderHeight = (pieceHeight - nibSize) / 2;
+ let clipPath = [];
+ clipPath.push(`M${nibSize} ${nibSize}`);
+ clipPath.push(`l${shoulderWidth} 0`);
+ if (nibTypes[0]) {
+ clipPath.push(nibTypes[0].path());
+ }
+ clipPath.push(`L${pieceWidth + nibSize} ${nibSize}`);
+ clipPath.push(`l0 ${shoulderHeight}`);
+ if (nibTypes[1]) {
+ clipPath.push(nibTypes[1].path());
+ }
+ clipPath.push(`L${pieceWidth + nibSize} ${pieceHeight + nibSize}`);
+ clipPath.push(`l-${shoulderWidth} 0`);
+ if (nibTypes[2]) {
+ clipPath.push(nibTypes[2].path());
+ }
+ clipPath.push(`L${nibSize} ${pieceHeight + nibSize}`);
+ clipPath.push(`l0 -${shoulderHeight}`);
+ if (nibTypes[3]) {
+ clipPath.push(nibTypes[3].path());
+ }
+ clipPath.push(`L${nibSize} ${nibSize}`);
+ return clipPath.join(' ');
+ }
}
window.addEventListener('mouseup', function() {
if (draggingPiece) {
let anyConnected = false;
for (const piece of draggingPiece.connectedComponent) {
- piece.element.classList.remove('no-animation');
+ piece.setAnimate(true);
piece.element.style.zIndex = pieceZIndexCounter;
if (solved) break;
piece.needsServerUpdate = true;
@@ -323,7 +358,7 @@ window.addEventListener('load', function () {
let sqDist = diff[0] * diff[0] + diff[1] * diff[1];
if (sqDist < connectRadius * connectRadius) {
anyConnected = true;
- connectPieces(piece, neighbour);
+ connectPieces(piece, neighbour, true);
socket.send(`connect ${piece.id} ${neighbour.id}`);
}
}
@@ -340,15 +375,15 @@ window.addEventListener('load', function () {
let dy = (e.clientY - draggingPieceLastPos.y) / playArea.clientHeight;
for (const piece of draggingPiece.connectedComponent) {
// ensure pieces don't go past left edge
- dx = Math.max(dx, 0.001 - piece.x);
- dy = Math.max(dy, 0.001 - piece.y);
+ dx = Math.max(dx, 0.0001 - piece.x);
+ dy = Math.max(dy, 0.0001 - piece.y);
// ensure pieces don't go past right edge
dx = Math.min(dx, 1.5 - piece.x);
dy = Math.min(dy, 1.5 - piece.y);
}
for (const piece of draggingPiece.connectedComponent) {
piece.element.style.zIndex = pieceZIndexCounter;
- piece.element.classList.add('no-animation');
+ piece.setAnimate(false);
piece.x += dx;
piece.y += dy;
piece.updatePosition();
@@ -373,8 +408,20 @@ window.addEventListener('load', function () {
console.assert(connectivity.length === pieces.length);
let anyConnected = false;
for (let i = 0; i < pieces.length; i++) {
- anyConnected |= connectPieces(pieces[i], pieces[connectivity[i]]);
+ anyConnected |= connectPieces(pieces[i], pieces[connectivity[i]], false);
}
+ for (let i = 0; i < pieces.length; i++) {
+ const piece = pieces[i];
+ const connectedComponent = piece.connectedComponent;
+ if (i === connectivity[i] && piece !== connectedComponent[0]) {
+ // ensure piece i comes first in my connected component if i === connectivity[i]
+ const index = connectedComponent.indexOf(piece);
+ connectedComponent.splice(index, 1);
+ connectedComponent.unshift(piece);
+ console.log(connectedComponent[0]);
+ }
+ }
+ deriveConnectedPiecePositions();
if (anyConnected) connectAudio.play();
}
async function initPuzzle(payload) {
@@ -405,18 +452,18 @@ window.addEventListener('load', function () {
}
let nibTypeIndex = 0;
if (playArea.clientWidth / puzzleWidth < playArea.clientHeight / puzzleHeight) {
- pieceWidth = 0.6 * playArea.clientWidth / puzzleWidth;
+ pieceWidth = 0.8 * playArea.clientWidth / puzzleWidth;
pieceHeight = pieceWidth * (puzzleWidth / puzzleHeight) * (image.height / image.width);
} else {
- pieceHeight = 0.6 * playArea.clientHeight / puzzleHeight;
+ pieceHeight = 0.8 * playArea.clientHeight / puzzleHeight;
pieceWidth = pieceHeight * (puzzleHeight / puzzleWidth) * (image.width / image.height);
}
- nibSize = Math.min(pieceWidth / 4, pieceHeight / 4);
- document.body.style.setProperty('--piece-width', (pieceWidth) + 'px');
- document.body.style.setProperty('--piece-height', (pieceHeight) + 'px');
- document.body.style.setProperty('--nib-size', (nibSize) + 'px');
- document.body.style.setProperty('--image-width', (pieceWidth * puzzleWidth) + 'px');
- document.body.style.setProperty('--image-height', (pieceHeight * puzzleHeight) + 'px');
+ // ensure full puzzle doesn't take up too much screen space
+ while (pieceWidth * puzzleWidth * pieceHeight * puzzleHeight > Math.max(1000, 0.5 * playArea.clientWidth * playArea.clientHeight)) {
+ pieceWidth *= 0.9;
+ pieceHeight *= 0.9;
+ }
+ setPieceSize(pieceWidth, pieceHeight);
for (let v = 0; v < puzzleHeight; v++) {
for (let u = 0; u < puzzleWidth; u++) {
let nibs = [null, null, null, null];
@@ -431,23 +478,30 @@ window.addEventListener('load', function () {
nibs[2] = NibType.random(Math.floor(random() * 2) ? BOTTOM_IN : BOTTOM_OUT);
}
if (u > 0) nibs[3] = pieces[id - 1].nibTypes[1].inverse();
- pieces.push(new Piece(id, u * pieceWidth, v * pieceHeight, 0, 0, nibs));
+ pieces.push(new Piece(id, 0, 0, nibs));
}
}
console.assert(nibTypeIndex === nibTypeCount);
for (let id = 0; id < pieces.length; id++) {
+ if (id !== connectivity[id]) continue; // only set one piece positions per piece group
pieces[id].x = piecePositions[2 * connectivity[id]];
pieces[id].y = piecePositions[2 * connectivity[id] + 1];
pieces[id].updatePosition();
}
updateConnectivity(connectivity);
+ // a bit janky, but it stops the pieces from animating to their starting positions
+ setTimeout(() => {
+ for (const piece of pieces) {
+ piece.setAnimate(true);
+ }
+ }, 100);
}
function applyUpdate(update) {
const piecePositions = new Float32Array(update, 8, puzzleWidth * puzzleHeight * 2);
const connectivity = new Uint16Array(update, 8 + piecePositions.length * 4, puzzleWidth * puzzleHeight);
updateConnectivity(connectivity);
for (let i = 0; i < pieces.length; i++) {
- // only receive the position of one piece per equivalence class mod is-connected-to
+ // only receive the position of one piece per piece group
if (connectivity[i] !== i) continue;
const piece = pieces[i];
if (piece.needsServerUpdate) continue;
@@ -555,4 +609,30 @@ window.addEventListener('load', function () {
}
}
});
+ const prevPlayAreaSize = Object.preventExtensions({width: playArea.clientWidth, height: playArea.clientHeight});
+ function everyFrame() {
+ if (prevPlayAreaSize !== playArea.clientWidth || prevPlayAreaSize !== playArea.clientHeight) {
+ // disable animations while moving the pieces
+ for (const piece of pieces) {
+ piece.setAnimate(false);
+ }
+ // re-derive piece positions so connected pieces don't disconnect
+ deriveConnectedPiecePositions();
+ setTimeout(() => {
+ for (const piece of pieces) {
+ piece.setAnimate(true);
+ }
+ }, 100);
+ prevPlayAreaSize.width = playArea.clientWidth;
+ prevPlayAreaSize.height = playArea.clientHeight;
+ }
+ requestAnimationFrame(everyFrame);
+ }
+ getById('piece-size-plus').addEventListener('click', () => {
+ setPieceSize(pieceWidth * 1.2, pieceHeight * 1.2);
+ });
+ getById('piece-size-minus').addEventListener('click', () => {
+ setPieceSize(pieceWidth / 1.2, pieceHeight / 1.2);
+ });
+ requestAnimationFrame(everyFrame);
});
diff --git a/server/src/main.rs b/server/src/main.rs
index c923c7e..7de3d68 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -312,6 +312,9 @@ async fn handle_websocket(
.ok_or(Error::BadSyntax)?
.parse()
.map_err(|_| Error::BadSyntax)?;
+ if width < 3 || height < 3 {
+ return Err(Error::BadSyntax);
+ }
let url: String = parts.next().ok_or(Error::BadSyntax)?.replace(';', " ");
if url.len() > 2048 {
return Err(Error::ImageURLTooLong);
diff --git a/style.css b/style.css
index 47e9f71..b074ea1 100644
--- a/style.css
+++ b/style.css
@@ -32,6 +32,19 @@ a, a:visited {
padding: 0 0.5em;
}
+#header button {
+ border: 2px solid #000;
+ background: #ddd;
+}
+#header button:hover {
+ border: 2px solid #000;
+ background: #bbb;
+}
+#header button:active {
+ border: 2px solid #000;
+ background: #999;
+}
+
#play-area {
width: 100vw;
/* annoyingly, firefox ESR 115 still doesn't support lh */