Prepare 2XKO overlay for local Shapiro fonts without binaries

This commit is contained in:
Pandipipas
2026-02-20 20:19:11 +01:00
parent ff2786a304
commit ca467f37f9
7 changed files with 879 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# 2XKO Overlay local font setup
This overlay expects the same Shapiro fonts used by FGCaster's 2XKO template.
Place these files locally in `src/graphics/scoreboard-2xko/fonts/` (do not commit):
- `Shapiro 45 Welter Text.otf`
- `Shapiro 55 Middle Extd.otf`
- `Shapiro-115-Plus-Extd.otf`
The CSS is already wired to load them via `@font-face` from that path.
+8
View File
@@ -0,0 +1,8 @@
import { createHead } from '@unhead/vue/client';
import { createApp } from 'vue';
import App from './main.vue';
const app = createApp(App);
const head = createHead();
app.use(head);
app.mount('#app');
+380
View File
@@ -0,0 +1,380 @@
<script setup lang="ts">
import { useHead } from '@unhead/vue';
import { computed, ref, watch } from 'vue';
import { commentaryReplicant, playersReplicant, scoreboardReplicant } from '../../browser_shared/replicants';
import { resolveCountryCode } from '../../shared/countries';
import { getCharactersByGame } from '../../shared/fighting-characters';
import type { Schemas } from '../../types';
useHead({ title: 'Scoreboard 2XKO' });
const defaultScoreboard: Schemas.Scoreboard = {
leftPlayerId: '', rightPlayerId: '', leftNameOverride: '', rightNameOverride: '', leftTeamOverride: '', rightTeamOverride: '',
leftCountryOverride: '', rightCountryOverride: '', leftCharacter: '', rightCharacter: '', leftScore: 0, rightScore: 0, round: '', game: '',
};
const defaultCommentary: Schemas.Commentary = {
leftCommentator: '', leftCommentatorTwitter: '', rightCommentator: '', rightCommentatorTwitter: '',
};
const players = computed<Schemas.Players>(() => playersReplicant?.data ?? {});
const scoreboard = computed<Schemas.Scoreboard>(() => scoreboardReplicant?.data ?? defaultScoreboard);
const commentary = computed<Schemas.Commentary>(() => commentaryReplicant?.data ?? defaultCommentary);
const leftName = computed(() => scoreboard.value.leftNameOverride || players.value[scoreboard.value.leftPlayerId]?.gamertag || 'PLAYER 1');
const rightName = computed(() => scoreboard.value.rightNameOverride || players.value[scoreboard.value.rightPlayerId]?.gamertag || 'PLAYER 2');
const leftTeam = computed(() => scoreboard.value.leftTeamOverride);
const rightTeam = computed(() => scoreboard.value.rightTeamOverride);
const charMap = new Map(getCharactersByGame('2XKO').map((char) => [char.value, char.image]));
const leftCharacterImage = computed(() => charMap.get(scoreboard.value.leftCharacter) ?? '');
const rightCharacterImage = computed(() => charMap.get(scoreboard.value.rightCharacter) ?? '');
const flagModules = import.meta.glob('/node_modules/flag-icons/flags/4x3/*.svg', { import: 'default', query: '?url' }) as Record<string, () => Promise<string>>;
const flagUrlCache: Record<string, string> = {};
const leftFlagUrl = ref('');
const rightFlagUrl = ref('');
const loadFlagUrl = async (country: string | undefined) => {
const code = resolveCountryCode(country)?.toLowerCase();
if (!code) return '';
if (flagUrlCache[code]) return flagUrlCache[code];
const moduleLoader = flagModules[`/node_modules/flag-icons/flags/4x3/${code}.svg`];
if (!moduleLoader) return '';
const url = await moduleLoader();
flagUrlCache[code] = url;
return url;
};
watch(() => scoreboard.value.leftCountryOverride, async (country) => { leftFlagUrl.value = await loadFlagUrl(country); }, { immediate: true });
watch(() => scoreboard.value.rightCountryOverride, async (country) => { rightFlagUrl.value = await loadFlagUrl(country); }, { immediate: true });
const roundText = computed(() => scoreboard.value.round || 'WINNERS FINALS');
const gameText = computed(() => scoreboard.value.game || '2XKO');
</script>
<template>
<div class="debug-bg-container">
<svg
style="width: 0; height: 0; position: absolute"
aria-hidden="true"
>
<defs>
<filter
id="coloredGlow_p1"
x="-50%"
y="-50%"
width="200%"
height="200%"
><feFlood
flood-color="var(--glow-color, #F70041)"
flood-opacity="var(--glow-opacity, 1)"
result="glowColor"
/><feComposite
in="glowColor"
in2="SourceAlpha"
operator="in"
result="coloredShape"
/><feGaussianBlur
in="coloredShape"
stdDeviation="2"
result="blurredGlow"
/><feMerge><feMergeNode in="blurredGlow" /><feMergeNode in="SourceGraphic" /></feMerge></filter>
<filter
id="coloredGlow_p2"
x="-50%"
y="-50%"
width="200%"
height="200%"
><feFlood
flood-color="var(--glow-color, #F70041)"
flood-opacity="var(--glow-opacity, 1)"
result="glowColor"
/><feComposite
in="glowColor"
in2="SourceAlpha"
operator="in"
result="coloredShape"
/><feGaussianBlur
in="coloredShape"
stdDeviation="2"
result="blurredGlow"
/><feMerge><feMergeNode in="blurredGlow" /><feMergeNode in="SourceGraphic" /></feMerge></filter>
<filter
id="elShadow"
x="-50%"
y="-50%"
width="200%"
height="200%"
><feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 0"
result="boostedInput"
/><feGaussianBlur stdDeviation="5" /><feComposite
operator="out"
in2="boostedInput"
/></filter>
<linearGradient
id="linear-gradient-p1-noimg"
x1="6.82"
y1="24.94"
x2="409.57"
y2="24.94"
gradientUnits="userSpaceOnUse"
><stop
offset="0.13"
stop-color="var(--gradient-stop-1, #15bcba)"
/><stop
offset="0.5"
stop-color="var(--gradient-stop-2, #19c2a4)"
/><stop
offset="0.87"
stop-color="var(--gradient-stop-3, #1ecb8b)"
/></linearGradient>
<linearGradient
id="linear-gradient-p1-full"
x1="6.57"
y1="24.93"
x2="456.57"
y2="24.93"
gradientUnits="userSpaceOnUse"
><stop
offset="0.13"
stop-color="var(--gradient-stop-1, #15bcba)"
/><stop
offset="0.5"
stop-color="var(--gradient-stop-2, #19c2a4)"
/><stop
offset="0.87"
stop-color="var(--gradient-stop-3, #1ecb8b)"
/></linearGradient>
<linearGradient
id="linear-gradient-p2-noimg"
y1="17.94"
x2="402.75"
y2="17.94"
gradientUnits="userSpaceOnUse"
><stop
offset="0.13"
stop-color="var(--gradient-stop-1, #15bcba)"
/><stop
offset="0.5"
stop-color="var(--gradient-stop-2, #19c2a4)"
/><stop
offset="0.87"
stop-color="var(--gradient-stop-3, #1ecb8b)"
/></linearGradient>
<linearGradient
id="linear-gradient-p2-full"
x1="0"
y1="17.94"
x2="450"
y2="17.94"
gradientUnits="userSpaceOnUse"
><stop
offset="0.13"
stop-color="var(--gradient-stop-1, #15bcba)"
/><stop
offset="0.5"
stop-color="var(--gradient-stop-2, #19c2a4)"
/><stop
offset="0.87"
stop-color="var(--gradient-stop-3, #1ecb8b)"
/></linearGradient>
</defs>
</svg>
<div style="display:flex;position:absolute;width:100%;justify-content:center;">
<svg
id="topRowBG"
xmlns="http://www.w3.org/2000/svg"
width="625"
height="70"
viewBox="-10 0 625 70"
class="bg-img"
style="margin-top:-1px;display:block;"
><path
id="topRowShadow"
filter="url(#elShadow)"
d="M670.1.5h579.8l12.6,36.39-180.43,0a5,5,0,0,0-4.83,3.68l-2.59,9.71a5,5,0,0,1-4.84,3.68H850.19a5,5,0,0,1-4.84-3.68l-2.59-9.71a5,5,0,0,0-4.83-3.68l-180.43,0Z"
transform="translate(-657.5)"
/><path
id="topRowBody"
d="M670.1.5h579.8l12.6,36.39-180.43,0a5,5,0,0,0-4.83,3.68l-2.59,9.71a5,5,0,0,1-4.84,3.68H850.19a5,5,0,0,1-4.84-3.68l-2.59-9.71a5,5,0,0,0-4.83-3.68l-180.43,0Z"
transform="translate(-657.5)"
/></svg>
</div>
<div style="display:flex;position:absolute;width:100%;justify-content:center;gap:582px;top:0;">
<div style="position:relative;width:100%;">
<svg
id="p1BGfull"
class="bg-img playerPlaque"
style="position:absolute;top:-7px;right:-8px;"
xmlns="http://www.w3.org/2000/svg"
width="464"
height="50"
viewBox="0 0 464 50"
><defs><clipPath id="p1-clip-path"><rect
x="410.18"
y="7"
width="47.25"
height="35.87"
/></clipPath></defs><g id="P1_full"><g id="P1_bg"><polygon
points="444.32 42.87 6.57 42.87 6.57 7 456.56 7 444.32 42.87"
style="fill:url(#linear-gradient-p1-full)"
/></g><g filter="url(#coloredGlow_p1)"><g id="P1_bg_stroke"><polygon
points="444.32 42.87 6.57 42.87 6.57 7 456.56 7 444.32 42.87"
style="fill:none;stroke-miterlimit:10"
/></g><g id="P1_diag"><line
x1="395.92"
y1="42.39"
x2="408.17"
y2="7"
style="fill:none;stroke-miterlimit:10"
/></g><g id="P1_vert"><line
x1="53.82"
y1="7"
x2="53.82"
y2="42.38"
style="fill:none;stroke-miterlimit:10"
/></g></g></g><image
class="playerImg"
:href="leftFlagUrl || leftCharacterImage"
width="46"
height="34.5"
x="410.75"
y="7.7"
preserveAspectRatio="xMidYMid meet"
clip-path="url(#p1-clip-path)"
/></svg>
</div>
<div style="position:relative;width:100%;">
<svg
id="p2BGfull"
class="bg-img playerPlaque"
style="position:absolute;top:-7px;left:-8px;"
xmlns="http://www.w3.org/2000/svg"
width="464"
height="50"
viewBox="0 0 464 50"
><defs><clipPath id="p2-clip-path"><rect
x="410.18"
y="7"
width="47.25"
height="35.87"
/></clipPath></defs><g
id="P2_full"
transform="matrix(-1 0 0 1 464 0)"
><g id="P2_bg"><polygon
points="444.32 42.87 6.57 42.87 6.57 7 456.56 7 444.32 42.87"
style="fill:url(#linear-gradient-p1-full)"
/></g><g filter="url(#coloredGlow_p2)"><g id="P2_bg_stroke"><polygon
points="444.32 42.87 6.57 42.87 6.57 7 456.56 7 444.32 42.87"
style="fill:none;stroke-miterlimit:10"
/></g><g id="P2_diag"><line
x1="395.92"
y1="42.39"
x2="408.17"
y2="7"
style="fill:none;stroke-miterlimit:10"
/></g><g id="P2_vert"><line
x1="53.82"
y1="7"
x2="53.82"
y2="42.38"
style="fill:none;stroke-miterlimit:10"
/></g></g></g><image
class="playerImg"
:href="rightFlagUrl || rightCharacterImage"
width="46"
height="34.5"
x="410.75"
y="7.7"
preserveAspectRatio="xMidYMid meet"
clip-path="url(#p2-clip-path)"
/></svg>
</div>
</div>
<div style="display:flex;position:absolute;bottom:0;width:100%;justify-content:center;">
<svg
id="castersBG"
xmlns="http://www.w3.org/2000/svg"
width="700"
height="67.83"
viewBox="0 -40 340.24 67.83"
class="bg-img"
><path
id="castersShadow"
filter="url(#elShadow)"
d="M789.88,1080h340.24l-12.4-25a5,5,0,0,0-4.49-2.79H806.77a5,5,0,0,0-4.49,2.79Z"
transform="translate(-789.88 -1052.17)"
/><path
id="castersBody"
d="M789.88,1080h340.24l-12.4-25a5,5,0,0,0-4.49-2.79H806.77a5,5,0,0,0-4.49,2.79Z"
transform="translate(-789.88 -1052.17)"
/></svg>
</div>
<div class="scoreboardContainer">
<div
id="nameBlockP1"
class="nameBlock"
>
<div class="name">
{{ leftTeam }} <span v-if="leftTeam">|</span> {{ leftName }}
</div>
</div>
<div class="scoreBlock">
<div class="score">
{{ scoreboard.leftScore }}
</div>
</div>
<div class="centerPanel">
<div class="infoRow">
<div
id="stageText"
class="infoBar"
>
{{ gameText }}
</div><div
id="topText"
class="infoBar"
>
{{ roundText }}
</div><div
id="matchTypeText"
class="infoBar"
>
FT2
</div>
</div>
</div>
<div class="scoreBlock">
<div class="score">
{{ scoreboard.rightScore }}
</div>
</div>
<div
id="nameBlockP2"
class="nameBlock"
>
<div class="name">
{{ rightName }} <span v-if="rightTeam">|</span> {{ rightTeam }}
</div>
</div>
</div>
<div
id="casterContainer"
class="scoreboardContainer"
>
<div class="commentators">
<span class="casterText">{{ commentary.leftCommentator || 'Commentator 1' }}</span><span class="casterIcon">🎙</span><span class="casterText">{{ commentary.rightCommentator || 'Commentator 2' }}</span>
</div>
</div>
</div>
</template>
<style scoped src="./template.css"></style>
+466
View File
@@ -0,0 +1,466 @@
/* ==== SVG ELEMENTS COLOR SETTINGS ==== */
/* == Player Plaques == */
/* Gradient for background/fill - Player 1 */
#linear-gradient-p1-noimg,
#linear-gradient-p1-full {
--gradient-stop-1: #15bcba; /* 15bcba */
--gradient-stop-2: #19c2a4; /* 19c2a4 */
--gradient-stop-3: #1ecb8b; /* 1ecb8b */
}
/* Gradient for background/fill - Player 2 */
#linear-gradient-p2-noimg,
#linear-gradient-p2-full {
--gradient-stop-1: #15bcba; /* 15bcba */
--gradient-stop-2: #19c2a4; /* 19c2a4 */
--gradient-stop-3: #1ecb8b; /* 1ecb8b */
}
/* Stroke color - Player 1 */
#P1_bg_stroke_noimg polygon,
#P1_diag_noimg line,
#P1_vert_noimg line,
#P1_bg_stroke polygon,
#P1_diag line,
#P1_vert line {
stroke: #232323; /* 232323 */
}
/* Stroke color - Player 2 */
#P2_bg_stroke_noimg polygon,
#P2_diag_noimg line,
#P2_vert_noimg line,
#P2_bg_stroke polygon,
#P2_diag line,
#P2_vert line {
stroke: #232323; /* 232323 */
}
/* Faint outer glow effect on stroke - Player 1 */
#coloredGlow_p1 {
--glow-color: #232323; /* 232323 */
--glow-opacity: 1;
}
/* Faint outer glow effect on stroke - Player 2 */
#coloredGlow_p2 {
--glow-color: #232323; /* 232323 */
--glow-opacity: 1;
}
/* Drop shadow */
.playerPlaque {
filter: drop-shadow(rgba(0, 0, 0, 0.2) 0px 0px 0.2rem);
}
/* == Team Plaques == */
/* Background color - Team 1 */
#T1_noimg_bg,
#T1_full_bg {
fill: #000000; /* 000000 */
opacity: 0.9; /* 0.9 */
}
/* Background color - Team 2 */
#T2_noimg_bg,
#T2_full_bg {
fill: #000000; /* 000000 */
opacity: 0.9; /* 0.9 */
}
/* Line color - Team 1 */
#T1_noimg_line line,
#T1_full_line line {
stroke: #ffffff; /* ffffff */
}
/* Line color - Team 2 */
#T2_noimg_line line,
#T2_full_line line {
stroke: #ffffff; /* ffffff */
}
/* Drop shadow */
#T1_noimg_bg,
#T1_full_bg,
#T2_noimg_bg,
#T2_full_bg {
filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 0px 0.3rem);
}
/* == Top Row == */
/* Drop shadow */
#topRowShadow {
fill: #000000; /* 000000 */
opacity: 0.8; /* 0.8 */
}
/* Element body */
#topRowBody {
stroke: #000000; /* 000000 */
fill: #000000; /* 000000 */
opacity: 0.9; /* 0.9 */
}
/* == Casters Row == */
/* Drop shadow */
#castersShadow {
fill: #000000; /* 000000 */
opacity: 0.8; /* 0.8 */
}
/* Element body */
#castersBody {
fill: #000000; /* 000000 */
opacity: 0.9; /* 0.9 */
}
/* ==== END OF SVG ELEMENTS COLOR SETTINGS ==== */
@font-face {
font-family: 'Shapiro 115 Plus Ext';
src: url('fonts/Shapiro-115-Plus-Extd.otf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Shapiro 55 Middle Ext';
src: url('fonts/Shapiro 55 Middle Extd.otf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Shapiro 45 Welter Text';
src: url('fonts/Shapiro 45 Welter Text.otf');
font-weight: normal;
font-style: normal;
}
body {
margin: 0;
font-family: 'Titillium Web', sans-serif; /*<--- Default placeholder font*/
font-family: 'Akko Pro', sans-serif;
color: white;
background: transparent;
}
.debug {
background-color: var(--debug-bg-color, black) !important;
/* border: solid 1px red;*/
}
.debug-bg-container {
position: fixed;
width: 1920px;
height: 1080px;
overflow: hidden;
}
.debug-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* keep aspect ratio while filling space */
z-index: 0;
}
.bg-img {
z-index: 0;
}
.scoreboardContainer {
display: flex;
justify-content: center;
align-items: flex-start;
height: 54px;
padding: 0 20px;
background-color: transparent;
position: relative;
z-index: 1;
}
.nameBlock {
background-color: transparent;
width: 334px;
height: 36px;
padding: 0px 5px 0px 5px;
align-content: center;
text-align: center;
text-shadow: 0 0 10px black, 2px 0 black, -2px 0 black, 0 2px black, 0 -2px black, 1px 1px black, -1px -1px black, 1px -1px black, -1px 1px black;
}
.name {
font-family: 'Shapiro 55 Middle Ext';
font-size: 20px;
white-space: nowrap;
text-align: center;
}
#nameBlockP1 {
margin: 0px 0px 0px 0px;
}
#nameBlockP2 {
margin: 0px 0px 0px 0px;
}
.scoreBlock {
background-color: transparent;
padding: 0px;
width: 58px;
height: 36px;
align-content: center;
text-align: center;
align-items: center;
text-shadow: 0 0 4px black, 2px 0 black, -2px 0 black, 0 2px black, 0 -2px black, 1px 1px black, -1px -1px black, 1px -1px black, -1px 1px black, 0px 5px 0px black;
}
.score {
font-family: 'Shapiro 115 Plus Ext';
font-size: 24px;
text-align: center;
}
.centerPanel {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
margin: 0px 0px;
}
.infoRow {
background-color: transparent;
display: flex;
width: 582px;
justify-content: center;
}
.infoBar {
background-color: transparent;
text-align: center;
align-content: center;
padding: 2px 10px;
margin: 0px 0px;
}
#topText {
width: 240px;
height: 50px;
font-size: 20px;
font-family: 'Shapiro 45 Welter Text';
padding: 2px 0px;
line-height: 1.2;
}
#stageText, #matchTypeText {
width: 170px;
height: 32px;
font-family: 'Shapiro 45 Welter Text';
font-size: 16px;
padding-left: 0px;
padding-right: 0px;
padding-bottom: 4px;
}
.imgBlock {
min-width: 65px;
height: 49px;
vertical-align: middle;
align-items: center;
display: flex;
justify-content: center;
margin-top: 14px;
}
.playerImg {
vertical-align: middle;
}
.flag-style {
height: 34.5px;
width: 46px;
}
.custom-image-style {
max-height: 34.5px;
max-width: 46px;
}
.team-custom-image-style {
max-height: 29px;
max-width: 39px;
}
.teamsContainer {
position: absolute;
display: flex;
justify-content: center;
width: 100%;
margin-top: -13px;
}
#teamsDivider {
width: 348px;
}
.team {
display: flex;
min-width: 276px;
min-height: 28px;
align-items: center;
padding-top: 1px;
}
.team-left {
justify-content: flex-end;
}
.team-right {
justify-content: flex-start;
}
.teamText {
display: flex;
gap: 0px;
}
#team1Name,
#team2Name {
width: 236px;
height: 28px;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-family: 'Shapiro 55 Middle Ext';
}
#team1Score,
#team2Score {
height: 28px;
width: 40px;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-family: 'Shapiro 55 Middle Ext';
}
.teamImg {
width: 39px;
height: 29px;
object-fit: contain;
}
.commentators {
background-color: transparent;
align-items: center;
gap: 6px;
font-family: 'Titillium Web', sans-serif; /*<--- Default placeholder font*/
font-family: 'Akko Pro', sans-serif;
font-size: 16px;
display: flex;
justify-content: center;
height: 20px;
position: relative;
z-index: 1;
}
.micIcon {
width: 12px;
height: 12px;
background-color: red;
border-radius: 50%;
}
#casterContainer {
align-items: center;
height: 18px;
position: relative;
top: 1002px;
}
.casterIcon {
width: 18px;
height: 18px;
margin: 0 4px;
font-size: 16px;
text-align: center;
align-content: center;
}
.casterText {
display: inline-flex;
height: 18px;
width: 130px;
align-items: flex-start;
justify-content: center;
}
:root {
--fade-duration: 500ms; /* length of .fade animation, set here */
}
.fade {
opacity: 1;
transition: opacity var(--fade-duration) ease-in-out;
}
.fade-hidden {
opacity: 0;
}
/* Default values for the animation */
[data-anim] {
opacity: 0;
will-change: transform, opacity;
/* Customizable via CSS variables (set by JS from data-*): */
--delay: 0ms;
--dur: 500ms;
}
/* After the entrance animations finish, keep everything visible and prevent any replays */
body.post-entrance [data-anim] {
opacity: 1;
transform: none;
animation: none !important;
}
/* Types of animations */
body.anim-start [data-anim="fade"] { animation: fade-in var(--dur) ease-out var(--delay) both; }
body.anim-start [data-anim="up"] { animation: slide-up-fade var(--dur) ease-out var(--delay) both; }
body.anim-start [data-anim="down"] { animation: slide-down-fade var(--dur) ease-out var(--delay) both; }
body.anim-start [data-anim="left"] { animation: slide-left-fade var(--dur) ease-out var(--delay) both; }
body.anim-start [data-anim="right"] { animation: slide-right-fade var(--dur) ease-out var(--delay) both; }
.bg-img[data-anim] { pointer-events: none; }
/* Overlay animation keyframes */
@keyframes fade-in {
from { opacity: 0 }
to { opacity: 1 }
}
@keyframes slide-up-fade {
from { transform: translateY(24px); opacity: 0 }
to { transform: none; opacity: 1 }
}
@keyframes slide-down-fade {
from { transform: translateY(-24px); opacity: 0 }
to { transform: none; opacity: 1 }
}
@keyframes slide-left-fade {
from { transform: translateX(50px); opacity: 0 }
to { transform: none; opacity: 1 }
}
@keyframes slide-right-fade {
from { transform: translateX(-50px); opacity: 0 }
to { transform: none; opacity: 1 }
}