@fuzo/soccer-board 0.2.0-alpha.1 → 0.2.0-alpha.3

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.
@@ -1,971 +0,0 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, ElementRef, input, output, signal, computed, effect, HostListener, Component, PLATFORM_ID, viewChild, HostBinding } from '@angular/core';
3
- import { isPlatformBrowser } from '@angular/common';
4
- import html2canvas from 'html2canvas-pro';
5
-
6
- /**
7
- * Soccer Board Field dimension constants
8
- * Based on FIFA regulations: 105m x 68m
9
- */
10
- const SoccerBoardFieldConstants = {
11
- /**
12
- * Field aspect ratio (width / height)
13
- * FIFA standard: 105m / 68m ≈ 1.544
14
- */
15
- RATIO: 105 / 68,
16
- };
17
-
18
- /**
19
- * Soccer Board Fit Mode types for responsive field sizing
20
- */
21
- var SoccerBoardFitMode;
22
- (function (SoccerBoardFitMode) {
23
- SoccerBoardFitMode["Contain"] = "contain";
24
- })(SoccerBoardFitMode || (SoccerBoardFitMode = {}));
25
-
26
- /**
27
- * Soccer Board Field Orientation types
28
- */
29
- var SoccerBoardOrientation;
30
- (function (SoccerBoardOrientation) {
31
- SoccerBoardOrientation["Portrait"] = "portrait";
32
- SoccerBoardOrientation["Landscape"] = "landscape";
33
- })(SoccerBoardOrientation || (SoccerBoardOrientation = {}));
34
-
35
- /**
36
- * Soccer Board Player Card Size types
37
- */
38
- var SoccerBoardPlayerSize;
39
- (function (SoccerBoardPlayerSize) {
40
- SoccerBoardPlayerSize["Small"] = "small";
41
- SoccerBoardPlayerSize["Medium"] = "medium";
42
- SoccerBoardPlayerSize["Large"] = "large";
43
- })(SoccerBoardPlayerSize || (SoccerBoardPlayerSize = {}));
44
-
45
- /**
46
- * Soccer Board Team Side types
47
- */
48
- var SoccerBoardTeamSide;
49
- (function (SoccerBoardTeamSide) {
50
- SoccerBoardTeamSide["Home"] = "home";
51
- SoccerBoardTeamSide["Away"] = "away";
52
- })(SoccerBoardTeamSide || (SoccerBoardTeamSide = {}));
53
-
54
- /**
55
- * Get tactical position zones data
56
- * All tactical positions with coordinates for each orientation
57
- * Portrait: x=left-right, y=center(0)-goal(100)
58
- * Landscape: x=center(0)-goal(100), y=top(0)-bottom(100)
59
- */
60
- function getPositionsData() {
61
- // Colors by position type
62
- const colors = {
63
- goalkeeper: 'rgba(255, 200, 0, 0.5)', // Yellow
64
- defense: 'rgba(70, 130, 255, 0.5)', // Blue
65
- defensiveMid: 'rgba(100, 180, 255, 0.5)', // Light blue
66
- midfield: 'rgba(50, 200, 100, 0.5)', // Green
67
- attackingMid: 'rgba(150, 220, 100, 0.5)', // Light green
68
- forward: 'rgba(255, 100, 100, 0.5)', // Red
69
- striker: 'rgba(255, 50, 50, 0.5)', // Dark red
70
- };
71
- return [
72
- // ============ DEFENSE ZONE ============
73
- {
74
- id: 'LB',
75
- label: 'LB',
76
- color: colors.defense,
77
- portrait: { x: 0, y: 70, width: 20, height: 30 },
78
- landscape: { x: 70, y: 0, width: 30, height: 20 },
79
- },
80
- {
81
- id: 'CB',
82
- label: 'CB',
83
- color: colors.defense,
84
- portrait: { x: 20, y: 70, width: 60, height: 19.5 },
85
- landscape: { x: 70, y: 20, width: 19.5, height: 60 },
86
- },
87
- {
88
- id: 'CB',
89
- label: '',
90
- color: colors.defense,
91
- portrait: { x: 20, y: 89.5, width: 16.55, height: 10.5 },
92
- landscape: { x: 89.5, y: 20, width: 10.5, height: 16.55 },
93
- },
94
- {
95
- id: 'GK',
96
- label: 'GK',
97
- color: colors.goalkeeper,
98
- portrait: { x: 36.55, y: 89.5, width: 26.9, height: 10.5 },
99
- landscape: { x: 89.5, y: 36.55, width: 10.5, height: 26.9 },
100
- },
101
- {
102
- id: 'CB',
103
- label: '',
104
- color: colors.defense,
105
- portrait: { x: 63.45, y: 89.5, width: 16.55, height: 10.5 },
106
- landscape: { x: 89.5, y: 63.45, width: 10.5, height: 16.55 },
107
- },
108
- {
109
- id: 'RB',
110
- label: 'RB',
111
- color: colors.defense,
112
- portrait: { x: 80, y: 70, width: 20, height: 30 },
113
- landscape: { x: 70, y: 80, width: 30, height: 20 },
114
- },
115
- // ============ DEFENSIVE MIDFIELD ZONE ============
116
- {
117
- id: 'LWB',
118
- label: 'LWB',
119
- color: colors.defensiveMid,
120
- portrait: { x: 0, y: 55, width: 25, height: 15 },
121
- landscape: { x: 55, y: 0, width: 15, height: 25 },
122
- },
123
- {
124
- id: 'CDM',
125
- label: 'CDM',
126
- color: colors.defensiveMid,
127
- portrait: { x: 25, y: 55, width: 50, height: 15 },
128
- landscape: { x: 55, y: 25, width: 15, height: 50 },
129
- },
130
- {
131
- id: 'RWB',
132
- label: 'RWB',
133
- color: colors.defensiveMid,
134
- portrait: { x: 75, y: 55, width: 25, height: 15 },
135
- landscape: { x: 55, y: 75, width: 15, height: 25 },
136
- },
137
- // ============ CENTRAL MIDFIELD ZONE ============
138
- {
139
- id: 'LM',
140
- label: 'LM',
141
- color: colors.midfield,
142
- portrait: { x: 0, y: 40, width: 25, height: 15 },
143
- landscape: { x: 40, y: 0, width: 15, height: 25 },
144
- },
145
- {
146
- id: 'CM',
147
- label: 'CM',
148
- color: colors.midfield,
149
- portrait: { x: 25, y: 40, width: 50, height: 15 },
150
- landscape: { x: 40, y: 25, width: 15, height: 50 },
151
- },
152
- {
153
- id: 'RM',
154
- label: 'RM',
155
- color: colors.midfield,
156
- portrait: { x: 75, y: 40, width: 25, height: 15 },
157
- landscape: { x: 40, y: 75, width: 15, height: 25 },
158
- },
159
- // ============ ATTACKING ZONE ============
160
- {
161
- id: 'LW',
162
- label: 'LW',
163
- color: colors.forward,
164
- portrait: { x: 0, y: 0, width: 25, height: 40 },
165
- landscape: { x: 0, y: 0, width: 40, height: 25 },
166
- },
167
- {
168
- id: 'CAM',
169
- label: 'CAM',
170
- color: colors.attackingMid,
171
- portrait: { x: 25, y: 25, width: 50, height: 15 },
172
- landscape: { x: 25, y: 25, width: 15, height: 50 },
173
- },
174
- {
175
- id: 'RW',
176
- label: 'RW',
177
- color: colors.forward,
178
- portrait: { x: 75, y: 0, width: 25, height: 40 },
179
- landscape: { x: 0, y: 75, width: 40, height: 25 },
180
- },
181
- {
182
- id: 'CF',
183
- label: 'CF',
184
- color: colors.forward,
185
- portrait: { x: 25, y: 10, width: 50, height: 15 },
186
- landscape: { x: 10, y: 25, width: 15, height: 50 },
187
- },
188
- {
189
- id: 'ST',
190
- label: 'ST',
191
- color: colors.striker,
192
- portrait: { x: 25, y: 0, width: 50, height: 10 },
193
- landscape: { x: 0, y: 25, width: 10, height: 50 },
194
- },
195
- ];
196
- }
197
-
198
- class SoccerBoardPlayerComponent {
199
- // Host element reference
200
- hostElement = inject((ElementRef));
201
- // Inputs using signals
202
- player = input.required(...(ngDevMode ? [{ debugName: "player" }] : []));
203
- size = input(SoccerBoardPlayerSize.Medium, ...(ngDevMode ? [{ debugName: "size" }] : []));
204
- // Outputs
205
- dragStart = output();
206
- dragging = output();
207
- dragEnd = output();
208
- removeFromField = output();
209
- playerClicked = output();
210
- // Drag state signals
211
- isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
212
- dragOffsetX = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetX" }] : []));
213
- dragOffsetY = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetY" }] : []));
214
- // Computed drag styles
215
- dragStyles = computed(() => {
216
- if (this.isDragging()) {
217
- return {
218
- opacity: '0.7',
219
- cursor: 'grabbing',
220
- zIndex: '1000',
221
- };
222
- }
223
- return {
224
- opacity: '1',
225
- cursor: 'grab',
226
- zIndex: '',
227
- };
228
- }, ...(ngDevMode ? [{ debugName: "dragStyles" }] : []));
229
- // Computed values
230
- playerPhoto = computed(() => {
231
- const photo = this.player()?.photo;
232
- return photo || null;
233
- }, ...(ngDevMode ? [{ debugName: "playerPhoto" }] : []));
234
- playerName = computed(() => this.player()?.name || '', ...(ngDevMode ? [{ debugName: "playerName" }] : []));
235
- playerNumber = computed(() => this.player()?.number, ...(ngDevMode ? [{ debugName: "playerNumber" }] : []));
236
- // Determine if player is on the field (has fieldX and fieldY coordinates)
237
- isOnField = computed(() => {
238
- const player = this.player();
239
- return player?.fieldX !== undefined && player?.fieldY !== undefined;
240
- }, ...(ngDevMode ? [{ debugName: "isOnField" }] : []));
241
- // Show fieldPosition if on field, otherwise show preferredPosition
242
- playerPosition = computed(() => {
243
- const player = this.player();
244
- if (this.isOnField() && player?.fieldPosition) {
245
- return player.fieldPosition;
246
- }
247
- return player?.preferredPosition;
248
- }, ...(ngDevMode ? [{ debugName: "playerPosition" }] : []));
249
- // Size classes
250
- sizeClasses = computed(() => {
251
- const size = this.size();
252
- switch (size) {
253
- case SoccerBoardPlayerSize.Small:
254
- return 'w-12 h-12';
255
- case SoccerBoardPlayerSize.Large:
256
- return 'w-20 h-20';
257
- default:
258
- return 'w-16 h-16';
259
- }
260
- }, ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
261
- constructor() {
262
- // Effect to apply drag styles
263
- effect(() => {
264
- const styles = this.dragStyles();
265
- const element = this.hostElement.nativeElement;
266
- element.style.opacity = styles.opacity;
267
- element.style.cursor = styles.cursor;
268
- element.style.zIndex = styles.zIndex;
269
- });
270
- }
271
- onMouseDown(event) {
272
- event.preventDefault();
273
- this.startDrag(event.clientX, event.clientY);
274
- }
275
- onTouchStart(event) {
276
- if (event.touches.length !== 1)
277
- return;
278
- event.preventDefault();
279
- const touch = event.touches[0];
280
- this.startDrag(touch.clientX, touch.clientY);
281
- }
282
- startDrag(clientX, clientY) {
283
- console.log('[Player] Start drag');
284
- const element = this.hostElement.nativeElement;
285
- const rect = element.getBoundingClientRect();
286
- // Calculate offset relative to the click point
287
- // This maintains the relative position between cursor and element during drag
288
- const offsetX = clientX - rect.left;
289
- const offsetY = clientY - rect.top;
290
- // Apply fixed position first to maintain visual position
291
- element.style.position = 'fixed';
292
- element.style.left = `${rect.left}px`;
293
- element.style.top = `${rect.top}px`;
294
- element.style.width = `${rect.width}px`;
295
- element.style.height = `${rect.height}px`;
296
- element.style.margin = '0';
297
- // Store offset after fixing position
298
- this.dragOffsetX.set(offsetX);
299
- this.dragOffsetY.set(offsetY);
300
- this.isDragging.set(true);
301
- const player = this.player();
302
- console.log('[Player] Emitting dragStart', { playerId: player.id, playerName: player.name });
303
- this.dragStart.emit({
304
- playerId: player.id,
305
- playerName: player.name,
306
- });
307
- }
308
- onMouseMove(event) {
309
- if (!this.isDragging())
310
- return;
311
- event.preventDefault();
312
- this.handleDrag(event.clientX, event.clientY);
313
- }
314
- onTouchMove(event) {
315
- if (!this.isDragging() || event.touches.length !== 1)
316
- return;
317
- event.preventDefault();
318
- const touch = event.touches[0];
319
- this.handleDrag(touch.clientX, touch.clientY);
320
- }
321
- handleDrag(clientX, clientY) {
322
- const element = this.hostElement.nativeElement;
323
- const offsetX = this.dragOffsetX();
324
- const offsetY = this.dragOffsetY();
325
- // Move visually during drag
326
- // Position is already fixed from startDrag, just update coordinates
327
- element.style.left = `${clientX - offsetX}px`;
328
- element.style.top = `${clientY - offsetY}px`;
329
- element.style.pointerEvents = 'none';
330
- const player = this.player();
331
- this.dragging.emit({
332
- playerId: player.id,
333
- clientX,
334
- clientY,
335
- offsetX,
336
- offsetY,
337
- });
338
- }
339
- onMouseUp(event) {
340
- if (!this.isDragging())
341
- return;
342
- this.endDrag(event.clientX, event.clientY);
343
- }
344
- onTouchEnd(event) {
345
- if (!this.isDragging())
346
- return;
347
- const touch = event.changedTouches[0];
348
- this.endDrag(touch.clientX, touch.clientY);
349
- }
350
- endDrag(clientX, clientY) {
351
- console.log('[Player] End drag', { clientX, clientY });
352
- this.isDragging.set(false);
353
- const element = this.hostElement.nativeElement;
354
- // Restore original styles
355
- element.style.pointerEvents = '';
356
- element.style.position = '';
357
- element.style.left = '';
358
- element.style.top = '';
359
- element.style.width = '';
360
- element.style.height = '';
361
- element.style.margin = '';
362
- const player = this.player();
363
- const dragEndEvent = {
364
- playerId: player.id,
365
- clientX,
366
- clientY,
367
- offsetX: this.dragOffsetX(),
368
- offsetY: this.dragOffsetY(),
369
- };
370
- console.log('[Player] Emitting dragEnd', dragEndEvent);
371
- this.dragEnd.emit(dragEndEvent);
372
- }
373
- onRemoveClick(event) {
374
- event.stopPropagation();
375
- event.preventDefault();
376
- const player = this.player();
377
- console.log('[Player] Remove from field clicked', player.id);
378
- this.removeFromField.emit({ playerId: player.id });
379
- }
380
- onPlayerClick(event) {
381
- // Don't emit click if we're dragging or if the click was on the remove button
382
- if (this.isDragging()) {
383
- return;
384
- }
385
- event.stopPropagation();
386
- const player = this.player();
387
- console.log('[Player] Player clicked', player.id);
388
- this.playerClicked.emit({ playerId: player.id, player });
389
- }
390
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
391
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardPlayerComponent, isStandalone: true, selector: "lib-soccer-board-player", inputs: { player: { classPropertyName: "player", publicName: "player", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragStart: "dragStart", dragging: "dragging", dragEnd: "dragEnd", removeFromField: "removeFromField", playerClicked: "playerClicked" }, host: { listeners: { "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)", "document:mousemove": "onMouseMove($event)", "document:touchmove": "onTouchMove($event)", "document:mouseup": "onMouseUp($event)", "document:touchend": "onTouchEnd($event)" } }, ngImport: i0, template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute bg-red-500 hover:bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{box-shadow:0 2px 8px #0006;top:-6px;right:-6px}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] });
392
- }
393
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, decorators: [{
394
- type: Component,
395
- args: [{ selector: 'lib-soccer-board-player', standalone: true, imports: [], template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute bg-red-500 hover:bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{box-shadow:0 2px 8px #0006;top:-6px;right:-6px}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] }]
396
- }], ctorParameters: () => [], propDecorators: { player: [{ type: i0.Input, args: [{ isSignal: true, alias: "player", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragging: [{ type: i0.Output, args: ["dragging"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }], removeFromField: [{ type: i0.Output, args: ["removeFromField"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], onMouseDown: [{
397
- type: HostListener,
398
- args: ['mousedown', ['$event']]
399
- }], onTouchStart: [{
400
- type: HostListener,
401
- args: ['touchstart', ['$event']]
402
- }], onMouseMove: [{
403
- type: HostListener,
404
- args: ['document:mousemove', ['$event']]
405
- }], onTouchMove: [{
406
- type: HostListener,
407
- args: ['document:touchmove', ['$event']]
408
- }], onMouseUp: [{
409
- type: HostListener,
410
- args: ['document:mouseup', ['$event']]
411
- }], onTouchEnd: [{
412
- type: HostListener,
413
- args: ['document:touchend', ['$event']]
414
- }] } });
415
-
416
- /**
417
- * Soccer Board Component
418
- * Renders a FIFA-compliant soccer field with two halves (HOME and AWAY)
419
- * Supports portrait and landscape orientations
420
- */
421
- class SoccerBoardComponent {
422
- // Host element reference
423
- hostElement = inject((ElementRef));
424
- platformId = inject(PLATFORM_ID);
425
- // Expose SoccerBoardPlayerSize to template
426
- PlayerSize = SoccerBoardPlayerSize;
427
- // Inputs using signals
428
- orientation = input(SoccerBoardOrientation.Landscape, ...(ngDevMode ? [{ debugName: "orientation" }] : []));
429
- teamSide = input(null, ...(ngDevMode ? [{ debugName: "teamSide" }] : []));
430
- showPositions = input(false, ...(ngDevMode ? [{ debugName: "showPositions" }] : []));
431
- fit = input(null, ...(ngDevMode ? [{ debugName: "fit" }] : []));
432
- players = input([], ...(ngDevMode ? [{ debugName: "players" }] : []));
433
- // Layout configuration
434
- showPlayersSidebar = input(true, ...(ngDevMode ? [{ debugName: "showPlayersSidebar" }] : []));
435
- playersPosition = input('left', ...(ngDevMode ? [{ debugName: "playersPosition" }] : []));
436
- playersColumns = input(2, ...(ngDevMode ? [{ debugName: "playersColumns" }] : []));
437
- // Maximum players per side (default: 7)
438
- maxPlayersPerSide = input(7, ...(ngDevMode ? [{ debugName: "maxPlayersPerSide" }] : []));
439
- // ViewChild references
440
- fieldContainer = viewChild('fieldContainer', ...(ngDevMode ? [{ debugName: "fieldContainer" }] : []));
441
- homeHalf = viewChild('homeHalf', ...(ngDevMode ? [{ debugName: "homeHalf" }] : []));
442
- awayHalf = viewChild('awayHalf', ...(ngDevMode ? [{ debugName: "awayHalf" }] : []));
443
- soccerBoardContainer = viewChild('soccerBoardContainer', ...(ngDevMode ? [{ debugName: "soccerBoardContainer" }] : []));
444
- // Outputs
445
- playerPositioned = output();
446
- playerRemovedFromField = output();
447
- imageExported = output();
448
- playerClicked = output();
449
- // Method to handle player drag end (called from template)
450
- onPlayerDragEnd(event) {
451
- this.handlePlayerDrop(event);
452
- }
453
- // Method to handle player click (called from template)
454
- onPlayerClick(event) {
455
- console.log('[SoccerBoard] Player clicked', event.playerId);
456
- this.playerClicked.emit(event);
457
- }
458
- // Method to handle player remove from field (called from template)
459
- onPlayerRemoveFromField(event) {
460
- // Emit event to parent to handle removal
461
- console.log('[SoccerBoard] Player remove requested', event.playerId);
462
- this.playerRemovedFromField.emit({ playerId: event.playerId });
463
- }
464
- // Reactive width signal for host element
465
- hostWidth = signal('', ...(ngDevMode ? [{ debugName: "hostWidth" }] : []));
466
- // Host binding for width (reactive)
467
- get hostWidthStyle() {
468
- return this.hostWidth();
469
- }
470
- // Computed values
471
- isLandscape = computed(() => this.orientation() === SoccerBoardOrientation.Landscape, ...(ngDevMode ? [{ debugName: "isLandscape" }] : []));
472
- showHome = computed(() => {
473
- const side = this.teamSide();
474
- return side === null || side === SoccerBoardTeamSide.Home;
475
- }, ...(ngDevMode ? [{ debugName: "showHome" }] : []));
476
- showAway = computed(() => {
477
- const side = this.teamSide();
478
- return side === null || side === SoccerBoardTeamSide.Away;
479
- }, ...(ngDevMode ? [{ debugName: "showAway" }] : []));
480
- // Computed position zones
481
- homeZones = computed(() => {
482
- if (!this.showPositions()) {
483
- return [];
484
- }
485
- const positionsData = getPositionsData();
486
- const orientation = this.orientation();
487
- return positionsData.map((pos) => ({
488
- ...pos,
489
- coords: pos[orientation],
490
- }));
491
- }, ...(ngDevMode ? [{ debugName: "homeZones" }] : []));
492
- awayZones = computed(() => {
493
- if (!this.showPositions()) {
494
- return [];
495
- }
496
- const positionsData = getPositionsData();
497
- const orientation = this.orientation();
498
- return positionsData.map((pos) => {
499
- const coords = pos[orientation];
500
- let awayCoords;
501
- if (orientation === SoccerBoardOrientation.Portrait) {
502
- // Portrait: invert Y
503
- awayCoords = {
504
- ...coords,
505
- y: 100 - coords.y - coords.height,
506
- };
507
- }
508
- else {
509
- // Landscape: invert X
510
- awayCoords = {
511
- ...coords,
512
- x: 100 - coords.x - coords.width,
513
- };
514
- }
515
- return {
516
- ...pos,
517
- coords: awayCoords,
518
- };
519
- });
520
- }, ...(ngDevMode ? [{ debugName: "awayZones" }] : []));
521
- // Computed players by side
522
- homePlayers = computed(() => {
523
- return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home && player.fieldX !== undefined && player.fieldY !== undefined);
524
- }, ...(ngDevMode ? [{ debugName: "homePlayers" }] : []));
525
- awayPlayers = computed(() => {
526
- return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away && player.fieldX !== undefined && player.fieldY !== undefined);
527
- }, ...(ngDevMode ? [{ debugName: "awayPlayers" }] : []));
528
- // Computed: players available (not on field)
529
- availablePlayers = computed(() => {
530
- return this.players().filter((player) => player.fieldX === undefined || player.fieldY === undefined);
531
- }, ...(ngDevMode ? [{ debugName: "availablePlayers" }] : []));
532
- // Computed: layout classes based on players position
533
- layoutClasses = computed(() => {
534
- if (!this.showPlayersSidebar()) {
535
- return 'layout-no-sidebar';
536
- }
537
- const position = this.playersPosition();
538
- switch (position) {
539
- case 'right':
540
- return 'layout-players-right';
541
- case 'top':
542
- return 'layout-players-top';
543
- case 'bottom':
544
- return 'layout-players-bottom';
545
- default:
546
- return 'layout-players-left';
547
- }
548
- }, ...(ngDevMode ? [{ debugName: "layoutClasses" }] : []));
549
- // Computed: players grid columns
550
- playersGridColumns = computed(() => {
551
- return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
552
- }, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
553
- // Helper method to get visual position for a player (public for template)
554
- getPlayerPosition(player) {
555
- if (player.fieldX === undefined || player.fieldY === undefined || !player.side) {
556
- return { left: 0, top: 0 };
557
- }
558
- return this.fieldToHalfCoordinates(player.fieldX, player.fieldY, player.side);
559
- }
560
- resizeObserver = null;
561
- constructor() {
562
- // Only run effects in browser (SSR protection)
563
- if (isPlatformBrowser(this.platformId)) {
564
- // Effect to update field size when fit or orientation changes
565
- effect(() => {
566
- this.fit();
567
- this.orientation();
568
- // Use setTimeout to ensure view is initialized
569
- setTimeout(() => {
570
- this.setupResizeObserver();
571
- this.updateFieldSize();
572
- }, 0);
573
- });
574
- // Effect to watch for --soccer-board-width CSS custom property changes
575
- effect(() => {
576
- // Trigger update when orientation changes (affects width calculation)
577
- this.orientation();
578
- if (this.fit() !== SoccerBoardFitMode.Contain) {
579
- this.updateFieldSizeFromCSS();
580
- }
581
- });
582
- }
583
- }
584
- /**
585
- * Handles player drag end event
586
- * Called from parent component when a player is dropped
587
- * @public
588
- */
589
- handlePlayerDrop(event) {
590
- console.log('[SoccerBoard] handlePlayerDrop called', event);
591
- const position = this.calculateFieldPosition(event.clientX, event.clientY);
592
- console.log('[SoccerBoard] Calculated position', position);
593
- if (position) {
594
- // Check if player is already on the field (moving to a different position)
595
- const currentPlayer = this.players().find((p) => p.id === event.playerId);
596
- const isMovingPlayer = currentPlayer?.fieldX !== undefined && currentPlayer?.fieldY !== undefined;
597
- // Check maximum players per side (only for new players, not when moving)
598
- if (!isMovingPlayer) {
599
- const playersOnSide = this.players().filter((p) => p.side === position.side && p.fieldX !== undefined && p.fieldY !== undefined).length;
600
- if (playersOnSide >= this.maxPlayersPerSide()) {
601
- console.warn(`[SoccerBoard] Maximum players (${this.maxPlayersPerSide()}) reached for ${position.side} side`);
602
- return; // Don't allow drop if max players reached
603
- }
604
- }
605
- const tacticalPosition = this.detectTacticalPosition(position.fieldX, position.fieldY, position.side);
606
- console.log('🎯 Player dropped:', {
607
- playerId: event.playerId,
608
- position: tacticalPosition,
609
- coordinates: {
610
- fieldX: position.fieldX,
611
- fieldY: position.fieldY,
612
- side: position.side,
613
- },
614
- });
615
- // Emit event with position information
616
- this.playerPositioned.emit({
617
- playerId: event.playerId,
618
- fieldX: position.fieldX,
619
- fieldY: position.fieldY,
620
- side: position.side,
621
- position: tacticalPosition,
622
- });
623
- }
624
- else {
625
- console.warn('[SoccerBoard] Position is null - player not dropped on field');
626
- }
627
- }
628
- /**
629
- * Calculates field position from client coordinates
630
- */
631
- calculateFieldPosition(clientX, clientY) {
632
- const homeHalfEl = this.homeHalf()?.nativeElement;
633
- const awayHalfEl = this.awayHalf()?.nativeElement;
634
- if (!homeHalfEl || !awayHalfEl) {
635
- return null;
636
- }
637
- // Get the center point of the dragged element
638
- // Use clientX/clientY directly as they represent where the mouse/touch is
639
- // The offset is already accounted for in the drag handling
640
- const centerX = clientX;
641
- const centerY = clientY;
642
- // Check HOME half
643
- const homeRect = homeHalfEl.getBoundingClientRect();
644
- const isInHome = centerX >= homeRect.left &&
645
- centerX <= homeRect.right &&
646
- centerY >= homeRect.top &&
647
- centerY <= homeRect.bottom;
648
- // Check AWAY half
649
- const awayRect = awayHalfEl.getBoundingClientRect();
650
- const isInAway = centerX >= awayRect.left &&
651
- centerX <= awayRect.right &&
652
- centerY >= awayRect.top &&
653
- centerY <= awayRect.bottom;
654
- if (!isInHome && !isInAway) {
655
- return null; // Not over the field
656
- }
657
- const side = isInHome ? SoccerBoardTeamSide.Home : SoccerBoardTeamSide.Away;
658
- const halfRect = isInHome ? homeRect : awayRect;
659
- // Calculate position within the half (0-100%)
660
- const relativeX = ((centerX - halfRect.left) / halfRect.width) * 100;
661
- const relativeY = ((centerY - halfRect.top) / halfRect.height) * 100;
662
- // Clamp to 0-100%
663
- const clampedX = Math.max(0, Math.min(100, relativeX));
664
- const clampedY = Math.max(0, Math.min(100, relativeY));
665
- // Convert to field coordinates (normalized)
666
- const orientation = this.orientation();
667
- let fieldX;
668
- let fieldY;
669
- if (orientation === SoccerBoardOrientation.Portrait) {
670
- // Portrait: X is width, Y is length
671
- fieldX = clampedX;
672
- if (side === SoccerBoardTeamSide.Home) {
673
- // HOME is bottom half (50-100% of field length)
674
- fieldY = 50 + clampedY / 2;
675
- }
676
- else {
677
- // AWAY is top half (0-50% of field length)
678
- fieldY = clampedY / 2;
679
- }
680
- }
681
- else {
682
- // Landscape: X is length, Y is width
683
- fieldY = clampedY; // Y is the width (top-bottom in visual space)
684
- if (side === SoccerBoardTeamSide.Home) {
685
- // HOME is right half (50-100% of field length)
686
- fieldX = 50 + clampedX / 2;
687
- }
688
- else {
689
- // AWAY is left half (0-50% of field length)
690
- fieldX = clampedX / 2;
691
- }
692
- }
693
- return { fieldX, fieldY, side };
694
- }
695
- /**
696
- * Converts field coordinates to half coordinates
697
- */
698
- fieldToHalfCoordinates(fieldX, fieldY, side) {
699
- const orientation = this.orientation();
700
- let left;
701
- let top;
702
- if (orientation === SoccerBoardOrientation.Portrait) {
703
- // Portrait mode
704
- left = fieldX;
705
- if (side === SoccerBoardTeamSide.Home) {
706
- // HOME: fieldY 50-100% maps to top 0-100%
707
- top = (fieldY - 50) * 2;
708
- }
709
- else {
710
- // AWAY: fieldY 0-50% maps to top 0-100%
711
- top = fieldY * 2;
712
- }
713
- }
714
- else {
715
- // Landscape mode
716
- top = fieldY;
717
- if (side === SoccerBoardTeamSide.Home) {
718
- // HOME: fieldX 50-100% maps to left 0-100%
719
- left = (fieldX - 50) * 2;
720
- }
721
- else {
722
- // AWAY: fieldX 0-50% maps to left 0-100%
723
- left = fieldX * 2;
724
- }
725
- }
726
- return { left, top };
727
- }
728
- /**
729
- * Detects tactical position based on field coordinates
730
- */
731
- detectTacticalPosition(fieldX, fieldY, side) {
732
- // Convert field coordinates to half coordinates
733
- const { left: halfX, top: halfY } = this.fieldToHalfCoordinates(fieldX, fieldY, side);
734
- // Get position data (same as show-positions)
735
- const positionsData = getPositionsData();
736
- const orientation = this.orientation();
737
- // Find which position zone contains this point
738
- let detectedPosition = 'CM'; // Default fallback
739
- let smallestArea = Infinity;
740
- for (const pos of positionsData) {
741
- let coords = pos[orientation];
742
- // For AWAY, we need to invert coordinates (same logic as renderPositionZones)
743
- if (side === SoccerBoardTeamSide.Away) {
744
- if (orientation === SoccerBoardOrientation.Portrait) {
745
- // Portrait: invert Y
746
- coords = {
747
- ...coords,
748
- y: 100 - coords.y - coords.height,
749
- };
750
- }
751
- else {
752
- // Landscape: invert X
753
- coords = {
754
- ...coords,
755
- x: 100 - coords.x - coords.width,
756
- };
757
- }
758
- }
759
- // Check if point is inside this zone
760
- const isInside = halfX >= coords.x &&
761
- halfX <= coords.x + coords.width &&
762
- halfY >= coords.y &&
763
- halfY <= coords.y + coords.height;
764
- if (isInside) {
765
- // If multiple zones overlap, prefer the smallest one (most specific)
766
- const area = coords.width * coords.height;
767
- if (area < smallestArea) {
768
- smallestArea = area;
769
- detectedPosition = pos.id;
770
- }
771
- }
772
- }
773
- return detectedPosition;
774
- }
775
- ngAfterViewInit() {
776
- if (!isPlatformBrowser(this.platformId)) {
777
- return;
778
- }
779
- // Initial setup
780
- setTimeout(() => {
781
- this.updateFieldSize();
782
- this.setupResizeObserver();
783
- }, 0);
784
- }
785
- ngOnDestroy() {
786
- this.cleanupResizeObserver();
787
- }
788
- setupResizeObserver() {
789
- if (!isPlatformBrowser(this.platformId)) {
790
- return;
791
- }
792
- this.cleanupResizeObserver();
793
- if (this.fit() !== SoccerBoardFitMode.Contain) {
794
- return;
795
- }
796
- // Observe the host element's parent (like in the original)
797
- const parentElement = this.hostElement.nativeElement.parentElement;
798
- if (!parentElement) {
799
- return;
800
- }
801
- this.resizeObserver = new ResizeObserver(() => {
802
- this.updateFieldSize();
803
- });
804
- this.resizeObserver.observe(parentElement);
805
- }
806
- cleanupResizeObserver() {
807
- if (this.resizeObserver) {
808
- this.resizeObserver.disconnect();
809
- this.resizeObserver = null;
810
- }
811
- }
812
- updateFieldSize() {
813
- if (!isPlatformBrowser(this.platformId)) {
814
- return;
815
- }
816
- const hostElement = this.hostElement.nativeElement;
817
- // If fit="contain", calculate size based on parent container (like original)
818
- if (this.fit() === SoccerBoardFitMode.Contain && hostElement.parentElement) {
819
- const parent = hostElement.parentElement;
820
- const parentStyle = getComputedStyle(parent);
821
- const parentWidth = parent.clientWidth -
822
- parseFloat(parentStyle.paddingLeft) -
823
- parseFloat(parentStyle.paddingRight);
824
- const parentHeight = parent.clientHeight -
825
- parseFloat(parentStyle.paddingTop) -
826
- parseFloat(parentStyle.paddingBottom);
827
- if (parentWidth > 0 && parentHeight > 0) {
828
- const fieldAspectRatio = this.orientation() === SoccerBoardOrientation.Landscape
829
- ? SoccerBoardFieldConstants.RATIO
830
- : 1 / SoccerBoardFieldConstants.RATIO;
831
- const containerAspectRatio = parentWidth / parentHeight;
832
- let width;
833
- if (containerAspectRatio > fieldAspectRatio) {
834
- // Container is wider - height is the constraint
835
- const height = parentHeight;
836
- width = height * fieldAspectRatio;
837
- }
838
- else {
839
- // Container is taller - width is the constraint
840
- width = parentWidth;
841
- }
842
- // Update width signal reactively
843
- this.hostWidth.set(`${width}px`);
844
- return;
845
- }
846
- }
847
- // Otherwise, use --soccer-board-width CSS custom property (like original)
848
- this.updateFieldSizeFromCSS();
849
- }
850
- updateFieldSizeFromCSS() {
851
- if (!isPlatformBrowser(this.platformId)) {
852
- return;
853
- }
854
- const hostElement = this.hostElement.nativeElement;
855
- const computedStyle = getComputedStyle(hostElement);
856
- const fieldWidth = computedStyle.getPropertyValue('--soccer-board-width').trim();
857
- if (!fieldWidth || fieldWidth === '100%') {
858
- // Default behavior: 100% width, let CSS handle it
859
- this.hostWidth.set(fieldWidth || '100%');
860
- return;
861
- }
862
- // Parse the width value
863
- const match = fieldWidth.match(/^([\d.]+)(px|rem|em|vw|vh|%)?$/);
864
- if (!match) {
865
- this.hostWidth.set(fieldWidth);
866
- return;
867
- }
868
- const value = parseFloat(match[1]);
869
- const unit = match[2] || 'px';
870
- if (this.orientation() === SoccerBoardOrientation.Landscape) {
871
- // In landscape, the CSS width represents the field length (105m)
872
- // So we multiply the physical width (68m) by the ratio to get the CSS width
873
- const landscapeWidth = value * SoccerBoardFieldConstants.RATIO;
874
- this.hostWidth.set(`${landscapeWidth}${unit}`);
875
- }
876
- else {
877
- // In portrait, the CSS width represents the field width (68m)
878
- this.hostWidth.set(`${value}${unit}`);
879
- }
880
- }
881
- /**
882
- * Exports the soccer board field (without sidebar) to PNG image
883
- * @public
884
- */
885
- async exportToPNG() {
886
- if (!isPlatformBrowser(this.platformId)) {
887
- console.warn('[SoccerBoard] Export to PNG is only available in browser');
888
- return;
889
- }
890
- const container = this.fieldContainer()?.nativeElement;
891
- if (!container) {
892
- console.error('[SoccerBoard] Field container not found for export');
893
- return;
894
- }
895
- try {
896
- console.log('[SoccerBoard] Starting PNG export...');
897
- // Wait for all images to load before exporting
898
- const images = container.querySelectorAll('img');
899
- const imagePromises = Array.from(images).map((img) => {
900
- if (img.complete) {
901
- return Promise.resolve();
902
- }
903
- return new Promise((resolve) => {
904
- img.onload = () => resolve();
905
- img.onerror = () => resolve(); // Continue even if image fails to load
906
- // Timeout after 5 seconds
907
- setTimeout(() => resolve(), 5000);
908
- });
909
- });
910
- await Promise.all(imagePromises);
911
- console.log('[SoccerBoard] All images loaded, capturing...');
912
- // Use html2canvas-pro to capture the field (supports oklab/oklch colors)
913
- const canvas = await html2canvas(container, {
914
- backgroundColor: null, // Transparent background
915
- scale: 2, // Higher quality
916
- logging: false,
917
- useCORS: true, // Allow cross-origin images
918
- allowTaint: false,
919
- width: container.offsetWidth,
920
- height: container.offsetHeight,
921
- ignoreElements: (element) => {
922
- // Ignore remove badges
923
- return element.classList?.contains('remove-badge') || false;
924
- },
925
- });
926
- // Convert canvas to blob and emit event
927
- canvas.toBlob((blob) => {
928
- if (!blob) {
929
- console.error('[SoccerBoard] Failed to create blob');
930
- return;
931
- }
932
- // Get data URL for the image
933
- const dataUrl = canvas.toDataURL('image/png');
934
- // Emit event with blob and data URL
935
- this.imageExported.emit({
936
- blob,
937
- dataUrl,
938
- });
939
- // Also trigger automatic download (optional - app can handle it via event)
940
- const url = URL.createObjectURL(blob);
941
- const link = document.createElement('a');
942
- link.href = url;
943
- link.download = `soccer-board-${Date.now()}.png`;
944
- document.body.appendChild(link);
945
- link.click();
946
- document.body.removeChild(link);
947
- URL.revokeObjectURL(url);
948
- console.log('[SoccerBoard] PNG exported successfully');
949
- }, 'image/png');
950
- }
951
- catch (error) {
952
- console.error('[SoccerBoard] Error exporting to PNG:', error);
953
- }
954
- }
955
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
956
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardComponent, isStandalone: true, selector: "fuzo-soccer-board", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, teamSide: { classPropertyName: "teamSide", publicName: "teamSide", isSignal: true, isRequired: false, transformFunction: null }, showPositions: { classPropertyName: "showPositions", publicName: "showPositions", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, players: { classPropertyName: "players", publicName: "players", isSignal: true, isRequired: false, transformFunction: null }, showPlayersSidebar: { classPropertyName: "showPlayersSidebar", publicName: "showPlayersSidebar", isSignal: true, isRequired: false, transformFunction: null }, playersPosition: { classPropertyName: "playersPosition", publicName: "playersPosition", isSignal: true, isRequired: false, transformFunction: null }, playersColumns: { classPropertyName: "playersColumns", publicName: "playersColumns", isSignal: true, isRequired: false, transformFunction: null }, maxPlayersPerSide: { classPropertyName: "maxPlayersPerSide", publicName: "maxPlayersPerSide", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playerPositioned: "playerPositioned", playerRemovedFromField: "playerRemovedFromField", imageExported: "imageExported", playerClicked: "playerClicked" }, host: { properties: { "style.width": "this.hostWidthStyle" } }, viewQueries: [{ propertyName: "fieldContainer", first: true, predicate: ["fieldContainer"], descendants: true, isSignal: true }, { propertyName: "homeHalf", first: true, predicate: ["homeHalf"], descendants: true, isSignal: true }, { propertyName: "awayHalf", first: true, predicate: ["awayHalf"], descendants: true, isSignal: true }, { propertyName: "soccerBoardContainer", first: true, predicate: ["soccerBoardContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"], dependencies: [{ kind: "component", type: SoccerBoardPlayerComponent, selector: "lib-soccer-board-player", inputs: ["player", "size"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
957
- }
958
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, decorators: [{
959
- type: Component,
960
- args: [{ selector: 'fuzo-soccer-board', standalone: true, imports: [SoccerBoardPlayerComponent], template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"] }]
961
- }], ctorParameters: () => [], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], teamSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "teamSide", required: false }] }], showPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPositions", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], players: [{ type: i0.Input, args: [{ isSignal: true, alias: "players", required: false }] }], showPlayersSidebar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPlayersSidebar", required: false }] }], playersPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersPosition", required: false }] }], playersColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersColumns", required: false }] }], maxPlayersPerSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPlayersPerSide", required: false }] }], fieldContainer: [{ type: i0.ViewChild, args: ['fieldContainer', { isSignal: true }] }], homeHalf: [{ type: i0.ViewChild, args: ['homeHalf', { isSignal: true }] }], awayHalf: [{ type: i0.ViewChild, args: ['awayHalf', { isSignal: true }] }], soccerBoardContainer: [{ type: i0.ViewChild, args: ['soccerBoardContainer', { isSignal: true }] }], playerPositioned: [{ type: i0.Output, args: ["playerPositioned"] }], playerRemovedFromField: [{ type: i0.Output, args: ["playerRemovedFromField"] }], imageExported: [{ type: i0.Output, args: ["imageExported"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], hostWidthStyle: [{
962
- type: HostBinding,
963
- args: ['style.width']
964
- }] } });
965
-
966
- /**
967
- * Generated bundle index. Do not edit.
968
- */
969
-
970
- export { SoccerBoardComponent, SoccerBoardFieldConstants, SoccerBoardFitMode, SoccerBoardOrientation, SoccerBoardPlayerComponent, SoccerBoardPlayerSize, SoccerBoardTeamSide };
971
- //# sourceMappingURL=fuzo-soccer-board.mjs.map