Spaces:
Running
Running
<template> | |
<div class="football-field"> | |
<svg viewBox="-7 -2 119 72" preserveAspectRatio="xMidYMid meet" @click="handleBackgroundClick"> | |
<g> | |
<!-- Base field --> | |
<rect x="0" y="0" width="105" height="68" fill="none" stroke="#333" stroke-width="0.3"/> | |
<!-- Clickable lines --> | |
<g class="lines"> | |
<line v-for="(line, name) in lineCoordinates" | |
:key="name" | |
class="field-line" | |
:stroke="getLineColor(name)" | |
:x1="line.x1" | |
:y1="line.y1" | |
:x2="line.x2" | |
:y2="line.y2" | |
@click="selectLine(name)" /> | |
</g> | |
<!-- Key points --> | |
<circle v-for="(point, index) in keypoints" | |
:key="index" | |
:cx="point[0]" | |
:cy="point[1]" | |
r="2" | |
:fill="getPointColor(index)" | |
class="keypoint" | |
@click="selectPoint(index)" /> | |
<!-- Center circle --> | |
<circle | |
cx="52.5" | |
cy="34" | |
r="9.15" | |
fill="none" | |
:stroke="getLineColor('Circle central')" | |
class="field-line" | |
@click="selectLine('Circle central')" /> | |
<!-- Penalty area arcs --> | |
<path | |
v-for="arc in circle_left_right" | |
:key="'Circle ' + arc.side" | |
:d="getPenaltyArc(arc)" | |
fill="none" | |
:stroke="getLineColor('Circle ' + arc.side)" | |
class="field-line" | |
@click="selectLine('Circle ' + arc.side)" /> | |
</g> | |
</svg> | |
<!-- Selected line or point info --> | |
<div v-if="selectedLine && LINES[selectedLine]" class="info-overlay"> | |
{{ LINES[selectedLine].name }} | |
</div> | |
<div v-if="selectedPointIndex !== null" class="info-overlay"> | |
{{ POINTS[selectedPointIndex].name }} | |
</div> | |
</div> | |
</template> | |
<script> | |
// Définition exacte des classes de lignes comme dans SoccerNet | |
const LINES = { | |
'Big rect. left bottom': { name: 'Big rect. left bottom', description: 'Left penalty area - bottom line' }, | |
'Big rect. left main': { name: 'Big rect. left main', description: 'Left penalty area - parallel line' }, | |
'Big rect. left top': { name: 'Big rect. left top', description: 'Left penalty area - top line' }, | |
'Big rect. right bottom': { name: 'Big rect. right bottom', description: 'Right penalty area - bottom line' }, | |
'Big rect. right main': { name: 'Big rect. right main', description: 'Right penalty area - parallel line' }, | |
'Big rect. right top': { name: 'Big rect. right top', description: 'Right penalty area - top line' }, | |
'Circle central': { name: 'Center circle', description: 'Center circle' }, | |
'Circle left': { name: 'Left circle', description: 'Left arc' }, | |
'Circle right': { name: 'Right circle', description: 'Right arc' }, | |
'Goal left crossbar': { name: 'Goal left crossbar', description: 'Left goal crossbar' }, | |
'Goal left post left': { name: 'Goal left post left', description: 'Left goal - left post' }, | |
'Goal left post right': { name: 'Goal left post right', description: 'Left goal - right post' }, | |
'Goal right crossbar': { name: 'Goal right crossbar', description: 'Right goal crossbar' }, | |
'Goal right post left': { name: 'Goal right post left', description: 'Right goal - left post' }, | |
'Goal right post right': { name: 'Goal right post right', description: 'Right goal - right post' }, | |
'Goal unknown': { name: 'Goal unknown', description: 'Unidentified goal' }, | |
'Line unknown': { name: 'Line unknown', description: 'Unidentified line' }, | |
'Middle line': { name: 'Middle line', description: 'Center line' }, | |
'Side line bottom': { name: 'Side line bottom', description: 'Bottom goal line' }, | |
'Side line left': { name: 'Side line left', description: 'Left touch line' }, | |
'Side line right': { name: 'Side line right', description: 'Right touch line' }, | |
'Side line top': { name: 'Side line top', description: 'Top goal line' }, | |
'Small rect. left bottom': { name: 'Small rect. left bottom', description: 'Left goal area - bottom line' }, | |
'Small rect. left main': { name: 'Small rect. left main', description: 'Left goal area - parallel line' }, | |
'Small rect. left top': { name: 'Small rect. left top', description: 'Left goal area - top line' }, | |
'Small rect. right bottom': { name: 'Small rect. right bottom', description: 'Right goal area - bottom line' }, | |
'Small rect. right main': { name: 'Small rect. right main', description: 'Right goal area - parallel line' }, | |
'Small rect. right top': { name: 'Small rect. right top', description: 'Right goal area - top line' }, | |
center_circle: { | |
name: "Center circle", | |
type: "circle", | |
color: "#00FF15" | |
}, | |
circle_left: { | |
name: "Left circle", | |
type: "arc", | |
color: "#00FF15" | |
}, | |
circle_right: { | |
name: "Right circle", | |
type: "arc", | |
color: "#00FF15" | |
} | |
}; | |
// Définition des dimensions standard d'un terrain de football | |
const FIELD_DIMENSIONS = { | |
PITCH_LENGTH: 105, | |
PITCH_WIDTH: 68, | |
GOAL_LINE_TO_PENALTY_MARK: 11.0, | |
PENALTY_AREA_WIDTH: 40.32, | |
PENALTY_AREA_LENGTH: 16.5, | |
GOAL_AREA_WIDTH: 18.32, | |
GOAL_AREA_LENGTH: 5.5, | |
CENTER_CIRCLE_RADIUS: 9.15, | |
GOAL_HEIGHT: 2.44, | |
GOAL_LENGTH: 7.32 | |
}; | |
const POINTS = { | |
0: { name: "Center point" }, | |
1: { name: "Left penalty point" }, | |
2: { name: "Right penalty point" } | |
}; | |
export default { | |
name: 'FootballField', | |
props: { | |
positionedLines: { | |
type: Object, | |
default: () => ({}) | |
}, | |
positionedPoints: { | |
type: Object, | |
default: () => ({}) | |
} | |
}, | |
data() { | |
return { | |
selectedPointIndex: null, | |
selectedLine: null, | |
LINES, | |
FIELD_DIMENSIONS, | |
POINTS, | |
keypoints: [ | |
[52.5, 34], // Center point | |
[11, 34], // Left penalty point | |
[94, 34], // Right penalty point | |
], | |
lineCoordinates: { | |
'Side line top': { x1: 0, y1: 0, x2: 105, y2: 0 }, | |
'Side line bottom': { x1: 0, y1: 68, x2: 105, y2: 68 }, | |
'Side line left': { x1: 0, y1: 0, x2: 0, y2: 68 }, | |
'Side line right': { x1: 105, y1: 0, x2: 105, y2: 68 }, | |
'Middle line': { x1: 52.5, y1: 0, x2: 52.5, y2: 68 }, | |
// Penalty areas | |
'Big rect. left bottom': { x1: 0, y1: 54.16, x2: 16.5, y2: 54.16 }, | |
'Big rect. left main': { x1: 16.5, y1: 13.84, x2: 16.5, y2: 54.16 }, | |
'Big rect. left top': { x1: 0, y1: 13.84, x2: 16.5, y2: 13.84 }, | |
'Big rect. right bottom': { x1: 88.5, y1: 54.16, x2: 105, y2: 54.16 }, | |
'Big rect. right main': { x1: 88.5, y1: 13.84, x2: 88.5, y2: 54.16 }, | |
'Big rect. right top': { x1: 88.5, y1: 13.84, x2: 105, y2: 13.84 }, | |
// Goal areas | |
'Small rect. left bottom': { x1: 0, y1: 43.16, x2: 5.5, y2: 43.16 }, | |
'Small rect. left main': { x1: 5.5, y1: 24.84, x2: 5.5, y2: 43.16 }, | |
'Small rect. left top': { x1: 0, y1: 24.84, x2: 5.5, y2: 24.84 }, | |
'Small rect. right bottom': { x1: 99.5, y1: 43.16, x2: 105, y2: 43.16 }, | |
'Small rect. right main': { x1: 99.5, y1: 24.84, x2: 99.5, y2: 43.16 }, | |
'Small rect. right top': { x1: 99.5, y1: 24.84, x2: 105, y2: 24.84 }, | |
// Goals | |
'Goal left post left': { x1: -5, y1: 37.66, x2: 0, y2: 37.66 }, | |
'Goal left crossbar': { x1: -5, y1: 30.34, x2: -5, y2: 37.66 }, | |
'Goal left post right': { x1: -5, y1: 30.34, x2: 0, y2: 30.34 }, | |
'Goal right post left': { x1: 105, y1: 30.34, x2: 110, y2: 30.34 }, | |
'Goal right crossbar': { x1: 110, y1: 30.34, x2: 110, y2: 37.66 }, | |
'Goal right post right': { x1: 105, y1: 37.66, x2: 110, y2: 37.66 }, | |
}, | |
lastSelected: null, // 'point' or 'line' | |
circle_left_right: [ | |
{ x: 11, y: 34, side: 'left' }, | |
{ x: 94, y: 34, side: 'right' } | |
] | |
} | |
}, | |
methods: { | |
handleBackgroundClick(event) { | |
// Vérifie si le clic vient directement du SVG (pas d'un enfant) | |
if (event.target.tagName === 'svg') { | |
if (this.selectedPointIndex !== null) { | |
this.selectedPointIndex = null; | |
this.$emit('point-selected', null); | |
} | |
if (this.selectedLine) { | |
this.selectedLine = null; | |
this.$emit('line-selected', null); | |
} | |
this.lastSelected = null; | |
} | |
}, | |
selectPoint(index, event) { | |
if (event) { | |
event.stopPropagation(); | |
} | |
if (this.selectedLine) this.selectedLine = null; | |
this.selectedPointIndex = index; | |
this.lastSelected = 'point'; | |
this.$emit('point-selected', { | |
index, | |
coordinates: this.keypoints[index], | |
name: this.POINTS[index].name | |
}); | |
}, | |
selectLine(lineName, event) { | |
if (event) { | |
event.stopPropagation(); | |
} | |
if (this.selectedPointIndex !== null) this.selectedPointIndex = null; | |
this.selectedLine = lineName; | |
this.lastSelected = 'line'; | |
if (this.LINES[lineName]) { | |
this.$emit('line-selected', { | |
id: lineName, | |
name: this.LINES[lineName].name, | |
description: this.LINES[lineName].description | |
}); | |
} | |
}, | |
getPointColor(index) { | |
if (this.selectedPointIndex === index && this.lastSelected === 'point') { | |
return index in this.positionedPoints ? '#FFFF00' : 'red'; | |
} | |
return index in this.positionedPoints ? '#00FF15' : 'white'; | |
}, | |
getLineColor(lineName) { | |
if (this.selectedLine === lineName && this.lastSelected === 'line') { | |
return this.positionedLines[lineName] ? '#FFFF00' : 'red'; | |
} | |
return this.positionedLines[lineName] ? '#00FF15' : 'white'; | |
}, | |
getPenaltyArc(arc) { | |
const radius = 9.15; | |
const startAngle = arc.side === 'left' ? -53 : -127; | |
const endAngle = arc.side === 'left' ? 53 : 127; | |
const start = { | |
x: arc.x + radius * Math.cos(startAngle * Math.PI / 180), | |
y: arc.y + radius * Math.sin(startAngle * Math.PI / 180) | |
}; | |
const end = { | |
x: arc.x + radius * Math.cos(endAngle * Math.PI / 180), | |
y: arc.y + radius * Math.sin(endAngle * Math.PI / 180) | |
}; | |
const largeArc = 0; | |
const sweep = arc.side === 'left' ? 1 : 0; | |
return `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArc} ${sweep} ${end.x} ${end.y}`; | |
} | |
} | |
} | |
</script> | |
<style scoped> | |
.football-field { | |
position: relative; | |
width: 100%; | |
height: 100%; | |
} | |
.field-line { | |
stroke-width: 0.8; | |
cursor: pointer; | |
} | |
/* Specific style for goal lines */ | |
.field-line[class*="Goal"] { | |
stroke-width: 1; /* Thicker line for goals */ | |
} | |
.field-line:hover { | |
stroke-width: 1.2; /* Even thicker on hover */ | |
opacity: 0.8; | |
} | |
.line-info { | |
position: absolute; | |
bottom: 10px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.7); | |
color: white; | |
padding: 5px 10px; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
} | |
.keypoint { | |
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5)); /* Glow effect for points */ | |
cursor: pointer; | |
} | |
.keypoint:hover { | |
filter: drop-shadow(0 0 4px rgba(255, 0, 0, 0.8)); | |
} | |
.info-overlay { | |
position: absolute; | |
bottom: 10px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.7); | |
color: white; | |
padding: 5px 10px; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
} | |
</style> |