commit cf7b8e1a5d3f1bca2ba916fe97a787ca6712de16 Author: itten Date: Sun Aug 2 11:32:45 2026 +0300 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ca3344 --- /dev/null +++ b/README.md @@ -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 +``` \ No newline at end of file diff --git a/assets/transfers-list.csv b/assets/transfers-list.csv new file mode 100644 index 0000000..c1a8704 --- /dev/null +++ b/assets/transfers-list.csv @@ -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 + + + + diff --git a/clubs-logo/ajax.png b/clubs-logo/ajax.png new file mode 100644 index 0000000..6c3ee03 Binary files /dev/null and b/clubs-logo/ajax.png differ diff --git a/clubs-logo/arsenal.png b/clubs-logo/arsenal.png new file mode 100644 index 0000000..be094a6 Binary files /dev/null and b/clubs-logo/arsenal.png differ diff --git a/clubs-logo/atalanta.png b/clubs-logo/atalanta.png new file mode 100644 index 0000000..0c6e17a Binary files /dev/null and b/clubs-logo/atalanta.png differ diff --git a/clubs-logo/atheltico-madrid.png b/clubs-logo/atheltico-madrid.png new file mode 100644 index 0000000..1f5aa80 Binary files /dev/null and b/clubs-logo/atheltico-madrid.png differ diff --git a/clubs-logo/athletico-bilbao.png b/clubs-logo/athletico-bilbao.png new file mode 100644 index 0000000..ad70251 Binary files /dev/null and b/clubs-logo/athletico-bilbao.png differ diff --git a/clubs-logo/barcelona.png b/clubs-logo/barcelona.png new file mode 100644 index 0000000..e4d43f6 Binary files /dev/null and b/clubs-logo/barcelona.png differ diff --git a/clubs-logo/bayern-munchen.png b/clubs-logo/bayern-munchen.png new file mode 100644 index 0000000..da70609 Binary files /dev/null and b/clubs-logo/bayern-munchen.png differ diff --git a/clubs-logo/benficca.png b/clubs-logo/benficca.png new file mode 100644 index 0000000..7064539 Binary files /dev/null and b/clubs-logo/benficca.png differ diff --git a/clubs-logo/borussia.png b/clubs-logo/borussia.png new file mode 100644 index 0000000..d939908 Binary files /dev/null and b/clubs-logo/borussia.png differ diff --git a/clubs-logo/brugge.png b/clubs-logo/brugge.png new file mode 100644 index 0000000..c96e5d8 Binary files /dev/null and b/clubs-logo/brugge.png differ diff --git a/clubs-logo/bude-glimt.png b/clubs-logo/bude-glimt.png new file mode 100644 index 0000000..8aed481 Binary files /dev/null and b/clubs-logo/bude-glimt.png differ diff --git a/clubs-logo/chelsea.png b/clubs-logo/chelsea.png new file mode 100644 index 0000000..d09c258 Binary files /dev/null and b/clubs-logo/chelsea.png differ diff --git a/clubs-logo/eintracht.png b/clubs-logo/eintracht.png new file mode 100644 index 0000000..c3c00b3 Binary files /dev/null and b/clubs-logo/eintracht.png differ diff --git a/clubs-logo/galatasarai.png b/clubs-logo/galatasarai.png new file mode 100644 index 0000000..fc53cef Binary files /dev/null and b/clubs-logo/galatasarai.png differ diff --git a/clubs-logo/inter.png b/clubs-logo/inter.png new file mode 100644 index 0000000..c32264e Binary files /dev/null and b/clubs-logo/inter.png differ diff --git a/clubs-logo/juventus.png b/clubs-logo/juventus.png new file mode 100644 index 0000000..2c2f8aa Binary files /dev/null and b/clubs-logo/juventus.png differ diff --git a/clubs-logo/kairat.png b/clubs-logo/kairat.png new file mode 100644 index 0000000..36b59d4 Binary files /dev/null and b/clubs-logo/kairat.png differ diff --git a/clubs-logo/karabah.png b/clubs-logo/karabah.png new file mode 100644 index 0000000..936badc Binary files /dev/null and b/clubs-logo/karabah.png differ diff --git a/clubs-logo/kopenhagen.png b/clubs-logo/kopenhagen.png new file mode 100644 index 0000000..1462124 Binary files /dev/null and b/clubs-logo/kopenhagen.png differ diff --git a/clubs-logo/leverkusen.png b/clubs-logo/leverkusen.png new file mode 100644 index 0000000..b83c48f Binary files /dev/null and b/clubs-logo/leverkusen.png differ diff --git a/clubs-logo/liverpool.png b/clubs-logo/liverpool.png new file mode 100644 index 0000000..bb49708 Binary files /dev/null and b/clubs-logo/liverpool.png differ diff --git a/clubs-logo/manchester-city.png b/clubs-logo/manchester-city.png new file mode 100644 index 0000000..f88b851 Binary files /dev/null and b/clubs-logo/manchester-city.png differ diff --git a/clubs-logo/marseile-olympique.png b/clubs-logo/marseile-olympique.png new file mode 100644 index 0000000..5ae7466 Binary files /dev/null and b/clubs-logo/marseile-olympique.png differ diff --git a/clubs-logo/monaco.png b/clubs-logo/monaco.png new file mode 100644 index 0000000..0d98c96 Binary files /dev/null and b/clubs-logo/monaco.png differ diff --git a/clubs-logo/napoli.png b/clubs-logo/napoli.png new file mode 100644 index 0000000..557b26f Binary files /dev/null and b/clubs-logo/napoli.png differ diff --git a/clubs-logo/newcastle.png b/clubs-logo/newcastle.png new file mode 100644 index 0000000..c42aa88 Binary files /dev/null and b/clubs-logo/newcastle.png differ diff --git a/clubs-logo/olympiacos.png b/clubs-logo/olympiacos.png new file mode 100644 index 0000000..19311c4 Binary files /dev/null and b/clubs-logo/olympiacos.png differ diff --git a/clubs-logo/pafos.png b/clubs-logo/pafos.png new file mode 100644 index 0000000..215fd2f Binary files /dev/null and b/clubs-logo/pafos.png differ diff --git a/clubs-logo/psg.png b/clubs-logo/psg.png new file mode 100644 index 0000000..48f6896 Binary files /dev/null and b/clubs-logo/psg.png differ diff --git a/clubs-logo/psv.png b/clubs-logo/psv.png new file mode 100644 index 0000000..3c796bf Binary files /dev/null and b/clubs-logo/psv.png differ diff --git a/clubs-logo/real-ma.png b/clubs-logo/real-ma.png new file mode 100644 index 0000000..3b43cad Binary files /dev/null and b/clubs-logo/real-ma.png differ diff --git a/clubs-logo/slavia.png b/clubs-logo/slavia.png new file mode 100644 index 0000000..59ff6fe Binary files /dev/null and b/clubs-logo/slavia.png differ diff --git a/clubs-logo/sporting.png b/clubs-logo/sporting.png new file mode 100644 index 0000000..1167c51 Binary files /dev/null and b/clubs-logo/sporting.png differ diff --git a/clubs-logo/tottenham.png b/clubs-logo/tottenham.png new file mode 100644 index 0000000..82a8cfd Binary files /dev/null and b/clubs-logo/tottenham.png differ diff --git a/clubs-logo/union.png b/clubs-logo/union.png new file mode 100644 index 0000000..14feef8 Binary files /dev/null and b/clubs-logo/union.png differ diff --git a/clubs-logo/villareal.png b/clubs-logo/villareal.png new file mode 100644 index 0000000..9442e91 Binary files /dev/null and b/clubs-logo/villareal.png differ diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..5d5afc7 --- /dev/null +++ b/css/style.css @@ -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; +} \ No newline at end of file diff --git a/data/transfers.json b/data/transfers.json new file mode 100644 index 0000000..62e3cb0 --- /dev/null +++ b/data/transfers.json @@ -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" + } + ] +} diff --git a/generate_placeholders.py b/generate_placeholders.py new file mode 100644 index 0000000..37b357e --- /dev/null +++ b/generate_placeholders.py @@ -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() \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..271458a --- /dev/null +++ b/index.html @@ -0,0 +1,72 @@ + + + + + + TRANSFER | Broadcast Graphics + + + + + + + +
+ + +
+ +
+ + +
+ +
+
+ + +
+ + +
+
+
+ + +
+ +
+
КЛУБ
+
+
+
+ + +
+ СУММА + +
+ +
+ +
+ +
+ + +
+
- / -
+
+ Prev   / Enter Next   R Replay   H Hide   A Autoplay +
+
+ +
+ + + + \ No newline at end of file diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..370357d --- /dev/null +++ b/js/app.js @@ -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(); +})(); \ No newline at end of file diff --git a/tmp-player.png b/tmp-player.png new file mode 100644 index 0000000..1fa1507 Binary files /dev/null and b/tmp-player.png differ