Skip to content

Commit 70cf2ba

Browse files
ScavieFaeclaude
andcommitted
Viewer UX: fix text bleed, zoom fighters, glow, resolve Random char, bolder overlay
- Fix bleed-through: clear stream-area content before inserting SVG canvas - Tighter viewBox (-100 -80 200 120) zooms fighters ~1.4x larger - SVG glow filter on player layer for better visibility - Brighter green (#44ff88) + colored stroke for fighter outlines - Remove redundant LIVE badge (header already says LIVESTREAM) - Resolve "Random" to actual character name from match_start characterId - Bolder gradient overlay (multi-stop, higher opacity) for stats readability Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3cc7e59 commit 70cf2ba

2 files changed

Lines changed: 110 additions & 34 deletions

File tree

tournaments/viewer.html

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@
428428
align-items: flex-end;
429429
padding: 8px 16px;
430430
pointer-events: none;
431-
background: linear-gradient(to top, rgba(16,8,4,0.9) 0%, transparent 100%);
431+
background: linear-gradient(to top, rgba(16,8,4,0.95) 0%, rgba(16,8,4,0.4) 60%, transparent 100%);
432432
}
433433

434434
.stream-stats {
@@ -983,8 +983,17 @@
983983
};
984984
const DEFAULT_STAGE = { name:"Stage", main:{x:0,y:0,w:150,h:6}, plats:[] };
985985

986-
// Player colors (port-indexed, same as nojohns.gg/live)
987-
const PLAYER_COLORS = ["#22c55e", "#ff6b35", "#3b82f6", "#f59e0b"];
986+
// External character ID → display name (resolves "RANDOM" in bracket data)
987+
const EXT_TO_CHAR = {
988+
0:"CAPTAINFALCON", 1:"DONKEYKONG", 2:"FOX", 3:"MRGAMEANDWATCH", 4:"KIRBY",
989+
5:"BOWSER", 6:"LINK", 7:"LUIGI", 8:"MARIO", 9:"MARTH", 10:"MEWTWO",
990+
11:"NESS", 12:"PEACH", 13:"PIKACHU", 14:"ICECLIMBERS", 15:"JIGGLYPUFF",
991+
16:"SAMUS", 17:"YOSHI", 18:"ZELDA", 19:"SHEIK", 20:"FALCO",
992+
21:"YOUNGLINK", 22:"DRMARIO", 23:"ROY", 24:"PICHU", 25:"GANONDORF",
993+
};
994+
995+
// Player colors (port-indexed)
996+
const PLAYER_COLORS = ["#44ff88", "#ff6b35", "#3b82f6", "#f59e0b"];
988997

989998
// ── Animation Cache ─────────────────────────────────────────────
990999

@@ -1043,14 +1052,38 @@
10431052
const area = document.getElementById('stream-area');
10441053
if (!area || area.querySelector('.stream-canvas-wrap')) return;
10451054

1055+
// Clear any existing content (text nodes, fallback stats)
1056+
area.innerHTML = '';
1057+
10461058
const wrap = document.createElement('div');
10471059
wrap.className = 'stream-canvas-wrap';
10481060

10491061
const svg = document.createElementNS(SVG_NS, 'svg');
1050-
svg.setAttribute('viewBox', '-140 -120 280 200');
1062+
svg.setAttribute('viewBox', '-100 -80 200 120');
10511063
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
10521064
svg.id = 'fighter-svg';
10531065

1066+
// Glow filter for fighter visibility
1067+
const defs = document.createElementNS(SVG_NS, 'defs');
1068+
const filter = document.createElementNS(SVG_NS, 'filter');
1069+
filter.setAttribute('id', 'fighter-glow');
1070+
filter.setAttribute('x', '-50%'); filter.setAttribute('y', '-50%');
1071+
filter.setAttribute('width', '200%'); filter.setAttribute('height', '200%');
1072+
const blur = document.createElementNS(SVG_NS, 'feGaussianBlur');
1073+
blur.setAttribute('stdDeviation', '1.5');
1074+
blur.setAttribute('result', 'glow');
1075+
const merge = document.createElementNS(SVG_NS, 'feMerge');
1076+
const mergeGlow = document.createElementNS(SVG_NS, 'feMergeNode');
1077+
mergeGlow.setAttribute('in', 'glow');
1078+
const mergeSrc = document.createElementNS(SVG_NS, 'feMergeNode');
1079+
mergeSrc.setAttribute('in', 'SourceGraphic');
1080+
merge.appendChild(mergeGlow);
1081+
merge.appendChild(mergeSrc);
1082+
filter.appendChild(blur);
1083+
filter.appendChild(merge);
1084+
defs.appendChild(filter);
1085+
svg.appendChild(defs);
1086+
10541087
// Y-axis inverted group (positive = up in game coords)
10551088
const gameG = document.createElementNS(SVG_NS, 'g');
10561089
gameG.setAttribute('transform', 'scale(1 -1)');
@@ -1062,13 +1095,14 @@
10621095
stageG.id = 'stage-layer';
10631096
gameG.appendChild(stageG);
10641097

1065-
// Players group
1098+
// Players group with glow
10661099
const playersG = document.createElementNS(SVG_NS, 'g');
10671100
playersG.id = 'players-layer';
1101+
playersG.setAttribute('filter', 'url(#fighter-glow)');
10681102
gameG.appendChild(playersG);
10691103

10701104
wrap.appendChild(svg);
1071-
area.insertBefore(wrap, area.firstChild);
1105+
area.appendChild(wrap);
10721106
svgInitialized = true;
10731107
}
10741108

@@ -1157,9 +1191,9 @@
11571191
`translate(${x} ${y}) scale(${scale} ${scale}) scale(${facing} 1) scale(0.1 -0.1) translate(-500 -500)`
11581192
);
11591193
path.setAttribute('fill', color);
1160-
path.setAttribute('stroke', 'black');
1161-
path.setAttribute('stroke-width', '0.5');
1162-
path.setAttribute('opacity', '0.9');
1194+
path.setAttribute('stroke', color);
1195+
path.setAttribute('stroke-width', '1.5');
1196+
path.setAttribute('opacity', '1');
11631197
playersG.appendChild(path);
11641198
}
11651199
}
@@ -1174,6 +1208,15 @@
11741208
}
11751209
}
11761210

1211+
// Resolve actual character names from characterId (overrides "RANDOM")
1212+
for (const p of players) {
1213+
if (p.characterId != null && streamPlayers[p.port]) {
1214+
const extId = EXT_ID_BY_INT[p.characterId];
1215+
const actualChar = EXT_TO_CHAR[extId];
1216+
if (actualChar) streamPlayers[p.port].character = actualChar;
1217+
}
1218+
}
1219+
11771220
// Initialize SVG canvas and render stage
11781221
initSvgCanvas();
11791222
renderStage(svgStageId);
@@ -1910,14 +1953,9 @@
19101953
}
19111954
overlay.innerHTML = statsHtml;
19121955

1913-
// Update LIVE badge
1914-
let badge = area.querySelector('.stream-connected');
1915-
if (!badge) {
1916-
badge = document.createElement('span');
1917-
badge.className = 'stream-connected';
1918-
area.appendChild(badge);
1919-
}
1920-
badge.innerHTML = '● LIVE';
1956+
// No separate LIVE badge — header already says LIVESTREAM
1957+
const oldBadge = area.querySelector('.stream-connected');
1958+
if (oldBadge) oldBadge.remove();
19211959
} else {
19221960
// Fallback: text-only layout (no SVG loaded)
19231961
area.innerHTML = `

web/public/tournament/index.html

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@
428428
align-items: flex-end;
429429
padding: 8px 16px;
430430
pointer-events: none;
431-
background: linear-gradient(to top, rgba(16,8,4,0.9) 0%, transparent 100%);
431+
background: linear-gradient(to top, rgba(16,8,4,0.95) 0%, rgba(16,8,4,0.4) 60%, transparent 100%);
432432
}
433433

434434
.stream-stats {
@@ -983,8 +983,17 @@
983983
};
984984
const DEFAULT_STAGE = { name:"Stage", main:{x:0,y:0,w:150,h:6}, plats:[] };
985985

986-
// Player colors (port-indexed, same as nojohns.gg/live)
987-
const PLAYER_COLORS = ["#22c55e", "#ff6b35", "#3b82f6", "#f59e0b"];
986+
// External character ID → display name (resolves "RANDOM" in bracket data)
987+
const EXT_TO_CHAR = {
988+
0:"CAPTAINFALCON", 1:"DONKEYKONG", 2:"FOX", 3:"MRGAMEANDWATCH", 4:"KIRBY",
989+
5:"BOWSER", 6:"LINK", 7:"LUIGI", 8:"MARIO", 9:"MARTH", 10:"MEWTWO",
990+
11:"NESS", 12:"PEACH", 13:"PIKACHU", 14:"ICECLIMBERS", 15:"JIGGLYPUFF",
991+
16:"SAMUS", 17:"YOSHI", 18:"ZELDA", 19:"SHEIK", 20:"FALCO",
992+
21:"YOUNGLINK", 22:"DRMARIO", 23:"ROY", 24:"PICHU", 25:"GANONDORF",
993+
};
994+
995+
// Player colors (port-indexed)
996+
const PLAYER_COLORS = ["#44ff88", "#ff6b35", "#3b82f6", "#f59e0b"];
988997

989998
// ── Animation Cache ─────────────────────────────────────────────
990999

@@ -1043,14 +1052,38 @@
10431052
const area = document.getElementById('stream-area');
10441053
if (!area || area.querySelector('.stream-canvas-wrap')) return;
10451054

1055+
// Clear any existing content (text nodes, fallback stats)
1056+
area.innerHTML = '';
1057+
10461058
const wrap = document.createElement('div');
10471059
wrap.className = 'stream-canvas-wrap';
10481060

10491061
const svg = document.createElementNS(SVG_NS, 'svg');
1050-
svg.setAttribute('viewBox', '-140 -120 280 200');
1062+
svg.setAttribute('viewBox', '-100 -80 200 120');
10511063
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
10521064
svg.id = 'fighter-svg';
10531065

1066+
// Glow filter for fighter visibility
1067+
const defs = document.createElementNS(SVG_NS, 'defs');
1068+
const filter = document.createElementNS(SVG_NS, 'filter');
1069+
filter.setAttribute('id', 'fighter-glow');
1070+
filter.setAttribute('x', '-50%'); filter.setAttribute('y', '-50%');
1071+
filter.setAttribute('width', '200%'); filter.setAttribute('height', '200%');
1072+
const blur = document.createElementNS(SVG_NS, 'feGaussianBlur');
1073+
blur.setAttribute('stdDeviation', '1.5');
1074+
blur.setAttribute('result', 'glow');
1075+
const merge = document.createElementNS(SVG_NS, 'feMerge');
1076+
const mergeGlow = document.createElementNS(SVG_NS, 'feMergeNode');
1077+
mergeGlow.setAttribute('in', 'glow');
1078+
const mergeSrc = document.createElementNS(SVG_NS, 'feMergeNode');
1079+
mergeSrc.setAttribute('in', 'SourceGraphic');
1080+
merge.appendChild(mergeGlow);
1081+
merge.appendChild(mergeSrc);
1082+
filter.appendChild(blur);
1083+
filter.appendChild(merge);
1084+
defs.appendChild(filter);
1085+
svg.appendChild(defs);
1086+
10541087
// Y-axis inverted group (positive = up in game coords)
10551088
const gameG = document.createElementNS(SVG_NS, 'g');
10561089
gameG.setAttribute('transform', 'scale(1 -1)');
@@ -1062,13 +1095,14 @@
10621095
stageG.id = 'stage-layer';
10631096
gameG.appendChild(stageG);
10641097

1065-
// Players group
1098+
// Players group with glow
10661099
const playersG = document.createElementNS(SVG_NS, 'g');
10671100
playersG.id = 'players-layer';
1101+
playersG.setAttribute('filter', 'url(#fighter-glow)');
10681102
gameG.appendChild(playersG);
10691103

10701104
wrap.appendChild(svg);
1071-
area.insertBefore(wrap, area.firstChild);
1105+
area.appendChild(wrap);
10721106
svgInitialized = true;
10731107
}
10741108

@@ -1157,9 +1191,9 @@
11571191
`translate(${x} ${y}) scale(${scale} ${scale}) scale(${facing} 1) scale(0.1 -0.1) translate(-500 -500)`
11581192
);
11591193
path.setAttribute('fill', color);
1160-
path.setAttribute('stroke', 'black');
1161-
path.setAttribute('stroke-width', '0.5');
1162-
path.setAttribute('opacity', '0.9');
1194+
path.setAttribute('stroke', color);
1195+
path.setAttribute('stroke-width', '1.5');
1196+
path.setAttribute('opacity', '1');
11631197
playersG.appendChild(path);
11641198
}
11651199
}
@@ -1174,6 +1208,15 @@
11741208
}
11751209
}
11761210

1211+
// Resolve actual character names from characterId (overrides "RANDOM")
1212+
for (const p of players) {
1213+
if (p.characterId != null && streamPlayers[p.port]) {
1214+
const extId = EXT_ID_BY_INT[p.characterId];
1215+
const actualChar = EXT_TO_CHAR[extId];
1216+
if (actualChar) streamPlayers[p.port].character = actualChar;
1217+
}
1218+
}
1219+
11771220
// Initialize SVG canvas and render stage
11781221
initSvgCanvas();
11791222
renderStage(svgStageId);
@@ -1910,14 +1953,9 @@
19101953
}
19111954
overlay.innerHTML = statsHtml;
19121955

1913-
// Update LIVE badge
1914-
let badge = area.querySelector('.stream-connected');
1915-
if (!badge) {
1916-
badge = document.createElement('span');
1917-
badge.className = 'stream-connected';
1918-
area.appendChild(badge);
1919-
}
1920-
badge.innerHTML = '● LIVE';
1956+
// No separate LIVE badge — header already says LIVESTREAM
1957+
const oldBadge = area.querySelector('.stream-connected');
1958+
if (oldBadge) oldBadge.remove();
19211959
} else {
19221960
// Fallback: text-only layout (no SVG loaded)
19231961
area.innerHTML = `

0 commit comments

Comments
 (0)