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?