Pause menu: shared map blur, PC back, Xbox glyphs

Wrap map art and markers in a new .map-world so blur, pan and zoom animate together; add a PC-facing top-left ESC BACK button and a map-mode prompt bar using Xbox PNG glyphs. Implement JS glyph-tinting, map-mode enter/exit logic, and pan/zoom handlers; update styles for .map-world, markers, prompt bars, and the PC back button. Add Xbox icon assets and update deploy/stage scripts to copy them. Update changelog and dev-log entries to document the changes.
This commit is contained in:
2026-07-04 17:41:29 +12:00
parent 7c1346c95d
commit 38b6011de8
17 changed files with 483 additions and 60 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -8,18 +8,18 @@
</head>
<body>
<div class="pause-screen" id="pauseScreen">
<!-- Map background -->
<div class="map-layer" aria-hidden="true"></div>
<div class="map-overlay" aria-hidden="true"></div>
<!-- Subtle multiplayer markers on the map -->
<div class="map-markers" aria-hidden="true">
<span class="marker marker--player" style="left: 58%; top: 42%;"></span>
<span class="marker marker--ally" style="left: 64%; top: 38%;"></span>
<span class="marker marker--ally" style="left: 52%; top: 48%;"></span>
<span class="marker marker--ally" style="left: 71%; top: 55%;"></span>
<span class="marker marker--ally" style="left: 45%; top: 36%;"></span>
<!-- Map + markers share one filter so blur/unblur stays in lockstep -->
<div class="map-world" id="mapWorld" aria-hidden="true">
<div class="map-layer"></div>
<div class="map-markers">
<span class="marker marker--player" style="left: 58%; top: 42%;"></span>
<span class="marker marker--ally" style="left: 64%; top: 38%;"></span>
<span class="marker marker--ally" style="left: 52%; top: 48%;"></span>
<span class="marker marker--ally" style="left: 71%; top: 55%;"></span>
<span class="marker marker--ally" style="left: 45%; top: 36%;"></span>
</div>
</div>
<div class="map-overlay" aria-hidden="true"></div>
<!-- CRT / HUD texture overlays -->
<div class="screen-effects" aria-hidden="true"></div>
@@ -85,22 +85,52 @@
<p class="server-panel__players">12 / 24</p>
</section>
<!-- Bottom controller prompts -->
<footer class="prompt-bar" aria-label="Controller prompts">
<!-- Bottom controller prompts (pause menu) -->
<footer class="prompt-bar" id="pausePromptBar" aria-label="Controller prompts">
<div class="prompt">
<span class="prompt__glyph" aria-hidden="true"></span>
<span class="prompt__label">MOVE</span>
</div>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--rs" aria-hidden="true">RS</span>
<span class="prompt__label">ZOOM</span>
</div>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--round" aria-hidden="true">A</span>
<span class="prompt__glyph prompt__glyph--icon" aria-hidden="true">
<img class="prompt__glyph-img" data-glyph-src="./assets/icons/Xbox/T_X_A_White_Alt.png" src="./assets/icons/Xbox/T_X_A_White_Alt.png" alt="" draggable="false">
</span>
<span class="prompt__label">SELECT</span>
</div>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--round" aria-hidden="true">B</span>
<span class="prompt__glyph prompt__glyph--icon" aria-hidden="true">
<img class="prompt__glyph-img" data-glyph-src="./assets/icons/Xbox/T_X_B_White_Alt.png" src="./assets/icons/Xbox/T_X_B_White_Alt.png" alt="" draggable="false">
</span>
<span class="prompt__label">BACK</span>
</div>
</footer>
<!-- PC back button (map mode only) -->
<button type="button" class="map-pc-back" id="mapBackButton" aria-hidden="true">
<span class="map-pc-back__key" aria-hidden="true">ESC</span>
<span class="map-pc-back__label">BACK</span>
</button>
<!-- Map mode prompt bar (controller: LS MOVE / RS ZOOM / A SELECT / B BACK) -->
<footer class="prompt-bar prompt-bar--map-mode" id="mapPromptBar" aria-label="Map mode prompts" hidden>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--icon prompt__glyph--icon-stick" aria-hidden="true">
<img class="prompt__glyph-img" data-glyph-src="./assets/icons/Xbox/T_X_L_2D_Alt.png" src="./assets/icons/Xbox/T_X_L_2D_Alt.png" alt="" draggable="false">
</span>
<span class="prompt__label">MOVE</span>
</div>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--icon prompt__glyph--icon-stick" aria-hidden="true">
<img class="prompt__glyph-img" data-glyph-src="./assets/icons/Xbox/T_X_R_Y_Alt.png" src="./assets/icons/Xbox/T_X_R_Y_Alt.png" alt="" draggable="false">
</span>
<span class="prompt__label">ZOOM</span>
</div>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--icon" aria-hidden="true">
<img class="prompt__glyph-img" data-glyph-src="./assets/icons/Xbox/T_X_A_White_Alt.png" src="./assets/icons/Xbox/T_X_A_White_Alt.png" alt="" draggable="false">
</span>
<span class="prompt__label">SELECT</span>
</div>
<div class="prompt">
<span class="prompt__glyph prompt__glyph--icon" aria-hidden="true">
<img class="prompt__glyph-img" data-glyph-src="./assets/icons/Xbox/T_X_B_White_Alt.png" src="./assets/icons/Xbox/T_X_B_White_Alt.png" alt="" draggable="false">
</span>
<span class="prompt__label">BACK</span>
</div>
</footer>
@@ -21,6 +21,120 @@
updateScale();
window.addEventListener("resize", updateScale);
// ── Tint Xbox prompt glyphs to pause-menu amber (same approach as browser) ──
// White pixels become --co-amber; black stays black; transparent stays transparent.
var GLYPH_WHITE_THRESHOLD = 200;
var GLYPH_BLACK_THRESHOLD = 40;
var glyphTintCache = {};
function getAmberTint() {
var style = getComputedStyle(document.documentElement);
var amber = style.getPropertyValue("--co-amber").trim() || "#c9a84a";
var hex = amber.replace("#", "");
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
return {
r: parseInt(hex.slice(0, 2), 16) || 201,
g: parseInt(hex.slice(2, 4), 16) || 168,
b: parseInt(hex.slice(4, 6), 16) || 74
};
}
function classifyGlyphPixel(r, g, b) {
if (r >= GLYPH_WHITE_THRESHOLD && g >= GLYPH_WHITE_THRESHOLD && b >= GLYPH_WHITE_THRESHOLD) {
return "white";
}
if (r <= GLYPH_BLACK_THRESHOLD && g <= GLYPH_BLACK_THRESHOLD && b <= GLYPH_BLACK_THRESHOLD) {
return "black";
}
return "edge";
}
function tintGlyphImageData(imageData, color) {
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
if (data[i + 3] < 4) {
data[i + 3] = 0;
continue;
}
var kind = classifyGlyphPixel(data[i], data[i + 1], data[i + 2]);
if (kind === "white") {
data[i] = color.r;
data[i + 1] = color.g;
data[i + 2] = color.b;
} else if (kind === "black") {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
} else {
var lum = (data[i] + data[i + 1] + data[i + 2]) / (3 * 255);
data[i] = Math.round(lum * color.r);
data[i + 1] = Math.round(lum * color.g);
data[i + 2] = Math.round(lum * color.b);
}
}
return imageData;
}
function tintGlyphImage(url, color, done) {
var key = url + "|" + color.r + "," + color.g + "," + color.b;
if (glyphTintCache[key]) {
done(glyphTintCache[key]);
return;
}
var img = new Image();
img.onload = function () {
var canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
var ctx = canvas.getContext("2d");
if (!ctx) {
done(null);
return;
}
ctx.drawImage(img, 0, 0);
try {
ctx.putImageData(
tintGlyphImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), color),
0,
0
);
} catch (err) {
// file:// or cross-origin can taint the canvas; keep the original white glyph.
done(null);
return;
}
var dataUrl = canvas.toDataURL("image/png");
glyphTintCache[key] = dataUrl;
done(dataUrl);
};
img.onerror = function () {
done(null);
};
img.src = url;
}
function hydratePromptGlyphs() {
var color = getAmberTint();
var imgs = document.querySelectorAll(".prompt__glyph-img[data-glyph-src]");
for (var i = 0; i < imgs.length; i++) {
(function (img) {
var src = img.getAttribute("data-glyph-src");
if (!src) {
return;
}
tintGlyphImage(src, color, function (dataUrl) {
if (dataUrl) {
img.src = dataUrl;
}
});
})(imgs[i]);
}
}
hydratePromptGlyphs();
var menu = document.getElementById("menu");
if (!menu) {
return;
@@ -30,11 +144,11 @@
var selectedIndex = -1; // No default selection
function setSelected(index) {
if (index < 0 || index >= items.length) {
if (index >= items.length) {
return;
}
selectedIndex = index;
selectedIndex = index < 0 ? -1 : index;
items.forEach(function (item, i) {
var isSelected = i === selectedIndex;
@@ -55,8 +169,12 @@
if (action === "resume") {
console.log("[CO Pause Menu] Resume game (concept)");
setSelected(-1);
} else if (action === "map") {
enterMapMode();
} else if (action === "quit") {
console.log("[CO Pause Menu] Quit to main menu (concept)");
setSelected(-1);
}
}
@@ -94,7 +212,7 @@
break;
case "Escape":
event.preventDefault();
console.log("[CO Pause Menu] Back / close pause menu (concept)");
exitMapMode();
break;
default:
break;
@@ -105,13 +223,88 @@
setSelected(selectedIndex);
}
// ── Map mode state management ───────────────────────────────────
var isMapMode = false;
function enterMapMode() {
isMapMode = true;
console.log("[CO Pause Menu] Entering map mode");
var leftPanel = document.querySelector(".left-panel");
var mapWorld = document.getElementById("mapWorld");
var pausePromptBar = document.getElementById("pausePromptBar");
var mapPromptBar = document.getElementById("mapPromptBar");
var mapBackButton = document.getElementById("mapBackButton");
var serverPanel = document.querySelector(".server-panel");
setSelected(-1);
if (leftPanel) leftPanel.classList.add("is-hidden");
if (serverPanel) serverPanel.style.display = "none";
if (mapWorld) {
mapWorld.classList.remove("is-blurred");
mapWorld.classList.add("is-interactive");
}
if (pausePromptBar) pausePromptBar.hidden = true;
if (mapPromptBar) mapPromptBar.hidden = false;
if (mapBackButton) {
mapBackButton.classList.add("is-visible");
mapBackButton.setAttribute("aria-hidden", "false");
}
}
function exitMapMode() {
if (!isMapMode) {
return;
}
isMapMode = false;
console.log("[CO Pause Menu] Exiting map mode");
var leftPanel = document.querySelector(".left-panel");
var mapWorld = document.getElementById("mapWorld");
var pausePromptBar = document.getElementById("pausePromptBar");
var mapPromptBar = document.getElementById("mapPromptBar");
var mapBackButton = document.getElementById("mapBackButton");
var serverPanel = document.querySelector(".server-panel");
if (mapBackButton) {
mapBackButton.classList.remove("is-visible");
mapBackButton.setAttribute("aria-hidden", "true");
}
if (leftPanel) leftPanel.classList.remove("is-hidden");
if (serverPanel) serverPanel.style.display = "block";
if (mapWorld) {
mapWorld.classList.add("is-blurred");
mapWorld.classList.remove("is-interactive");
}
if (pausePromptBar) pausePromptBar.hidden = false;
if (mapPromptBar) mapPromptBar.hidden = true;
setSelected(-1);
}
// ── Blur map initially ────────────────────────────────────────
var initialMapWorld = document.getElementById("mapWorld");
if (initialMapWorld) {
initialMapWorld.classList.add("is-blurred");
}
// ── Map mode PC back button handler ───────────────────────────
var mapBackButton = document.getElementById("mapBackButton");
if (mapBackButton) {
mapBackButton.addEventListener("click", function (event) {
event.stopPropagation();
exitMapMode();
});
}
// ── Map pan/zoom (concept preview of future Fallout 76-style navigation) ──
// Scroll to zoom, click-drag to pan once zoomed in. Real implementation
// will eventually read controller right-stick input via F4SE instead.
var pauseScreen = document.getElementById("pauseScreen");
var mapLayer = document.querySelector(".map-layer");
var mapWorld = document.getElementById("mapWorld");
if (pauseScreen && mapLayer) {
if (pauseScreen && mapWorld) {
var MAX_ZOOM = 4;
var ZOOM_STEP = 0.18;
var zoom = 1;
@@ -201,7 +394,7 @@
panY = Math.max(-maxPanY, Math.min(maxPanY, panY));
}
mapLayer.addEventListener(
mapWorld.addEventListener(
"wheel",
function (event) {
event.preventDefault();
@@ -218,7 +411,7 @@
{ passive: false }
);
mapLayer.addEventListener("mousedown", function (event) {
mapWorld.addEventListener("mousedown", function (event) {
if (zoom <= MIN_ZOOM) {
return;
}
+158 -30
View File
@@ -69,12 +69,37 @@ body {
overflow: hidden;
}
/* ── Map layers ──────────────────────────────────────────────── */
/* ── Map world (map art + markers, one shared blur/pan/zoom) ── */
.map-world {
position: absolute;
inset: 0;
z-index: 1;
filter: brightness(0.74) contrast(1.04) saturate(0.55);
transform: translate(var(--map-pan-x, 0px), var(--map-pan-y, 0px))
scale(var(--map-zoom, 1));
transform-origin: center center;
transition: transform 160ms ease-out, filter 200ms ease-out;
cursor: grab;
pointer-events: none;
}
.map-world.is-interactive {
pointer-events: auto;
}
.map-world.is-blurred {
filter: brightness(0.74) contrast(1.04) saturate(0.55) blur(12px);
}
.pause-screen.is-panning .map-world {
transition: none;
cursor: grabbing;
}
.map-layer {
position: absolute;
inset: 0;
z-index: 1;
/*
* "contain" keeps the whole regional map visible (Fallout 76-style full
* overview) instead of "cover" cropping it down to a tight, zoomed-in
@@ -84,12 +109,6 @@ body {
background:
url("assets/map.png") center center / contain no-repeat;
background-color: #0b100c;
filter: brightness(0.74) contrast(1.04) saturate(0.55);
transform: translate(var(--map-pan-x, 0px), var(--map-pan-y, 0px))
scale(var(--map-zoom, 1));
transform-origin: center center;
transition: transform 160ms ease-out;
cursor: grab;
}
.map-overlay {
@@ -109,24 +128,12 @@ body {
}
/* ── Map markers ───────────────────────────────────────────── */
/* Shares the same pan/zoom transform (inherited custom properties) as
.map-layer so markers stay pinned to their map position while navigating. */
/* Inside .map-world so they share the same filter and transform as the map. */
.map-markers {
position: absolute;
inset: 0;
z-index: 3;
pointer-events: none;
transform: translate(var(--map-pan-x, 0px), var(--map-pan-y, 0px))
scale(var(--map-zoom, 1));
transform-origin: center center;
transition: transform 160ms ease-out;
}
.pause-screen.is-panning .map-layer,
.pause-screen.is-panning .map-markers {
transition: none;
cursor: grabbing;
}
.marker {
@@ -243,6 +250,12 @@ body {
rgba(8, 14, 10, 0.55) 70%,
var(--co-panel-fade) 100%
);
transition: opacity 200ms ease-out, visibility 200ms ease-out;
}
.left-panel.is-hidden {
opacity: 0;
visibility: hidden;
}
.brand-logo {
@@ -321,11 +334,11 @@ body {
box-shadow: 0 0 12px rgba(196, 168, 75, 0.25);
}
.menu-item:hover:not(.is-selected) {
.menu-item:hover {
color: var(--co-text-pale);
}
.menu-item:hover:not(.is-selected) .menu-item__bar {
.menu-item:hover .menu-item__bar {
opacity: 1;
transform: translateY(-50%) scaleX(1);
}
@@ -431,6 +444,16 @@ body {
rgba(6, 10, 8, 0.88) 100%
);
border-top: 1px solid rgba(201, 168, 74, 0.15);
transition: opacity 200ms ease-out, visibility 200ms ease-out;
}
.prompt-bar[hidden] {
opacity: 0;
visibility: hidden;
}
.prompt-bar--map-mode {
justify-content: center;
}
.prompt {
@@ -454,15 +477,30 @@ body {
line-height: 1;
}
.prompt__glyph--rs {
border-radius: 4px;
font-size: calc(9px * var(--co-ui-scale));
letter-spacing: 0.02em;
min-width: calc(26px * var(--co-ui-scale));
/* Xbox PNG glyphs (from ui/Icons); white pixels are tinted amber in script.js */
.prompt__glyph--icon {
border: none;
border-radius: 0;
background: transparent;
min-width: calc(28px * var(--co-ui-scale));
width: calc(28px * var(--co-ui-scale));
height: calc(28px * var(--co-ui-scale));
padding: 0;
}
.prompt__glyph--round {
font-size: calc(10px * var(--co-ui-scale));
/* Stick glyphs have more detail (arrows/rings) and read better slightly larger */
.prompt__glyph--icon-stick {
min-width: calc(34px * var(--co-ui-scale));
width: calc(34px * var(--co-ui-scale));
height: calc(34px * var(--co-ui-scale));
}
.prompt__glyph-img {
width: 100%;
height: 100%;
display: block;
pointer-events: none;
user-select: none;
}
.prompt__label {
@@ -473,6 +511,96 @@ body {
opacity: 0.85;
}
/* ── Map mode PC back button ─────────────────────────────────── */
/*
* Fades in after the left panel (logo) finishes hiding (200ms), and
* fades out immediately on exit so it never sits on top of the logo.
*/
.map-pc-back {
position: absolute;
top: calc(28px * var(--co-ui-scale));
left: calc(28px * var(--co-ui-scale));
z-index: 12;
display: flex;
align-items: center;
gap: calc(12px * var(--co-ui-scale));
padding:
calc(10px * var(--co-ui-scale))
calc(16px * var(--co-ui-scale));
border: 1px solid var(--co-border);
background: rgba(6, 12, 8, 0.82);
box-shadow:
inset 0 0 16px rgba(0, 0, 0, 0.35),
0 2px 8px rgba(0, 0, 0, 0.35);
color: var(--co-text-pale);
font-family: inherit;
font-size: calc(14px * var(--co-ui-scale));
font-weight: 700;
letter-spacing: 0.16em;
cursor: pointer;
outline: none;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
color var(--co-transition),
border-color var(--co-transition),
background var(--co-transition),
opacity 150ms ease-out,
visibility 0ms linear 150ms;
}
.map-pc-back.is-visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
transition:
color var(--co-transition),
border-color var(--co-transition),
background var(--co-transition),
opacity 200ms ease-out 200ms,
visibility 0ms linear 200ms;
}
.map-pc-back__key {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: calc(36px * var(--co-ui-scale));
padding:
calc(3px * var(--co-ui-scale))
calc(8px * var(--co-ui-scale));
border: 1px solid var(--co-amber-muted);
border-radius: 2px;
background: rgba(0, 0, 0, 0.4);
color: var(--co-amber);
font-size: calc(11px * var(--co-ui-scale));
font-weight: 700;
letter-spacing: 0.08em;
line-height: 1.2;
}
.map-pc-back__label {
line-height: 1;
}
.map-pc-back:hover {
color: var(--co-green);
border-color: var(--co-green-dim);
background: rgba(8, 18, 10, 0.9);
}
.map-pc-back:hover .map-pc-back__key {
color: var(--co-green);
border-color: var(--co-green-dim);
}
.map-pc-back:focus-visible {
outline: 1px solid var(--co-green-dim);
outline-offset: 2px;
}
/* ── Responsive tweaks ───────────────────────────────────────── */
/*
* Sizing is already fluid via --co-ui-scale (see :root), which now scales