From 49dad26138d3dee0cacbb94ad8d3d3eff12c477a Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Mon, 30 Mar 2020 01:55:54 +0200 Subject: [PATCH] First draft of arrows + circles on board. Fix multi-connect detection --- client/public/images/circle.svg | 56 ++++++ client/public/images/mark.svg | 59 +++++- client/src/App.vue | 12 +- client/src/components/BaseGame.vue | 27 ++- client/src/components/Board.vue | 300 +++++++++++++++++++++++------ client/src/components/MoveList.vue | 3 + client/src/views/Game.vue | 105 +++++----- client/src/views/Hall.vue | 27 +-- server/sockets.js | 38 +--- 9 files changed, 463 insertions(+), 164 deletions(-) create mode 100644 client/public/images/circle.svg diff --git a/client/public/images/circle.svg b/client/public/images/circle.svg new file mode 100644 index 00000000..970670fd --- /dev/null +++ b/client/public/images/circle.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/client/public/images/mark.svg b/client/public/images/mark.svg index 3ee8e351..b7140678 100644 --- a/client/public/images/mark.svg +++ b/client/public/images/mark.svg @@ -1,5 +1,56 @@ - - - - + + + + + + image/svg+xml + + + + + + + diff --git a/client/src/App.vue b/client/src/App.vue index 369391e7..a961d7fc 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -336,18 +336,24 @@ div.board12 img.piece width: 100% -img.piece, img.mark-square +img.piece, img.mark-square, img.circle-square max-width: 100% height: auto display: block img.mark-square - opacity: 0.6 + opacity: .7 width: 76% position: absolute top: 12% left: 12% - opacity: .7 + +img.circle-square + opacity: 0.7 + width: 100% + position: absolute + top: 0 + left: 0 .in-shadow filter: brightness(50%) diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue index 1ec412e1..c3387e82 100644 --- a/client/src/components/BaseGame.vue +++ b/client/src/components/BaseGame.vue @@ -54,6 +54,7 @@ div#baseGame @showrules="showRules" @analyze="analyzePosition" @goto-move="gotoMove" + @reset-arrows="resetArrows" ) .clearer @@ -186,6 +187,10 @@ export default { if (e.deltaY < 0) this.undo(); else if (e.deltaY > 0) this.play(); }, + resetArrows: function() { + // TODO: make arrows scale with board, and remove this + this.$refs["board"].cancelResetArrows(); + }, showRules: function() { //this.$router.push("/variants/" + this.game.vname); window.open("#/variants/" + this.game.vname, "_blank"); //better @@ -250,11 +255,8 @@ export default { this.game.vname + "/?fen=" + this.vr.getFen().replace(/ /g, "_"); - if (this.game.mycolor) - newUrl += "&side=" + this.game.mycolor; - // Open in same tab in live games (against cheating) - if (this.game.type == "live") this.$router.push(newUrl); - else window.open("#" + newUrl); + if (!!this.game.mycolor) newUrl += "&side=" + this.game.mycolor; + window.open("#" + newUrl); }, download: function() { const content = this.getPgn(); @@ -279,15 +281,22 @@ export default { pgn += '\n'; for (let i = 0; i < this.moves.length; i += 2) { if (i > 0) pgn += " "; - pgn += (i/2+1) + "." + getFullNotation(this.moves[i]); + // Adjust dots notation for a better display: + let fullNotation = getFullNotation(this.moves[i]); + if (fullNotation == "...") fullNotation = ".."; + pgn += (i/2+1) + "." + fullNotation; if (i+1 < this.moves.length) pgn += " " + getFullNotation(this.moves[i+1]); } pgn += "\n\n"; for (let i = 0; i < this.moves.length; i += 2) { - pgn += getFullNotation(this.moves[i], "unambiguous") + "\n"; - if (i+1 < this.moves.length) - pgn += getFullNotation(this.moves[i+1], "unambiguous") + "\n"; + const moveNumber = i / 2 + 1; + pgn += moveNumber + "." + i + " " + + getFullNotation(this.moves[i], "unambiguous") + "\n"; + if (i+1 < this.moves.length) { + pgn += moveNumber + "." + (i+1) + " " + + getFullNotation(this.moves[i+1], "unambiguous") + "\n"; + } } return pgn; }, diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue index 80a79a4a..83dc8630 100644 --- a/client/src/components/Board.vue +++ b/client/src/components/Board.vue @@ -24,6 +24,10 @@ export default { choices: [], //promotion pieces, or checkered captures... (as moves) selectedPiece: null, //moving piece (or clicked piece) start: null, //pixels coordinates + id of starting square (click or drag) + startArrow: null, + movingArrow: { x: -1, y: -1 }, + arrows: [], //object of {start: x,y / end: x,y} + circles: {}, //object of squares' ID --> true (TODO: use a set?) click: "", clickTime: 0, settings: store.state.settings @@ -104,6 +108,7 @@ export default { }, [...Array(sizeY).keys()].map(j => { const cj = orientation == "w" ? j : sizeY - j - 1; + const squareId = "sq-" + ci + "-" + cj; let elems = []; if (showPiece(ci, cj)) { elems.push( @@ -112,7 +117,7 @@ export default { piece: true, ghost: !!this.selectedPiece && - this.selectedPiece.parentNode.id == "sq-" + ci + "-" + cj + this.selectedPiece.parentNode.id == squareId }, attrs: { src: @@ -140,6 +145,18 @@ export default { }) ); } + if (!!this.circles[squareId]) { + elems.push( + h("img", { + "class": { + "circle-square": true + }, + attrs: { + src: "/images/circle.svg" + } + }) + ); + } const lightSquare = (ci + cj) % 2 == lightSquareMod; return h( "div", @@ -363,6 +380,89 @@ export default { ); elementArray.unshift(choices); } + if ( + !this.mobileBrowser && + (this.arrows.length > 0 || this.movingArrow.x >= 0) + ) { + let svgArrows = []; + this.arrows.forEach(a => { + svgArrows.push( + h( + "path", + { + "class": { "svg-arrow": true }, + attrs: { + d: ( + "M" + a.start.x + "," + a.start.y + " " + + "L" + a.end.x + "," + a.end.y + ) + } + } + ) + ); + }); + if (this.movingArrow.x >= 0) { + svgArrows.push( + h( + "path", + { + "class": { "svg-arrow": true }, + attrs: { + d: ( + "M" + this.startArrow.x + "," + this.startArrow.y + " " + + "L" + this.movingArrow.x + "," + this.movingArrow.y + ) + } + } + ) + ); + } + // Add SVG element for drawing arrows + elementArray.push( + h( + "svg", + { + attrs: { + id: "arrowCanvas", + stroke: "none" + } + }, + [ + h( + "defs", + {}, + [ + h( + "marker", + { + attrs: { + id: "arrow", + markerWidth: "2", + markerHeight: "2", + markerUnits: "strokeWidth", + refX: "0", + refY: "1", + orient: "auto" + } + }, + [ + h( + "path", + { + attrs: { + d: "M0,0 L0,2 L2,1 z", + style: "fill: blue" + } + } + ) + ] + ) + ] + ) + ].concat(svgArrows) + ) + ); + } let onEvents = {}; // NOTE: click = mousedown + mouseup if (this.mobileBrowser) { @@ -378,80 +478,132 @@ export default { on: { mousedown: this.mousedown, mousemove: this.mousemove, - mouseup: this.mouseup + mouseup: this.mouseup, + contextmenu: this.blockContextMenu } }; } return h("div", onEvents, elementArray); }, methods: { + blockContextMenu: function(e) { + e.preventDefault(); + e.stopPropagation(); + return false; + }, + cancelResetArrows: function() { + this.startArrow = null; + this.arrows = []; + this.circles = {}; + }, mousedown: function(e) { + if (!([1, 3].includes(e.which))) return; e.preventDefault(); - if (!this.start) { - // NOTE: classList[0] is enough: 'piece' is the first assigned class - const withPiece = e.target.classList[0] == "piece"; - // Emit the click event which could be used by some variants - this.$emit( - "click-square", - getSquareFromId(withPiece ? e.target.parentNode.id : e.target.id) - ); - // Start square must contain a piece. - if (!withPiece) return; - let parent = e.target.parentNode; //surrounding square - // Show possible moves if current player allowed to play - const startSquare = getSquareFromId(parent.id); - this.possibleMoves = []; - const color = this.analyze ? this.vr.turn : this.userColor; - if (this.vr.canIplay(color, startSquare)) - this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare); - // For potential drag'n drop, remember start coordinates - // (to center the piece on mouse cursor) - let rect = parent.getBoundingClientRect(); - this.start = { + if (e.which != 3) + // Cancel current drawing and circles, if any + this.cancelResetArrows(); + if (e.which == 1 || this.mobileBrowser) { + // Mouse left button + if (!this.start) { + // NOTE: classList[0] is enough: 'piece' is the first assigned class + const withPiece = (e.target.classList[0] == "piece"); + // Emit the click event which could be used by some variants + this.$emit( + "click-square", + getSquareFromId(withPiece ? e.target.parentNode.id : e.target.id) + ); + // Start square must contain a piece. + if (!withPiece) return; + let parent = e.target.parentNode; //surrounding square + // Show possible moves if current player allowed to play + const startSquare = getSquareFromId(parent.id); + this.possibleMoves = []; + const color = this.analyze ? this.vr.turn : this.userColor; + if (this.vr.canIplay(color, startSquare)) + this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare); + // For potential drag'n drop, remember start coordinates + // (to center the piece on mouse cursor) + const rect = parent.getBoundingClientRect(); + this.start = { + x: rect.x + rect.width / 2, + y: rect.y + rect.width / 2, + id: parent.id + }; + // Add the moving piece to the board, just after current image + this.selectedPiece = e.target.cloneNode(); + Object.assign( + this.selectedPiece.style, + { + position: "absolute", + top: 0, + display: "inline-block", + zIndex: 3000 + } + ); + parent.insertBefore(this.selectedPiece, e.target.nextSibling); + } else { + this.processMoveAttempt(e); + } + } else { + // e.which == 3 : mouse right button + let elem = e.target; + // Next loop because of potential marks + while (elem.tagName == "IMG") elem = elem.parentNode; + // To center the arrow in square: + const rect = elem.getBoundingClientRect(); + this.startArrow = { x: rect.x + rect.width / 2, y: rect.y + rect.width / 2, - id: parent.id + id: elem.id }; - // Add the moving piece to the board, just after current image - this.selectedPiece = e.target.cloneNode(); + } + }, + mousemove: function(e) { + if (!this.selectedPiece && !this.startArrow) return; + e.preventDefault(); + if (!!this.selectedPiece) { + // There is an active element: move it around + const [offsetX, offsetY] = + this.mobileBrowser + ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY] + : [e.clientX, e.clientY]; Object.assign( this.selectedPiece.style, { - position: "absolute", - top: 0, - display: "inline-block", - zIndex: 3000 + left: offsetX - this.start.x + "px", + top: offsetY - this.start.y + "px" } ); - parent.insertBefore(this.selectedPiece, e.target.nextSibling); - } else { - this.processMoveAttempt(e); } - }, - mousemove: function(e) { - if (!this.selectedPiece) return; - e.preventDefault(); - // There is an active element: move it around - const [offsetX, offsetY] = - this.mobileBrowser - ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY] - : [e.clientX, e.clientY]; - Object.assign( - this.selectedPiece.style, - { - left: offsetX - this.start.x + "px", - top: offsetY - this.start.y + "px" + else { + let elem = e.target; + // Next loop because of potential marks + while (elem.tagName == "IMG") elem = elem.parentNode; + // To center the arrow in square: + if (elem.id != this.startArrow.id) { + const rect = elem.getBoundingClientRect(); + this.movingArrow = { + x: rect.x + rect.width / 2, + y: rect.y + rect.width / 2 + }; } - ); + } }, mouseup: function(e) { - if (!this.selectedPiece) return; + if (!([1, 3].includes(e.which))) return; e.preventDefault(); - // Drag'n drop. Selected piece is no longer needed: - this.selectedPiece.parentNode.removeChild(this.selectedPiece); - delete this.selectedPiece; - this.selectedPiece = null; - this.processMoveAttempt(e); + if (e.which == 1) { + if (!this.selectedPiece) return; + // Drag'n drop. Selected piece is no longer needed: + this.selectedPiece.parentNode.removeChild(this.selectedPiece); + delete this.selectedPiece; + this.selectedPiece = null; + this.processMoveAttempt(e); + } else { + // Mouse right button (e.which == 3) + this.movingArrow = { x: -1, y: -1 }; + this.processArrowAttempt(e); + } }, processMoveAttempt: function(e) { // Obtain the move from start and end squares @@ -482,6 +634,31 @@ export default { } else if (moves.length == 1) this.play(moves[0]); // else: forbidden move attempt }, + processArrowAttempt: function(e) { + // Obtain the arrow from start and end squares + const [offsetX, offsetY] = [e.clientX, e.clientY]; + let landing = document.elementFromPoint(offsetX, offsetY); + // Next condition: classList.contains(piece) fails because of marks + while (landing.tagName == "IMG") landing = landing.parentNode; + if (this.startArrow.id == landing.id) + // Draw (or erase) a circle + this.$set(this.circles, landing.id, !this.circles[landing.id]); + else { + // OK: add arrow, landing is a new square + const rect = landing.getBoundingClientRect(); + this.arrows.push({ + start: { + x: this.startArrow.x, + y: this.startArrow.y + }, + end: { + x: rect.x + rect.width / 2, + y: rect.y + rect.width / 2 + } + }); + } + this.startArrow = null; + }, findMatchingMoves: function(endSquare) { // Run through moves list and return the matching set (if promotions...) return ( @@ -538,6 +715,21 @@ img.ghost opacity: 0.5 top: 0 +#arrowCanvas + pointer-events: none + position: absolute + top: 0 + left: 0 + width: 100% + height: 100% + +.svg-arrow + opacity: 0.65 + stroke: #5f0e78 + stroke-width: 10px + fill: none + marker-end: url(#arrow) + .incheck-light background-color: rgba(204, 51, 0, 0.7) !important .incheck-dark diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue index 6eaf43ce..d2ae3f35 100644 --- a/client/src/components/MoveList.vue +++ b/client/src/components/MoveList.vue @@ -149,6 +149,9 @@ export default { adjustBoard: function() { const boardContainer = document.getElementById("boardContainer"); if (!boardContainer) return; //no board on page + let arrows = document.getElementById("arrowCanvas"); + // TODO: arrows on board don't scale + if (!!arrows) this.$emit("reset-arrows"); const k = document.getElementById("boardSize").value; const movesWidth = window.innerWidth >= 768 ? 280 : 0; const minBoardWidth = 240; //TODO: these 240 and 280 are arbitrary... diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 19033f50..b77f6b0f 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -96,7 +96,7 @@ main ) img(src="/images/icons/rematch.svg") #playersInfo - p + p(v-if="largeScreen") span.name(:class="{connected: isConnected(0)}") | {{ game.players[0].name || "@nonymous" }} span.time( @@ -118,6 +118,29 @@ main span.time-separator(v-if="!!virtualClocks[1][1]") : span.time-right(v-if="!!virtualClocks[1][1]") | {{ virtualClocks[1][1] }} + p(v-else) + span.name(:class="{connected: isConnected(0)}") + | {{ game.players[0].name || "@nonymous" }} + span.split-names - + span.name(:class="{connected: isConnected(1)}") + | {{ game.players[1].name || "@nonymous" }} + br + span.time( + v-if="game.score=='*'" + :class="{yourturn: !!vr && vr.turn == 'w'}" + ) + span.time-left {{ virtualClocks[0][0] }} + span.time-separator(v-if="!!virtualClocks[0][1]") : + span.time-right(v-if="!!virtualClocks[0][1]") + | {{ virtualClocks[0][1] }} + span.time( + v-if="game.score=='*'" + :class="{yourturn: !!vr && vr.turn == 'b'}" + ) + span.time-left {{ virtualClocks[1][0] }} + span.time-separator(v-if="!!virtualClocks[1][1]") : + span.time-right(v-if="!!virtualClocks[1][1]") + | {{ virtualClocks[1][1] }} BaseGame( ref="basegame" :game="game" @@ -186,8 +209,7 @@ export default { retrySendmove: null, clockUpdate: null, // Related to (killing of) self multi-connects: - newConnect: {}, - killed: {} + newConnect: {} }; }, watch: { @@ -317,7 +339,6 @@ export default { this.retrySendmove = null; this.clockUpdate = null; this.newConnect = {}; - this.killed = {}; // 1] Initialize connection this.connexionString = params.socketUrl + @@ -522,7 +543,8 @@ export default { } } ); - this.newConnect[data.from] = true; //for self multi-connects tests + // For self multi-connects tests: + this.newConnect[data.from[0]] = true; this.send("askidentity", { target: data.from[0] }); } else { this.people[data.from[0]].tmpIds[data.from[1]] = { focus: true }; @@ -552,13 +574,6 @@ export default { } break; } - case "killed": - // I logged in elsewhere: - this.conn.removeEventListener("message", this.socketMessageListener); - this.conn.removeEventListener("close", this.socketCloseListener); - this.conn = null; - alert(this.st.tr["New connexion detected: tab now offline"]); - break; case "askidentity": { // Request for identification const me = { @@ -579,46 +594,44 @@ export default { this.$forceUpdate(); //TODO: shouldn't be required // If I multi-connect, kill current connexion if no mark (I'm older) if (this.newConnect[user.sid]) { + delete this.newConnect[user.sid]; if ( user.id > 0 && user.id == this.st.user.id && - user.sid != this.st.user.sid && - !this.killed[this.st.user.sid] + user.sid != this.st.user.sid ) { - this.send("killme", { sid: this.st.user.sid }); - this.killed[this.st.user.sid] = true; + this.cleanBeforeDestroy(); + alert(this.st.tr["New connexion detected: tab now offline"]); + break; } - delete this.newConnect[user.sid]; } - if (!this.killed[this.st.user.sid]) { - // Ask potentially missed last state, if opponent and I play - if ( - !this.gotLastate && - !!this.game.mycolor && - this.game.type == "live" && - this.game.score == "*" && - this.game.players.some(p => p.sid == user.sid) - ) { - this.send("asklastate", { target: user.sid }); - let counter = 1; - this.askLastate = setInterval( - () => { - // Ask at most 3 times: - // if no reply after that there should be a network issue. - if ( - counter < 3 && - !this.gotLastate && - !!this.people[user.sid] - ) { - this.send("asklastate", { target: user.sid }); - counter++; - } else { - clearInterval(this.askLastate); - } - }, - 1500 - ); - } + // Ask potentially missed last state, if opponent and I play + if ( + !this.gotLastate && + !!this.game.mycolor && + this.game.type == "live" && + this.game.score == "*" && + this.game.players.some(p => p.sid == user.sid) + ) { + this.send("asklastate", { target: user.sid }); + let counter = 1; + this.askLastate = setInterval( + () => { + // Ask at most 3 times: + // if no reply after that there should be a network issue. + if ( + counter < 3 && + !this.gotLastate && + !!this.people[user.sid] + ) { + this.send("asklastate", { target: user.sid }); + counter++; + } else { + clearInterval(this.askLastate); + } + }, + 1500 + ); } break; } diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 0ed355bf..067a50db 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -257,8 +257,7 @@ export default { connexionString: "", socketCloseListener: 0, // Related to (killing of) self multi-connects: - newConnect: {}, - killed: {} + newConnect: {} }; }, watch: { @@ -638,13 +637,13 @@ export default { const page = data.page || "/"; if (data.code == "connect") { // Ask challenges only on first connexion: - if (!this.people[data.from]) + if (!this.people[data.from[0]]) this.send("askchallenges", { target: data.from[0] }); } // Ask game only if live: else if (!page.match(/\/[0-9]+$/)) this.send("askgame", { target: data.from[0], page: page }); - if (!this.people[data.from]) { + if (!this.people[data.from[0]]) { this.$set( this.people, data.from[0], @@ -654,7 +653,8 @@ export default { } } ); - this.newConnect[data.from] = true; //for self multi-connects tests + // For self multi-connects tests: + this.newConnect[data.from[0]] = true; this.send("askidentity", { target: data.from[0], page: page }); } else { this.people[data.from[0]].tmpIds[data.from[1]] = @@ -713,13 +713,6 @@ export default { } break; } - case "killed": - // I logged in elsewhere: - this.conn.removeEventListener("message", this.socketMessageListener); - this.conn.removeEventListener("close", this.socketCloseListener); - this.conn = null; - alert(this.st.tr["New connexion detected: tab now offline"]); - break; case "askidentity": { // Request for identification const me = { @@ -742,16 +735,16 @@ export default { this.$forceUpdate(); // If I multi-connect, kill current connexion if no mark (I'm older) if (this.newConnect[user.sid]) { + delete this.newConnect[user.sid]; if ( user.id > 0 && user.id == this.st.user.id && - user.sid != this.st.user.sid && - !this.killed[this.st.user.sid] + user.sid != this.st.user.sid ) { - this.send("killme", { sid: this.st.user.sid }); - this.killed[this.st.user.sid] = true; + // I logged in elsewhere: + this.cleanBeforeDestroy(); + alert(this.st.tr["New connexion detected: tab now offline"]); } - delete this.newConnect[user.sid]; } break; } diff --git a/server/sockets.js b/server/sockets.js index e791293e..3a776d1d 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -90,36 +90,6 @@ module.exports = function(wss) { // When page changes: doDisconnect(); break; - case "killme": { - // Self multi-connect: manual removal + disconnect - const doKill = (pg) => { - Object.keys(clients[pg][obj.sid]).forEach(x => { - send(clients[pg][obj.sid][x].socket, { code: "killed" }); - }); - delete clients[pg][obj.sid]; - }; - const disconnectFromOtherConnexion = (pg,code,o={}) => { - Object.keys(clients[pg]).forEach(k => { - if (k != obj.sid) { - Object.keys(clients[pg][k]).forEach(x => { - send( - clients[pg][k][x].socket, - Object.assign({ code: code, from: obj.sid }, o) - ); - }); - } - }); - }; - Object.keys(clients).forEach(pg => { - if (clients[pg][obj.sid]) { - doKill(pg); - disconnectFromOtherConnexion(pg, "disconnect"); - if (pg.indexOf("/game/") >= 0 && clients["/"]) - disconnectFromOtherConnexion("/", "gdisconnect", { page: pg }); - } - }); - break; - } case "pollclients": { // From Game let sockIds = {}; @@ -319,7 +289,13 @@ module.exports = function(wss) { case "getfocus": case "losefocus": - clients[page][sid][tmpId].focus = (obj.code == "getfocus"); + if ( + !!clients[page] && + !!clients[page][sid] && + !!clients[page][sid][tmpId] + ) { + clients[page][sid][tmpId].focus = (obj.code == "getfocus"); + } if (page == "/") notifyRoom("/", obj.code, { page: "/" }, [sid]); else { // Notify game room + Hall: -- 2.44.0