Consolidate shared UI assets
Move common logo and controller glyphs into `ui/views/CommonwealthOnline/shared/` and update the pause menu, main menu, and browser glyph loader to use the shared paths. Also add a sync script and update deploy/staging scripts to copy controller icons once into the shared folder.
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ── C++ / F4SE bridge placeholders ─────────────────────────────
|
||||
window.CommonwealthOnlineUI = {
|
||||
openGameSettings: function () {
|
||||
console.log("[CO UI] Request open vanilla Fallout 4 SettingsMenu");
|
||||
},
|
||||
setMultiplayerSetting: function (key, value) {
|
||||
console.log("[CO UI] Multiplayer setting changed:", key, value);
|
||||
},
|
||||
onBack: function () {
|
||||
console.log("[CO UI] Back requested from main menu");
|
||||
},
|
||||
onOpenView: function (viewName) {
|
||||
console.log("[CO UI] Open view:", viewName);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Compute UI scale ───────────────────────────────────────────
|
||||
function updateScale() {
|
||||
var MIN_SCALE = 0.5;
|
||||
var MAX_SCALE = 1.6;
|
||||
var BASELINE_WIDTH = 1920;
|
||||
var BASELINE_HEIGHT = 1080;
|
||||
var scale = Math.min(
|
||||
window.innerWidth / BASELINE_WIDTH,
|
||||
window.innerHeight / BASELINE_HEIGHT
|
||||
);
|
||||
scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
|
||||
document.documentElement.style.setProperty("--co-ui-scale", String(scale));
|
||||
}
|
||||
|
||||
updateScale();
|
||||
window.addEventListener("resize", updateScale);
|
||||
|
||||
// ── Tint Xbox prompt glyphs to menu amber ──────────────────────
|
||||
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) {
|
||||
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();
|
||||
|
||||
// ── DOM refs ───────────────────────────────────────────────────
|
||||
|
||||
var mainScreen = document.getElementById("mainScreen");
|
||||
var menu = document.getElementById("menu");
|
||||
var settingsPanel = document.getElementById("settingsPanel");
|
||||
var mainPromptBar = document.getElementById("mainPromptBar");
|
||||
var settingsPromptBar = document.getElementById("settingsPromptBar");
|
||||
|
||||
if (!menu || !settingsPanel || !window.CO_Settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsController = null;
|
||||
|
||||
var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item"));
|
||||
var SETTINGS_INDEX = 2;
|
||||
|
||||
// viewMode: "menu" | "settings"
|
||||
var viewMode = "menu";
|
||||
var selectedIndex = -1;
|
||||
var SETTINGS_TRANSITION_MS = 220;
|
||||
var settingsHideFallbackId = 0;
|
||||
var settingsHideOnTransitionEnd = null;
|
||||
|
||||
// ── Main menu selection ────────────────────────────────────────
|
||||
|
||||
function setSelected(index) {
|
||||
if (index >= items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex = index < 0 ? -1 : index;
|
||||
|
||||
items.forEach(function (item, i) {
|
||||
var isSelected = i === selectedIndex;
|
||||
item.classList.toggle("is-selected", isSelected);
|
||||
item.setAttribute("aria-selected", isSelected ? "true" : "false");
|
||||
item.tabIndex = isSelected ? 0 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
function moveSelection(delta) {
|
||||
var next = selectedIndex + delta;
|
||||
if (selectedIndex < 0) {
|
||||
next = delta > 0 ? 0 : items.length - 1;
|
||||
} else if (next < 0) {
|
||||
next = items.length - 1;
|
||||
} else if (next >= items.length) {
|
||||
next = 0;
|
||||
}
|
||||
|
||||
// Skip disabled items
|
||||
var guard = 0;
|
||||
while (items[next] && items[next].classList.contains("is-disabled") && guard < items.length) {
|
||||
next += delta > 0 ? 1 : -1;
|
||||
if (next < 0) {
|
||||
next = items.length - 1;
|
||||
} else if (next >= items.length) {
|
||||
next = 0;
|
||||
}
|
||||
guard += 1;
|
||||
}
|
||||
|
||||
setSelected(next);
|
||||
}
|
||||
|
||||
function activateSelected() {
|
||||
var item = items[selectedIndex];
|
||||
if (!item || item.classList.contains("is-disabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var action = item.getAttribute("data-action");
|
||||
console.log("[CO Main Menu] Selected:", action);
|
||||
|
||||
if (action === "settings") {
|
||||
enterSettingsMode();
|
||||
} else {
|
||||
window.CommonwealthOnlineUI.onOpenView(action);
|
||||
if (action !== "quit") {
|
||||
setSelected(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach(function (item, index) {
|
||||
item.addEventListener("click", function () {
|
||||
if (viewMode === "settings") {
|
||||
if (index === SETTINGS_INDEX) {
|
||||
return;
|
||||
}
|
||||
exitSettingsMode();
|
||||
}
|
||||
if (item.classList.contains("is-disabled")) {
|
||||
return;
|
||||
}
|
||||
setSelected(index);
|
||||
activateSelected();
|
||||
});
|
||||
});
|
||||
|
||||
// ── View mode transitions ──────────────────────────────────────
|
||||
|
||||
function cancelSettingsPanelHide() {
|
||||
if (settingsHideFallbackId) {
|
||||
window.clearTimeout(settingsHideFallbackId);
|
||||
settingsHideFallbackId = 0;
|
||||
}
|
||||
if (settingsHideOnTransitionEnd) {
|
||||
settingsPanel.removeEventListener("transitionend", settingsHideOnTransitionEnd);
|
||||
settingsHideOnTransitionEnd = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showSettingsPanelAnimated() {
|
||||
cancelSettingsPanelHide();
|
||||
if (mainScreen) {
|
||||
mainScreen.classList.add("is-settings-mode");
|
||||
}
|
||||
settingsPanel.hidden = false;
|
||||
settingsPanel.classList.remove("is-open");
|
||||
void settingsPanel.offsetWidth;
|
||||
settingsPanel.classList.add("is-open");
|
||||
}
|
||||
|
||||
function hideSettingsPanelAnimated(done) {
|
||||
if (settingsPanel.hidden) {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cancelSettingsPanelHide();
|
||||
settingsPanel.classList.remove("is-open");
|
||||
|
||||
var completed = false;
|
||||
|
||||
function finishHide() {
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
completed = true;
|
||||
cancelSettingsPanelHide();
|
||||
if (viewMode === "settings" || settingsPanel.classList.contains("is-open")) {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mainScreen) {
|
||||
mainScreen.classList.remove("is-settings-mode");
|
||||
}
|
||||
settingsPanel.hidden = true;
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
settingsHideOnTransitionEnd = function (event) {
|
||||
if (event.target !== settingsPanel || event.propertyName !== "opacity") {
|
||||
return;
|
||||
}
|
||||
finishHide();
|
||||
};
|
||||
|
||||
settingsPanel.addEventListener("transitionend", settingsHideOnTransitionEnd);
|
||||
settingsHideFallbackId = window.setTimeout(finishHide, SETTINGS_TRANSITION_MS + 80);
|
||||
}
|
||||
|
||||
function enterSettingsMode() {
|
||||
viewMode = "settings";
|
||||
|
||||
console.log("[CO Main Menu] Entering settings mode");
|
||||
window.CommonwealthOnlineUI.onOpenView("settings");
|
||||
|
||||
setSelected(SETTINGS_INDEX);
|
||||
|
||||
showSettingsPanelAnimated();
|
||||
if (mainPromptBar) {
|
||||
mainPromptBar.hidden = true;
|
||||
}
|
||||
|
||||
settingsController.show();
|
||||
}
|
||||
|
||||
function exitSettingsMode() {
|
||||
if (viewMode !== "settings") {
|
||||
return;
|
||||
}
|
||||
|
||||
viewMode = "menu";
|
||||
console.log("[CO Main Menu] Exiting settings mode");
|
||||
window.CommonwealthOnlineUI.onBack();
|
||||
|
||||
settingsController.hide();
|
||||
|
||||
hideSettingsPanelAnimated(function () {
|
||||
if (mainPromptBar) {
|
||||
mainPromptBar.hidden = false;
|
||||
}
|
||||
setSelected(SETTINGS_INDEX);
|
||||
});
|
||||
}
|
||||
|
||||
settingsController = window.CO_Settings.create({
|
||||
mount: settingsPanel,
|
||||
promptBar: settingsPromptBar,
|
||||
bridge: window.CommonwealthOnlineUI,
|
||||
layout: "embedded",
|
||||
onBack: function () {
|
||||
exitSettingsMode();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Keyboard navigation ────────────────────────────────────────
|
||||
|
||||
function handleMenuKeydown(event) {
|
||||
switch (event.key) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
moveSelection(-1);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
moveSelection(1);
|
||||
break;
|
||||
case "Enter":
|
||||
case " ":
|
||||
event.preventDefault();
|
||||
activateSelected();
|
||||
break;
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
console.log("[CO Main Menu] Back");
|
||||
window.CommonwealthOnlineUI.onBack();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (viewMode === "settings") {
|
||||
settingsController.handleKeydown(event);
|
||||
return;
|
||||
}
|
||||
handleMenuKeydown(event);
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user