Move the Commonwealth Online settings screen into a shared module with its own data, styles, and controller API. The pause menu now mounts the reusable component, keeps the prompt bar behavior consistent, and animates the panel open/close with a subtler transition.
828 lines
23 KiB
JavaScript
828 lines
23 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
// ── C++ / F4SE bridge placeholders ─────────────────────────────
|
|
// Real integration will replace these stubs from the plugin side.
|
|
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 Settings");
|
|
},
|
|
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 pause-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 pauseScreen = document.getElementById("pauseScreen");
|
|
var menu = document.getElementById("menu");
|
|
var settingsPanel = document.getElementById("settingsPanel");
|
|
var pausePromptBar = document.getElementById("pausePromptBar");
|
|
var settingsPromptBar = document.getElementById("settingsPromptBar");
|
|
var mapPromptBar = document.getElementById("mapPromptBar");
|
|
var mapBackButton = document.getElementById("mapBackButton");
|
|
var serverPanel = document.getElementById("serverPanel");
|
|
var leftPanel = document.querySelector(".left-panel");
|
|
var mapWorld = document.getElementById("mapWorld");
|
|
|
|
if (!menu || !settingsPanel || !window.CO_Settings) {
|
|
return;
|
|
}
|
|
|
|
var settingsController = null;
|
|
|
|
var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item"));
|
|
var SETTINGS_INDEX = 4;
|
|
|
|
// viewMode: "pause" | "map" | "settings"
|
|
var viewMode = "pause";
|
|
var selectedIndex = -1;
|
|
var SETTINGS_TRANSITION_MS = 220;
|
|
var settingsHideFallbackId = 0;
|
|
var settingsHideOnTransitionEnd = null;
|
|
|
|
// ── Pause 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;
|
|
}
|
|
setSelected(next);
|
|
}
|
|
|
|
function activateSelected() {
|
|
var item = items[selectedIndex];
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
var action = item.getAttribute("data-action");
|
|
console.log("[CO Pause Menu] Selected:", action);
|
|
|
|
if (action === "resume") {
|
|
console.log("[CO Pause Menu] Resume game (concept)");
|
|
window.CommonwealthOnlineUI.onOpenView("resume");
|
|
setSelected(-1);
|
|
} else if (action === "map") {
|
|
enterMapMode();
|
|
} else if (action === "settings") {
|
|
enterSettingsMode();
|
|
} else if (action === "social") {
|
|
window.CommonwealthOnlineUI.onOpenView("social");
|
|
} else if (action === "server") {
|
|
window.CommonwealthOnlineUI.onOpenView("server");
|
|
} else if (action === "help") {
|
|
window.CommonwealthOnlineUI.onOpenView("help");
|
|
} else if (action === "quit") {
|
|
console.log("[CO Pause Menu] Quit to main menu (concept)");
|
|
window.CommonwealthOnlineUI.onOpenView("quit");
|
|
setSelected(-1);
|
|
}
|
|
}
|
|
|
|
items.forEach(function (item, index) {
|
|
item.addEventListener("click", function () {
|
|
if (viewMode === "settings") {
|
|
if (index === SETTINGS_INDEX) {
|
|
return;
|
|
}
|
|
exitSettingsMode();
|
|
}
|
|
if (viewMode === "map") {
|
|
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 (pauseScreen) {
|
|
pauseScreen.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 (pauseScreen) {
|
|
pauseScreen.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() {
|
|
if (viewMode === "map") {
|
|
exitMapMode();
|
|
}
|
|
|
|
viewMode = "settings";
|
|
|
|
console.log("[CO Pause Menu] Entering settings mode");
|
|
window.CommonwealthOnlineUI.onOpenView("settings");
|
|
|
|
setSelected(SETTINGS_INDEX);
|
|
|
|
showSettingsPanelAnimated();
|
|
if (pausePromptBar) {
|
|
pausePromptBar.hidden = true;
|
|
}
|
|
if (mapPromptBar) {
|
|
mapPromptBar.hidden = true;
|
|
}
|
|
if (mapWorld) {
|
|
mapWorld.classList.add("is-blurred");
|
|
mapWorld.classList.remove("is-interactive");
|
|
}
|
|
|
|
settingsController.show();
|
|
}
|
|
|
|
function exitSettingsMode() {
|
|
if (viewMode !== "settings") {
|
|
return;
|
|
}
|
|
|
|
viewMode = "pause";
|
|
console.log("[CO Pause Menu] Exiting settings mode");
|
|
window.CommonwealthOnlineUI.onBack();
|
|
|
|
settingsController.hide();
|
|
|
|
hideSettingsPanelAnimated(function () {
|
|
if (pausePromptBar) {
|
|
pausePromptBar.hidden = false;
|
|
}
|
|
setSelected(SETTINGS_INDEX);
|
|
});
|
|
}
|
|
|
|
settingsController = window.CO_Settings.create({
|
|
mount: settingsPanel,
|
|
promptBar: settingsPromptBar,
|
|
bridge: window.CommonwealthOnlineUI,
|
|
layout: "embedded",
|
|
onBack: function () {
|
|
exitSettingsMode();
|
|
}
|
|
});
|
|
|
|
function enterMapMode() {
|
|
if (viewMode === "settings") {
|
|
cancelSettingsPanelHide();
|
|
settingsController.hide();
|
|
settingsPanel.classList.remove("is-open");
|
|
settingsPanel.hidden = true;
|
|
if (pauseScreen) {
|
|
pauseScreen.classList.remove("is-settings-mode");
|
|
}
|
|
}
|
|
|
|
viewMode = "map";
|
|
console.log("[CO Pause Menu] Entering map mode");
|
|
window.CommonwealthOnlineUI.onOpenView("map");
|
|
|
|
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 (viewMode !== "map") {
|
|
return;
|
|
}
|
|
|
|
viewMode = "pause";
|
|
console.log("[CO Pause Menu] Exiting map mode");
|
|
window.CommonwealthOnlineUI.onBack();
|
|
|
|
if (mapBackButton) {
|
|
mapBackButton.classList.remove("is-visible");
|
|
mapBackButton.setAttribute("aria-hidden", "true");
|
|
}
|
|
if (leftPanel) {
|
|
leftPanel.classList.remove("is-hidden");
|
|
}
|
|
if (serverPanel) {
|
|
serverPanel.style.display = "";
|
|
}
|
|
if (mapWorld) {
|
|
mapWorld.classList.add("is-blurred");
|
|
mapWorld.classList.remove("is-interactive");
|
|
}
|
|
if (pausePromptBar) {
|
|
pausePromptBar.hidden = false;
|
|
}
|
|
if (mapPromptBar) {
|
|
mapPromptBar.hidden = true;
|
|
}
|
|
|
|
setSelected(-1);
|
|
}
|
|
|
|
// ── Keyboard navigation ────────────────────────────────────────
|
|
|
|
function handlePauseKeydown(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 Pause Menu] Back");
|
|
window.CommonwealthOnlineUI.onBack();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleMapKeydown(event) {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
exitMapMode();
|
|
}
|
|
}
|
|
|
|
document.addEventListener("keydown", function (event) {
|
|
if (viewMode === "settings") {
|
|
settingsController.handleKeydown(event);
|
|
return;
|
|
}
|
|
if (viewMode === "map") {
|
|
handleMapKeydown(event);
|
|
return;
|
|
}
|
|
handlePauseKeydown(event);
|
|
});
|
|
|
|
// ── Map mode PC back button ────────────────────────────────────
|
|
|
|
if (mapBackButton) {
|
|
mapBackButton.addEventListener("click", function (event) {
|
|
event.stopPropagation();
|
|
exitMapMode();
|
|
});
|
|
}
|
|
|
|
// ── Blur map initially ─────────────────────────────────────────
|
|
|
|
if (mapWorld) {
|
|
mapWorld.classList.add("is-blurred");
|
|
}
|
|
|
|
// ── Map pan/zoom ───────────────────────────────────────────────
|
|
|
|
if (pauseScreen && mapWorld) {
|
|
var MAX_ZOOM = 4;
|
|
var ZOOM_STEP = 0.18;
|
|
var ZOOM_SMOOTHING = 0.18;
|
|
var ZOOM_SNAP_EPSILON = 0.001;
|
|
var PAN_SMOOTHING = 0.28;
|
|
var PAN_SNAP_EPSILON = 0.15;
|
|
var PAN_INERTIA_FRICTION = 0.92;
|
|
var PAN_INERTIA_MIN_SPEED = 0.35;
|
|
var PAN_VELOCITY_IDLE_MS = 80;
|
|
var zoom = 1;
|
|
var targetZoom = 1;
|
|
var MIN_ZOOM = 1;
|
|
var panX = 0;
|
|
var panY = 0;
|
|
var targetPanX = 0;
|
|
var targetPanY = 0;
|
|
var velocityX = 0;
|
|
var velocityY = 0;
|
|
var isDragging = false;
|
|
var lastX = 0;
|
|
var lastY = 0;
|
|
var lastMoveTime = 0;
|
|
var zoomRafId = 0;
|
|
var panRafId = 0;
|
|
var zoomFocusScreenX = 0;
|
|
var zoomFocusScreenY = 0;
|
|
var zoomFocusWorldX = 0;
|
|
var zoomFocusWorldY = 0;
|
|
|
|
var mapImg = new Image();
|
|
var mapNaturalWidth = 1024;
|
|
var mapNaturalHeight = 768;
|
|
var mapDisplayWidth = 1024;
|
|
var mapDisplayHeight = 768;
|
|
|
|
mapImg.onload = function () {
|
|
mapNaturalWidth = mapImg.naturalWidth;
|
|
mapNaturalHeight = mapImg.naturalHeight;
|
|
updateMapDisplayBounds();
|
|
};
|
|
mapImg.src = "assets/map.png";
|
|
|
|
function updateMapDisplayBounds() {
|
|
var viewportWidth = window.innerWidth;
|
|
var viewportHeight = window.innerHeight;
|
|
var mapAspect = mapNaturalWidth / mapNaturalHeight;
|
|
var viewportAspect = viewportWidth / viewportHeight;
|
|
|
|
if (mapAspect > viewportAspect) {
|
|
mapDisplayWidth = viewportWidth;
|
|
mapDisplayHeight = viewportWidth / mapAspect;
|
|
} else {
|
|
mapDisplayHeight = viewportHeight;
|
|
mapDisplayWidth = viewportHeight * mapAspect;
|
|
}
|
|
|
|
MIN_ZOOM = Math.max(
|
|
viewportWidth / mapDisplayWidth,
|
|
viewportHeight / mapDisplayHeight
|
|
);
|
|
|
|
if (zoom < MIN_ZOOM) {
|
|
zoom = MIN_ZOOM;
|
|
targetZoom = MIN_ZOOM;
|
|
panX = 0;
|
|
panY = 0;
|
|
applyMapTransform();
|
|
}
|
|
}
|
|
|
|
updateMapDisplayBounds();
|
|
window.addEventListener("resize", updateMapDisplayBounds);
|
|
|
|
function applyMapTransform() {
|
|
pauseScreen.style.setProperty("--map-zoom", String(zoom));
|
|
pauseScreen.style.setProperty("--map-pan-x", panX + "px");
|
|
pauseScreen.style.setProperty("--map-pan-y", panY + "px");
|
|
}
|
|
|
|
function getPanLimits() {
|
|
var zoomedMapWidth = mapDisplayWidth * zoom;
|
|
var zoomedMapHeight = mapDisplayHeight * zoom;
|
|
var viewportWidth = window.innerWidth;
|
|
var viewportHeight = window.innerHeight;
|
|
return {
|
|
maxX: Math.max(0, (zoomedMapWidth - viewportWidth) / 2),
|
|
maxY: Math.max(0, (zoomedMapHeight - viewportHeight) / 2),
|
|
};
|
|
}
|
|
|
|
function clampPanValue(x, y) {
|
|
var limits = getPanLimits();
|
|
return {
|
|
x: Math.max(-limits.maxX, Math.min(limits.maxX, x)),
|
|
y: Math.max(-limits.maxY, Math.min(limits.maxY, y)),
|
|
};
|
|
}
|
|
|
|
function clampPan() {
|
|
var clamped = clampPanValue(panX, panY);
|
|
panX = clamped.x;
|
|
panY = clamped.y;
|
|
}
|
|
|
|
function clampTargetPan() {
|
|
var clamped = clampPanValue(targetPanX, targetPanY);
|
|
targetPanX = clamped.x;
|
|
targetPanY = clamped.y;
|
|
}
|
|
|
|
function stopZoomAnimation() {
|
|
if (zoomRafId !== 0) {
|
|
cancelAnimationFrame(zoomRafId);
|
|
zoomRafId = 0;
|
|
}
|
|
targetZoom = zoom;
|
|
}
|
|
|
|
function stopPanAnimation() {
|
|
if (panRafId !== 0) {
|
|
cancelAnimationFrame(panRafId);
|
|
panRafId = 0;
|
|
}
|
|
targetPanX = panX;
|
|
targetPanY = panY;
|
|
velocityX = 0;
|
|
velocityY = 0;
|
|
}
|
|
|
|
function applyZoomFocusPan() {
|
|
// Keep the focused map point under the cursor while scale changes
|
|
// (transform-origin is center; pan is applied after scale).
|
|
panX = zoomFocusScreenX - zoomFocusWorldX * zoom;
|
|
panY = zoomFocusScreenY - zoomFocusWorldY * zoom;
|
|
clampPan();
|
|
targetPanX = panX;
|
|
targetPanY = panY;
|
|
}
|
|
|
|
function tickZoomAnimation() {
|
|
zoomRafId = 0;
|
|
|
|
var zoomDelta = targetZoom - zoom;
|
|
if (Math.abs(zoomDelta) <= ZOOM_SNAP_EPSILON) {
|
|
zoom = targetZoom;
|
|
} else {
|
|
zoom += zoomDelta * ZOOM_SMOOTHING;
|
|
}
|
|
|
|
applyZoomFocusPan();
|
|
applyMapTransform();
|
|
|
|
if (zoom !== targetZoom) {
|
|
zoomRafId = requestAnimationFrame(tickZoomAnimation);
|
|
}
|
|
}
|
|
|
|
function startZoomAnimation() {
|
|
stopPanAnimation();
|
|
if (zoomRafId === 0) {
|
|
zoomRafId = requestAnimationFrame(tickZoomAnimation);
|
|
}
|
|
}
|
|
|
|
function tickPanAnimation() {
|
|
panRafId = 0;
|
|
var keepGoing = false;
|
|
|
|
if (isDragging) {
|
|
var dragDx = targetPanX - panX;
|
|
var dragDy = targetPanY - panY;
|
|
if (Math.abs(dragDx) <= PAN_SNAP_EPSILON && Math.abs(dragDy) <= PAN_SNAP_EPSILON) {
|
|
panX = targetPanX;
|
|
panY = targetPanY;
|
|
} else {
|
|
panX += dragDx * PAN_SMOOTHING;
|
|
panY += dragDy * PAN_SMOOTHING;
|
|
keepGoing = true;
|
|
}
|
|
clampPan();
|
|
} else {
|
|
var nextX = panX + velocityX;
|
|
var nextY = panY + velocityY;
|
|
var clamped = clampPanValue(nextX, nextY);
|
|
panX = clamped.x;
|
|
panY = clamped.y;
|
|
targetPanX = panX;
|
|
targetPanY = panY;
|
|
|
|
if (panX !== nextX) {
|
|
velocityX = 0;
|
|
}
|
|
if (panY !== nextY) {
|
|
velocityY = 0;
|
|
}
|
|
|
|
velocityX *= PAN_INERTIA_FRICTION;
|
|
velocityY *= PAN_INERTIA_FRICTION;
|
|
|
|
if (
|
|
Math.abs(velocityX) > PAN_INERTIA_MIN_SPEED ||
|
|
Math.abs(velocityY) > PAN_INERTIA_MIN_SPEED
|
|
) {
|
|
keepGoing = true;
|
|
} else {
|
|
velocityX = 0;
|
|
velocityY = 0;
|
|
}
|
|
}
|
|
|
|
applyMapTransform();
|
|
|
|
if (keepGoing) {
|
|
panRafId = requestAnimationFrame(tickPanAnimation);
|
|
}
|
|
}
|
|
|
|
function startPanAnimation() {
|
|
if (panRafId === 0) {
|
|
panRafId = requestAnimationFrame(tickPanAnimation);
|
|
}
|
|
}
|
|
|
|
mapWorld.addEventListener(
|
|
"wheel",
|
|
function (event) {
|
|
if (viewMode !== "map") {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
|
|
var nextTarget = Math.max(
|
|
MIN_ZOOM,
|
|
Math.min(MAX_ZOOM, targetZoom + (event.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP))
|
|
);
|
|
if (nextTarget === targetZoom && zoom === targetZoom) {
|
|
return;
|
|
}
|
|
|
|
// Anchor on the map point currently under the cursor.
|
|
zoomFocusScreenX = event.clientX - window.innerWidth / 2;
|
|
zoomFocusScreenY = event.clientY - window.innerHeight / 2;
|
|
zoomFocusWorldX = (zoomFocusScreenX - panX) / zoom;
|
|
zoomFocusWorldY = (zoomFocusScreenY - panY) / zoom;
|
|
targetZoom = nextTarget;
|
|
startZoomAnimation();
|
|
},
|
|
{ passive: false }
|
|
);
|
|
|
|
mapWorld.addEventListener("mousedown", function (event) {
|
|
if (viewMode !== "map" || zoom <= MIN_ZOOM) {
|
|
return;
|
|
}
|
|
stopZoomAnimation();
|
|
isDragging = true;
|
|
targetPanX = panX;
|
|
targetPanY = panY;
|
|
velocityX = 0;
|
|
velocityY = 0;
|
|
lastX = event.clientX;
|
|
lastY = event.clientY;
|
|
lastMoveTime = performance.now();
|
|
pauseScreen.classList.add("is-panning");
|
|
startPanAnimation();
|
|
});
|
|
|
|
window.addEventListener("mousemove", function (event) {
|
|
if (!isDragging) {
|
|
return;
|
|
}
|
|
|
|
var dx = event.clientX - lastX;
|
|
var dy = event.clientY - lastY;
|
|
var now = performance.now();
|
|
|
|
targetPanX += dx;
|
|
targetPanY += dy;
|
|
clampTargetPan();
|
|
|
|
// Blend recent movement into release velocity for inertia.
|
|
velocityX = velocityX * 0.55 + dx * 0.45;
|
|
velocityY = velocityY * 0.55 + dy * 0.45;
|
|
|
|
lastX = event.clientX;
|
|
lastY = event.clientY;
|
|
lastMoveTime = now;
|
|
startPanAnimation();
|
|
});
|
|
|
|
window.addEventListener("mouseup", function () {
|
|
if (!isDragging) {
|
|
return;
|
|
}
|
|
|
|
isDragging = false;
|
|
pauseScreen.classList.remove("is-panning");
|
|
|
|
// If the pointer stopped before release, do not coast.
|
|
if (performance.now() - lastMoveTime > PAN_VELOCITY_IDLE_MS) {
|
|
velocityX = 0;
|
|
velocityY = 0;
|
|
}
|
|
|
|
// Fold any remaining ease-catchup into the coast.
|
|
velocityX += (targetPanX - panX) * 0.2;
|
|
velocityY += (targetPanY - panY) * 0.2;
|
|
targetPanX = panX;
|
|
targetPanY = panY;
|
|
startPanAnimation();
|
|
});
|
|
}
|
|
|
|
})();
|