Match scoreboard overlay: identity/ticker/scorebar/timeline + VS intro + gear-menu toggles
- Scoreboard overlay driven by video.currentTime (scores/ticker/timeline as pure
functions of playback time; scrub-back removes points; slow-mo doesn't affect).
- Score bar + timeline + VS intro rendered verbatim from the design handoff
(drafts/score-bar.html, drafts/vs-screen.html) — only {{ }} data holes filled.
- Fixed 1150 px design canvas scaled to any player size via ResizeObserver so
the design's pixel-perfect measurements survive at every player width.
- Gear-menu injection adds a "Scoreboard" section with 7 toggle rows (Score bar,
Live scoring feed, Match info, Point timeline, Club logos, Country flags,
Penalties) using a custom 26x14 track/knob toggle matching the design.
Persists per user in localStorage['msb_prefs']; penalties defaults off.
- VS card fades in on first play of a session (skippable by click/tap/key,
2 s hold then 0.5 s fade); skipped entirely when no athlete metadata.
- SportsMatch::headerData() extended to expose sport, headshots, club logos,
division, format, match_date and per-video scoreboard defaults from existing
JSON columns (no schema migration).
- Every missing field hides its element (no "NOT SET" placeholders); athlete
name falls back to RED/BLUE per brief.
- Pushed .replay-badge down from top:2.5cqi to top:8cqi so it clears the
new KARATE KUMITE identity block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ed921a4ccf
commit
19741f489b
@ -44,31 +44,48 @@ class SportsMatch extends Model
|
||||
$p = $this->participants ?? [];
|
||||
$c = $this->competition ?? [];
|
||||
$v = $this->venue ?? [];
|
||||
$m = $this->media ?? [];
|
||||
$trim = fn($x) => (is_string($x) && trim($x) !== '') ? trim($x) : null;
|
||||
|
||||
return [
|
||||
'sport' => $trim($this->sport),
|
||||
'blue' => [
|
||||
'name' => $this->participant1_name ?: null,
|
||||
'club' => $p['p1_club'] ?? null,
|
||||
'name' => $trim($this->participant1_name),
|
||||
'club' => $trim($p['p1_club'] ?? null),
|
||||
'flag' => $this->flagFor($p['p1_country'] ?? null),
|
||||
'club_logo' => $trim($m['p1_club_logo'] ?? null),
|
||||
'headshot' => $trim($m['p1_headshot'] ?? null),
|
||||
],
|
||||
'red' => [
|
||||
'name' => $this->participant2_name ?: null,
|
||||
'club' => $p['p2_club'] ?? null,
|
||||
'name' => $trim($this->participant2_name),
|
||||
'club' => $trim($p['p2_club'] ?? null),
|
||||
'flag' => $this->flagFor($p['p2_country'] ?? null),
|
||||
'club_logo' => $trim($m['p2_club_logo'] ?? null),
|
||||
'headshot' => $trim($m['p2_headshot'] ?? null),
|
||||
],
|
||||
'weight_category' => $p['weight_class'] ?? null,
|
||||
'championship' => $c['championship_name'] ?? ($this->event_name ?: null),
|
||||
'match_number' => $c['match_number'] ?? null,
|
||||
'court' => $c['court'] ?? null,
|
||||
'weight_category' => $trim($p['weight_class'] ?? null),
|
||||
'division' => $trim($c['division'] ?? null),
|
||||
'championship' => $trim($c['championship_name'] ?? null) ?? $trim($this->event_name),
|
||||
'match_number' => $trim(($c['match_number'] ?? null) !== null ? (string) $c['match_number'] : null),
|
||||
'court' => $trim($c['court'] ?? null),
|
||||
'format' => $trim($c['format'] ?? null), // "3 min", "Best of 3", …
|
||||
'match_date' => $this->match_date?->format('Y-m-d'),
|
||||
'referee' => [
|
||||
'name' => $this->referee_name ?: null,
|
||||
'name' => $trim($this->referee_name),
|
||||
'flag' => $this->flagFor($p['referee_country'] ?? null),
|
||||
],
|
||||
'venue' => [
|
||||
'name' => $this->venue_name ?: ($v['name'] ?? null),
|
||||
'map_link' => $v['map_link'] ?? null,
|
||||
'name' => $trim($this->venue_name) ?? $trim($v['name'] ?? null),
|
||||
'map_link' => $trim($v['map_link'] ?? null),
|
||||
],
|
||||
'event_logo' => $this->media['event_poster'] ?? null, // rel path or null
|
||||
'event_logo' => $trim($m['event_poster'] ?? null),
|
||||
/*
|
||||
* Per-video overlay defaults set by the uploader. Shape:
|
||||
* ['scorebar'=>bool, 'feed'=>bool, 'matchInfo'=>bool, 'timeline'=>bool,
|
||||
* 'clubs'=>bool, 'flags'=>bool, 'penalties'=>bool, 'cornerLabels'=>'auto|redblue|karate|taekwondo']
|
||||
* Anything not set falls back to the app defaults in the JS.
|
||||
*/
|
||||
'scoreboard_defaults' => is_array($c['scoreboard'] ?? null) ? $c['scoreboard'] : [],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
BIN
drafts/2026-08-09_20h35_44.png
Normal file
BIN
drafts/2026-08-09_20h35_44.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
drafts/2026-08-09_20h37_12.png
Normal file
BIN
drafts/2026-08-09_20h37_12.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
drafts/Karate match scoreboard overlay.zip
Normal file
BIN
drafts/Karate match scoreboard overlay.zip
Normal file
Binary file not shown.
94
drafts/score-bar.html
Normal file
94
drafts/score-bar.html
Normal file
@ -0,0 +1,94 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Score bar — exact markup</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700;800&family=Zen+Old+Mincho:wght@700&display=swap" rel="stylesheet">
|
||||
<style>body{margin:0;background:#111;font-family:'Barlow Condensed',sans-serif}</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!--
|
||||
SCORE BAR — copy this markup verbatim. Do not restructure it.
|
||||
Placement: inside the player container, which must be position:relative.
|
||||
Data holes are marked with {{ }} — replace ONLY those, nothing else.
|
||||
Hide a part by setting its element's display to none (never remove the element's siblings' flex sizing).
|
||||
|
||||
Rules that break the design if changed:
|
||||
- the panel has transform:skewX(-9deg); ONE inner row has transform:skewX(9deg). Never counter-skew individual children.
|
||||
- every text span keeps flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis
|
||||
- the logo (46px) and score (60px) boxes keep flex:none
|
||||
- bottom:56px keeps the bar above the 48px control band
|
||||
-->
|
||||
|
||||
<div style="position:relative;width:1150px;height:200px;background:#1b1b1e">
|
||||
<div style="position:absolute;left:0;right:0;bottom:56px;padding:0 26px;display:flex;align-items:stretch;gap:0;height:84px;z-index:2;pointer-events:none">
|
||||
|
||||
<!-- RED / AKA -->
|
||||
<div style="flex:1 1 0;min-width:0;transform:skewX(-9deg);overflow:hidden;background:linear-gradient(90deg, rgba(122,26,22,.94), rgba(180,52,44,.9));border-bottom:3px solid #ff6a5e">
|
||||
<div style="transform:skewX(9deg);height:100%;padding:0 16px;display:flex;align-items:center;gap:12px">
|
||||
<div style="width:46px;height:46px;flex:none;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;letter-spacing:.05em;color:rgba(255,255,255,.65);text-align:center;line-height:1.25">CLUB<br>LOGO</div>
|
||||
<div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px">
|
||||
<div style="display:flex;align-items:center;gap:10px;min-width:0">
|
||||
<span style="width:26px;height:17px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 4px,rgba(255,255,255,.1) 4px 8px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:25px;line-height:1;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ redName }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:12px;letter-spacing:.03em;color:rgba(255,232,228,.85);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ redClub }}</span>
|
||||
<span style="display:none;font-size:10px;letter-spacing:.18em;padding:1px 6px;background:#ffdf6b;color:#3a2a00;font-weight:700;flex:none">SENSHU</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:60px;flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:5px">
|
||||
<span style="font-size:11px;letter-spacing:.24em;color:rgba(255,236,232,.7)">{{ redLabel }}</span>
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:46px;line-height:.8;font-weight:700;color:#fff;text-shadow:0 6px 20px rgba(0,0,0,.5)">{{ redScore }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CENTER: round + clock -->
|
||||
<div style="width:118px;flex:none;transform:skewX(-9deg);background:rgba(9,9,11,.9);backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;border-bottom:3px solid #2c2a28">
|
||||
<div style="transform:skewX(9deg);font-size:11px;letter-spacing:.3em;color:#8f8a83;text-transform:uppercase">{{ roundLabel }}</div>
|
||||
<div style="transform:skewX(9deg);font-size:34px;line-height:1;font-weight:800;color:#efe9e0;font-variant-numeric:tabular-nums">{{ clock }}</div>
|
||||
<div style="transform:skewX(9deg);display:flex;gap:4px">
|
||||
<span style="width:20px;height:3px;background:#e8534a"></span>
|
||||
<span style="width:20px;height:3px;background:#e8534a"></span>
|
||||
<span style="width:20px;height:3px;background:#3a3734"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BLUE / AO — exact mirror -->
|
||||
<div style="flex:1 1 0;min-width:0;transform:skewX(-9deg);overflow:hidden;background:linear-gradient(90deg, rgba(30,72,140,.9), rgba(18,44,92,.94));border-bottom:3px solid #6aa6ff">
|
||||
<div style="transform:skewX(9deg);height:100%;padding:0 16px;display:flex;align-items:center;gap:12px">
|
||||
<div style="width:60px;flex:none;display:flex;flex-direction:column;align-items:flex-start;gap:5px">
|
||||
<span style="font-size:11px;letter-spacing:.24em;color:rgba(226,238,255,.7)">{{ blueLabel }}</span>
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:46px;line-height:.8;font-weight:700;color:#fff;text-shadow:0 6px 20px rgba(0,0,0,.5)">{{ blueScore }}</span>
|
||||
</div>
|
||||
<div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;align-items:flex-end;text-align:right">
|
||||
<div style="display:flex;align-items:center;gap:10px;min-width:0">
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:25px;line-height:1;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ blueName }}</span>
|
||||
<span style="width:26px;height:17px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 4px,rgba(255,255,255,.1) 4px 8px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span style="display:none;font-size:10px;letter-spacing:.18em;padding:1px 6px;background:#c8492f;color:#fff;font-weight:700;flex:none">C1</span>
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:12px;letter-spacing:.03em;color:rgba(226,238,255,.85);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ blueClub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:46px;height:46px;flex:none;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;letter-spacing:.05em;color:rgba(255,255,255,.65);text-align:center;line-height:1.25">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POINT TIMELINE — one segment per point event, red/blue, #2a2724 when empty -->
|
||||
<div style="position:absolute;left:26px;right:26px;bottom:48px;height:5px;display:flex;gap:2px;z-index:2;pointer-events:none">
|
||||
<div style="flex:1;background:#e8534a"></div><div style="flex:1;background:#2a2724"></div>
|
||||
<div style="flex:1;background:#6aa6ff"></div><div style="flex:1;background:#e8534a"></div>
|
||||
<div style="flex:1;background:#2a2724"></div><div style="flex:1;background:#2a2724"></div>
|
||||
<div style="flex:1;background:#6aa6ff"></div><div style="flex:1;background:#e8534a"></div>
|
||||
<div style="flex:1;background:#2a2724"></div><div style="flex:1;background:#e8534a"></div>
|
||||
<div style="flex:1;background:#6aa6ff"></div><div style="flex:1;background:#2a2724"></div>
|
||||
<div style="flex:1;background:#e8534a"></div><div style="flex:1;background:#2a2724"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,349 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700;800&family=Zen+Old+Mincho:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { margin:0; background:#0a0a0c; }
|
||||
a { color:#e8534a; } a:hover { color:#ff7d70; }
|
||||
@keyframes riseIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:none; } }
|
||||
@keyframes driftA { 0%,100% { transform: translate3d(0,0,0) scale(1); } 50% { transform: translate3d(60px,-28px,0) scale(1.14); } }
|
||||
@keyframes driftB { 0%,100% { transform: translate3d(0,0,0) scale(1.08); } 50% { transform: translate3d(-54px,26px,0) scale(1); } }
|
||||
@keyframes sweep { 0% { transform: translateX(-60%) skewX(-14deg); opacity:0; } 25% { opacity:.5; } 60% { opacity:0; } 100% { transform: translateX(160%) skewX(-14deg); opacity:0; } }
|
||||
@keyframes pulseDot { 0%,100% { opacity:1; } 50% { opacity:.25; } }
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="min-height:100vh;background:#0a0a0c;padding:40px;display:flex;flex-direction:column;align-items:flex-start;gap:22px;overflow-x:auto;font-family:'Barlow Condensed',sans-serif">
|
||||
|
||||
<div style="position:relative;width:1150px;flex:none;aspect-ratio:16/9;overflow:hidden;background:#08080a;box-shadow:0 30px 80px rgba(0,0,0,.6)">
|
||||
<div style="position:absolute;inset:-12%;background:radial-gradient(closest-side, rgba(168,42,34,.62), rgba(8,8,10,0) 72%);width:70%;left:-6%;animation:driftA 19s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;inset:-12%;background:radial-gradient(closest-side, rgba(28,64,136,.62), rgba(8,8,10,0) 72%);width:70%;left:36%;animation:driftB 23s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:26%;background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.09), rgba(255,255,255,0));animation:sweep 11s linear infinite"></div>
|
||||
<div style="position:absolute;inset:0;opacity:.12;mix-blend-mode:overlay;background:repeating-linear-gradient(115deg, rgba(255,255,255,.6) 0 1px, transparent 1px 5px)"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:54%;clip-path:polygon(0 0, 100% 0, 78% 100%, 0 100%);background:linear-gradient(120deg, rgba(122,26,22,.5), rgba(8,8,10,0) 76%)"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;right:0;width:54%;clip-path:polygon(22% 0, 100% 0, 100% 100%, 0 100%);background:linear-gradient(300deg, rgba(24,52,110,.5), rgba(8,8,10,0) 76%)"></div>
|
||||
|
||||
<div style="position:absolute;top:34px;left:0;right:0;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-family:'Zen Old Mincho',serif;font-size:24px;letter-spacing:.44em;color:#efe9e0;text-transform:uppercase;text-indent:.44em">Takeone Karate Series</div>
|
||||
<div style="display:flex;align-items:center;gap:14px;font-size:12px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase">
|
||||
<span>Ladies</span><span style="width:4px;height:4px;background:#e8534a"></span><span>Classification</span><span style="width:4px;height:4px;background:#6aa6ff"></span><span>Round 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;inset:96px 0 54px;display:grid;grid-template-columns:1fr 200px 1fr;align-items:start">
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
||||
<div style="position:relative;width:228px;height:304px">
|
||||
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div style="position:absolute;bottom:-26px;right:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #e8534a;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">Fawzia Ahmed</div>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span style="font-size:14px;letter-spacing:.3em;color:#d3b3ad;text-transform:uppercase">Bahrain</span>
|
||||
</div>
|
||||
<div style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">Manama Karate Club</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;padding-top:96px">
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:88px;line-height:.8;font-weight:700;color:#efe9e0;text-shadow:0 8px 34px rgba(0,0,0,.75)">VS</span>
|
||||
<span style="padding:7px 16px;border:1px solid rgba(255,255,255,.3);font-size:14px;letter-spacing:.32em;color:#efe9e0;text-transform:uppercase">−61 kg</span>
|
||||
<span style="font-size:11px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase">Ladies · 3 min</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
||||
<div style="position:relative;width:228px;height:304px">
|
||||
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div style="position:absolute;bottom:-26px;left:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #6aa6ff;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">Noor Salman</div>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span style="font-size:14px;letter-spacing:.3em;color:#a8b6cf;text-transform:uppercase">Bahrain</span>
|
||||
<span style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
<div style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">Riffa Martial Arts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;bottom:26px;left:0;right:0;display:flex;align-items:center;justify-content:center;gap:16px;font-size:11px;letter-spacing:.32em;color:#8f8a83;text-transform:uppercase">
|
||||
<span>Bout 12</span><span style="width:4px;height:4px;background:#e8534a"></span><span>Tatami 1</span><span style="width:4px;height:4px;background:#6aa6ff"></span><span>Aug 08, 2026 · Manama</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:relative;width:1150px;flex:none;aspect-ratio:16/9;overflow:hidden;border-radius:10px;background:#111;box-shadow:0 30px 80px rgba(0,0,0,.6)">
|
||||
<img src="assets/mat.png" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover">
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(6,6,8,.9) 0%, rgba(6,6,8,.45) 15%, rgba(6,6,8,0) 30%), linear-gradient(to bottom, rgba(6,6,8,.85) 0%, rgba(6,6,8,.4) 14%, rgba(6,6,8,0) 30%)"></div>
|
||||
|
||||
|
||||
|
||||
<div style="display:{{ overlayDisplay }}">
|
||||
<!-- top left: match identity -->
|
||||
<div style="position:absolute;top:22px;left:26px;pointer-events:none;display:{{ dIdentity }};align-items:stretch;gap:12px">
|
||||
<div style="width:4px;background:linear-gradient(#e8534a,#7c1d18)"></div>
|
||||
<div style="display:flex;flex-direction:column;gap:2px">
|
||||
<div style="font-family:'Zen Old Mincho',serif;font-size:15px;letter-spacing:.34em;color:#efe9e0;text-transform:uppercase">{{ disciplineLabel }}</div>
|
||||
<div style="font-size:13px;letter-spacing:.28em;color:#cfc9c1;text-transform:uppercase;text-shadow:0 1px 6px rgba(0,0,0,.9)">Ladies Classification · −61 kg · Round 1</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- top right: live point ticker -->
|
||||
<div style="position:absolute;top:20px;right:24px;width:262px;pointer-events:none;display:{{ dTicker }};flex-direction:column;gap:7px">
|
||||
<div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;font-size:12px;letter-spacing:.3em;color:#d5cfc7;text-transform:uppercase;text-shadow:0 1px 6px rgba(0,0,0,.9)">
|
||||
<span style="width:7px;height:7px;border-radius:50%;background:#e8534a;animation:pulseDot 1.6s infinite"></span>Live scoring
|
||||
</div>
|
||||
<sc-for list="{{ feed }}" as="ev" hint-placeholder-count="4">
|
||||
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:7px 10px;background:rgba(10,10,12,.62);backdrop-filter:blur(6px);border-right:3px solid transparent;animation:riseIn .45s ease both;border-color:{{ ev.color }}">
|
||||
<span style="font-size:12px;letter-spacing:.16em;color:#79736c">{{ ev.time }}</span>
|
||||
<span style="font-size:17px;letter-spacing:.12em;font-weight:700;color:#efe9e0;text-transform:uppercase">{{ ev.name }}</span>
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:19px;font-weight:700;color:{{ ev.color }}">{{ ev.pts }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<!-- bottom scoreboard -->
|
||||
<div style="position:absolute;left:0;right:0;bottom:56px;padding:0 26px;display:{{ dBar }};align-items:stretch;gap:0;height:84px;z-index:2;pointer-events:none">
|
||||
|
||||
<div style="flex:1 1 0;min-width:0;transform:skewX(-9deg);overflow:hidden;background:linear-gradient(90deg, rgba(122,26,22,.94), rgba(180,52,44,.9));border-bottom:3px solid #ff6a5e">
|
||||
<div style="transform:skewX(9deg);height:100%;padding:0 16px;display:flex;align-items:center;gap:12px">
|
||||
<div style="width:46px;height:46px;flex:none;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.24);display:{{ dClubs }};align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;letter-spacing:.05em;color:rgba(255,255,255,.65);text-align:center;line-height:1.25">CLUB<br>LOGO</div>
|
||||
<div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px">
|
||||
<div style="display:flex;align-items:center;gap:10px;min-width:0">
|
||||
<span style="width:26px;height:17px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 4px,rgba(255,255,255,.1) 4px 8px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:{{ dFlags }};align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:25px;line-height:1;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">Fawzia A.</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:12px;letter-spacing:.03em;color:rgba(255,232,228,.85);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">Manama Karate Club</span>
|
||||
<span style="display:{{ dPenalty }};font-size:10px;letter-spacing:.18em;padding:1px 6px;background:#ffdf6b;color:#3a2a00;font-weight:700;flex:none">SENSHU</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:60px;flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:5px"><span style="font-size:11px;letter-spacing:.24em;color:rgba(255,236,232,.7)">{{ redCorner }}</span><span style="font-family:'Zen Old Mincho',serif;font-size:46px;line-height:.8;font-weight:700;color:#fff;text-shadow:0 6px 20px rgba(0,0,0,.5)">{{ akaScore }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="width:118px;flex:none;transform:skewX(-9deg);background:rgba(9,9,11,.9);backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;border-bottom:3px solid #2c2a28">
|
||||
<div style="transform:skewX(9deg);font-size:11px;letter-spacing:.3em;color:#8f8a83;text-transform:uppercase">{{ roundLabel }}</div>
|
||||
<div style="transform:skewX(9deg);font-size:34px;line-height:1;font-weight:800;color:#efe9e0;font-variant-numeric:tabular-nums">{{ clock }}</div>
|
||||
<div style="transform:skewX(9deg);display:flex;gap:4px">
|
||||
<span style="width:20px;height:3px;background:#e8534a"></span>
|
||||
<span style="width:20px;height:3px;background:#e8534a"></span>
|
||||
<span style="width:20px;height:3px;background:#3a3734"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex:1 1 0;min-width:0;transform:skewX(-9deg);overflow:hidden;background:linear-gradient(90deg, rgba(30,72,140,.9), rgba(18,44,92,.94));border-bottom:3px solid #6aa6ff">
|
||||
<div style="transform:skewX(9deg);height:100%;padding:0 16px;display:flex;align-items:center;gap:12px">
|
||||
<div style="width:60px;flex:none;display:flex;flex-direction:column;align-items:flex-start;gap:5px"><span style="font-size:11px;letter-spacing:.24em;color:rgba(226,238,255,.7)">{{ blueCorner }}</span><span style="font-family:'Zen Old Mincho',serif;font-size:46px;line-height:.8;font-weight:700;color:#fff;text-shadow:0 6px 20px rgba(0,0,0,.5)">{{ aoScore }}</span></div>
|
||||
<div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;align-items:flex-end;text-align:right">
|
||||
<div style="display:flex;align-items:center;gap:10px;min-width:0">
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:25px;line-height:1;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">Noor S.</span>
|
||||
<span style="width:26px;height:17px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 4px,rgba(255,255,255,.1) 4px 8px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:{{ dFlags }};align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span style="display:{{ dPenalty }};font-size:10px;letter-spacing:.18em;padding:1px 6px;background:#c8492f;color:#fff;font-weight:700;flex:none">{{ penaltyLabel }}</span>
|
||||
<span style="flex:1 1 auto;min-width:0;font-size:12px;letter-spacing:.1em;color:rgba(226,238,255,.85);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">Riffa Martial Arts</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:46px;height:46px;flex:none;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.24);display:{{ dClubs }};align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;letter-spacing:.05em;color:rgba(255,255,255,.65);text-align:center;line-height:1.25">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- point timeline strip -->
|
||||
<div style="position:absolute;left:26px;right:26px;bottom:48px;height:5px;display:{{ dTimeline }};gap:2px;z-index:2;pointer-events:none">
|
||||
<sc-for list="{{ timeline }}" as="seg" hint-placeholder-count="14">
|
||||
<div style="flex:1;background:{{ seg.color }}"></div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;inset:0;z-index:8;display:{{ introDisplay }};opacity:{{ introOpacity }};transition:opacity .55s ease;background:#08080a;overflow:hidden">
|
||||
<div style="position:absolute;inset:-12%;width:70%;left:-6%;background:radial-gradient(closest-side, rgba(168,42,34,.62), rgba(8,8,10,0) 72%);animation:driftA 19s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;inset:-12%;width:70%;left:36%;background:radial-gradient(closest-side, rgba(28,64,136,.62), rgba(8,8,10,0) 72%);animation:driftB 23s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:26%;background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.09), rgba(255,255,255,0));animation:sweep 11s linear infinite"></div>
|
||||
<div style="position:absolute;top:26px;left:0;right:0;display:flex;flex-direction:column;align-items:center;gap:5px">
|
||||
<div style="font-family:'Zen Old Mincho',serif;font-size:19px;letter-spacing:.42em;color:#efe9e0;text-transform:uppercase;text-indent:.42em">Takeone Karate Series</div>
|
||||
<div style="font-size:11px;letter-spacing:.32em;color:#8f8a83;text-transform:uppercase">Ladies · Classification · Round 1</div>
|
||||
</div>
|
||||
<div style="position:absolute;inset:82px 0 46px;display:grid;grid-template-columns:1fr 150px 1fr;align-items:center">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:0 20px">
|
||||
<div style="position:relative;width:168px;height:224px">
|
||||
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:10px;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div style="position:absolute;bottom:-18px;right:-18px;width:70px;height:70px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.7);text-align:center;line-height:1.3">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:30px;padding-top:12px;border-top:3px solid #e8534a;display:flex;flex-direction:column;align-items:center;gap:6px">
|
||||
<div style="font-size:30px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">Fawzia Ahmed</div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="width:30px;height:20px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span style="font-size:12px;letter-spacing:.26em;color:#d3b3ad;text-transform:uppercase">Bahrain · Manama Karate Club</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:10px">
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:64px;line-height:.8;font-weight:700;color:#efe9e0;text-shadow:0 8px 30px rgba(0,0,0,.75)">VS</span>
|
||||
<span style="padding:5px 13px;border:1px solid rgba(255,255,255,.3);font-size:12px;letter-spacing:.3em;color:#efe9e0;text-transform:uppercase">−61 kg</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:0 20px">
|
||||
<div style="position:relative;width:168px;height:224px">
|
||||
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:10px;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div style="position:absolute;bottom:-18px;left:-18px;width:70px;height:70px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.7);text-align:center;line-height:1.3">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:30px;padding-top:12px;border-top:3px solid #6aa6ff;display:flex;flex-direction:column;align-items:center;gap:6px">
|
||||
<div style="font-size:30px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">Noor Salman</div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:12px;letter-spacing:.26em;color:#a8b6cf;text-transform:uppercase">Bahrain · Riffa Martial Arts</span>
|
||||
<span style="width:30px;height:20px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:0;right:0;bottom:0;display:flex;flex-direction:column;background:linear-gradient(to top, rgba(6,6,8,.94) 40%, rgba(6,6,8,0));z-index:9">
|
||||
<div style="height:4px;background:rgba(255,255,255,.28);position:relative">
|
||||
<div style="position:absolute;left:0;top:0;bottom:0;width:23%;background:#e8534a"></div>
|
||||
<div style="position:absolute;left:23%;top:-3px;width:10px;height:10px;border-radius:50%;background:#e8534a"></div>
|
||||
</div>
|
||||
<div style="height:44px;padding:0 16px;display:flex;align-items:center;gap:18px;color:#efe9e0">
|
||||
<button onClick="{{ replayIntro }}" style="display:flex;gap:4px;padding:0;background:transparent;border:0;cursor:pointer;color:inherit" style-hover="opacity:.75"><span style="width:4px;height:15px;background:currentColor"></span><span style="width:4px;height:15px;background:currentColor"></span></button>
|
||||
<span style="display:flex;align-items:center;gap:2px"><span style="width:6px;height:9px;background:currentColor"></span><span style="width:0;height:0;border-right:8px solid currentColor;border-top:8px solid transparent;border-bottom:8px solid transparent"></span><span style="width:5px;height:5px;border-right:2px solid currentColor;border-top:2px solid currentColor;transform:rotate(45deg);margin-left:2px"></span></span>
|
||||
<span style="font-size:13px;letter-spacing:.1em;color:#cfc9c1;font-variant-numeric:tabular-nums">0:57 / 4:11</span>
|
||||
<span style="flex:1"></span>
|
||||
<button onClick="{{ toggleMenu }}" style="width:24px;height:24px;flex:none;display:flex;align-items:center;justify-content:center;background:transparent;border:0;cursor:pointer;color:{{ gearColor }}" style-hover="color:#fff">
|
||||
<span style="width:15px;height:15px;border:2px solid currentColor;border-radius:50%;display:block;position:relative"><span style="position:absolute;inset:3px;border:2px solid currentColor;border-radius:50%"></span></span>
|
||||
</button>
|
||||
<span style="width:22px;height:16px;border:2px solid #e8534a;border-radius:3px;display:block"></span>
|
||||
<span style="width:22px;height:16px;border:2px solid currentColor;border-radius:2px;display:flex;align-items:flex-end;justify-content:flex-end;padding:2px"><span style="width:9px;height:6px;background:currentColor"></span></span>
|
||||
<span style="width:22px;height:14px;border:2px solid currentColor;border-radius:2px;display:block"></span>
|
||||
<span style="width:18px;height:16px;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:4px">
|
||||
<span style="border-left:2px solid currentColor;border-top:2px solid currentColor"></span><span style="border-right:2px solid currentColor;border-top:2px solid currentColor"></span>
|
||||
<span style="border-left:2px solid currentColor;border-bottom:2px solid currentColor"></span><span style="border-right:2px solid currentColor;border-bottom:2px solid currentColor"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;right:118px;bottom:58px;width:236px;z-index:10;display:{{ menuDisplay }};flex-direction:column;background:rgba(10,10,12,.94);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.16);box-shadow:0 20px 50px rgba(0,0,0,.6)">
|
||||
<div style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.1);font-size:11px;letter-spacing:.3em;color:#8f8a83;text-transform:uppercase">Scoreboard</div>
|
||||
<sc-for list="{{ menuItems }}" as="it" hint-placeholder-count="7">
|
||||
<button onClick="{{ it.toggle }}" style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;background:transparent;border:0;cursor:pointer;font-family:'Barlow Condensed',sans-serif;font-size:14px;letter-spacing:.14em;color:#efe9e0;text-transform:uppercase;text-align:left" style-hover="background:rgba(255,255,255,.07)">
|
||||
<span>{{ it.label }}</span>
|
||||
<span style="width:26px;height:14px;flex:none;background:{{ it.track }};position:relative"><span style="position:absolute;top:2px;left:{{ it.knob }};width:10px;height:10px;background:#efe9e0"></span></span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="width:1150px;flex:none;display:flex;align-items:center;justify-content:space-between;color:#6f6a64;font-size:14px;letter-spacing:.22em;text-transform:uppercase">
|
||||
<span>Overlay is a function of playback time · points come from the Points panel</span>
|
||||
<span style="color:#efe9e0">{{ statusLine }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1230,"height":1400},"sport":{"editor":"enum","options":["Karate","Taekwondo"],"default":"Karate","tsType":"string"},"showTicker":{"editor":"boolean","default":true,"tsType":"boolean"}}">
|
||||
class Component extends DCLogic {
|
||||
state = {
|
||||
t: 107, aka: 6, ao: 4, menu: false,
|
||||
parts: { scorebar: true, ticker: true, identity: true, timeline: true, clubs: true, flags: true, penalties: false }
|
||||
};
|
||||
|
||||
playIntro = () => {
|
||||
clearTimeout(this.t1); clearTimeout(this.t2);
|
||||
this.setState({ intro: true, introFade: false });
|
||||
this.t1 = setTimeout(() => this.setState({ introFade: true }), 2000);
|
||||
this.t2 = setTimeout(() => this.setState({ intro: false }), 2600);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.playIntro();
|
||||
this.iv = setInterval(() => this.setState(s => ({ t: s.t > 0 ? s.t - 1 : 0 })), 1000);
|
||||
}
|
||||
componentWillUnmount() { clearInterval(this.iv); }
|
||||
|
||||
renderVals() {
|
||||
const tkd = (this.props.sport ?? 'Karate') === 'Taekwondo';
|
||||
const RED = '#ff6a5e', BLUE = '#6aa6ff';
|
||||
const ev = [
|
||||
{ at: 11, corner: 'blue', pts: 1 }, { at: 11, corner: 'red', pts: 1 },
|
||||
{ at: 30, corner: 'red', pts: 2 }, { at: 51, corner: 'red', pts: 1 },
|
||||
{ at: 112, corner: 'blue', pts: 1 }, { at: 159, corner: 'blue', pts: 1 }
|
||||
];
|
||||
const aka = ev.filter(e => e.corner === 'red').reduce((a, e) => a + e.pts, 0);
|
||||
const ao = ev.filter(e => e.corner === 'blue').reduce((a, e) => a + e.pts, 0);
|
||||
const p = this.state.parts;
|
||||
const m = Math.floor(this.state.t / 60), s = String(this.state.t % 60).padStart(2, '0');
|
||||
|
||||
// mirrors the platform's Points data: round, @timestamp, corner, point value
|
||||
const events = [
|
||||
{ at: 11, corner: 'blue', pts: 1 }, { at: 11, corner: 'red', pts: 1 },
|
||||
{ at: 30, corner: 'red', pts: 2 }, { at: 51, corner: 'red', pts: 1 },
|
||||
{ at: 112, corner: 'blue', pts: 1 }, { at: 159, corner: 'blue', pts: 1 }
|
||||
];
|
||||
const fmt = n => Math.floor(n / 60) + ":" + String(n % 60).padStart(2, '0');
|
||||
const karateFeed = events.slice().reverse().slice(0, 4).map(e => ({
|
||||
time: "@" + fmt(e.at),
|
||||
name: e.corner === 'red' ? "Point · Red" : "Point · Blue",
|
||||
pts: "+" + e.pts,
|
||||
color: e.corner === 'red' ? RED : BLUE
|
||||
}));
|
||||
const totals = events.reduce((a, e) => (a[e.corner] += e.pts, a), { red: 0, blue: 0 });
|
||||
const tkdFeed = [
|
||||
{ time: "1:12", name: "Head kick", pts: "+3", color: RED },
|
||||
{ time: "1:44", name: "Turning body", pts: "+4", color: BLUE },
|
||||
{ time: "2:03", name: "Body kick", pts: "+2", color: RED },
|
||||
{ time: "2:21", name: "Gam-jeom", pts: "+1", color: BLUE }
|
||||
];
|
||||
|
||||
return {
|
||||
disciplineLabel: tkd ? "Taekwondo Kyorugi" : "Karate Kumite",
|
||||
overlayDisplay: 'contents',
|
||||
introDisplay: this.state.intro ? 'block' : 'none',
|
||||
introOpacity: this.state.introFade ? 0 : 1,
|
||||
replayIntro: this.playIntro,
|
||||
menuDisplay: this.state.menu ? 'flex' : 'none',
|
||||
gearColor: this.state.menu ? '#e8534a' : '#efe9e0',
|
||||
toggleMenu: () => this.setState(s => ({ menu: !s.menu })),
|
||||
menuItems: [
|
||||
['scorebar', 'Score bar'], ['ticker', 'Live scoring feed'], ['identity', 'Match info'],
|
||||
['timeline', 'Point timeline'], ['clubs', 'Club logos'], ['flags', 'Country flags'], ['penalties', 'Penalties']
|
||||
].map(([k, label]) => ({
|
||||
label,
|
||||
toggle: () => this.setState(s => ({ parts: Object.assign({}, s.parts, { [k]: !s.parts[k] }) })),
|
||||
track: p[k] ? '#e8534a' : '#3a3734',
|
||||
knob: p[k] ? '14px' : '2px'
|
||||
})),
|
||||
dIdentity: p.identity ? 'flex' : 'none',
|
||||
dTicker: p.ticker ? 'flex' : 'none',
|
||||
dBar: p.scorebar ? 'flex' : 'none',
|
||||
dTimeline: p.timeline ? 'flex' : 'none',
|
||||
dClubs: p.clubs ? 'flex' : 'none',
|
||||
dFlags: p.flags ? 'flex' : 'none',
|
||||
dPenalty: p.penalties ? 'inline-block' : 'none',
|
||||
redCorner: "Red",
|
||||
blueCorner: "Blue",
|
||||
penaltyLabel: tkd ? "GAM-JEOM 1" : "C1",
|
||||
roundLabel: "Round 1",
|
||||
penaltyLabel2: null,
|
||||
akaScore: aka,
|
||||
aoScore: ao,
|
||||
clock: "2:39",
|
||||
feed: (this.props.showTicker ?? true) ? (tkd ? tkdFeed : karateFeed) : [],
|
||||
timeline: [RED, '#2a2724', BLUE, RED, '#2a2724', '#2a2724', BLUE, RED, '#2a2724', RED, BLUE, '#2a2724', RED, '#2a2724']
|
||||
.map(c => ({ color: c })),
|
||||
statusLine: aka > ao ? "Red leads +" + (aka - ao) : ao > aka ? "Blue leads +" + (ao - aka) : "Level"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
102
drafts/unpacked/design_handoff_scoreboard_overlay/PROMPT.md
Normal file
102
drafts/unpacked/design_handoff_scoreboard_overlay/PROMPT.md
Normal file
@ -0,0 +1,102 @@
|
||||
# TASK: Add a match scoreboard overlay + VS intro to the video player on video.takeone.bh
|
||||
|
||||
Implement this end to end in this codebase, in the existing player and video page. Follow the codebase's own framework, component and styling patterns. Do not ship the reference HTML as-is — recreate it.
|
||||
|
||||
Reference prototype (design intent, exact colors/geometry): `Match Points Overlay.dc.html` in this folder. Open it in a browser: it shows the standalone VS card, then the player frame with the VS intro, score bar, live scoring feed, point timeline, control bar and gear menu.
|
||||
|
||||
---
|
||||
|
||||
## 1. What exists today (do not rebuild)
|
||||
|
||||
- **Points panel** on the video page: rounds (number, optional name, optional start time) and point events shaped `@mm:ss · Point (N pt) · Blue | Red | Both`, with a running score printed `Score <blue> – <red>`.
|
||||
- **Coach review** panel: time-ranged notes with author, plus reaction icons.
|
||||
- **Player**: progress bar, play/pause, volume, time `0:57 / 4:11`, gear (settings) menu, loop, mini player (PiP), theater, fullscreen; playback-speed menu and ¼× ½× ¾× slow motion; a "Highlights" badge top-right.
|
||||
|
||||
## 2. What to add to the data model
|
||||
|
||||
A **Match metadata** form for sports videos, next to "Add Round":
|
||||
|
||||
- Event/series name, division/category, weight class, bout number, tatami/ring, date, venue.
|
||||
- Per corner (red, blue): athlete name, country name + ISO code, flag image, club name, club logo, optional headshot (3:4).
|
||||
|
||||
Rules:
|
||||
- Every field is optional. **An empty field hides its element.** Never render placeholder strings ("NOT SET", "—", empty boxes).
|
||||
- No athlete name → show the corner label ("RED" / "BLUE") in the name slot.
|
||||
- No flag image → hide the flag chip. No club logo → hide the logo slot. No headshot → hide the photo frame and center the text block.
|
||||
- Corners in data stay **red** and **blue**. Display labels are a per-video option: Red/Blue (default), Aka/Ao (karate), Hong/Chung (taekwondo).
|
||||
- A `Both` point event increments both corners and emits two feed rows.
|
||||
|
||||
## 3. Overlay: driven by playback time
|
||||
|
||||
The overlay is a pure function of `video.currentTime`. No timers, no countdown. Slow motion and playback speed must not affect it; scrubbing backwards must remove points again.
|
||||
|
||||
- **Scores** = sum of point values for events with `at <= currentTime`, per corner.
|
||||
- **Round label** = the round whose start time is the latest `<= currentTime`.
|
||||
- **Clock** = `currentTime` as `m:ss` (offset from round start when round start times exist).
|
||||
- **Live scoring feed** = last 4 events with `at <= currentTime`, newest on top, each animating in (translateY 10px → 0, opacity 0 → 1, .45s ease) as playback crosses it.
|
||||
- **Point timeline** = one segment per event across the full duration, red or blue, positioned by timestamp; empty segments `#2a2724`.
|
||||
|
||||
## 4. Layout spec (design width 1150px, frame 16:9)
|
||||
|
||||
Overlay layer: `position:absolute; inset:0; pointer-events:none;` inside the player container. Only the gear menu is interactive. Scrims: bottom `rgba(6,6,8,.9) → .45 @15% → 0 @30%`, top `rgba(6,6,8,.85) → .4 @14% → 0 @30%`. No texture/grain layer over the footage.
|
||||
|
||||
**Match info** — top-left, 22px/26px inset: 4px vertical rule `linear-gradient(#e8534a,#7c1d18)`; discipline line in `Zen Old Mincho` 15px, .34em tracking, `#efe9e0`; subtitle 13px, .28em, `#cfc9c1`, with `text-shadow: 0 1px 6px rgba(0,0,0,.9)`.
|
||||
|
||||
**Live scoring feed** — top-right, 20px/24px inset, 262px wide. Header: 7px red dot pulsing (opacity 1→.25→1, 1.6s) + "LIVE SCORING" 12px, .3em, `#d5cfc7`. Rows: `rgba(10,10,12,.62)` + `blur(6px)`, padding 7px/10px, `border-right:3px solid <corner color>`; timestamp 12px `#79736c` · label 17px/700 .12em uppercase `#efe9e0` · points in `Zen Old Mincho` 19px in the corner color.
|
||||
|
||||
**Score bar** — 84px tall, inset 26px left/right, `bottom: 56px` (clears the 48px control band), z-index below controls. Three panels, no gaps:
|
||||
|
||||
- Red panel: `flex:1 1 0; min-width:0; overflow:hidden; transform:skewX(-9deg)`, background `linear-gradient(90deg, rgba(122,26,22,.94), rgba(180,52,44,.9))`, `border-bottom:3px solid #ff6a5e`.
|
||||
Inside, ONE inner row un-skews: `transform:skewX(9deg); height:100%; padding:0 16px; display:flex; align-items:center; gap:12px`. Never counter-skew individual children — that is what made scores collide with names in the first attempt.
|
||||
Children: club logo 46×46 (`rgba(0,0,0,.32)`, 1px white 24% border) → text column (`flex:1 1 auto; min-width:0`) → score box (`width:60px; flex:none`, column, right-aligned).
|
||||
Text column row 1: flag chip 26×17 + athlete name (25px/800 uppercase, `flex:1 1 auto; min-width:0`, nowrap + ellipsis). Row 2: club name (12px, .03em, uppercase, ellipsis) + penalty chip.
|
||||
Score box: corner label 11px .24em above the numeral; numeral `Zen Old Mincho` 46px/700 white, `text-shadow:0 6px 20px rgba(0,0,0,.5)`.
|
||||
- Center: `width:118px; flex:none`, `rgba(9,9,11,.9)` + `blur(8px)`, `border-bottom:3px solid #2c2a28`; round label 11px .3em `#8f8a83`, clock 34px/800 `#efe9e0` tabular-nums, three 20×3px pips (`#e8534a` done, `#3a3734` pending).
|
||||
- Blue panel: exact mirror — score box, text column right-aligned, club logo. Gradient `rgba(30,72,140,.9) → rgba(18,44,92,.94)`, `border-bottom:3px solid #6aa6ff`.
|
||||
|
||||
Both panels must be true mirrors: logo → flag+name → club+penalty → score, with the corner label above its own score on both sides.
|
||||
|
||||
**Point timeline** — `left/right: 26px; bottom: 48px; height: 5px`, segments with 2px gaps.
|
||||
|
||||
## 5. Gear-menu controls
|
||||
|
||||
No floating on-screen toggle. Add a **Scoreboard** section to the existing gear menu (same pattern as quality/speed), anchored above the gear button:
|
||||
|
||||
Panel `rgba(10,10,12,.94)` + `blur(10px)`, 1px white 16% border, 236px wide, header "SCOREBOARD" 11px .3em `#8f8a83`. One switch row per part:
|
||||
|
||||
`Score bar · Live scoring feed · Match info · Point timeline · Club logos · Country flags · Penalties`
|
||||
|
||||
Rows: 14px uppercase label, .14em tracking, hover `rgba(255,255,255,.07)`; switch = 26×14 track (`#e8534a` on, `#3a3734` off) with a 10px knob. Persist per user; the uploader's per-video settings seed the defaults. **Penalties defaults off** — the Points panel has no penalty events yet; enable when it does. Gear icon turns `#e8534a` while the menu is open. Menu closes on outside click and Esc, and never covers the score bar.
|
||||
|
||||
## 6. VS intro before playback
|
||||
|
||||
Part of the play experience, inside the video box at 16:9:
|
||||
|
||||
- On first play of a session, render the VS card over the frame while the video is paused at frame 0.
|
||||
- Hold **2s**, then fade out over 0.5s; call `video.play()` as the fade begins so motion is already running when the card clears. Scoreboard fades in after.
|
||||
- Any click, tap or key press skips it immediately and starts playback.
|
||||
- Replays and scrubs do not re-trigger it. If match metadata is missing, skip the intro entirely.
|
||||
|
||||
**VS card content**: event name in `Zen Old Mincho` 24px .44em uppercase; division line 12px .34em with red/blue 4px square separators; per fighter a **3:4 photo frame** with a **104px circular club crest** overlapping its inner-bottom corner (`bottom:-26px`, `#0d0d10`, 1px white 28% border, `0 10px 30px rgba(0,0,0,.6)`), then a 3px corner-colored rule, name 42px/800 uppercase, flag chip 40×27 + country 14px .3em, club name 13px .18em; center column: "VS" `Zen Old Mincho` 88px, weight class in a 1px-bordered chip, format line; bottom line: bout · tatami · date · venue.
|
||||
|
||||
**Background animation** (CSS only, no video): two radial glows — red `rgba(168,42,34,.62)` and blue `rgba(28,64,136,.62)`, 70% width, drifting `translate3d(±60px, ∓28px) scale(1→1.14)` over 19s and 23s ease-in-out infinite — plus a 26%-wide white 9% light sweep crossing every 11s. No center divider line.
|
||||
|
||||
Also expose the VS card as a 1200×630 share image for `og-image`.
|
||||
|
||||
## 7. Design tokens
|
||||
|
||||
Ink `#0a0a0c` · panel `rgba(9,9,11,.9)` · bone `#efe9e0` · muted `#8f8a83` · brand red `#e8534a` · red edge `#ff6a5e` · red panel `#7a1a16 → #b4342c` · blue panel `#1e488c → #122c5c` · blue edge `#6aa6ff` · senshu `#ffdf6b` on `#3a2a00` · penalty `#c8492f`.
|
||||
Type: **Barlow Condensed** 400/600/700/800 for all UI; **Zen Old Mincho** 700 for score numerals, "VS" and the discipline/event lines.
|
||||
Geometry: skew `-9deg` with a single `+9deg` counter-skew row; bar 84px; frame inset 26px; gaps 12px; no border radius on overlay parts.
|
||||
|
||||
## 8. Acceptance criteria
|
||||
|
||||
1. Overlay never blocks the scrubber or any control (`pointer-events:none`, controls above it).
|
||||
2. Score bar sits fully above the control band; nothing crosses the progress bar or the panels' bottom borders.
|
||||
3. No text is clipped or overlapping at 1150px design width: every text span has `min-width:0` with ellipsis; fixed boxes are `flex:none`.
|
||||
4. Red and blue panels are exact mirrors.
|
||||
5. Scores, feed, round and timeline all match the Points panel at any `currentTime`, including after scrubbing backwards and at ¼×/½×/¾× speed.
|
||||
6. Missing metadata hides elements — no placeholder text anywhere.
|
||||
7. Overlay scales with the player (fixed 1150px layer scaled, or clamp-based sizing); minimum rendered text 12px; below ~700px player width only the score bar remains.
|
||||
8. VS intro holds 2s, fades, playback starts; skippable; first play only.
|
||||
9. Every one of the seven parts toggles independently from the gear menu and persists per user.
|
||||
200
drafts/unpacked/design_handoff_scoreboard_overlay/README.md
Normal file
200
drafts/unpacked/design_handoff_scoreboard_overlay/README.md
Normal file
@ -0,0 +1,200 @@
|
||||
# Handoff: Live Match Scoreboard Overlay (Karate / Taekwondo)
|
||||
|
||||
Paste this file to Claude Code as the brief. The bundled `Match Points Overlay.dc.html` is a **design reference prototype written in HTML** — not production code. Recreate it inside the existing video platform (React/Vue/whatever the player is built in), using the codebase's own component and styling patterns. Fidelity: **high** — colors, type, sizes below are final.
|
||||
|
||||
## Goal
|
||||
|
||||
An overlay that sits on top of the video player and shows live match state for a karate kumite or taekwondo kyorugi bout. **Every part must be independently switchable on and off** (per-viewer preference, and per-broadcast defaults set by the uploader).
|
||||
|
||||
## Structure
|
||||
|
||||
The overlay is a layer inside the player container:
|
||||
|
||||
```
|
||||
<div class="player"> position: relative
|
||||
<video> base layer
|
||||
<div class="overlay"> position:absolute; inset:0; pointer-events:none
|
||||
...parts... each part pointer-events:none except the toggle chip
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Nothing about the video element changes. The overlay never intercepts clicks except on its own toggle control.
|
||||
|
||||
## Switchable parts
|
||||
|
||||
Each is a boolean, defaulting to on, stored per user (localStorage or profile) and overridable per video:
|
||||
|
||||
| key | part | position in frame |
|
||||
|---|---|---|
|
||||
| `identity` | discipline label + match subtitle, with a 4px red-gradient rule at its left | top-left, 22px / 26px inset |
|
||||
| `ticker` | "LIVE SCORING" header + last 4 judged calls | top-right, 20px / 24px inset, 262px wide |
|
||||
| `scorebar` | the main bottom bar (both corner panels + center clock) | bottom, full width minus 26px, height 84px, 18px from bottom |
|
||||
| `timeline` | thin segmented point-history strip | bottom, 5px tall, 8px from bottom |
|
||||
| `clubs` | club logo slot + club name inside each corner panel | inside scorebar |
|
||||
| `flags` | country flag chip next to each athlete name | inside scorebar |
|
||||
| `penalties` | SENSHU chip (red) and C1 / GAM-JEOM chip (blue) | inside scorebar |
|
||||
|
||||
Also a master `overlay` toggle: a chip in the video box (bottom-right, `SCOREBOARD ON/OFF`, dark translucent, 1px white 18% border, red dot when on, grey when off) that hides everything at once. This chip is the only pointer-events:auto element. When the overlay is off, the chip drops from 112px above the bottom to 18px.
|
||||
|
||||
Where the platform has a player settings menu (like captions), expose the same booleans there.
|
||||
|
||||
## Layout spec (design width 1150px, 16:9-ish frame)
|
||||
|
||||
Scrim over the video so text reads: bottom gradient `rgba(6,6,8,.9) 0% → .45 at 15% → 0 at 30%`, top gradient `rgba(6,6,8,.85) 0% → .4 at 14% → 0 at 30%`. A diagonal texture layer over the frame: `repeating-linear-gradient(115deg, rgba(255,255,255,.5) 0 1px, transparent 1px 4px)`, opacity .16, `mix-blend-mode: overlay`.
|
||||
|
||||
### Bottom bar (84px tall, flex row, no gaps between panels)
|
||||
|
||||
- **Red panel** — `flex:1 1 0; min-width:0; overflow:hidden; transform:skewX(-9deg)`, background `linear-gradient(90deg, rgba(122,26,22,.94), rgba(180,52,44,.9))`, `border-bottom:3px solid #ff6a5e`.
|
||||
Inner row un-skews once: `transform:skewX(9deg); height:100%; padding:0 16px; display:flex; align-items:center; gap:12px`. Applying the counter-skew to the row (not to each child) is what keeps the score numeral from overlapping the name.
|
||||
Children in order: club logo box (46×46, `rgba(0,0,0,.32)` fill, 1px white 24% border, monospace 8px "CLUB LOGO" placeholder) → text column (`flex:1 1 auto; min-width:0`) → score box (`width:56px; flex:none; text-align:right`).
|
||||
Text column row 1: flag chip 26×17 → athlete name (25px/800, uppercase, `flex:1 1 auto; min-width:0`, nowrap + ellipsis) → corner label "AKA" (11px, .24em tracking, 66% white).
|
||||
Text column row 2: club name (12px, .03em tracking, 85% white, uppercase, ellipsis) → SENSHU chip (10px/700, .18em, 1px 6px padding, `#ffdf6b` on `#3a2a00`).
|
||||
- **Center clock** — `width:118px; flex:none`, `rgba(9,9,11,.9)` + `backdrop-filter: blur(8px)`, `border-bottom:3px solid #2c2a28`. Stacked: round label (11px, .3em, `#8f8a83`), clock `mm:ss` (34px/800, `#efe9e0`, tabular-nums), three 20×3px round pips (`#e8534a` for completed, `#3a3734` for pending).
|
||||
- **Blue panel** — mirror of red: gradient `rgba(30,72,140,.9) → rgba(18,44,92,.94)`, `border-bottom:3px solid #6aa6ff`, order reversed (score, text column right-aligned, club logo), penalty chip `#c8492f` on white text.
|
||||
- Score numerals: `'Zen Old Mincho', serif`, 46px, weight 700, white, `text-shadow: 0 6px 20px rgba(0,0,0,.5)`.
|
||||
|
||||
### Ticker (top-right)
|
||||
|
||||
Header row: 7px red dot pulsing (`opacity 1 → .25 → 1`, 1.6s infinite) + "LIVE SCORING" (12px, .3em, `#d5cfc7`, text-shadow for legibility). Each entry: `rgba(10,10,12,.62)` + `blur(6px)`, 7px/10px padding, `border-right: 3px solid <corner color>`, animating in with `translateY(10px) → 0`, opacity 0 → 1, .45s ease. Entry content: timestamp (12px, `#79736c`) · call name (17px/700, .12em, uppercase, `#efe9e0`) · points (`Zen Old Mincho` 19px, corner color). Newest at top, keep 4.
|
||||
|
||||
Karate calls: Ippon +3, Waza-ari +2, Yuko +1, Chukoku/Keikoku/Hansoku penalties.
|
||||
Taekwondo calls: head kick +3, turning body +4, body kick +2, punch +1, gam-jeom (awards +1 to the opponent).
|
||||
|
||||
### Timeline strip
|
||||
|
||||
14 equal segments, 5px tall, 2px gaps; red / blue for scored points in chronological order, `#2a2724` for empty. Grows as the bout progresses.
|
||||
|
||||
## Sport switch
|
||||
|
||||
One `sport` setting flips labels without changing layout:
|
||||
|
||||
| | Karate | Taekwondo |
|
||||
|---|---|---|
|
||||
| discipline label | KARATE KUMITE | TAEKWONDO KYORUGI |
|
||||
| corners | AKA / AO | HONG / CHUNG |
|
||||
| round label | KUMITE | ROUND n / 3 |
|
||||
| penalty chip | C1 | GAM-JEOM n |
|
||||
| point names | Ippon / Waza-ari / Yuko | head, turning, body, punch |
|
||||
|
||||
## Data contract
|
||||
|
||||
Drive the overlay from the existing Points panel. Suggested shape:
|
||||
|
||||
```ts
|
||||
type Corner = 'red' | 'blue';
|
||||
interface Athlete { name: string; countryCode: string; flagUrl?: string; clubName: string; clubLogoUrl?: string; }
|
||||
interface ScoreEvent { id: string; at: string; corner: Corner; label: string; points: number; }
|
||||
interface MatchState {
|
||||
sport: 'karate' | 'taekwondo';
|
||||
title: string; subtitle: string; // "Finals", "Senior −75 kg · Tatami 1"
|
||||
red: Athlete; blue: Athlete;
|
||||
redScore: number; blueScore: number;
|
||||
senshu: Corner | null;
|
||||
penalties: { red: number; blue: number };
|
||||
round: number; totalRounds: number;
|
||||
clockSeconds: number; running: boolean;
|
||||
events: ScoreEvent[];
|
||||
}
|
||||
```
|
||||
|
||||
Scores, clock and events come from the scorekeeper; the overlay is presentational. The clock counts down once per second while `running`. New events prepend to the ticker and push a segment onto the timeline.
|
||||
|
||||
## Design tokens
|
||||
|
||||
Colors: ink `#0a0a0c` / panel `rgba(9,9,11,.9)` / bone `#efe9e0` / muted `#8f8a83` / brand red `#e8534a` / red edge `#ff6a5e` / red panel `#7a1a16 → #b4342c` / blue panel `#1e488c → #122c5c` / blue edge `#6aa6ff` / senshu `#ffdf6b` on `#3a2a00` / penalty `#c8492f`.
|
||||
Type: `Barlow Condensed` 400/600/700/800 for all UI; `Zen Old Mincho` 700 for score numerals and the discipline label.
|
||||
Geometry: panel skew `-9deg` (content counter-skewed `+9deg`), bar height 84px, frame inset 26px, bottom inset 18px, gaps 12px, no border radius on overlay parts.
|
||||
|
||||
## Accessibility / safety
|
||||
|
||||
- Keep every text span `min-width:0` inside its flex column so names truncate instead of overflowing.
|
||||
- Minimum on-screen text size 12px at 1150px design width; scale the whole overlay proportionally with the player (`transform: scale()` on a fixed 1150px layer, or clamp-based sizing) so it stays legible at small player sizes and in fullscreen.
|
||||
- Below ~700px player width, hide `identity` and `ticker` automatically and keep only the score bar.
|
||||
- Overlay is decorative for screen readers; expose the same data as text in the existing Points panel.
|
||||
|
||||
## Files
|
||||
|
||||
- `Match Points Overlay.dc.html` — the reference prototype (open in a browser; the SCOREBOARD chip demonstrates the master toggle).
|
||||
- `assets/mat.png` — still frame used as the video stand-in. Not part of the deliverable.
|
||||
- Flags and club logos are mockup placeholders in the prototype; wire real images in.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Corrections after first implementation pass
|
||||
|
||||
The first build broke the score bar. Specific fixes:
|
||||
|
||||
1. **Keep the skew, but counter-skew ONCE.** The panel gets `transform: skewX(-9deg)`; a single inner row gets `transform: skewX(9deg); height:100%; display:flex; align-items:center`. Do not apply the counter-skew to individual children, and do not drop the skew altogether — the parallelogram edges are the design.
|
||||
2. **The bar is a fixed-height 84px strip, vertically centered content.** In the broken build the names sat on the very bottom edge and the panels were full-bleed to the frame. The bar is inset 26px left/right, sits above the player control bar, and its content is vertically centered — nothing touches the bar's top or bottom edge.
|
||||
3. **Both scores must render.** Red score right-aligned in a 56px `flex:none` box at the panel's inner end; blue score left-aligned in the mirrored position. In the broken build the red score was missing and the blue "1" was floating at the top edge.
|
||||
4. **Never let panel content overflow.** Text spans need `min-width:0` and `overflow:hidden; text-overflow:ellipsis`; fixed boxes (logo 46px, score 56px) are `flex:none`.
|
||||
5. **Empty data:** when an athlete/club is unknown, render the corner as "—" or hide the row — do not print "NOT SET".
|
||||
6. **Club logo and flag slots must be filled or hidden.** If no image, hide the slot (toggle default off) rather than showing an empty box.
|
||||
|
||||
## Controls belong in the player's gear menu
|
||||
|
||||
Remove any floating on-screen toggle chip. The scoreboard switches live in the **existing player settings (gear) menu**, as a "Scoreboard" section — same pattern as quality/captions:
|
||||
|
||||
- Gear button in the control bar opens a popup anchored bottom-right, above the control bar.
|
||||
- Popup: dark translucent panel (`rgba(10,10,12,.94)` + `blur(10px)`, 1px white 16% border), 236px wide, a "SCOREBOARD" header (11px, .3em tracking, `#8f8a83`), then one switch row per part: Score bar, Live scoring feed, Match info, Point timeline, Club logos, Country flags, Penalties.
|
||||
- Rows: 14px label, uppercase, .14em tracking; switch is a 26×14 track (`#e8534a` on, `#3a3734` off) with a 10px knob; hover `rgba(255,255,255,.07)`.
|
||||
- Persist each switch per user; the uploader's per-video defaults seed them.
|
||||
- The popup closes on outside click / Esc, and never covers the score bar.
|
||||
|
||||
The updated prototype in this folder demonstrates the gear menu and all seven switches.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Target: video.takeone.bh video page (e.g. /videos/11vu7R)
|
||||
|
||||
Studied the live page. The overlay must be driven by what the platform already stores, and by **playback time** — not by a live clock.
|
||||
|
||||
### What the platform already has
|
||||
|
||||
- **Points panel**: rounds (number, name, optional start time) and point events shaped like `@00:11 · Point (1 pt) · Blue|Red|Both` with a running score printed as `Score <blue> – <red>`.
|
||||
- **Coach review**: time-ranged notes (`01:40 — 01:45`, text, author) and reaction icons.
|
||||
- **Player**: Highlights badge, mini player, playback speed menu, ¼× ½× ¾× slow-motion buttons, scrub speeds 1× 5× 20× in the points editor.
|
||||
- **No fighter metadata at all** — no athlete names, clubs, flags, weight class, or event name. This is exactly why the first implementation printed "NOT SET".
|
||||
|
||||
### Required changes on the platform side
|
||||
|
||||
1. **Add match metadata to the video's sports settings** (one form, next to Add Round): event name, division/category, weight class, bout number, tatami/ring, date, venue — and per corner: athlete name, country (ISO code + flag image), club name, club logo, plus optional headshot for the VS card.
|
||||
2. **Any field left empty hides its element.** Never render a placeholder string like "NOT SET". If no flag → hide the flag chip; no club logo → hide the logo slot; no athlete name → fall back to "RED" / "BLUE".
|
||||
3. **Corners are Red and Blue** in the existing data. Karate labels (AKA/AO) or taekwondo (HONG/CHUNG) are a display option, not new data.
|
||||
4. **A "Both" point event** awards to both corners — increment both scores and emit two ticker rows.
|
||||
|
||||
### Binding to playback
|
||||
|
||||
The overlay is a pure function of `video.currentTime`:
|
||||
|
||||
- Scores = sum of point values for events with `at <= currentTime`, per corner.
|
||||
- Round label = the round whose start time is the latest `<= currentTime`.
|
||||
- Clock = `currentTime` formatted mm:ss (offset from the round start if round start times exist). It does **not** count down.
|
||||
- Ticker = the last 4 events with `at <= currentTime`, newest on top; a new event animates in as playback crosses it. Scrubbing backwards removes them again.
|
||||
- Timeline strip = one segment per point event across the video duration, red/blue, positioned by timestamp — clicking a segment can seek to that point (nice-to-have; keep `pointer-events` off otherwise).
|
||||
- Slow-motion (¼× ½× ¾×) and playback speed must not affect the overlay: it reads `currentTime`, never a timer.
|
||||
|
||||
### Pre-match VS card
|
||||
|
||||
Rendered as a poster/intro layer at 16:9 over the player before playback starts (and available as an exportable share image at 1200×630 for the og-image). Content: event name, division line, both fighters' 3:4 photos with 104px circular club crests overlapping the inner corner, name, flag + country, club, the weight class under the VS mark, and a bout/tatami/date line. Background: two slow drifting red/blue radial glows plus a periodic light sweep — CSS animations only, no video.
|
||||
|
||||
### Controls
|
||||
|
||||
Everything toggles from the **gear menu** in the existing control bar, under a "Scoreboard" section: Score bar, Live scoring feed, Match info, Point timeline, Club logos, Country flags, Penalties. Penalties defaults **off** because the current data model has no penalty events; enable it when penalties are added to the Points panel. The overlay layer is `pointer-events:none` so the scrubber and buttons stay clickable, and the score bar sits above the 48px control band (bottom ≈ 56px) so nothing crosses the progress bar.
|
||||
|
||||
The prototype in this folder uses the real point data from /videos/11vu7R (blue 1 @0:11, red 1 @0:11, red 2 @0:30, red 1 @0:51, blue 1 @1:52, blue 1 @2:39 → 4–3 red).
|
||||
|
||||
|
||||
## VS intro before playback
|
||||
|
||||
The VS card is part of the play experience, not a separate screen:
|
||||
|
||||
- On load / on pressing play, the player renders the VS card **inside the video box** at 16:9, covering the frame, while the video is buffering/paused at frame 0.
|
||||
- It holds for **2 seconds**, then fades out over ~0.5s and playback starts (`video.play()` fires as the fade begins, so the first frames are already moving as the card clears).
|
||||
- The scoreboard overlay fades in after the card clears.
|
||||
- The card is skippable: any click, key press, or tap on the frame ends it immediately and starts playback.
|
||||
- Only on the first play of a session — a replay/scrub does not re-trigger it (the prototype's play button replays it for demo purposes).
|
||||
- If match metadata is missing, skip the intro entirely rather than showing an empty card.
|
||||
BIN
drafts/unpacked/design_handoff_scoreboard_overlay/assets/mat.png
Normal file
BIN
drafts/unpacked/design_handoff_scoreboard_overlay/assets/mat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1002 KiB |
94
drafts/vs-screen.html
Normal file
94
drafts/vs-screen.html
Normal file
@ -0,0 +1,94 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>VS screen — exact markup</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700;800&family=Zen+Old+Mincho:wght@700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { margin:0; background:#0a0a0c; font-family:'Barlow Condensed',sans-serif; }
|
||||
@keyframes driftA { 0%,100% { transform: translate3d(0,0,0) scale(1); } 50% { transform: translate3d(60px,-28px,0) scale(1.14); } }
|
||||
@keyframes driftB { 0%,100% { transform: translate3d(0,0,0) scale(1.08); } 50% { transform: translate3d(-54px,26px,0) scale(1); } }
|
||||
@keyframes sweep { 0% { transform: translateX(-60%) skewX(-14deg); opacity:0; } 25% { opacity:.5; } 60% { opacity:0; } 100% { transform: translateX(160%) skewX(-14deg); opacity:0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!--
|
||||
VS SCREEN — copy verbatim. Replace ONLY the {{ }} holes.
|
||||
Sits inside the player container as an absolutely positioned 16:9 layer:
|
||||
position:absolute; inset:0; z-index:8; opacity 1 -> 0 over .55s after a 2s hold.
|
||||
Photo frames are 3:4. The club crest overlaps the photo's INNER-bottom corner (right on the left fighter, left on the right one).
|
||||
Hide a missing element with display:none — never substitute placeholder text.
|
||||
The three animated background layers are decorative; keep them behind everything (they are first in source order).
|
||||
-->
|
||||
|
||||
<div style="position:relative;width:1150px;aspect-ratio:16/9;overflow:hidden;background:#08080a">
|
||||
|
||||
<div style="position:absolute;inset:-12%;width:70%;left:-6%;background:radial-gradient(closest-side, rgba(168,42,34,.62), rgba(8,8,10,0) 72%);animation:driftA 19s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;inset:-12%;width:70%;left:36%;background:radial-gradient(closest-side, rgba(28,64,136,.62), rgba(8,8,10,0) 72%);animation:driftB 23s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:26%;background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.09), rgba(255,255,255,0));animation:sweep 11s linear infinite"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:54%;clip-path:polygon(0 0, 100% 0, 78% 100%, 0 100%);background:linear-gradient(120deg, rgba(122,26,22,.5), rgba(8,8,10,0) 76%)"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;right:0;width:54%;clip-path:polygon(22% 0, 100% 0, 100% 100%, 0 100%);background:linear-gradient(300deg, rgba(24,52,110,.5), rgba(8,8,10,0) 76%)"></div>
|
||||
|
||||
<!-- event header -->
|
||||
<div style="position:absolute;top:34px;left:0;right:0;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-family:'Zen Old Mincho',serif;font-size:24px;letter-spacing:.44em;color:#efe9e0;text-transform:uppercase;text-indent:.44em">{{ eventName }}</div>
|
||||
<div style="display:flex;align-items:center;gap:14px;font-size:12px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase">
|
||||
<span>{{ category }}</span><span style="width:4px;height:4px;background:#e8534a"></span><span>{{ division }}</span><span style="width:4px;height:4px;background:#6aa6ff"></span><span>{{ roundName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;inset:96px 0 54px;display:grid;grid-template-columns:1fr 200px 1fr;align-items:start">
|
||||
|
||||
<!-- RED fighter -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
||||
<div style="position:relative;width:228px;height:304px">
|
||||
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div style="position:absolute;bottom:-26px;right:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #e8534a;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">{{ redName }}</div>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span style="font-size:14px;letter-spacing:.3em;color:#d3b3ad;text-transform:uppercase">{{ redCountry }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-top:2px">
|
||||
<span style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">{{ redClub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- centre -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;padding-top:96px">
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:88px;line-height:.8;font-weight:700;color:#efe9e0;text-shadow:0 8px 34px rgba(0,0,0,.75)">VS</span>
|
||||
<span style="padding:7px 16px;border:1px solid rgba(255,255,255,.3);font-size:14px;letter-spacing:.32em;color:#efe9e0;text-transform:uppercase">{{ weightClass }}</span>
|
||||
<span style="font-size:11px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase">{{ format }}</span>
|
||||
</div>
|
||||
|
||||
<!-- BLUE fighter — mirror -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
||||
<div style="position:relative;width:228px;height:304px">
|
||||
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div style="position:absolute;bottom:-26px;left:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #6aa6ff;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">{{ blueName }}</div>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span style="font-size:14px;letter-spacing:.3em;color:#a8b6cf;text-transform:uppercase">{{ blueCountry }}</span>
|
||||
<span style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-top:2px">
|
||||
<span style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">{{ blueClub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- bout line -->
|
||||
<div style="position:absolute;bottom:26px;left:0;right:0;display:flex;align-items:center;justify-content:center;gap:16px;font-size:11px;letter-spacing:.32em;color:#8f8a83;text-transform:uppercase">
|
||||
<span>{{ bout }}</span><span style="width:4px;height:4px;background:#e8534a"></span><span>{{ tatami }}</span><span style="width:4px;height:4px;background:#6aa6ff"></span><span>{{ dateVenue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
138
resources/views/videos/partials/match/scoreboard/index.blade.php
Normal file
138
resources/views/videos/partials/match/scoreboard/index.blade.php
Normal file
@ -0,0 +1,138 @@
|
||||
@php
|
||||
/*
|
||||
* Match scoreboard entry partial. Builds the JSON payload the client
|
||||
* script consumes ($msbState) plus a rich $vs payload for the VS card.
|
||||
* Every value is null-safe — missing fields become empty strings so
|
||||
* the CSS :empty rules hide their elements (no placeholder text).
|
||||
*/
|
||||
$hd = $video->sportsMatch?->headerData();
|
||||
|
||||
/* ── Discipline / subtitle text ───────────────────────────────────── */
|
||||
$subtitlePieces = array_filter([
|
||||
$hd['championship'] ?? null,
|
||||
$hd['weight_category'] ?? null,
|
||||
!empty($hd['court']) ? ('Tatami '.$hd['court']) : null,
|
||||
], fn ($p) => is_string($p) && trim($p) !== '');
|
||||
|
||||
/* ── Country display names (from Countries::all()) ───────────────── */
|
||||
$countries = \App\Data\Countries::all();
|
||||
$countryName = function (?string $iso2) use ($countries) {
|
||||
if (!$iso2) return null;
|
||||
$key = strtoupper($iso2);
|
||||
return $countries[$key]['name'] ?? null;
|
||||
};
|
||||
|
||||
/* ── Playback-time-driven data (rounds + points) ─────────────────── */
|
||||
$msbRounds = $video->matchRounds()
|
||||
->orderBy('round_number')
|
||||
->get(['round_number', 'name', 'start_time_seconds'])
|
||||
->map(fn ($r) => [
|
||||
'n' => (int) $r->round_number,
|
||||
'name' => (string) ($r->name ?? ''),
|
||||
'start' => (float) ($r->start_time_seconds ?? 0),
|
||||
])->values()->all();
|
||||
|
||||
$msbPoints = $video->matchPoints()
|
||||
->orderBy('timestamp_seconds')
|
||||
->get(['timestamp_seconds', 'action', 'points', 'competitor', 'score_red', 'score_blue'])
|
||||
->flatMap(function ($p) {
|
||||
// "Both" events (if ever recorded) emit two feed rows — one per corner.
|
||||
$rows = [];
|
||||
$rows[] = [
|
||||
't' => (float) $p->timestamp_seconds,
|
||||
'action' => (string) ($p->action ?? 'Point'),
|
||||
'pts' => (int) $p->points,
|
||||
'side' => $p->competitor === 'both' ? 'red' : $p->competitor,
|
||||
'sr' => (int) ($p->score_red ?? 0),
|
||||
'sb' => (int) ($p->score_blue ?? 0),
|
||||
];
|
||||
if ($p->competitor === 'both') {
|
||||
$rows[] = $rows[0];
|
||||
$rows[1]['side'] = 'blue';
|
||||
$rows[1]['t'] = (float) $p->timestamp_seconds + 0.001;
|
||||
}
|
||||
return $rows;
|
||||
})->values()->all();
|
||||
|
||||
$msbState = [
|
||||
'sport' => $hd['sport'] ?? null,
|
||||
'discipline' => null,
|
||||
'subtitle' => implode(' · ', $subtitlePieces),
|
||||
'red' => [
|
||||
'name' => $hd['red']['name'] ?? null,
|
||||
'flag' => $hd['red']['flag'] ?? null,
|
||||
'club' => $hd['red']['club'] ?? null,
|
||||
'club_logo' => !empty($hd['red']['club_logo'])
|
||||
? route('media.thumbnail', $hd['red']['club_logo']) : null,
|
||||
],
|
||||
'blue' => [
|
||||
'name' => $hd['blue']['name'] ?? null,
|
||||
'flag' => $hd['blue']['flag'] ?? null,
|
||||
'club' => $hd['blue']['club'] ?? null,
|
||||
'club_logo' => !empty($hd['blue']['club_logo'])
|
||||
? route('media.thumbnail', $hd['blue']['club_logo']) : null,
|
||||
],
|
||||
'rounds' => $msbRounds ?: [['n' => 1, 'name' => '', 'start' => 0]],
|
||||
'points' => $msbPoints,
|
||||
'defaults' => is_array($hd['scoreboard_defaults'] ?? null) ? $hd['scoreboard_defaults'] : [],
|
||||
'vs' => null, // populated below once $vs is built
|
||||
];
|
||||
|
||||
/* ── VS card — only render if there is anything to show ─────────── */
|
||||
$vs = [
|
||||
'event' => $hd['championship'] ?? null,
|
||||
'division' => $hd['division'] ?? null,
|
||||
'category' => $hd['weight_category'] ?? null,
|
||||
'round' => null,
|
||||
'red' => [
|
||||
'name' => $hd['red']['name'] ?? null,
|
||||
'flag' => $hd['red']['flag'] ?? null,
|
||||
'country_name' => $countryName($hd['red']['flag'] ?? null),
|
||||
'club' => $hd['red']['club'] ?? null,
|
||||
'club_logo' => $hd['red']['club_logo'] ?? null,
|
||||
'headshot' => $hd['red']['headshot'] ?? null,
|
||||
],
|
||||
'blue' => [
|
||||
'name' => $hd['blue']['name'] ?? null,
|
||||
'flag' => $hd['blue']['flag'] ?? null,
|
||||
'country_name' => $countryName($hd['blue']['flag'] ?? null),
|
||||
'club' => $hd['blue']['club'] ?? null,
|
||||
'club_logo' => $hd['blue']['club_logo'] ?? null,
|
||||
'headshot' => $hd['blue']['headshot'] ?? null,
|
||||
],
|
||||
'weight_category' => $hd['weight_category'] ?? null,
|
||||
'format_line' => $hd['format'] ?? null,
|
||||
'match_number' => $hd['match_number'] ?? null,
|
||||
'court' => !empty($hd['court']) ? ('Tatami '.$hd['court']) : null,
|
||||
'match_date' => $hd['match_date'] ?? null,
|
||||
'venue_name' => $hd['venue']['name'] ?? null,
|
||||
];
|
||||
|
||||
// Enough metadata for a VS card? Need at least one athlete name or headshot.
|
||||
$hasMatchMeta = ($vs['red']['name'] || $vs['red']['headshot'] ||
|
||||
$vs['blue']['name'] || $vs['blue']['headshot']);
|
||||
|
||||
// Feed image paths (headshot/logo) already routed to media.thumbnail for the
|
||||
// JS to consume — mirror them into $msbState.vs so setVsImg() can use them.
|
||||
$msbState['vs'] = [
|
||||
'red' => [
|
||||
'name' => $vs['red']['name'] ?? null,
|
||||
'flag' => $vs['red']['flag'] ?? null,
|
||||
'club_logo' => !empty($vs['red']['club_logo']) ? route('media.thumbnail', $vs['red']['club_logo']) : null,
|
||||
'headshot' => !empty($vs['red']['headshot']) ? route('media.thumbnail', $vs['red']['headshot']) : null,
|
||||
],
|
||||
'blue' => [
|
||||
'name' => $vs['blue']['name'] ?? null,
|
||||
'flag' => $vs['blue']['flag'] ?? null,
|
||||
'club_logo' => !empty($vs['blue']['club_logo']) ? route('media.thumbnail', $vs['blue']['club_logo']) : null,
|
||||
'headshot' => !empty($vs['blue']['headshot']) ? route('media.thumbnail', $vs['blue']['headshot']) : null,
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@include('videos.partials.match.scoreboard.styles')
|
||||
@include('videos.partials.match.scoreboard.overlay')
|
||||
@if ($hasMatchMeta)
|
||||
@include('videos.partials.match.scoreboard.vs-intro', ['vs' => $vs])
|
||||
@endif
|
||||
@include('videos.partials.match.scoreboard.script', ['msbState' => $msbState, 'hasMatchMeta' => $hasMatchMeta])
|
||||
@ -0,0 +1,99 @@
|
||||
{{--
|
||||
Overlay markup. The score bar + timeline blocks are the design's exact
|
||||
verbatim inline-styled markup from drafts/score-bar.html — only the
|
||||
{{ }} data holes have been filled and `data-msb` attributes added for
|
||||
the JS. `data-msb-part` attributes let the gear-menu toggles hide any
|
||||
part with CSS `display: none` without touching the inline styles.
|
||||
--}}
|
||||
<div class="msb-root" id="msbRoot" data-video-id="{{ $video->id }}">
|
||||
<div class="msb-scrim"></div>
|
||||
|
||||
{{-- Fixed-width scale layer (1150 px design canvas). JS scales it to
|
||||
fit the actual player, so the score bar keeps its pixel-perfect
|
||||
proportions at any player size. --}}
|
||||
<div class="msb-scale" id="msbScale">
|
||||
|
||||
{{-- Match info (top-left) --}}
|
||||
<div class="msb-info" data-msb="info" data-msb-part="info">
|
||||
<div class="rule"></div>
|
||||
<div class="txt">
|
||||
<div class="disc" data-msb="disc"></div>
|
||||
<div class="sub" data-msb="sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Live scoring feed (top-right) --}}
|
||||
<div class="msb-feed" data-msb="feedWrap" data-msb-part="feed">
|
||||
<div class="hdr"><span class="dot"></span><span>Live scoring</span></div>
|
||||
<div class="list" data-msb="feed"></div>
|
||||
</div>
|
||||
|
||||
{{-- ═══════════════════════════════════════════════════════════════════
|
||||
SCORE BAR — verbatim from drafts/score-bar.html (design handoff).
|
||||
Only {{ }} holes replaced with Blade data + data-msb hooks.
|
||||
═══════════════════════════════════════════════════════════════ --}}
|
||||
<div data-msb-part="scorebar" style="position:absolute;left:0;right:0;bottom:56px;padding:0 26px;display:flex;align-items:stretch;gap:0;height:84px;z-index:2;pointer-events:none">
|
||||
|
||||
{{-- RED / AKA --}}
|
||||
<div style="flex:1 1 0;min-width:0;transform:skewX(-9deg);overflow:hidden;background:linear-gradient(90deg, rgba(122,26,22,.94), rgba(180,52,44,.9));border-bottom:3px solid #ff6a5e">
|
||||
<div style="transform:skewX(9deg);height:100%;padding:0 16px;display:flex;align-items:center;gap:12px">
|
||||
<div data-msb="redClub" data-msb-part="clubs" style="width:46px;height:46px;flex:none;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;letter-spacing:.05em;color:rgba(255,255,255,.65);text-align:center;line-height:1.25">CLUB<br>LOGO</div>
|
||||
<div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px">
|
||||
<div style="display:flex;align-items:center;gap:10px;min-width:0">
|
||||
<span data-msb="redFlag" data-msb-part="flags" style="width:26px;height:17px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 4px,rgba(255,255,255,.1) 4px 8px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span data-msb="redName" style="flex:1 1 auto;min-width:0;font-size:25px;line-height:1;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ $msbState['red']['name'] ?: 'RED' }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span data-msb="redClubName" data-msb-part="clubs" style="flex:1 1 auto;min-width:0;font-size:12px;letter-spacing:.03em;color:rgba(255,232,228,.85);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ $msbState['red']['club'] ?? '' }}</span>
|
||||
<span data-msb="redSenshu" data-msb-part="penalties" style="display:none;font-size:10px;letter-spacing:.18em;padding:1px 6px;background:#ffdf6b;color:#3a2a00;font-weight:700;flex:none">SENSHU</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:60px;flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:5px">
|
||||
<span data-msb="redCorner" style="font-size:11px;letter-spacing:.24em;color:rgba(255,236,232,.7)">AKA</span>
|
||||
<span data-msb="redScore" style="font-family:'Zen Old Mincho',serif;font-size:46px;line-height:.8;font-weight:700;color:#fff;text-shadow:0 6px 20px rgba(0,0,0,.5)">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- CENTER: round + clock --}}
|
||||
<div style="width:118px;flex:none;transform:skewX(-9deg);background:rgba(9,9,11,.9);backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;border-bottom:3px solid #2c2a28">
|
||||
<div data-msb="roundLabel" style="transform:skewX(9deg);font-size:11px;letter-spacing:.3em;color:#8f8a83;text-transform:uppercase">Round 1</div>
|
||||
<div data-msb="clock" style="transform:skewX(9deg);font-size:34px;line-height:1;font-weight:800;color:#efe9e0;font-variant-numeric:tabular-nums">0:00</div>
|
||||
<div style="transform:skewX(9deg);display:flex;gap:4px">
|
||||
<span style="width:20px;height:3px;background:#e8534a"></span>
|
||||
<span style="width:20px;height:3px;background:#e8534a"></span>
|
||||
<span style="width:20px;height:3px;background:#3a3734"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- BLUE / AO — exact mirror --}}
|
||||
<div style="flex:1 1 0;min-width:0;transform:skewX(-9deg);overflow:hidden;background:linear-gradient(90deg, rgba(30,72,140,.9), rgba(18,44,92,.94));border-bottom:3px solid #6aa6ff">
|
||||
<div style="transform:skewX(9deg);height:100%;padding:0 16px;display:flex;align-items:center;gap:12px">
|
||||
<div style="width:60px;flex:none;display:flex;flex-direction:column;align-items:flex-start;gap:5px">
|
||||
<span data-msb="blueCorner" style="font-size:11px;letter-spacing:.24em;color:rgba(226,238,255,.7)">AO</span>
|
||||
<span data-msb="blueScore" style="font-family:'Zen Old Mincho',serif;font-size:46px;line-height:.8;font-weight:700;color:#fff;text-shadow:0 6px 20px rgba(0,0,0,.5)">0</span>
|
||||
</div>
|
||||
<div style="flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:3px;align-items:flex-end;text-align:right">
|
||||
<div style="display:flex;align-items:center;gap:10px;min-width:0">
|
||||
<span data-msb="blueName" style="flex:1 1 auto;min-width:0;font-size:25px;line-height:1;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ $msbState['blue']['name'] ?: 'BLUE' }}</span>
|
||||
<span data-msb="blueFlag" data-msb-part="flags" style="width:26px;height:17px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 4px,rgba(255,255,255,.1) 4px 8px);box-shadow:0 0 0 1px rgba(255,255,255,.3);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:7px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span data-msb="bluePenalty" data-msb-part="penalties" style="display:none;font-size:10px;letter-spacing:.18em;padding:1px 6px;background:#c8492f;color:#fff;font-weight:700;flex:none">C1</span>
|
||||
<span data-msb="blueClubName" data-msb-part="clubs" style="flex:1 1 auto;min-width:0;font-size:12px;letter-spacing:.03em;color:rgba(226,238,255,.85);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ $msbState['blue']['club'] ?? '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-msb="blueClub" data-msb-part="clubs" style="width:46px;height:46px;flex:none;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.24);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;letter-spacing:.05em;color:rgba(255,255,255,.65);text-align:center;line-height:1.25">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- POINT TIMELINE — one segment per point event, red/blue, #2a2724 when empty --}}
|
||||
<div data-msb="timeline" data-msb-part="timeline" style="position:absolute;left:26px;right:26px;bottom:48px;height:5px;display:flex;gap:2px;z-index:2;pointer-events:none">
|
||||
@for ($i = 0; $i < 14; $i++)
|
||||
<div style="flex:1;background:#2a2724"></div>
|
||||
@endfor
|
||||
</div>
|
||||
|
||||
</div>{{-- /.msb-scale --}}
|
||||
</div>
|
||||
@ -0,0 +1,386 @@
|
||||
{{--
|
||||
Scoreboard runtime — one IIFE, deferred to DOMContentLoaded.
|
||||
|
||||
Consumes the JSON blob `MSB_STATE` (produced by index.blade.php) and:
|
||||
• updates score/round/clock/feed/timeline every time the video's
|
||||
currentTime changes (memoised so the ticker animation only replays
|
||||
when the visible entries change);
|
||||
• injects a "Scoreboard" section into #ytpSettingsPanel with a toggle
|
||||
row for each part;
|
||||
• persists toggle state in localStorage['msb_prefs'];
|
||||
• drives the VS intro card (first play of a session, skippable).
|
||||
--}}
|
||||
<script>
|
||||
(function () {
|
||||
const MSB_STATE = @json($msbState);
|
||||
const HAS_VS = @json($hasMatchMeta);
|
||||
const VIDEO_ID = @json($video->id);
|
||||
const LS_KEY = 'msb_prefs';
|
||||
const SS_VS_KEY = 'msb_vs_shown_v' + VIDEO_ID;
|
||||
|
||||
const RUN = function () {
|
||||
const root = document.getElementById('msbRoot');
|
||||
if (!root) return;
|
||||
const $ = (name) => document.querySelector('[data-msb="' + name + '"]');
|
||||
const player = () => document.getElementById('videoPlayer');
|
||||
|
||||
// ── Preferences (per-user, persisted) ─────────────────────────────
|
||||
const APP_DEFAULTS = { scorebar: true, feed: true, info: true, timeline: true, clubs: true, flags: true, penalties: false };
|
||||
const VIDEO_DEFAULTS = MSB_STATE.defaults || {};
|
||||
const DEFAULTS = Object.assign({}, APP_DEFAULTS, VIDEO_DEFAULTS);
|
||||
|
||||
let prefs;
|
||||
try { prefs = Object.assign({}, DEFAULTS, JSON.parse(localStorage.getItem(LS_KEY) || '{}')); }
|
||||
catch (e) { prefs = { ...DEFAULTS }; }
|
||||
|
||||
const savePrefs = () => { try { localStorage.setItem(LS_KEY, JSON.stringify(prefs)); } catch (e) {} };
|
||||
const applyPrefsClasses = () => {
|
||||
for (const key of Object.keys(APP_DEFAULTS)) {
|
||||
document.body.classList.toggle('msb-off-' + key, !prefs[key]);
|
||||
}
|
||||
// Signal to the scrim which parts are visible
|
||||
root.classList.toggle('msb-has-scorebar', !!prefs.scorebar);
|
||||
root.classList.toggle('msb-has-feed', !!prefs.feed);
|
||||
root.classList.toggle('msb-has-info', !!prefs.info);
|
||||
root.classList.toggle('msb-has-timeline', !!prefs.timeline);
|
||||
};
|
||||
|
||||
// ── Corner-label localisation ─────────────────────────────────────
|
||||
const CORNER_LABELS = {
|
||||
redblue: { red: 'Red', blue: 'Blue', disc: '' },
|
||||
karate: { red: 'Aka', blue: 'Ao', disc: 'Karate Kumite' },
|
||||
taekwondo: { red: 'Hong', blue: 'Chung', disc: 'Taekwondo Kyorugi' },
|
||||
};
|
||||
function labelSet() {
|
||||
const sport = (MSB_STATE.sport || 'karate').toLowerCase();
|
||||
if (sport === 'taekwondo' || sport === 'taekwondo_kyorugi') return CORNER_LABELS.taekwondo;
|
||||
if (sport === 'karate' || sport === 'karate_kumite') return CORNER_LABELS.karate;
|
||||
return CORNER_LABELS.redblue;
|
||||
}
|
||||
function pointLabel(p) {
|
||||
const sport = (MSB_STATE.sport || '').toLowerCase();
|
||||
if (sport.startsWith('taekwondo')) {
|
||||
if (p.pts >= 4) return 'Turning body';
|
||||
if (p.pts === 3) return 'Head kick';
|
||||
if (p.pts === 2) return 'Body kick';
|
||||
if (p.pts === 1) return 'Punch';
|
||||
}
|
||||
if (p.pts === 3) return 'Ippon';
|
||||
if (p.pts === 2) return 'Waza-ari';
|
||||
if (p.pts === 1) return 'Yuko';
|
||||
return p.action || 'Point';
|
||||
}
|
||||
const colorFor = (side) => side === 'red' ? '#ff6a5e' : '#6aa6ff';
|
||||
|
||||
// ── Fixed-value paints (only once per load) ───────────────────────
|
||||
function paintFixed() {
|
||||
const labels = labelSet();
|
||||
const setText = (name, val, hideIfEmpty = true) => {
|
||||
const el = $(name); if (!el) return;
|
||||
el.textContent = val || '';
|
||||
if (hideIfEmpty) el.toggleAttribute('data-msb-empty', !val);
|
||||
};
|
||||
// Replace the placeholder "CLUB LOGO" text with a real <img>
|
||||
// (or leave the placeholder if no image). Never touches inline styles.
|
||||
const setImage = (name, path) => {
|
||||
const el = $(name); if (!el) return;
|
||||
if (!path) return;
|
||||
el.textContent = '';
|
||||
el.style.background = 'rgba(0,0,0,.32)';
|
||||
const img = document.createElement('img');
|
||||
img.src = path; img.alt = '';
|
||||
img.style.cssText = 'width:100%;height:100%;object-fit:contain;display:block';
|
||||
el.appendChild(img);
|
||||
};
|
||||
// Swap the placeholder "FLAG" box for a real flag image by pointing
|
||||
// its background-image at the self-hosted flag-icons SVG.
|
||||
const setFlag = (name, iso2) => {
|
||||
const el = $(name); if (!el) return;
|
||||
const code = (iso2 || '').toLowerCase();
|
||||
if (!/^[a-z]{2}$/.test(code) || code === 'xx') return;
|
||||
el.textContent = '';
|
||||
el.style.background = '#000';
|
||||
el.style.backgroundImage = 'url({{ asset('vendor/flag-icons/flags/4x3') }}/' + code + '.svg)';
|
||||
el.style.backgroundSize = 'cover';
|
||||
el.style.backgroundPosition = 'center';
|
||||
};
|
||||
|
||||
// Identity — default to karate discipline label when sport unknown
|
||||
setText('disc', labels.disc || MSB_STATE.discipline || '');
|
||||
setText('sub', MSB_STATE.subtitle || '');
|
||||
// Hide the whole info block (rule + text) when both rows are empty
|
||||
const infoBlock = document.querySelector('[data-msb="info"]');
|
||||
if (infoBlock) {
|
||||
const empty = !$('disc').textContent && !$('sub').textContent;
|
||||
infoBlock.style.display = empty ? 'none' : '';
|
||||
}
|
||||
|
||||
// Athletes — brief §2: "no athlete name → show the corner label
|
||||
// ('RED' / 'BLUE') in the name slot" (always uppercase, not the
|
||||
// sport-flavoured AKA/AO which would collide with the corner label
|
||||
// that already sits above the numeral).
|
||||
setText('redName', MSB_STATE.red.name || 'RED');
|
||||
setText('blueName', MSB_STATE.blue.name || 'BLUE');
|
||||
setText('redClubName', MSB_STATE.red.club || '');
|
||||
setText('blueClubName', MSB_STATE.blue.club || '');
|
||||
setText('redCorner', labels.red.toUpperCase(), false);
|
||||
setText('blueCorner', labels.blue.toUpperCase(), false);
|
||||
|
||||
setFlag('redFlag', MSB_STATE.red.flag);
|
||||
setFlag('blueFlag', MSB_STATE.blue.flag);
|
||||
setImage('redClub', MSB_STATE.red.club_logo);
|
||||
setImage('blueClub', MSB_STATE.blue.club_logo);
|
||||
|
||||
// ── VS card image slots (photo, crest, flag) ────────────────
|
||||
// If data exists, swap the placeholder for a real image.
|
||||
// If no data, hide the placeholder box entirely (never leave
|
||||
// "FIGHTER PHOTO" / "CLUB LOGO" / "FLAG" text visible in prod).
|
||||
const VS = (MSB_STATE.vs || {});
|
||||
const setVsImg = (name, path, hideWhenEmpty = true) => {
|
||||
const el = $(name); if (!el) return;
|
||||
if (path) {
|
||||
el.textContent = '';
|
||||
el.style.background = '#0d0d10';
|
||||
const img = document.createElement('img');
|
||||
img.src = path; img.alt = '';
|
||||
img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block';
|
||||
el.appendChild(img);
|
||||
} else if (hideWhenEmpty) {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
};
|
||||
setVsImg('vsRedPhoto', VS.red && VS.red.headshot);
|
||||
setVsImg('vsBluePhoto', VS.blue && VS.blue.headshot);
|
||||
setVsImg('vsRedCrest', VS.red && VS.red.club_logo);
|
||||
setVsImg('vsBlueCrest', VS.blue && VS.blue.club_logo);
|
||||
setFlag('vsRedFlag', VS.red && VS.red.flag);
|
||||
setFlag('vsBlueFlag', VS.blue && VS.blue.flag);
|
||||
}
|
||||
|
||||
// ── Playback-time-driven render (memoised) ────────────────────────
|
||||
function currentRound(t) {
|
||||
const rs = MSB_STATE.rounds || [];
|
||||
if (!rs.length) return { n: 1, name: '', start: 0 };
|
||||
let cur = rs[0];
|
||||
for (const r of rs) { if (t >= (r.start || 0)) cur = r; }
|
||||
return cur;
|
||||
}
|
||||
function pointsUpTo(t) {
|
||||
return (MSB_STATE.points || []).filter(p => p.t <= t + 0.001);
|
||||
}
|
||||
function fmtClock(sec) {
|
||||
sec = Math.max(0, Math.floor(sec || 0));
|
||||
return Math.floor(sec / 60) + ':' + String(sec % 60).padStart(2, '0');
|
||||
}
|
||||
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
let lastRound = '', lastClock = '', lastScore = '', lastFeed = '', lastTl = '';
|
||||
function updateForTime(t) {
|
||||
const round = currentRound(t);
|
||||
const totalRounds = Math.max(1, (MSB_STATE.rounds || []).length);
|
||||
const sport = (MSB_STATE.sport || '').toLowerCase();
|
||||
|
||||
// Round label
|
||||
let roundText = round.name || '';
|
||||
if (sport.startsWith('taekwondo')) roundText = 'Round ' + round.n + ' / ' + totalRounds;
|
||||
else if (sport.startsWith('karate')) roundText = roundText || 'Kumite';
|
||||
if (roundText !== lastRound) { $('roundLabel').textContent = roundText; lastRound = roundText; }
|
||||
|
||||
// Clock = time since current round's start (or since t=0 if no round starts)
|
||||
const inRound = Math.max(0, t - (round.start || 0));
|
||||
const clockStr = fmtClock(inRound);
|
||||
if (clockStr !== lastClock) { $('clock').textContent = clockStr; lastClock = clockStr; }
|
||||
|
||||
const seen = pointsUpTo(t);
|
||||
const last = seen[seen.length - 1];
|
||||
const red = last ? last.sr : 0;
|
||||
const blue = last ? last.sb : 0;
|
||||
const sSig = red + '|' + blue;
|
||||
if (sSig !== lastScore) {
|
||||
$('redScore').textContent = red;
|
||||
$('blueScore').textContent = blue;
|
||||
lastScore = sSig;
|
||||
}
|
||||
|
||||
// Timeline segments across the duration — writes into the design's
|
||||
// verbatim inline-styled container (14 pre-rendered <div>s).
|
||||
const SEG_COUNT = Math.max(14, (MSB_STATE.points || []).length);
|
||||
const tlSig = SEG_COUNT + '|' + seen.map(p => p.side[0]).join('');
|
||||
if (tlSig !== lastTl) {
|
||||
const tl = $('timeline');
|
||||
if (tl) {
|
||||
let html = '';
|
||||
for (let i = 0; i < SEG_COUNT; i++) {
|
||||
const p = seen[i];
|
||||
const color = p ? colorFor(p.side) : '#2a2724';
|
||||
html += '<div style="flex:1;background:' + color + '"></div>';
|
||||
}
|
||||
tl.innerHTML = html;
|
||||
}
|
||||
lastTl = tlSig;
|
||||
}
|
||||
|
||||
// Ticker: last 4, newest first
|
||||
const last4 = seen.slice(-4).reverse();
|
||||
const fSig = last4.map(p => p.t + ':' + p.side + ':' + p.pts).join(',');
|
||||
if (fSig !== lastFeed) {
|
||||
const feed = $('feed');
|
||||
if (feed) {
|
||||
feed.innerHTML = last4.map(p => {
|
||||
const col = colorFor(p.side);
|
||||
return '<div class="entry msb-new" style="border-color:' + col + '">'
|
||||
+ '<span class="ts">' + fmtClock(p.t) + '</span>'
|
||||
+ '<span class="name">' + esc(pointLabel(p)) + '</span>'
|
||||
+ '<span class="pts" style="color:' + col + '">+' + p.pts + '</span>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
lastFeed = fSig;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gear-menu injection ───────────────────────────────────────────
|
||||
// Custom row markup (per design): compact label + 26×14 track + 10 px
|
||||
// bone knob. Nothing on the platform side changes.
|
||||
function injectGearRows() {
|
||||
const panel = document.getElementById('ytpSettingsPanel');
|
||||
if (!panel) return false;
|
||||
if (panel.dataset.msbInjected) return true;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'msb-gear-header';
|
||||
header.textContent = 'Scoreboard';
|
||||
panel.appendChild(header);
|
||||
|
||||
const ROWS = [
|
||||
['scorebar', 'Score bar'],
|
||||
['feed', 'Live scoring feed'],
|
||||
['info', 'Match info'],
|
||||
['timeline', 'Point timeline'],
|
||||
['clubs', 'Club logos'],
|
||||
['flags', 'Country flags'],
|
||||
['penalties', 'Penalties'],
|
||||
];
|
||||
for (const [key, label] of ROWS) {
|
||||
const row = document.createElement('button');
|
||||
row.type = 'button';
|
||||
row.className = 'msb-gear-row' + (prefs[key] ? ' on' : '');
|
||||
row.dataset.msbToggle = key;
|
||||
row.innerHTML =
|
||||
'<span>' + label + '</span>' +
|
||||
'<span class="msb-gear-track"><span class="msb-gear-knob"></span></span>';
|
||||
row.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
prefs[key] = !prefs[key];
|
||||
row.classList.toggle('on', prefs[key]);
|
||||
savePrefs(); applyPrefsClasses();
|
||||
});
|
||||
panel.appendChild(row);
|
||||
}
|
||||
panel.dataset.msbInjected = '1';
|
||||
|
||||
// Design: gear icon turns red while the menu is open.
|
||||
const sync = () => document.body.classList.toggle('msb-menu-open', panel.classList.contains('open'));
|
||||
new MutationObserver(sync).observe(panel, { attributes: true, attributeFilter: ['class'] });
|
||||
sync();
|
||||
return true;
|
||||
}
|
||||
function tryInject(attemptsLeft) {
|
||||
if (injectGearRows()) return;
|
||||
if (attemptsLeft > 0) setTimeout(() => tryInject(attemptsLeft - 1), 200);
|
||||
}
|
||||
tryInject(30);
|
||||
|
||||
// ── VS intro lifecycle ────────────────────────────────────────────
|
||||
function initVsIntro() {
|
||||
if (!HAS_VS) return;
|
||||
const vs = document.getElementById('msbVs');
|
||||
const v = player();
|
||||
if (!vs || !v) return;
|
||||
|
||||
let alreadyShown = false;
|
||||
try { alreadyShown = sessionStorage.getItem(SS_VS_KEY) === '1'; } catch (e) {}
|
||||
if (alreadyShown) return;
|
||||
|
||||
let shown = false;
|
||||
const show = () => {
|
||||
if (shown) return;
|
||||
shown = true;
|
||||
try { sessionStorage.setItem(SS_VS_KEY, '1'); } catch (e) {}
|
||||
vs.hidden = false;
|
||||
vs.dataset.visible = 'true';
|
||||
try { v.pause(); } catch (e) {}
|
||||
|
||||
const dismiss = (playToo = true) => {
|
||||
if (!vs || vs.classList.contains('msb-vs-out')) return;
|
||||
vs.classList.add('msb-vs-out');
|
||||
if (playToo) { try { v.play(); } catch (e) {} }
|
||||
setTimeout(() => { if (vs && vs.parentNode) vs.parentNode.removeChild(vs); }, 520);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
vs.addEventListener('click', () => dismiss(true));
|
||||
vs.addEventListener('touchstart', () => dismiss(true), { passive: true });
|
||||
const onKey = () => dismiss(true);
|
||||
document.addEventListener('keydown', onKey);
|
||||
setTimeout(() => dismiss(true), 2000);
|
||||
};
|
||||
|
||||
// Trigger on the first play attempt (autoplay-friendly).
|
||||
const onPlay = () => { v.removeEventListener('play', onPlay); v.removeEventListener('playing', onPlay); show(); };
|
||||
v.addEventListener('play', onPlay, { once: true });
|
||||
v.addEventListener('playing', onPlay, { once: true });
|
||||
// If autoplay is already firing when we arrive:
|
||||
if (!v.paused && v.currentTime < 0.5) show();
|
||||
}
|
||||
|
||||
// ── Overlay scaling to a fixed 1150 px design canvas ──────────────
|
||||
// Keeps the score bar, ticker, and identity at their pixel-perfect
|
||||
// proportions no matter how wide the actual player is.
|
||||
function initScale() {
|
||||
const wrap = document.getElementById('ytpWrap');
|
||||
const layer = document.getElementById('msbScale');
|
||||
if (!wrap || !layer) return;
|
||||
const DESIGN_W = 1150;
|
||||
const update = () => {
|
||||
const w = wrap.clientWidth || 1;
|
||||
const h = wrap.clientHeight || 1;
|
||||
const s = w / DESIGN_W;
|
||||
layer.style.transform = 'scale(' + s + ')';
|
||||
layer.style.height = (h / s) + 'px';
|
||||
// Toggle the "small player" state once we're below the design
|
||||
// brief's ~700 px breakpoint (in real pixels).
|
||||
document.body.classList.toggle('msb-small', w < 700);
|
||||
};
|
||||
update();
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
new ResizeObserver(update).observe(wrap);
|
||||
}
|
||||
window.addEventListener('resize', update);
|
||||
}
|
||||
|
||||
// ── Wire everything ───────────────────────────────────────────────
|
||||
applyPrefsClasses();
|
||||
paintFixed();
|
||||
initScale();
|
||||
|
||||
function attachPlayer() {
|
||||
const v = player();
|
||||
if (!v) { setTimeout(attachPlayer, 250); return; }
|
||||
const tick = () => updateForTime(v.currentTime || 0);
|
||||
v.addEventListener('timeupdate', tick);
|
||||
v.addEventListener('seeked', tick);
|
||||
v.addEventListener('loadedmetadata', tick);
|
||||
v.addEventListener('ratechange', tick);
|
||||
tick();
|
||||
initVsIntro();
|
||||
}
|
||||
attachPlayer();
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', RUN);
|
||||
} else {
|
||||
setTimeout(RUN, 0);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@ -0,0 +1,176 @@
|
||||
{{--
|
||||
Karate/Taekwondo match scoreboard overlay — all CSS in one place.
|
||||
Class prefix: msb- (match scoreboard). Nothing else in the codebase uses it.
|
||||
Everything lives inside #msbRoot which is `position:absolute; inset:0; pointer-events:none`
|
||||
and is NOT z-indexed on purpose, so #ytpControls (emitted after the slot) paints on top.
|
||||
--}}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700;800&family=Zen+Old+Mincho:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ────────────────────────────── keyframes ────────────────────────────── */
|
||||
@keyframes msbRiseIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes msbPulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
||||
@keyframes msbFadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes msbFadeOut { from { opacity: 1; } to { opacity: 0; } }
|
||||
@keyframes msbDriftA { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
||||
50% { transform: translate3d(60px, -28px, 0) scale(1.14); } }
|
||||
@keyframes msbDriftB { 0%, 100% { transform: translate3d(0, 0, 0) scale(1.08); }
|
||||
50% { transform: translate3d(-54px, 26px, 0) scale(1); } }
|
||||
@keyframes msbSweep { 0% { transform: translateX(-60%) skewX(-14deg); opacity: 0; }
|
||||
25% { opacity: .5; }
|
||||
60% { opacity: 0; }
|
||||
100% { transform: translateX(160%) skewX(-14deg); opacity: 0; } }
|
||||
|
||||
/* ────────────────────────────── root layer ─────────────────────────────
|
||||
No z-index on the root: source order inside #ytpWrap places the video
|
||||
controls AFTER the overlay slot, so they paint on top and stay clickable.
|
||||
------------------------------------------------------------------------ */
|
||||
.msb-root {
|
||||
position: absolute; inset: 0;
|
||||
pointer-events: none;
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
color: #efe9e0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
}
|
||||
.msb-root * { box-sizing: border-box; }
|
||||
|
||||
/*
|
||||
* Fixed 1150 px design canvas. `msb-scale` is positioned at (0, 0), sized
|
||||
* to 1150 px wide, and JS-scaled by `transform: scale(playerWidth / 1150)`
|
||||
* so every child element (positioned in that 1150 px coord space) shrinks
|
||||
* or grows proportionally with the player. Height is set at runtime.
|
||||
*/
|
||||
.msb-scale {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 1150px;
|
||||
transform-origin: top left;
|
||||
/* transform + height set by JS */
|
||||
}
|
||||
|
||||
/* Scrims for legibility */
|
||||
.msb-scrim {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background:
|
||||
linear-gradient(to top, rgba(6,6,8,.9) 0%, rgba(6,6,8,.45) 15%, rgba(6,6,8,0) 30%),
|
||||
linear-gradient(to bottom, rgba(6,6,8,.85) 0%, rgba(6,6,8,.4) 14%, rgba(6,6,8,0) 30%);
|
||||
opacity: 0;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
/* Only show the scrims when there is at least one overlay part visible */
|
||||
.msb-has-scorebar .msb-scrim,
|
||||
.msb-has-feed .msb-scrim,
|
||||
.msb-has-info .msb-scrim,
|
||||
.msb-has-timeline .msb-scrim { opacity: 1; }
|
||||
|
||||
/* ────────────────────────────── match info (top-left) ────────────────── */
|
||||
.msb-info { position: absolute; top: 22px; left: 26px; display: flex; align-items: stretch; gap: 12px; }
|
||||
.msb-info .rule { width: 4px; background: linear-gradient(#e8534a, #7c1d18); }
|
||||
.msb-info .txt { display: flex; flex-direction: column; gap: 2px; }
|
||||
.msb-info .disc {
|
||||
font-family: 'Zen Old Mincho', serif; font-size: 15px; letter-spacing: .34em;
|
||||
color: #efe9e0; text-transform: uppercase;
|
||||
}
|
||||
.msb-info .sub {
|
||||
font-size: 13px; letter-spacing: .28em; color: #cfc9c1;
|
||||
text-transform: uppercase; text-shadow: 0 1px 6px rgba(0,0,0,.9);
|
||||
}
|
||||
/* Auto-hide row when its text is empty (server empties data-msb-empty) */
|
||||
.msb-info [data-msb-empty] { display: none; }
|
||||
|
||||
/* ────────────────────────────── live scoring ticker (top-right) ──────── */
|
||||
/* top:56px clears the existing Highlights toggle chip that sits at top:8/right:8 (h=24) */
|
||||
.msb-feed { position: absolute; top: 56px; right: 24px; width: 262px; display: flex; flex-direction: column; gap: 7px; }
|
||||
.msb-feed .hdr {
|
||||
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
|
||||
font-size: 12px; letter-spacing: .3em; color: #d5cfc7; text-transform: uppercase;
|
||||
text-shadow: 0 1px 6px rgba(0,0,0,.9);
|
||||
}
|
||||
.msb-feed .hdr .dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: #e8534a; animation: msbPulse 1.6s infinite;
|
||||
}
|
||||
.msb-feed .list { display: flex; flex-direction: column; gap: 7px; }
|
||||
.msb-feed .entry {
|
||||
display: flex; align-items: center; justify-content: flex-end; gap: 10px;
|
||||
padding: 7px 10px;
|
||||
background: rgba(10,10,12,.62); backdrop-filter: blur(6px);
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
.msb-feed .entry.msb-new { animation: msbRiseIn .45s ease both; }
|
||||
.msb-feed .entry .ts { font-size: 12px; line-height: 1; letter-spacing: .16em; color: #79736c; }
|
||||
.msb-feed .entry .name { font-size: 17px; line-height: 1; letter-spacing: .12em; font-weight: 700; color: #efe9e0; text-transform: uppercase; }
|
||||
.msb-feed .entry .pts { font-family: 'Zen Old Mincho', serif; font-size: 19px; line-height: 1; font-weight: 700; }
|
||||
|
||||
/*
|
||||
* Score bar + timeline visuals live entirely as inline styles on the verbatim
|
||||
* markup from drafts/score-bar.html. Nothing here targets those elements
|
||||
* except the visibility toggles below (via data-msb-part), so the design's
|
||||
* exact pixel measurements are never overridden.
|
||||
*/
|
||||
|
||||
/* ────────────────────────────── visibility toggles ──────────────────── */
|
||||
/* Body-level classes hide any part with the matching data-msb-part attr
|
||||
without touching its inline styles. Master-off hides everything. */
|
||||
body.msb-off-scorebar [data-msb-part="scorebar"],
|
||||
body.msb-off-timeline [data-msb-part="timeline"],
|
||||
body.msb-off-info [data-msb-part="info"],
|
||||
body.msb-off-feed [data-msb-part="feed"],
|
||||
body.msb-off-clubs [data-msb-part="clubs"],
|
||||
body.msb-off-flags [data-msb-part="flags"],
|
||||
body.msb-off-penalties [data-msb-part="penalties"] { display: none !important; }
|
||||
|
||||
/* ────────────────────────────── responsive ──────────────────────────── */
|
||||
/* Below ~700 px actual player width the identity + ticker disappear
|
||||
(brief §7). The `msb-small` class is toggled by JS off ResizeObserver. */
|
||||
body.msb-small .msb-info,
|
||||
body.msb-small .msb-feed { display: none !important; }
|
||||
|
||||
/*
|
||||
* VS intro card — visuals are entirely inline styles on the verbatim markup
|
||||
* from drafts/vs-screen.html. Only the outer wrapper + fade in/out lives
|
||||
* here; keyframes (msbDriftA/B, msbSweep, msbFadeIn/Out) are up top.
|
||||
*/
|
||||
.msb-vs {
|
||||
position: absolute; inset: 0; z-index: 12;
|
||||
background: #08080a; overflow: hidden;
|
||||
pointer-events: auto;
|
||||
animation: msbFadeIn .3s ease both;
|
||||
}
|
||||
.msb-vs.msb-vs-out { animation: msbFadeOut .5s ease both; }
|
||||
|
||||
/* ────────────────────────────── injected gear rows ──────────────────── */
|
||||
/* Matches the design prototype's custom look (26×14 track, 10 px bone knob),
|
||||
scoped so it never touches the platform's other rows. */
|
||||
.msb-gear-header {
|
||||
padding: 10px 14px;
|
||||
font-size: 11px; letter-spacing: .3em; color: #8f8a83; text-transform: uppercase;
|
||||
border-top: 1px solid rgba(255,255,255,.1); margin-top: 4px;
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
}
|
||||
.msb-gear-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; padding: 9px 14px;
|
||||
background: transparent; border: 0; width: 100%; cursor: pointer;
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
font-size: 14px; letter-spacing: .14em; color: #efe9e0;
|
||||
text-transform: uppercase; text-align: left;
|
||||
transition: background .15s ease;
|
||||
}
|
||||
.msb-gear-row:hover { background: rgba(255,255,255,.07); }
|
||||
.msb-gear-track {
|
||||
position: relative; width: 26px; height: 14px; flex: none;
|
||||
background: #3a3734; transition: background .18s ease;
|
||||
}
|
||||
.msb-gear-knob {
|
||||
position: absolute; top: 2px; left: 2px;
|
||||
width: 10px; height: 10px; background: #efe9e0;
|
||||
transition: left .18s ease;
|
||||
}
|
||||
.msb-gear-row.on .msb-gear-track { background: #e8534a; }
|
||||
.msb-gear-row.on .msb-gear-knob { left: 14px; }
|
||||
/* Turn the gear icon red while the settings panel is open — matches design */
|
||||
body.msb-menu-open #ytpSettingsBtn svg { fill: #e8534a; }
|
||||
</style>
|
||||
@ -0,0 +1,106 @@
|
||||
{{--
|
||||
VS SCREEN — verbatim markup from drafts/vs-screen.html. Only {{ }} holes
|
||||
filled with Blade data. `data-msb` hooks on the photo, crest, and flag
|
||||
boxes let the script swap in real images at runtime. Missing fields are
|
||||
hidden by inline display:none (never substituted with placeholder text).
|
||||
--}}
|
||||
@php
|
||||
$rc = strtoupper(trim($vs['red']['flag'] ?? ''));
|
||||
$bc = strtoupper(trim($vs['blue']['flag'] ?? ''));
|
||||
|
||||
$eventName = $vs['event'] ?? '';
|
||||
$category = $vs['category'] ?? '';
|
||||
$division = $vs['division'] ?? '';
|
||||
$roundName = $vs['round'] ?? '';
|
||||
$redName = $vs['red']['name'] ?? '';
|
||||
$redCountry = $vs['red']['country_name'] ?? '';
|
||||
$redClub = $vs['red']['club'] ?? '';
|
||||
$blueName = $vs['blue']['name'] ?? '';
|
||||
$blueCountry= $vs['blue']['country_name'] ?? '';
|
||||
$blueClub = $vs['blue']['club'] ?? '';
|
||||
$weightClass= $vs['weight_category'] ?? '';
|
||||
$format = $vs['format_line'] ?? '';
|
||||
$bout = !empty($vs['match_number']) ? ('Bout '.$vs['match_number']) : '';
|
||||
$tatami = $vs['court'] ?? '';
|
||||
$dateVenue = trim(($vs['match_date'] ?? '').' '.($vs['venue_name'] ? '· '.$vs['venue_name'] : ''));
|
||||
|
||||
$hide = fn($v) => (is_string($v) && trim($v) === '') ? 'display:none;' : '';
|
||||
@endphp
|
||||
|
||||
<div class="msb-vs" id="msbVs" hidden data-visible="false">
|
||||
<div style="position:relative;width:100%;height:100%;overflow:hidden;background:#08080a">
|
||||
|
||||
<div style="position:absolute;inset:-12%;width:70%;left:-6%;background:radial-gradient(closest-side, rgba(168,42,34,.62), rgba(8,8,10,0) 72%);animation:msbDriftA 19s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;inset:-12%;width:70%;left:36%;background:radial-gradient(closest-side, rgba(28,64,136,.62), rgba(8,8,10,0) 72%);animation:msbDriftB 23s ease-in-out infinite"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:26%;background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.09), rgba(255,255,255,0));animation:msbSweep 11s linear infinite"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;left:0;width:54%;clip-path:polygon(0 0, 100% 0, 78% 100%, 0 100%);background:linear-gradient(120deg, rgba(122,26,22,.5), rgba(8,8,10,0) 76%)"></div>
|
||||
<div style="position:absolute;top:0;bottom:0;right:0;width:54%;clip-path:polygon(22% 0, 100% 0, 100% 100%, 0 100%);background:linear-gradient(300deg, rgba(24,52,110,.5), rgba(8,8,10,0) 76%)"></div>
|
||||
|
||||
{{-- event header --}}
|
||||
<div style="position:absolute;top:34px;left:0;right:0;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-family:'Zen Old Mincho',serif;font-size:24px;letter-spacing:.44em;color:#efe9e0;text-transform:uppercase;text-indent:.44em;{{ $hide($eventName) }}">{{ $eventName }}</div>
|
||||
<div style="display:flex;align-items:center;gap:14px;font-size:12px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase">
|
||||
<span style="{{ $hide($category) }}">{{ $category }}</span>
|
||||
<span style="width:4px;height:4px;background:#e8534a;{{ $hide($category).$hide($division) }}"></span>
|
||||
<span style="{{ $hide($division) }}">{{ $division }}</span>
|
||||
<span style="width:4px;height:4px;background:#6aa6ff;{{ $hide($division).$hide($roundName) }}"></span>
|
||||
<span style="{{ $hide($roundName) }}">{{ $roundName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;inset:96px 0 54px;display:grid;grid-template-columns:1fr 200px 1fr;align-items:start">
|
||||
|
||||
{{-- RED fighter --}}
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
||||
<div style="position:relative;width:228px;height:304px">
|
||||
<div data-msb="vsRedPhoto" style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div data-msb="vsRedCrest" style="position:absolute;bottom:-26px;right:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #e8534a;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">{{ $redName ?: 'RED' }}</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;{{ $hide($redCountry).($rc ? '' : $hide('')) }}">
|
||||
<span data-msb="vsRedFlag" style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
<span style="font-size:14px;letter-spacing:.3em;color:#d3b3ad;text-transform:uppercase;{{ $hide($redCountry) }}">{{ $redCountry }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-top:2px;{{ $hide($redClub) }}">
|
||||
<span style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">{{ $redClub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- centre --}}
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;padding-top:96px">
|
||||
<span style="font-family:'Zen Old Mincho',serif;font-size:88px;line-height:.8;font-weight:700;color:#efe9e0;text-shadow:0 8px 34px rgba(0,0,0,.75)">VS</span>
|
||||
<span style="padding:7px 16px;border:1px solid rgba(255,255,255,.3);font-size:14px;letter-spacing:.32em;color:#efe9e0;text-transform:uppercase;{{ $hide($weightClass) }}">{{ $weightClass }}</span>
|
||||
<span style="font-size:11px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase;{{ $hide($format) }}">{{ $format }}</span>
|
||||
</div>
|
||||
|
||||
{{-- BLUE fighter — mirror --}}
|
||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
||||
<div style="position:relative;width:228px;height:304px">
|
||||
<div data-msb="vsBluePhoto" style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
||||
<div data-msb="vsBlueCrest" style="position:absolute;bottom:-26px;left:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
||||
</div>
|
||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #6aa6ff;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">{{ $blueName ?: 'BLUE' }}</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;{{ $hide($blueCountry) }}">
|
||||
<span style="font-size:14px;letter-spacing:.3em;color:#a8b6cf;text-transform:uppercase;{{ $hide($blueCountry) }}">{{ $blueCountry }}</span>
|
||||
<span data-msb="vsBlueFlag" style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-top:2px;{{ $hide($blueClub) }}">
|
||||
<span style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">{{ $blueClub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- bout line --}}
|
||||
<div style="position:absolute;bottom:26px;left:0;right:0;display:flex;align-items:center;justify-content:center;gap:16px;font-size:11px;letter-spacing:.32em;color:#8f8a83;text-transform:uppercase">
|
||||
<span style="{{ $hide($bout) }}">{{ $bout }}</span>
|
||||
<span style="width:4px;height:4px;background:#e8534a;{{ $hide($bout).$hide($tatami) }}"></span>
|
||||
<span style="{{ $hide($tatami) }}">{{ $tatami }}</span>
|
||||
<span style="width:4px;height:4px;background:#6aa6ff;{{ $hide($tatami).$hide($dateVenue) }}"></span>
|
||||
<span style="{{ $hide($dateVenue) }}">{{ $dateVenue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -150,7 +150,8 @@
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
.replay-badge {
|
||||
position: absolute;
|
||||
top: 2.5cqi; left: 2.5cqi;
|
||||
/* Pushed down so it clears the scoreboard's KARATE KUMITE identity block at top-left */
|
||||
top: 8cqi; left: 2.5cqi;
|
||||
z-index: 15;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: clamp(4px, 0.7cqi, 8px) clamp(7px, 1.1cqi, 12px);
|
||||
@ -2229,6 +2230,8 @@
|
||||
<span class="replay-badge-word">REPLAY</span>
|
||||
<span class="replay-badge-speed" id="replayBadgeSpeed">×1</span>
|
||||
</div>
|
||||
{{-- Karate/Taekwondo scoreboard overlay + VS intro + gear-menu toggles --}}
|
||||
@include('videos.partials.match.scoreboard.index')
|
||||
</x-slot:overlay>
|
||||
</x-video-player>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user