UNPKG

@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.

181 lines (180 loc) 8.43 kB
import { Position } from './types.js'; export class InteractiveController { constructor(canvas, config, renderCallback, translateX = 0, translateY = 0) { this.lineupData = null; this.playerCoordinates = []; this.customCoordinates = new Map(); this.dragState = null; this.isDragging = false; this.canvasTranslateX = 0; this.canvasTranslateY = 0; this.handleMouseDown = (event) => { const coords = this.getCanvasCoordinates(event.clientX, event.clientY); const player = this.findPlayerAtPosition(coords.x, coords.y); if (player) { this.dragState = { player: player.player, isHomeTeam: player.isHomeTeam, offsetX: coords.x - player.coordinates.x, offsetY: coords.y - player.coordinates.y, }; this.isDragging = true; this.canvas.style.cursor = 'grabbing'; event.preventDefault(); } }; this.handleMouseMove = (event) => { const coords = this.getCanvasCoordinates(event.clientX, event.clientY); if (this.isDragging && this.dragState) { // Update custom coordinates - store directly in canvas space // Custom coordinates are applied AFTER all transformations in render functions const newX = coords.x - this.dragState.offsetX; const newY = coords.y - this.dragState.offsetY; const key = `${this.dragState.player.team}-${this.dragState.player.player.id}`; this.customCoordinates.set(key, { x: newX, y: newY }); // Trigger re-render this.renderCallback(); event.preventDefault(); } else { // Update cursor based on whether we're hovering over a player const player = this.findPlayerAtPosition(coords.x, coords.y); this.canvas.style.cursor = player ? 'grab' : 'default'; } }; this.handleMouseUp = (event) => { if (this.isDragging && this.dragState) { const coords = this.getCanvasCoordinates(event.clientX, event.clientY); const newX = coords.x - this.dragState.offsetX; const newY = coords.y - this.dragState.offsetY; // Call the onPlayerMove callback if provided if (this.config.onPlayerMove) { this.config.onPlayerMove(this.dragState.player.player.id, this.dragState.player.team, newX, newY); } this.isDragging = false; this.dragState = null; // Update cursor const player = this.findPlayerAtPosition(coords.x, coords.y); this.canvas.style.cursor = player ? 'grab' : 'default'; } }; this.handleTouchStart = (event) => { if (event.touches.length === 1) { const touch = event.touches[0]; const coords = this.getCanvasCoordinates(touch.clientX, touch.clientY); const player = this.findPlayerAtPosition(coords.x, coords.y); if (player) { this.dragState = { player: player.player, isHomeTeam: player.isHomeTeam, offsetX: coords.x - player.coordinates.x, offsetY: coords.y - player.coordinates.y, }; this.isDragging = true; event.preventDefault(); } } }; this.handleTouchMove = (event) => { if (this.isDragging && this.dragState && event.touches.length === 1) { const touch = event.touches[0]; const coords = this.getCanvasCoordinates(touch.clientX, touch.clientY); // Update custom coordinates - store directly in canvas space const newX = coords.x - this.dragState.offsetX; const newY = coords.y - this.dragState.offsetY; const key = `${this.dragState.player.team}-${this.dragState.player.player.id}`; this.customCoordinates.set(key, { x: newX, y: newY }); // Trigger re-render this.renderCallback(); event.preventDefault(); } }; this.handleTouchEnd = (event) => { if (this.isDragging && this.dragState) { // Use the last known position from customCoordinates const key = `${this.dragState.player.team}-${this.dragState.player.player.id}`; const coords = this.customCoordinates.get(key); if (coords && this.config.onPlayerMove) { this.config.onPlayerMove(this.dragState.player.player.id, this.dragState.player.team, coords.x, coords.y); } this.isDragging = false; this.dragState = null; } }; this.canvas = canvas; this.config = config; this.renderCallback = renderCallback; this.canvasTranslateX = translateX; this.canvasTranslateY = translateY; if (this.config.interactive) { this.attachEventListeners(); } } attachEventListeners() { // Mouse events this.canvas.addEventListener('mousedown', this.handleMouseDown); this.canvas.addEventListener('mousemove', this.handleMouseMove); this.canvas.addEventListener('mouseup', this.handleMouseUp); this.canvas.addEventListener('mouseleave', this.handleMouseUp); // Touch events this.canvas.addEventListener('touchstart', this.handleTouchStart); this.canvas.addEventListener('touchmove', this.handleTouchMove); this.canvas.addEventListener('touchend', this.handleTouchEnd); this.canvas.addEventListener('touchcancel', this.handleTouchEnd); // Change cursor when hovering over players this.canvas.style.cursor = 'default'; } detachEventListeners() { this.canvas.removeEventListener('mousedown', this.handleMouseDown); this.canvas.removeEventListener('mousemove', this.handleMouseMove); this.canvas.removeEventListener('mouseup', this.handleMouseUp); this.canvas.removeEventListener('mouseleave', this.handleMouseUp); this.canvas.removeEventListener('touchstart', this.handleTouchStart); this.canvas.removeEventListener('touchmove', this.handleTouchMove); this.canvas.removeEventListener('touchend', this.handleTouchEnd); this.canvas.removeEventListener('touchcancel', this.handleTouchEnd); } updatePlayerCoordinates(coordinates) { this.playerCoordinates = coordinates; } updateLineupData(lineupData) { this.lineupData = lineupData; } getCustomCoordinates() { return this.customCoordinates; } setCustomCoordinate(playerId, team, coordinates) { const key = `${team}-${playerId}`; this.customCoordinates.set(key, coordinates); } clearCustomCoordinates() { this.customCoordinates.clear(); } getCanvasCoordinates(clientX, clientY) { const rect = this.canvas.getBoundingClientRect(); const scaleX = this.canvas.width / rect.width; const scaleY = this.canvas.height / rect.height; return { x: (clientX - rect.left) * scaleX - this.canvasTranslateX, y: (clientY - rect.top) * scaleY - this.canvasTranslateY, }; } findPlayerAtPosition(x, y) { const radius = this.config.playerCircleSize; // Check in reverse order so that players drawn on top are selected first for (let i = this.playerCoordinates.length - 1; i >= 0; i--) { const playerData = this.playerCoordinates[i]; // Skip substitutes if (playerData.player.position === Position.SUBSTITUTE) { continue; } const dx = x - playerData.coordinates.x; const dy = y - playerData.coordinates.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance <= radius) { return playerData; } } return null; } }