@fuzo/soccer-board 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +335 -0
- package/fesm2022/fuzo-soccer-board.mjs +1021 -0
- package/fesm2022/fuzo-soccer-board.mjs.map +1 -0
- package/package.json +51 -0
- package/types/fuzo-soccer-board.d.ts +347 -0
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# ⚽ Soccer Board Angular
|
|
2
|
+
|
|
3
|
+
A beautiful, interactive soccer field component for Angular applications. Create tactical formations, manage player positions, and export your field as PNG images. Built with modern Angular features (signals, standalone components) and styled with Tailwind CSS.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- 🎨 **FIFA-compliant field** with accurate dimensions and markings
|
|
12
|
+
- 🔄 **Portrait & Landscape** orientations
|
|
13
|
+
- 👥 **Drag & Drop** players onto the field
|
|
14
|
+
- 🎯 **Tactical position detection** (GK, CB, RB, LB, CDM, CM, CAM, LW, RW, ST, etc.)
|
|
15
|
+
- 📱 **Responsive design** with `fit="contain"` mode
|
|
16
|
+
- 🎨 **Beautiful player cards** with photos, numbers, and positions
|
|
17
|
+
- 📤 **Export to PNG** with high-quality images
|
|
18
|
+
- 🎛️ **Fully configurable** layout and behavior
|
|
19
|
+
- ⚡ **100% Reactive** using Angular signals
|
|
20
|
+
- 🎨 **Tailwind CSS** styling (optional, works without it)
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install fuzo-soccer-board
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it! The library includes `html2canvas-pro` as a dependency for PNG export functionality (supports modern CSS color functions like `oklab`).
|
|
29
|
+
|
|
30
|
+
## 🚀 Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Import the Component
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { SoccerBoardComponent, SoccerBoardPlayer, SoccerBoardOrientation, SoccerBoardTeamSide } from 'fuzo-soccer-board';
|
|
36
|
+
|
|
37
|
+
@Component({
|
|
38
|
+
selector: 'app-my-component',
|
|
39
|
+
standalone: true,
|
|
40
|
+
imports: [SoccerBoardComponent],
|
|
41
|
+
template: `
|
|
42
|
+
<fuzo-soccer-board
|
|
43
|
+
[players]="players"
|
|
44
|
+
[orientation]="SoccerBoardOrientation.Landscape"
|
|
45
|
+
(playerPositioned)="onPlayerPositioned($event)"
|
|
46
|
+
(imageExported)="onImageExported($event)"
|
|
47
|
+
/>
|
|
48
|
+
`,
|
|
49
|
+
})
|
|
50
|
+
export class MyComponent {
|
|
51
|
+
SoccerBoardOrientation = SoccerBoardOrientation;
|
|
52
|
+
|
|
53
|
+
players: SoccerBoardPlayer[] = [
|
|
54
|
+
{
|
|
55
|
+
id: '1',
|
|
56
|
+
name: 'Messi',
|
|
57
|
+
number: 10,
|
|
58
|
+
photo: '/path/to/photo.png',
|
|
59
|
+
preferredPosition: 'ST',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
onPlayerPositioned(event: any) {
|
|
64
|
+
console.log('Player positioned:', event);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onImageExported(event: { blob: Blob; dataUrl: string }) {
|
|
68
|
+
// Handle exported image
|
|
69
|
+
console.log('Image exported:', event.dataUrl);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Basic Usage
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<fuzo-soccer-board
|
|
78
|
+
[players]="players"
|
|
79
|
+
[orientation]="SoccerBoardOrientation.Landscape"
|
|
80
|
+
[showPositions]="true"
|
|
81
|
+
[fit]="SoccerBoardFitMode.Contain"
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 📚 API Reference
|
|
86
|
+
|
|
87
|
+
### Inputs
|
|
88
|
+
|
|
89
|
+
| Input | Type | Default | Description |
|
|
90
|
+
|-------|------|---------|-------------|
|
|
91
|
+
| `players` | `SoccerBoardPlayer[]` | `[]` | Array of players to display |
|
|
92
|
+
| `orientation` | `SoccerBoardOrientationType` | `SoccerBoardOrientation.Landscape` | Field orientation: `'portrait'` or `'landscape'` |
|
|
93
|
+
| `teamSide` | `SoccerBoardTeamSideType` | `null` | Show specific side: `SoccerBoardTeamSide.Home`, `SoccerBoardTeamSide.Away`, or `null` (both) |
|
|
94
|
+
| `showPositions` | `boolean` | `false` | Show tactical position zones |
|
|
95
|
+
| `fit` | `SoccerBoardFitModeType` | `null` | Responsive mode: `SoccerBoardFitMode.Contain` or `null` |
|
|
96
|
+
| `showPlayersSidebar` | `boolean` | `true` | Show sidebar with available players |
|
|
97
|
+
| `playersPosition` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Position of players sidebar |
|
|
98
|
+
| `playersColumns` | `number` | `2` | Number of columns in players grid |
|
|
99
|
+
| `maxPlayersPerSide` | `number` | `7` | Maximum players allowed per side (HOME/AWAY) |
|
|
100
|
+
|
|
101
|
+
### Outputs
|
|
102
|
+
|
|
103
|
+
| Output | Event Data | Description |
|
|
104
|
+
|--------|-----------|-------------|
|
|
105
|
+
| `playerPositioned` | `{ playerId: string; fieldX: number; fieldY: number; side: SoccerBoardTeamSide; position: string }` | Emitted when a player is dropped on the field |
|
|
106
|
+
| `playerRemovedFromField` | `{ playerId: string }` | Emitted when a player is removed from the field |
|
|
107
|
+
| `imageExported` | `{ blob: Blob; dataUrl: string }` | Emitted when field is exported to PNG |
|
|
108
|
+
|
|
109
|
+
### Public Methods
|
|
110
|
+
|
|
111
|
+
| Method | Parameters | Returns | Description |
|
|
112
|
+
|--------|-----------|---------|-------------|
|
|
113
|
+
| `exportToPNG()` | - | `Promise<void>` | Exports the field as PNG image (also triggers `imageExported` event) |
|
|
114
|
+
|
|
115
|
+
### Player Model
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
interface SoccerBoardPlayer {
|
|
119
|
+
id: string; // Unique identifier
|
|
120
|
+
name: string; // Player name
|
|
121
|
+
number?: number; // Jersey number
|
|
122
|
+
photo?: string; // Photo URL
|
|
123
|
+
preferredPosition?: string; // Natural position (shown when off field)
|
|
124
|
+
fieldPosition?: string; // Position on field (set when placed)
|
|
125
|
+
fieldX?: number; // Normalized X coordinate (0-100)
|
|
126
|
+
fieldY?: number; // Normalized Y coordinate (0-100)
|
|
127
|
+
side?: SoccerBoardTeamSide; // Which half: SoccerBoardTeamSide.Home or SoccerBoardTeamSide.Away
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Enums & Types
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
enum SoccerBoardOrientation {
|
|
135
|
+
Portrait = 'portrait',
|
|
136
|
+
Landscape = 'landscape',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
enum SoccerBoardTeamSide {
|
|
140
|
+
Home = 'home',
|
|
141
|
+
Away = 'away',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
enum SoccerBoardFitMode {
|
|
145
|
+
Contain = 'contain',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
enum SoccerBoardPlayerSize {
|
|
149
|
+
Small = 'small',
|
|
150
|
+
Medium = 'medium',
|
|
151
|
+
Large = 'large',
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 🎯 Examples
|
|
156
|
+
|
|
157
|
+
### Basic Field
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<fuzo-soccer-board [players]="players" />
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### With Position Zones
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<fuzo-soccer-board
|
|
167
|
+
[players]="players"
|
|
168
|
+
[showPositions]="true"
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Responsive Field
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<fuzo-soccer-board
|
|
176
|
+
[players]="players"
|
|
177
|
+
[fit]="SoccerBoardFitMode.Contain"
|
|
178
|
+
/>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Custom Layout
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<fuzo-soccer-board
|
|
185
|
+
[players]="players"
|
|
186
|
+
[showPlayersSidebar]="true"
|
|
187
|
+
[playersPosition]="'right'"
|
|
188
|
+
[playersColumns]="3"
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Handle Player Positioning
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
onPlayerPositioned(event: {
|
|
197
|
+
playerId: string;
|
|
198
|
+
fieldX: number;
|
|
199
|
+
fieldY: number;
|
|
200
|
+
side: SoccerBoardTeamSide;
|
|
201
|
+
position: string;
|
|
202
|
+
}) {
|
|
203
|
+
// Update player in your data
|
|
204
|
+
const player = this.players.find(p => p.id === event.playerId);
|
|
205
|
+
if (player) {
|
|
206
|
+
player.fieldX = event.fieldX;
|
|
207
|
+
player.fieldY = event.fieldY;
|
|
208
|
+
player.side = event.side;
|
|
209
|
+
player.fieldPosition = event.position;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Export to PNG
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Method 1: Call the method directly
|
|
218
|
+
@ViewChild(SoccerBoardComponent) soccerBoard!: SoccerBoardComponent;
|
|
219
|
+
|
|
220
|
+
async exportField() {
|
|
221
|
+
await this.soccerBoard.exportToPNG();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Method 2: Handle the event
|
|
225
|
+
onImageExported(event: { blob: Blob; dataUrl: string }) {
|
|
226
|
+
// Use blob for upload
|
|
227
|
+
const formData = new FormData();
|
|
228
|
+
formData.append('image', event.blob, 'soccer-field.png');
|
|
229
|
+
|
|
230
|
+
// Or use dataUrl for display/preview
|
|
231
|
+
this.imagePreview = event.dataUrl;
|
|
232
|
+
|
|
233
|
+
// Or send via email, etc.
|
|
234
|
+
this.sendEmailWithImage(event.dataUrl);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Custom Styling
|
|
239
|
+
|
|
240
|
+
You can customize the field using CSS custom properties:
|
|
241
|
+
|
|
242
|
+
```css
|
|
243
|
+
fuzo-soccer-board {
|
|
244
|
+
--soccer-board-width: 800px;
|
|
245
|
+
--soccer-board-max-width: 1200px;
|
|
246
|
+
--soccer-board-padding: 20px;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 🎨 Styling
|
|
251
|
+
|
|
252
|
+
The component uses Tailwind CSS for player cards, but the field itself is styled with pure CSS/SCSS. If you're using Tailwind in your project, the player cards will automatically use your Tailwind configuration.
|
|
253
|
+
|
|
254
|
+
### Without Tailwind
|
|
255
|
+
|
|
256
|
+
The component works without Tailwind CSS, but player cards will have minimal styling. You can add your own styles:
|
|
257
|
+
|
|
258
|
+
```css
|
|
259
|
+
lib-soccer-board-player {
|
|
260
|
+
/* Your custom styles */
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## 🔧 Advanced Usage
|
|
265
|
+
|
|
266
|
+
### Programmatic Player Positioning
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Set player position programmatically
|
|
270
|
+
const player = this.players.find(p => p.id === '1');
|
|
271
|
+
if (player) {
|
|
272
|
+
player.fieldX = 50; // Center X
|
|
273
|
+
player.fieldY = 50; // Center Y
|
|
274
|
+
player.side = SoccerBoardTeamSide.Home;
|
|
275
|
+
player.fieldPosition = 'CM';
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Filter Players by Side
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// Get only players on HOME side
|
|
283
|
+
const homePlayers = this.players.filter(
|
|
284
|
+
p => p.side === SoccerBoardTeamSide.Home && p.fieldX !== undefined
|
|
285
|
+
);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Remove Player from Field
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
removePlayer(playerId: string) {
|
|
292
|
+
const player = this.players.find(p => p.id === playerId);
|
|
293
|
+
if (player) {
|
|
294
|
+
player.fieldX = undefined;
|
|
295
|
+
player.fieldY = undefined;
|
|
296
|
+
player.side = undefined;
|
|
297
|
+
player.fieldPosition = undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## 🐛 Troubleshooting
|
|
303
|
+
|
|
304
|
+
### Images not loading
|
|
305
|
+
|
|
306
|
+
Make sure player photos are accessible and CORS is configured correctly if loading from external sources.
|
|
307
|
+
|
|
308
|
+
### Export not working
|
|
309
|
+
|
|
310
|
+
- Ensure `html2canvas-pro` is installed
|
|
311
|
+
- Check browser console for errors
|
|
312
|
+
- Make sure images are loaded before exporting
|
|
313
|
+
|
|
314
|
+
### Field not responsive
|
|
315
|
+
|
|
316
|
+
- Use `[fit]="SoccerBoardFitMode.Contain"` for responsive behavior
|
|
317
|
+
- Ensure parent container has defined dimensions
|
|
318
|
+
|
|
319
|
+
## 🤝 Contributing
|
|
320
|
+
|
|
321
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
322
|
+
|
|
323
|
+
## 📝 License
|
|
324
|
+
|
|
325
|
+
MIT License - see LICENSE file for details
|
|
326
|
+
|
|
327
|
+
## 🙏 Acknowledgments
|
|
328
|
+
|
|
329
|
+
- Built with [Angular](https://angular.io/)
|
|
330
|
+
- Field dimensions based on FIFA regulations
|
|
331
|
+
- Export functionality powered by [html2canvas-pro](https://www.npmjs.com/package/html2canvas-pro)
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
Made with ⚽ by the community
|