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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user