init
@@ -0,0 +1,148 @@
|
||||
# Two-Stands Transfers — Broadcast Graphics Template
|
||||
|
||||
Soccer player transfer broadcast graphics — **transparent overlay** for **1920×1080** frames.
|
||||
|
||||
## Specifications
|
||||
|
||||
| Property | State 1 (Reveal) | State 2 (Compact) |
|
||||
|-------------------|------------------|------------------------|
|
||||
| Frame | 1920×1080 px (transparent) | 1920×1080 px (transparent) |
|
||||
| Graphics area | 1920×758 px | 641×758 px (centered) |
|
||||
| Duration | 5 seconds | holds until next |
|
||||
| Player photo | 641×758 PNG (1:1) | 230px crop from 641-wide source |
|
||||
|
||||
## Layout — State 1 (0–5 s)
|
||||
|
||||
Full-width 1920×758 bar:
|
||||
|
||||
```
|
||||
┌─────────────────── 1920×758 ──────────────────────────────┐
|
||||
│ │
|
||||
│ ┌──────────┐ ПЕРЕХОД │
|
||||
│ │ │ │
|
||||
│ │ PLAYER │ PLAYER NAME │
|
||||
│ │ PHOTO │ │
|
||||
│ │ │ │
|
||||
│ │ 641px │ [MLS] ЧЕЛСИ ┌──────────┐ │
|
||||
│ │ (1:1) │ │ 138 млн │ │
|
||||
│ │ │ └──────────┘ │
|
||||
│ └──────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Layout — State 2 (after 5 s)
|
||||
|
||||
Shrinks to centered 641×758:
|
||||
|
||||
```
|
||||
┌──────── 641×758 ────────┐
|
||||
│ │
|
||||
│ ┌────────┐ ПЕРЕХОД │
|
||||
│ │ PHOTO │ PLAYER NAME │
|
||||
│ │ 230px │ │
|
||||
│ │ │ ЧЕЛСИ │
|
||||
│ │ │ ┌────────┐ │
|
||||
│ └────────┘ │ 138 млн│ │
|
||||
│ └────────┘ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
## Data — `data/transfers.json`
|
||||
|
||||
Generated from `assets/transfers-list.csv`. Each transfer object:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"playerName": "Морган Роджерс",
|
||||
"photo": "assets/players/player_01.png",
|
||||
"team": {
|
||||
"name": "Челси",
|
||||
"logo": "assets/teams/chelsea.svg",
|
||||
"color": "#034694",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "138 млн",
|
||||
"isFreeAgent": false
|
||||
}
|
||||
```
|
||||
|
||||
For free agents (`isFreeAgent: true`), the price badge turns green and the
|
||||
label shows "Статус" instead of "Сумма".
|
||||
|
||||
### Data source — `assets/transfers-list.csv`
|
||||
|
||||
```
|
||||
Player, Club, Sum
|
||||
Морган Роджерс, Челси, 138 млн
|
||||
Энтони Гордон, Барселона, 80 млн
|
||||
Ибрагим Конате, Реал, Свободный агент
|
||||
```
|
||||
|
||||
Edit the CSV, then re-run the generator to rebuild `transfers.json`
|
||||
and regenerate placeholder PNGs + team crests.
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Run a local server (needed for fetch)
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
### 2. Open in browser
|
||||
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
### 3. Controls
|
||||
|
||||
| Key | Action |
|
||||
|----------------|---------------------------|
|
||||
| → / Enter | Next transfer |
|
||||
| ← / Backspace | Previous transfer |
|
||||
| R | Replay current |
|
||||
| H | Hide / Show |
|
||||
| A | Toggle autoplay (8s loop) |
|
||||
|
||||
### External API (CasparCG / OBS)
|
||||
|
||||
```javascript
|
||||
window.transferGraphics.showAt(0); // show transfer #1 (starts State 1)
|
||||
window.transferGraphics.next(); // next transfer
|
||||
window.transferGraphics.prev(); // previous transfer
|
||||
window.transferGraphics.replay(); // replay current
|
||||
window.transferGraphics.hide(); // hide graphics
|
||||
window.transferGraphics.show(); // show graphics
|
||||
window.transferGraphics.autoplay(); // toggle autoplay
|
||||
```
|
||||
|
||||
## Generating Placeholder Assets
|
||||
|
||||
```bash
|
||||
python3 generate_placeholders.py
|
||||
```
|
||||
|
||||
This reads `assets/transfers-list.csv` and generates:
|
||||
- `assets/teams/*.svg` — team crest placeholders (colored shields)
|
||||
- `assets/players/player_*.png` — player PNG placeholders at **641×758**
|
||||
- `data/transfers.json` — the transfer data file
|
||||
|
||||
**Replace** the placeholder PNGs with real 641×758 player cutouts — just
|
||||
drop your files into `assets/players/` with the same names (`player_01.png`,
|
||||
`player_02.png`, …). No scaling needed — the source image is displayed 1:1
|
||||
in State 1, and CSS `object-fit: cover` crops it for the compact State 2.
|
||||
|
||||
## Timing
|
||||
|
||||
```
|
||||
0.0 s Card enters (slide + fade)
|
||||
0.15s Photo scales in
|
||||
0.35s "ПЕРЕХОД" label slides down
|
||||
0.55s Player name slides in
|
||||
1.0 s Team block appears
|
||||
1.35s Price badge pops in (bounce)
|
||||
5.0 s Container shrinks 1920 → 641 (0.85 s smooth transition)
|
||||
5.85s+ Compact state holds
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
Player, Club, Sum, Photo, Logo
|
||||
Морган Роджерс,Челси,138 млн,tmp-player.png,chelsea.png
|
||||
Эллиот Андерсон,Манчестер Сити,135 млн,tmp-player.png,manchester-city.png
|
||||
Сандро Тонали,Тоттенхэм,108 млн,tmp-player.png,tottenham.png
|
||||
Матеус Фернандес,Тоттенхэм,99 млн,tmp-player.png,tottenham.png
|
||||
Энтони Гордон,Барселона,80 млн,tmp-player.png,barcelona.png
|
||||
Гонсало Рамуш,Милан,74 млн,tmp-player.png,ajax.png
|
||||
Самервилл,Аль-Хилаль,"64,5 млн",tmp-player.png,ajax.png
|
||||
Жереми Жаке,Ливерпуль,"63,6 млн",tmp-player.png,liverpool.png
|
||||
Ван Хекке,Тоттенхэм,60 млн,tmp-player.png,tottenham.png
|
||||
Йохан Манзамби,Астон Вилла,60 млн,tmp-player.png,ajax.png
|
||||
Лука Вушкович,Брайтон,54 млн,tmp-player.png,chelsea.png
|
||||
Марко Палестра,Челси,57 млн,tmp-player.png,chelsea.png
|
||||
Андре Сантос,Манчестер Юнайтед,"56,3 млн",tmp-player.png,chelsea.png
|
||||
Марк Кукурелья,Реал,50 млн,tmp-player.png,chelsea.png
|
||||
Натаниэль Браун,Бавария,50 млн,tmp-player.png,chelsea.png
|
||||
Исмаэль Сайбари,Бавария,50 млн,tmp-player.png,chelsea.png
|
||||
Ибрагим Конате,Реал,Свободный агент,tmp-player.png,chelsea.png
|
||||
Опенда,Ювентус,"42,75 млн",tmp-player.png,chelsea.png
|
||||
Тилеманс,Манчестер Юнайтед,41 млн,tmp-player.png,chelsea.png
|
||||
Христос Цолис,Арсенал,40 млн,tmp-player.png,chelsea.png
|
||||
Кан Ли,Атлетико,40 млн,tmp-player.png,chelsea.png
|
||||
Карим Адейеми,Барселона,22 млн,tmp-player.png,chelsea.png
|
||||
Гарначо,Астон Вилла,Аренда,tmp-player.png,chelsea.png
|
||||
Александар Станкович,Интер,23 млн,tmp-player.png,chelsea.png
|
||||
Думфрис,Реал,20 млн,tmp-player.png,chelsea.png
|
||||
Гримальдо,Атлетико,22 млн,tmp-player.png,chelsea.png
|
||||
Бернардо Силва,Реал,Свободный агент,tmp-player.png,chelsea.png
|
||||
Оскар Мингеса,Кристал Пэлас,Свободный агент,tmp-player.png,chelsea.png
|
||||
Аканжи,Интер,15 млн,tmp-player.png,chelsea.png
|
||||
Альваро Мората,Комо,12 млн,tmp-player.png,chelsea.png
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,324 @@
|
||||
/* ==========================================================================
|
||||
TRANSFER Broadcast Graphics Template (transparent overlay)
|
||||
Frame: 1920 x 1080 px (transparent for compositor / OBS / CasparCG)
|
||||
Graphics: 1920 x 758 px -> 641 x 758 px (center, after 5s)
|
||||
Player photo source: 641 x 758 px PNG (displayed 1:1 in State 1)
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
--team-color: #FEBE10;
|
||||
--team-accent: #00529F;
|
||||
|
||||
--gfx-h: 758px;
|
||||
--gfx-w-full: 1920px;
|
||||
--gfx-w-mini: 641px;
|
||||
--photo-w: 641px; /* source image width — 1:1 in State 1 */
|
||||
--photo-w-c: 250px; /* compact State 2 width (cropped from 641 source) */
|
||||
|
||||
--ease: cubic-bezier(0.45, 0, 0.15, 1);
|
||||
--bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--speed: 1.0s;
|
||||
|
||||
--white: #FFFFFF;
|
||||
--dark: #0F0F14;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
font-family: 'ONY One', 'Barlow Condensed', 'Arial Narrow', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Frame ============================================================== */
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: transparent; /* overlay mode — no background */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Graphics container ================================================= */
|
||||
.graphics-container {
|
||||
position: absolute;
|
||||
bottom: 161px; /* (1080 - 758) / 2 = 161 -> vertically centered */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1920px;
|
||||
height: 758px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease, width var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.graphics-container.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.graphics-container.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.graphics-container.compact {
|
||||
width: 641px;
|
||||
}
|
||||
|
||||
/* Transfer card ====================================================== */
|
||||
.transfer-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: #222524;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: background var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
/* Photo section ====================================================== */
|
||||
.photo-section {
|
||||
flex: 0 0 var(--photo-w);
|
||||
width: var(--photo-w);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: flex-basis var(--speed) var(--ease),
|
||||
width var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .photo-section {
|
||||
flex: 0 0 var(--photo-w-c);
|
||||
width: var(--photo-w-c);
|
||||
}
|
||||
|
||||
.player-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
display: block;
|
||||
transition: object-position var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.photo-overlay {
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
transparent 55%,
|
||||
rgba(0, 0, 0, 0.4) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Info section ======================================================= */
|
||||
.info-section {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 55px 70px 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* always center — never changes */
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
transition: padding var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .info-section {
|
||||
padding: 50px 35px 50px;
|
||||
}
|
||||
|
||||
/* Player name ---------------------------------------------------- */
|
||||
.player-name {
|
||||
font-size: 72px;
|
||||
font-weight: 900;
|
||||
line-height: 0.95;
|
||||
text-transform: uppercase;
|
||||
color: var(--white);
|
||||
text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.22);
|
||||
margin-bottom: 36px;
|
||||
transition: font-size var(--speed) var(--ease),
|
||||
line-height var(--speed) var(--ease),
|
||||
margin var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .player-name {
|
||||
font-size: 48px;
|
||||
line-height: 0.95;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Team block ------------------------------------------------------ */
|
||||
.team-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 24px;
|
||||
transition: gap var(--speed) var(--ease),
|
||||
margin var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .team-block {
|
||||
gap: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.team-logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
transition: width var(--speed) var(--ease), height var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .team-logo { width: 56px; height: 56px; }
|
||||
|
||||
.team-text { display: flex; flex-direction: column; }
|
||||
|
||||
.team-direction {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
color: var(--white);
|
||||
opacity: 0.5;
|
||||
transition: font-size var(--speed) var(--ease),
|
||||
letter-spacing var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .team-direction { font-size: 13px; letter-spacing: 3px; }
|
||||
|
||||
.team-name {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--white);
|
||||
line-height: 1;
|
||||
margin-top: 4px;
|
||||
transition: font-size var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .team-name { font-size: 22px; }
|
||||
|
||||
/* Price ----------------------------------------------------------- */
|
||||
.price-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-shrink: 0;
|
||||
transition: align-self var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .price-badge {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--white);
|
||||
opacity: 0.5;
|
||||
transition: font-size var(--speed) var(--ease),
|
||||
letter-spacing var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
.compact .price-label { font-size: 13px; letter-spacing: 3px; }
|
||||
|
||||
.price-value {
|
||||
font-size: 46px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
color: var(--white);
|
||||
line-height: 1;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
transition: font-size var(--speed) var(--ease),
|
||||
color var(--speed) var(--ease),
|
||||
margin var(--speed) var(--ease);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.compact .price-value { font-size: 38px; }
|
||||
|
||||
/* ========================================================================
|
||||
Reveal animation (staggered entrance)
|
||||
======================================================================== */
|
||||
.reveal .transfer-card { animation: cardIn 0.5s var(--ease) both; }
|
||||
.reveal .photo-section { animation: photoIn 0.6s var(--ease) 0.15s both; }
|
||||
.reveal .player-name { animation: slideRight 0.5s var(--ease) 0.55s both; }
|
||||
.reveal .team-block { animation: slideRight 0.4s var(--ease) 1.0s both; }
|
||||
.reveal .price-badge { animation: slideRight 0.4s var(--ease) 1.3s both; }
|
||||
|
||||
@keyframes cardIn {
|
||||
from { transform: translateY(30px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
@keyframes photoIn {
|
||||
from { transform: scale(1.15); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from { transform: translateY(-15px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 0.55; }
|
||||
}
|
||||
@keyframes slideRight {
|
||||
from { transform: translateX(-25px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes popIn {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
/* ========================================================================
|
||||
Operator panel (dev overlay)
|
||||
======================================================================== */
|
||||
.operator-panel {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: #eee;
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
font-family: 'ONY One', sans-serif;
|
||||
font-size: 16px;
|
||||
z-index: 100;
|
||||
opacity: 0.25;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.operator-panel:hover { opacity: 0.95; }
|
||||
|
||||
.op-counter {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #FEBE10;
|
||||
}
|
||||
|
||||
.op-controls { font-size: 14px; color: #aaa; }
|
||||
|
||||
kbd {
|
||||
background: #333;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
padding: 1px 7px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"transfers": [
|
||||
{
|
||||
"id": 1,
|
||||
"playerName": "Морган Роджерс",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Челси",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#034694",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "138 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"playerName": "Эллиот Андерсон",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Манчестер Сити",
|
||||
"logo": "clubs-logo/manchester-city.png",
|
||||
"color": "#6CABDD",
|
||||
"accent": "#1C2C5B"
|
||||
},
|
||||
"price": "135 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"playerName": "Сандро Тонали",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Тоттенхэм",
|
||||
"logo": "clubs-logo/tottenham.png",
|
||||
"color": "#132257",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "108 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"playerName": "Матеус Фернандес",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Тоттенхэм",
|
||||
"logo": "clubs-logo/tottenham.png",
|
||||
"color": "#132257",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "99 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"playerName": "Энтони Гордон",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Барселона",
|
||||
"logo": "clubs-logo/barcelona.png",
|
||||
"color": "#A50044",
|
||||
"accent": "#004D98"
|
||||
},
|
||||
"price": "80 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"playerName": "Гонсало Рамуш",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Милан",
|
||||
"logo": "clubs-logo/ajax.png",
|
||||
"color": "#FB090B",
|
||||
"accent": "#000000"
|
||||
},
|
||||
"price": "74 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"playerName": "Самервилл",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Аль-Хилаль",
|
||||
"logo": "clubs-logo/ajax.png",
|
||||
"color": "#1B458F",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "64,5 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"playerName": "Жереми Жаке",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Ливерпуль",
|
||||
"logo": "clubs-logo/liverpool.png",
|
||||
"color": "#C8102E",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "63,6 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"playerName": "Ван Хекке",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Тоттенхэм",
|
||||
"logo": "clubs-logo/tottenham.png",
|
||||
"color": "#132257",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "60 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"playerName": "Йохан Манзамби",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Астон Вилла",
|
||||
"logo": "clubs-logo/ajax.png",
|
||||
"color": "#95BFE5",
|
||||
"accent": "#670E36"
|
||||
},
|
||||
"price": "60 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"playerName": "Лука Вушкович",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Брайтон",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#0057B8",
|
||||
"accent": "#FFCD00"
|
||||
},
|
||||
"price": "54 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"playerName": "Марко Палестра",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Челси",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#034694",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "57 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"playerName": "Андре Сантос",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Манчестер Юнайтед",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#DA291C",
|
||||
"accent": "#FFE500"
|
||||
},
|
||||
"price": "56,3 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"playerName": "Марк Кукурелья",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Реал",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#FEBE10",
|
||||
"accent": "#00529F"
|
||||
},
|
||||
"price": "50 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"playerName": "Натаниэль Браун",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Бавария",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#DC052D",
|
||||
"accent": "#0066B2"
|
||||
},
|
||||
"price": "50 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"playerName": "Исмаэль Сайбари",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Бавария",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#DC052D",
|
||||
"accent": "#0066B2"
|
||||
},
|
||||
"price": "50 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"playerName": "Ибрагим Конате",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Реал",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#FEBE10",
|
||||
"accent": "#00529F"
|
||||
},
|
||||
"price": "Свободный агент",
|
||||
"status": "free-agent"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"playerName": "Опенда",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Ювентус",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#000000",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "42,75 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"playerName": "Тилеманс",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Манчестер Юнайтед",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#DA291C",
|
||||
"accent": "#FFE500"
|
||||
},
|
||||
"price": "41 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"playerName": "Христос Цолис",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Арсенал",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#EF0107",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "40 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"playerName": "Кан Ли",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Атлетико",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#CB3524",
|
||||
"accent": "#261E60"
|
||||
},
|
||||
"price": "40 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"playerName": "Карим Адейеми",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Барселона",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#A50044",
|
||||
"accent": "#004D98"
|
||||
},
|
||||
"price": "22 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"playerName": "Гарначо",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Астон Вилла",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#95BFE5",
|
||||
"accent": "#670E36"
|
||||
},
|
||||
"price": "Аренда",
|
||||
"status": "loan"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"playerName": "Александар Станкович",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Интер",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#0068A8",
|
||||
"accent": "#000000"
|
||||
},
|
||||
"price": "23 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"playerName": "Думфрис",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Реал",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#FEBE10",
|
||||
"accent": "#00529F"
|
||||
},
|
||||
"price": "20 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"playerName": "Гримальдо",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Атлетико",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#CB3524",
|
||||
"accent": "#261E60"
|
||||
},
|
||||
"price": "22 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"playerName": "Бернардо Силва",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Реал",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#FEBE10",
|
||||
"accent": "#00529F"
|
||||
},
|
||||
"price": "Свободный агент",
|
||||
"status": "free-agent"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"playerName": "Оскар Мингеса",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Кристал Пэлас",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#1B458F",
|
||||
"accent": "#C4122E"
|
||||
},
|
||||
"price": "Свободный агент",
|
||||
"status": "free-agent"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"playerName": "Аканжи",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Интер",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#0068A8",
|
||||
"accent": "#000000"
|
||||
},
|
||||
"price": "15 млн",
|
||||
"status": "fee"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"playerName": "Альваро Мората",
|
||||
"photo": "tmp-player.png",
|
||||
"team": {
|
||||
"name": "Комо",
|
||||
"logo": "clubs-logo/chelsea.png",
|
||||
"color": "#0033A0",
|
||||
"accent": "#FFFFFF"
|
||||
},
|
||||
"price": "12 млн",
|
||||
"status": "fee"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate data/transfers.json from assets/transfers-list.csv.
|
||||
|
||||
CSV columns: Player, Club, Sum, Photo, Logo
|
||||
- Photo: filename of the player photo at project root (e.g. tmp-player.png)
|
||||
- Logo: filename of the club logo in clubs-logo/ (e.g. chelsea.png)
|
||||
|
||||
Team colours are looked up from the CLUBS dict below; unknown clubs
|
||||
get auto-generated colours.
|
||||
|
||||
Run: python3 generate_placeholders.py
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import colorsys
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "data")
|
||||
CSV_PATH = os.path.join(BASE_DIR, "assets", "transfers-list.csv")
|
||||
CLUBS_DIR = os.path.join(BASE_DIR, "clubs-logo")
|
||||
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Russian club name -> colours
|
||||
# ---------------------------------------------------------------------------
|
||||
CLUBS = {
|
||||
"Челси": {"color": "#034694", "accent": "#FFFFFF"},
|
||||
"Барселона": {"color": "#A50044", "accent": "#004D98"},
|
||||
"Реал": {"color": "#FEBE10", "accent": "#00529F"},
|
||||
"Арсенал": {"color": "#EF0107", "accent": "#FFFFFF"},
|
||||
"Бавария": {"color": "#DC052D", "accent": "#0066B2"},
|
||||
"ПСЖ": {"color": "#004170", "accent": "#DA291C"},
|
||||
"Ман Сити": {"color": "#6CABDD", "accent": "#1C2C5B"},
|
||||
"Ливерпуль": {"color": "#C8102E", "accent": "#FFFFFF"},
|
||||
"Ювентус": {"color": "#000000", "accent": "#FFFFFF"},
|
||||
"Интер": {"color": "#0068A8", "accent": "#000000"},
|
||||
"Милан": {"color": "#FB090B", "accent": "#000000"},
|
||||
"Атлетико": {"color": "#CB3524", "accent": "#261E60"},
|
||||
"Тоттенхэм": {"color": "#132257", "accent": "#FFFFFF"},
|
||||
"Ньюкасл": {"color": "#241F20", "accent": "#FFFFFF"},
|
||||
"Вест Хэм": {"color": "#7A263A", "accent": "#1BB1E7"},
|
||||
"Брайтон": {"color": "#0057B8", "accent": "#FFCD00"},
|
||||
"Байер": {"color": "#E32221", "accent": "#000000"},
|
||||
"Айнтрахт": {"color": "#000000", "accent": "#E1000F"},
|
||||
"Олимпиакос": {"color": "#E30613", "accent": "#FFFFFF"},
|
||||
"Галатасарай":{"color": "#A90432", "accent": "#FCB330"},
|
||||
"Аталанта": {"color": "#1E71B8", "accent": "#000000"},
|
||||
"Наполи": {"color": "#12A0E0", "accent": "#FFFFFF"},
|
||||
"Брюгге": {"color": "#0066B2", "accent": "#000000"},
|
||||
"Боде/Глимт": {"color": "#FCD116", "accent": "#000000"},
|
||||
"Унион": {"color": "#EA2127", "accent": "#FFFFFF"},
|
||||
"Копенгаген": {"color": "#000000", "accent": "#FFFFFF"},
|
||||
"Пафос": {"color": "#0033A0", "accent": "#FFFFFF"},
|
||||
"Славия": {"color": "#C8102E", "accent": "#FFFFFF"},
|
||||
"Карабах": {"color": "#000000", "accent": "#FFFFFF"},
|
||||
"Кайрат": {"color": "#000000", "accent": "#FFD700"},
|
||||
"Бенфика": {"color": "#E30613", "accent": "#FFFFFF"},
|
||||
"Вильярреал": {"color": "#FFE667", "accent": "#00529F"},
|
||||
"Аякс": {"color": "#D2122E", "accent": "#FFFFFF"},
|
||||
"Монако": {"color": "#E20E32", "accent": "#FFFFFF"},
|
||||
"ПСВ": {"color": "#ED1C24", "accent": "#FFFFFF"},
|
||||
"Спортинг": {"color": "#008053", "accent": "#FFFFFF"},
|
||||
"Марсель": {"color": "#009DDC", "accent": "#FFFFFF"},
|
||||
"Боруссия": {"color": "#FDE100", "accent": "#000000"},
|
||||
"Атлетик": {"color": "#EE2523", "accent": "#FFFFFF"},
|
||||
"Манчестер Сити": {"color": "#6CABDD", "accent": "#1C2C5B"},
|
||||
"Манчестер Юнайтед": {"color": "#DA291C", "accent": "#FFE500"},
|
||||
"Астон Вилла": {"color": "#95BFE5", "accent": "#670E36"},
|
||||
"Аль-Хилаль": {"color": "#1B458F", "accent": "#FFFFFF"},
|
||||
"Кристал Пэлас": {"color": "#1B458F", "accent": "#C4122E"},
|
||||
"Комо": {"color": "#0033A0", "accent": "#FFFFFF"},
|
||||
}
|
||||
|
||||
FREE_AGENT_KEYWORDS = ("свободн", "free")
|
||||
LOAN_KEYWORDS = ("аренд", "loan")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Colour helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
def _hex_to_rgb(h):
|
||||
h = h.lstrip("#")
|
||||
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
def _rgb_to_hex(rgb):
|
||||
return "#{:02X}{:02X}{:02X}".format(*rgb)
|
||||
|
||||
|
||||
def auto_colors(name):
|
||||
h = hash(name) % 360
|
||||
r, g, b = colorsys.hsv_to_rgb(h / 360, 0.65, 0.75)
|
||||
return _rgb_to_hex((int(r * 255), int(g * 255), int(b * 255))), "#FFFFFF"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse CSV and build JSON
|
||||
# ---------------------------------------------------------------------------
|
||||
def parse_csv():
|
||||
rows = []
|
||||
with open(CSV_PATH, encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f, skipinitialspace=True)
|
||||
for row in reader:
|
||||
player = (row.get("Player") or "").strip()
|
||||
if not player:
|
||||
continue
|
||||
rows.append({
|
||||
"player": player,
|
||||
"club": (row.get("Club") or "").strip(),
|
||||
"fee": (row.get("Sum") or "").strip(),
|
||||
"photo": (row.get("Photo") or "").strip(),
|
||||
"logo": (row.get("Logo") or "").strip(),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
rows = parse_csv()
|
||||
if not rows:
|
||||
print("ERROR: no data found in", CSV_PATH)
|
||||
return
|
||||
|
||||
transfers_json = []
|
||||
warnings = []
|
||||
|
||||
for i, r in enumerate(rows):
|
||||
club_name = r["club"]
|
||||
if not club_name:
|
||||
warnings.append(f"Row {i+1}: empty club for player '{r['player']}'")
|
||||
continue
|
||||
|
||||
meta = CLUBS.get(club_name)
|
||||
if meta:
|
||||
color = meta["color"]
|
||||
accent = meta["accent"]
|
||||
else:
|
||||
color, accent = auto_colors(club_name)
|
||||
warnings.append(f"Row {i+1}: unknown club '{club_name}' -> auto colours {color}")
|
||||
|
||||
photo = r["photo"] if r["photo"] else "tmp-player.png"
|
||||
photo_path = os.path.join(BASE_DIR, photo)
|
||||
if not os.path.exists(photo_path):
|
||||
warnings.append(f"Row {i+1}: photo '{photo}' not found at project root")
|
||||
|
||||
logo = r["logo"] if r["logo"] else ""
|
||||
logo_path = os.path.join(CLUBS_DIR, logo) if logo else ""
|
||||
if logo and not os.path.exists(logo_path):
|
||||
warnings.append(f"Row {i+1}: logo '{logo}' not found in clubs-logo/")
|
||||
|
||||
is_free = any(kw in r["fee"].lower() for kw in FREE_AGENT_KEYWORDS)
|
||||
is_loan = any(kw in r["fee"].lower() for kw in LOAN_KEYWORDS)
|
||||
price = r["fee"] if r["fee"] else ""
|
||||
|
||||
if is_free:
|
||||
status = "free-agent"
|
||||
elif is_loan:
|
||||
status = "loan"
|
||||
else:
|
||||
status = "fee"
|
||||
|
||||
transfers_json.append({
|
||||
"id": i + 1,
|
||||
"playerName": r["player"],
|
||||
"photo": photo,
|
||||
"team": {
|
||||
"name": club_name,
|
||||
"logo": f"clubs-logo/{logo}" if logo else "",
|
||||
"color": color,
|
||||
"accent": accent,
|
||||
},
|
||||
"price": price,
|
||||
"status": status,
|
||||
})
|
||||
|
||||
json_path = os.path.join(DATA_DIR, "transfers.json")
|
||||
with open(json_path, "w") as f:
|
||||
json.dump({"transfers": transfers_json}, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
print(f" Data -> {os.path.relpath(json_path, BASE_DIR)}")
|
||||
|
||||
if warnings:
|
||||
print("\n WARNINGS:")
|
||||
for w in warnings:
|
||||
print(f" ! {w}")
|
||||
|
||||
print(f"\nDone! {len(transfers_json)} transfers written to transfers.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1920, initial-scale=1">
|
||||
<title>TRANSFER | Broadcast Graphics</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<style>
|
||||
@font-face { font-family: 'ONY One'; src: local('ONY One Regular'), url('file:///home/itten/.local/share/fonts/ONYOne-Regular.otf') format('opentype'); font-weight: 400; }
|
||||
@font-face { font-family: 'ONY One'; src: local('ONY One Medium'), url('file:///home/itten/.local/share/fonts/ONYOne-Medium.otf') format('opentype'); font-weight: 500; }
|
||||
@font-face { font-family: 'ONY One'; src: local('ONY One Bold'), url('file:///home/itten/.local/share/fonts/ONYOne-Bold.otf') format('opentype'); font-weight: 700; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 1920x1080 broadcast frame (transparent overlay) -->
|
||||
<div class="frame" id="frame">
|
||||
|
||||
<!-- Graphics overlay: 1920x758 -> 641x758 (centered after 5s) -->
|
||||
<div class="graphics-container" id="graphicsContainer">
|
||||
|
||||
<div class="transfer-card" id="transferCard">
|
||||
|
||||
<!-- Photo section (source image: 641x758 PNG) -->
|
||||
<div class="photo-section">
|
||||
<img class="player-photo" id="playerPhoto" src="" alt="">
|
||||
<div class="photo-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- Info section -->
|
||||
<div class="info-section">
|
||||
|
||||
<!-- Player name -->
|
||||
<div class="info-mid">
|
||||
<div class="player-name" id="playerName"></div>
|
||||
</div>
|
||||
|
||||
<!-- Destination club -->
|
||||
<div class="team-block" id="teamBlock">
|
||||
<img class="team-logo" id="teamLogo" src="" alt="">
|
||||
<div class="team-text">
|
||||
<div class="team-direction" id="teamDirection">КЛУБ</div>
|
||||
<div class="team-name" id="teamName"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="price-badge" id="priceBadge">
|
||||
<span class="price-label" id="priceLabel">СУММА</span>
|
||||
<span class="price-value" id="priceValue"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Operator panel (dev overlay) -->
|
||||
<div class="operator-panel" id="operatorPanel">
|
||||
<div class="op-counter" id="currentTransfer">- / -</div>
|
||||
<div class="op-controls">
|
||||
<kbd>←</kbd> Prev <kbd>→</kbd> / <kbd>Enter</kbd> Next <kbd>R</kbd> Replay <kbd>H</kbd> Hide <kbd>A</kbd> Autoplay
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,180 @@
|
||||
/* ==========================================================================
|
||||
Transfer Broadcast Graphics - Controller
|
||||
Data: data/transfers.json (generated from assets/transfers-list.csv)
|
||||
========================================================================== */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var REVEAL_TIME = 5000; // 5 seconds in State 1, then -> compact
|
||||
var AUTOPLAY_MS = 8000; // auto-advance interval
|
||||
|
||||
var transfers = [];
|
||||
var currentIndex = 0;
|
||||
var compactTimer = null;
|
||||
var autoplayTimer = null;
|
||||
var isHidden = true;
|
||||
var autoplayOn = false;
|
||||
|
||||
var container, card, opCounter;
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
async function init() {
|
||||
container = $("graphicsContainer");
|
||||
card = $("transferCard");
|
||||
opCounter = $("currentTransfer");
|
||||
|
||||
try {
|
||||
var res = await fetch("data/transfers.json");
|
||||
var data = await res.json();
|
||||
transfers = data.transfers;
|
||||
} catch (e) {
|
||||
console.error("Failed to load transfers.json", e);
|
||||
opCounter.textContent = "DATA ERROR";
|
||||
return;
|
||||
}
|
||||
|
||||
updateCounter();
|
||||
setupKeys();
|
||||
console.log("Loaded " + transfers.length + " transfers. Press \u2192 or Enter to start.");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
function showTransfer(index) {
|
||||
if (!transfers.length) return;
|
||||
var t = transfers[index];
|
||||
if (!t) return;
|
||||
|
||||
container.classList.remove("compact");
|
||||
|
||||
// photo
|
||||
$("playerPhoto").src = t.photo;
|
||||
|
||||
// text
|
||||
$("playerName").textContent = t.playerName;
|
||||
$("teamName").textContent = t.team.name;
|
||||
$("teamLogo").src = t.team.logo;
|
||||
|
||||
// price / status
|
||||
var badge = $("priceBadge");
|
||||
var priceValue = $("priceValue");
|
||||
var priceLabel = $("priceLabel");
|
||||
|
||||
badge.classList.remove("free-agent", "loan");
|
||||
|
||||
if (t.status === "free-agent") {
|
||||
badge.classList.add("free-agent");
|
||||
priceLabel.textContent = "СТАТУС";
|
||||
priceValue.textContent = t.price;
|
||||
} else if (t.status === "loan") {
|
||||
badge.classList.add("loan");
|
||||
priceLabel.textContent = "СТАТУС";
|
||||
priceValue.textContent = t.price;
|
||||
} else {
|
||||
priceLabel.textContent = "СУММА";
|
||||
priceValue.textContent = t.price;
|
||||
}
|
||||
|
||||
// team colour vars
|
||||
card.style.setProperty("--team-color", t.team.color);
|
||||
card.style.setProperty("--team-accent", t.team.accent);
|
||||
|
||||
// trigger reveal animation (restart)
|
||||
card.classList.remove("reveal");
|
||||
void card.offsetWidth;
|
||||
card.classList.add("reveal");
|
||||
|
||||
// show
|
||||
container.classList.remove("hidden");
|
||||
container.classList.add("visible");
|
||||
isHidden = false;
|
||||
|
||||
// schedule 5 s compact transition
|
||||
clearTimeout(compactTimer);
|
||||
compactTimer = setTimeout(function () {
|
||||
card.classList.remove("reveal"); // clear animation fill-mode locks so transitions are clean
|
||||
container.classList.add("compact");
|
||||
}, REVEAL_TIME);
|
||||
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
function next() {
|
||||
if (isHidden) { showTransfer(currentIndex); return; }
|
||||
currentIndex = (currentIndex + 1) % transfers.length;
|
||||
showTransfer(currentIndex);
|
||||
}
|
||||
function prev() {
|
||||
if (isHidden) { showTransfer(currentIndex); return; }
|
||||
currentIndex = (currentIndex - 1 + transfers.length) % transfers.length;
|
||||
showTransfer(currentIndex);
|
||||
}
|
||||
function replay() { showTransfer(currentIndex); }
|
||||
|
||||
function toggleHide() {
|
||||
if (isHidden) {
|
||||
isHidden = false;
|
||||
replay();
|
||||
} else {
|
||||
isHidden = true;
|
||||
container.classList.add("hidden");
|
||||
container.classList.remove("visible");
|
||||
clearTimeout(compactTimer);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoplay() {
|
||||
autoplayOn = !autoplayOn;
|
||||
if (autoplayOn) {
|
||||
console.log("Autoplay ON");
|
||||
if (isHidden) replay();
|
||||
autoplayTimer = setInterval(next, AUTOPLAY_MS);
|
||||
} else {
|
||||
console.log("Autoplay OFF");
|
||||
clearInterval(autoplayTimer);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
function updateCounter() {
|
||||
if (transfers.length && opCounter)
|
||||
opCounter.textContent = (currentIndex + 1) + " / " + transfers.length;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
function setupKeys() {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
switch (e.key) {
|
||||
case "ArrowRight":
|
||||
case "Enter":
|
||||
e.preventDefault(); next(); break;
|
||||
case "ArrowLeft":
|
||||
case "Backspace":
|
||||
e.preventDefault(); prev(); break;
|
||||
case "r": case "R": replay(); break;
|
||||
case "h": case "H": toggleHide(); break;
|
||||
case "a": case "A": toggleAutoplay(); break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expose for CasparCG / OBS / external control
|
||||
window.transferGraphics = {
|
||||
next: next,
|
||||
prev: prev,
|
||||
replay: replay,
|
||||
hide: function () { if (!isHidden) toggleHide(); },
|
||||
show: function () { if (isHidden) toggleHide(); },
|
||||
showAt: function (i) { currentIndex = i; showTransfer(i); },
|
||||
autoplay: toggleAutoplay,
|
||||
};
|
||||
|
||||
// GO
|
||||
if (document.readyState === "loading")
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
else
|
||||
init();
|
||||
})();
|
||||
|
After Width: | Height: | Size: 666 KiB |