@talabes/football-lineup-generator
Version:
A TypeScript library for generating visual football lineup diagrams from team positioning data. Fork of ncamaa/football-lineup-generator with bug fixes and improvements.
155 lines (154 loc) • 7.91 kB
JavaScript
import { Position, LayoutType } from '../types.js';
export function calculatePlayerCoordinates(players, width, height, layoutType, fieldOffsetX = 0, isHalfPitch = false, isHomeTeam = true, teamOffsetX = 0) {
// Group players by position
const playersByPosition = new Map();
for (const player of players) {
if (!playersByPosition.has(player.position)) {
playersByPosition.set(player.position, []);
}
const positionPlayers = playersByPosition.get(player.position);
if (positionPlayers) {
positionPlayers.push(player);
}
}
const result = [];
// Calculate base coordinates for each position
for (const [position, positionPlayers] of playersByPosition.entries()) {
const baseCoords = getBasePositionCoordinates(position, width, height, layoutType, fieldOffsetX, isHalfPitch, isHomeTeam);
// Apply team offset to base coordinates
const teamAdjustedCoords = {
x: baseCoords.x + teamOffsetX,
y: baseCoords.y
};
if (positionPlayers.length === 1) {
// Single player - use team adjusted coordinates
result.push({
player: positionPlayers[0],
coordinates: teamAdjustedCoords
});
}
else {
// Multiple players - spread them around the team adjusted base position
for (let index = 0; index < positionPlayers.length; index++) {
const player = positionPlayers[index];
const offsetCoords = calculatePositionOffset(teamAdjustedCoords, index, positionPlayers.length, position, layoutType);
result.push({
player,
coordinates: offsetCoords
});
}
}
}
return result;
}
function getBasePositionCoordinates(position, width, height, layoutType, fieldOffsetX, isHalfPitch, isHomeTeam) {
if (isHalfPitch) {
return getHalfPitchBaseCoords(position, width, height, isHomeTeam);
}
return getFullPitchBaseCoords(position, width, height, layoutType, fieldOffsetX);
}
function getFullPitchBaseCoords(position, width, height, layoutType, fieldOffsetX) {
const actualWidth = layoutType === LayoutType.SPLIT_PITCH ? width : width;
const fieldMargin = 50;
const fieldWidth = actualWidth - 2 * fieldMargin;
const baseCoords = {
[]: { x: fieldMargin + fieldWidth * 0.08, y: height / 2 },
[]: { x: fieldMargin + fieldWidth * 0.27, y: height * 0.2 },
[]: { x: fieldMargin + fieldWidth * 0.25, y: height * 0.5 },
[]: { x: fieldMargin + fieldWidth * 0.27, y: height * 0.8 },
[]: { x: fieldMargin + fieldWidth * 0.45, y: height / 2 },
[]: { x: fieldMargin + fieldWidth * 0.6, y: height * 0.2 },
[]: { x: fieldMargin + fieldWidth * 0.55, y: height / 2 },
[]: { x: fieldMargin + fieldWidth * 0.6, y: height * 0.8 },
[]: { x: fieldMargin + fieldWidth * 0.67, y: height / 2 },
[]: { x: fieldMargin + fieldWidth * 0.75, y: height * 0.2 },
[]: { x: fieldMargin + fieldWidth * 0.75, y: height * 0.8 },
[]: { x: fieldMargin + fieldWidth * 0.88, y: height * 0.35 },
[]: { x: fieldMargin + fieldWidth * 0.85, y: height / 2 },
[]: { x: fieldMargin + fieldWidth * 0.85, y: height * 0.65 },
[]: { x: actualWidth + 20, y: height / 2 },
};
const coords = baseCoords[position];
return {
x: coords.x + fieldOffsetX,
y: coords.y
};
}
function getHalfPitchBaseCoords(position, width, height, isHomeTeam) {
const fieldMargin = 50;
const fieldWidth = width - 2 * fieldMargin;
const halfWidth = fieldWidth / 2;
const baseX = isHomeTeam ? fieldMargin : fieldMargin + halfWidth;
const baseCoords = {
[]: { x: baseX + halfWidth * 0.15, y: height / 2 },
[]: { x: baseX + halfWidth * 0.4, y: height * 0.15 },
[]: { x: baseX + halfWidth * 0.4, y: height * 0.5 },
[]: { x: baseX + halfWidth * 0.4, y: height * 0.85 },
[]: { x: baseX + halfWidth * 0.6, y: height * 0.35 },
[]: { x: baseX + halfWidth * 0.75, y: height * 0.2 },
[]: { x: baseX + halfWidth * 0.6, y: height * 0.65 },
[]: { x: baseX + halfWidth * 0.75, y: height * 0.8 },
[]: { x: baseX + halfWidth * 0.85, y: height * 0.5 },
[]: { x: baseX + halfWidth * 0.9, y: height * 0.15 },
[]: { x: baseX + halfWidth * 0.9, y: height * 0.85 },
[]: { x: baseX + halfWidth * 0.95, y: height * 0.35 },
[]: { x: baseX + halfWidth * 0.95, y: height * 0.5 },
[]: { x: baseX + halfWidth * 0.95, y: height * 0.65 },
[]: { x: width + 20, y: height / 2 },
};
return baseCoords[position];
}
function calculatePositionOffset(baseCoords, playerIndex, totalPlayers, position, layoutType) {
// Increased offset distance for better separation
const baseOffsetDistance = 35; // Increased from 25
// Additional offset for same-position players to prevent label overlap
const labelOffsetMultiplier = totalPlayers > 1 ? 1.5 : 1;
const offsetDistance = baseOffsetDistance * labelOffsetMultiplier;
if (totalPlayers === 2) {
// For 2 players, place them with larger separation
const offset = playerIndex === 0 ? -offsetDistance / 1.5 : offsetDistance / 1.5;
// Layout-aware offset direction
if (layoutType === LayoutType.SPLIT_PITCH) {
// For split pitch, prefer horizontal offset to avoid field edge issues
return { x: baseCoords.x + offset, y: baseCoords.y };
}
// Determine offset direction based on position for other layouts
if (isVerticalPosition(position)) {
return { x: baseCoords.x, y: baseCoords.y + offset };
}
return { x: baseCoords.x + offset, y: baseCoords.y };
}
if (totalPlayers === 3) {
// For 3 players, create a wider triangle formation
const triangleOffsets = [
{ x: 0, y: 0 }, // Left vertex (original position)
{ x: offsetDistance / 3, y: -offsetDistance * 2 }, // Right top vertex
{ x: offsetDistance / 3, y: offsetDistance * 2 } // Right bottom vertex
];
return {
x: baseCoords.x + triangleOffsets[playerIndex].x,
y: baseCoords.y + triangleOffsets[playerIndex].y
};
}
// For more than 3 players, create a wider grid
const cols = Math.ceil(Math.sqrt(totalPlayers));
const rows = Math.ceil(totalPlayers / cols);
const col = playerIndex % cols;
const row = Math.floor(playerIndex / cols);
const xOffset = (col - (cols - 1) / 2) * (offsetDistance / 1.2); // Wider spacing
const yOffset = (row - (rows - 1) / 2) * (offsetDistance / 1.2);
return {
x: baseCoords.x + xOffset,
y: baseCoords.y + yOffset
};
}
function isVerticalPosition(position) {
// Positions that should be offset vertically when there are multiple players
return [
Position.CENTER_BACK,
Position.DEFENSIVE_MIDFIELDER,
Position.CENTER_MIDFIELDER,
Position.ATTACKING_MIDFIELDER,
Position.CENTER_FORWARD
].includes(position);
}