I now this is very naive, but after watching a few soccer games I was vary curious to see if minimal spanning trees between soccer players could somehow model soccer tactics.
The file above can be opened in a browser, with no dependencies.
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Minimum Spanning Trees in Soccer Tactics</title> | |
| <style> | |
| /* The soccer field should a 1050/680 px green rectangle */ | |
| /* inside which we will place players */ | |
| svg#soccer-field { | |
| background-color: green; | |
| width: 1050px; | |
| height: 680px; | |
| } | |
| circle.soccer-player { | |
| fill: darkgreen; | |
| stroke: lightgreen; | |
| opacity: 50%; | |
| stroke-width: 2px; | |
| filter: drop-shadow(0 0 2px black); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Coordinates should be given in terms of METERS --> | |
| <!-- 1 METER = 10px --> | |
| <svg id="soccer-field" viewBox="0 0 105 68"> | |
| <!-- The goalie, numbered 11 --> | |
| <circle id="goalie" class="soccer-player" cx="10" cy="25" r="1" /> | |
| <!-- Four players in the back, numbered 1 2 3 4 --> | |
| <circle id="player-1" class="soccer-player" cx="30" cy="10" r="1" /> | |
| <circle id="player-2" class="soccer-player" cx="30" cy="20" r="1" /> | |
| <circle id="player-3" class="soccer-player" cx="30" cy="30" r="1" /> | |
| <circle id="player-4" class="soccer-player" cx="30" cy="40" r="1" /> | |
| <!-- Four players in the middle, numbered 5 6 7 8 --> | |
| <circle id="player-5" class="soccer-player" cx="50" cy="10" r="1" /> | |
| <circle id="player-6" class="soccer-player" cx="50" cy="20" r="1" /> | |
| <circle id="player-7" class="soccer-player" cx="50" cy="30" r="1" /> | |
| <circle id="player-8" class="soccer-player" cx="50" cy="40" r="1" /> | |
| <!-- Two players in the front, numbered 9 10 --> | |
| <circle id="player-9" class="soccer-player" cx="70" cy="20" r="1" /> | |
| <circle id="player-10" class="soccer-player" cx="70" cy="30" r="1" /> | |
| </svg> | |
| <script> | |
| function noise() {const players = document.querySelectorAll("circle.soccer-player"); | |
| // Move each player by a tiny, random amount by mutating cx/cy | |
| // attributes of each player | |
| players.forEach((player) => { | |
| const cx = parseFloat(player.getAttribute("cx")); | |
| const cy = parseFloat(player.getAttribute("cy")); | |
| const dx = Math.random() * 0.2 - 0.05; | |
| const dy = Math.random() * 0.2 - 0.05; | |
| player.setAttribute("cx", cx + dx); | |
| player.setAttribute("cy", cy + dy); | |
| }); | |
| drawMinimalSpanningTree(); | |
| } | |
| setInterval(noise, 50); | |
| function drawMinimalSpanningTree() { | |
| const players = document.querySelectorAll("circle.soccer-player"); | |
| const edges = []; | |
| players.forEach((player1) => { | |
| players.forEach((player2) => { | |
| if (player1 === player2) return; | |
| const cx1 = parseFloat(player1.getAttribute("cx")); | |
| const cy1 = parseFloat(player1.getAttribute("cy")); | |
| const cx2 = parseFloat(player2.getAttribute("cx")); | |
| const cy2 = parseFloat(player2.getAttribute("cy")); | |
| const dx = cx1 - cx2; | |
| const dy = cy1 - cy2; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| edges.push({ | |
| player1, | |
| player2, | |
| distance, | |
| selected: false | |
| }); | |
| }); | |
| }) | |
| // Select the edges belonging to the minimal spanning tree | |
| // using Kruskal's algorithm | |
| edges.sort((a, b) => a.distance - b.distance); | |
| const components = new Map(); | |
| edges.forEach((edge) => { | |
| const component1 = components.get(edge.player1); | |
| const component2 = components.get(edge.player2); | |
| if (component1 === undefined && component2 === undefined) { | |
| // Both players are in different components | |
| // Create a new component | |
| const component = new Set([edge.player1, edge.player2]); | |
| components.set(edge.player1, component); | |
| components.set(edge.player2, component); | |
| edge.selected = true; | |
| } else if (component1 === undefined) { | |
| // Player 1 is in a component, player 2 is not | |
| // Add player 1 to the component of player 2 | |
| component2.add(edge.player1); | |
| components.set(edge.player1, component2); | |
| edge.selected = true; | |
| } else if (component2 === undefined) { | |
| // Player 2 is in a component, player 1 is not | |
| // Add player 2 to the component of player 1 | |
| component1.add(edge.player2); | |
| components.set(edge.player2, component1); | |
| edge.selected = true; | |
| } else if (component1 !== component2) { | |
| // Both players are in different components | |
| // Merge the two components | |
| component1.forEach((player) => { | |
| components.set(player, component2); | |
| component2.add(player); | |
| }); | |
| edge.selected = true; | |
| } | |
| }); | |
| // Draw selected edges as new | |
| const newEdges = document.createElementNS("http://www.w3.org/2000/svg", "g"); | |
| edges.forEach((edge) => { | |
| if (edge.selected) { | |
| const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); | |
| line.setAttribute("x1", edge.player1.getAttribute("cx")); | |
| line.setAttribute("y1", edge.player1.getAttribute("cy")); | |
| line.setAttribute("x2", edge.player2.getAttribute("cx")); | |
| line.setAttribute("y2", edge.player2.getAttribute("cy")); | |
| line.setAttribute("stroke", "white"); | |
| line.setAttribute("stroke-width", "0.1"); | |
| newEdges.appendChild(line); | |
| } | |
| }); | |
| // Delete all edges in soccer-field | |
| document.querySelectorAll("line").forEach((g) => g.remove()); | |
| // Add new edges to soccer-field | |
| document.querySelector("svg").appendChild(newEdges); | |
| } | |
| // Make players dragable | |
| const players = document.querySelectorAll("circle.soccer-player"); | |
| // Apply a factor to the mouse movement to make dragging more natural | |
| players.forEach((player) => { | |
| player.addEventListener("mousedown", (event) => { | |
| const cx = parseFloat(player.getAttribute("cx")); | |
| const cy = parseFloat(player.getAttribute("cy")); | |
| const mousemove = (event) => { | |
| player.setAttribute("cx", event.clientX / 10); | |
| player.setAttribute("cy", event.clientY / 10); | |
| drawMinimalSpanningTree(); | |
| }; | |
| const mouseup = () => { | |
| document.removeEventListener("mousemove", mousemove); | |
| document.removeEventListener("mouseup", mouseup); | |
| }; | |
| document.addEventListener("mousemove", mousemove); | |
| document.addEventListener("mouseup", mouseup); | |
| }); | |
| }); | |
| </script> | |
| </div> | |
| </body> | |
| </html> |
Any one willing to hack along?
It looks like this currently: