Partially cleaned up UX from pathfinding PR

- Reorganized sidebar into tabs
- Limited pathfinding markers to needing the tab open
- Draw path over junctions but under speed signage
This commit is contained in:
Pyro Nicampt committed 2026-08-09 23:08:02 -05:00
1 parent 8f7f5ae384
commit 92b9ad2c1a
7 files changed
+182 -93

No files matched your search

+3 -1
View File
@@ -3,4 +3,6 @@ A map to let you see the valley in more detail than the in-game map, featuring s
## Current Plans
- [ ] Generally improve the look and UX of the map
- [ ] Add Points of Interest, like Stations, service points, shop locations, and maybe even the potential spawn locations of wrecked demonstrators.
## Contribution Notes
Please avoid using AI to generate code for pull requests. It makes it much more difficult to maintain and slows maintainence of features to a crawl. This project was built by hand with a specific architecture, AI may make functional code, but not sensible code.
+68 -37
View File
@@ -14,6 +14,21 @@
<script type="module" src="index.js"></script>
</head>
<body>
<svg width="0" height="0" style="display: none;">
<symbol id="Info" fill="currentColor" viewBox="0 0 100 100">
<path d="M50 2C23 2 2 23 2 50 2 77 23 98 50 98 77 98 98 77 98 50 98 23 77 2 50 2Zm0 12C70 14 86 30 86 50 86 70 70 86 50 86 30 86 14 70 14 50 14 30 30 14 50 14Zm0 8c-2 0-3 0-5 1-1 1-2 3-2 4 0 2 1 4 2 4 1 1 2 1 5 1 2 0 3 0 5-1 1-1 2-3 2-4 0-2-1-4-2-4C53 22 52 22 50 22Zm-6 16V78H56V38Z"></path>
</symbol>
<symbol id="Key" fill="currentColor" viewBox="0 0 100 100">
<path d="m3 71.3c0 12 9 22 22 22 12 0 21-10 21-22 0-4-1-8-3-12l18-21 10 8 5 4 8-9-5-4-10-9 6-7 11 10 5 4 8-9-5-4L74 5.3 34 51.3c-3-1-6-2-9-2-13 0-22 10-22 22zm12 0c0-5 4-10 10-10 5 0 9 5 9 10 0 6-4 10-9 10-6 0-10-4-10-10z"></path>
</symbol>
<symbol id="Settings" fill="currentColor" viewBox="0 0 100 100">
<path d="M45 2C42 2 39 5 39 8L39 16C34 17 30 20 26 23L20 20C19 19 18 19 17 19 15 18 12 20 11 22L6 30C4 33 6 37 8 39L15 42C14 45 14 47 14 50 14 53 14 55 15 58L8 61C6 63 4 67 6 70L11 78C12 81 17 82 20 80L26 77C30 80 34 83 39 84L39 92C39 95 42 98 45 98L55 98C58 98 61 95 61 92L61 84C66 83 70 80 74 77L80 80C83 82 88 81 89 78L94 70C96 67 94 63 92 61L85 58C86 55 86 53 86 50 86 47 86 45 85 42L92 39C94 37 96 33 94 30L89 22C88 20 85 18 83 19 82 19 81 19 80 20L74 23C70 20 66 17 61 16L61 8C61 5 58 2 55 2L45 2zM50 28C62 28 72 38 72 50 72 62 62 72 50 72 38 72 28 62 28 50 28 38 38 28 50 28z"></path>
</symbol>
<symbol id="Pathfinding" fill="currentColor" viewBox="0 0 100 100">
<path d="M62 5C55 5 46 8 36 12A7 7 0 0032 22 7 7 0 0042 25C54 19 62 19 66 19 71 20 71 21 71 23 71 28 64 40 45 43 21 46 9 65 15 80 18 88 25 94 34 95 43 96 54 93 67 85A7 7 0 0069 75 7 7 0 0059 73C48 80 41 82 36 81 31 80 29 78 28 76 26 70 29 59 47 57 71 53 86 38 85 23 84 15 78 8 69 6 67 5 65 5 62 5zM15 11C10 11 6 15 6 20 6 24 10 31 15 41 20 31 24 24 24 20 24 15 20 11 15 11zM84 53C80 53 76 57 76 61 76 66 79 73 84 82 90 73 93 66 93 61 93 57 89 53 84 53z"></path>
</symbol>
</svg>
<div id="mapContainer">
<canvas id="mapCanvas"></canvas>
<canvas id="dynCanvas"></canvas>
@@ -27,7 +42,7 @@
</div>
<div id="zoomLevelDisplay"></div>
<div id="cursorCoordDisplay"></div>
Map v0.6.7 alpha
Map v0.6.9 alpha
<br>
Updated for Game Build 99.7
<br>
@@ -39,14 +54,52 @@
<span id="redrawTrigger"></span>DV Community Map
</div>
</div>
<div id="directionsPanel">
<div id="directionsContents" class="panel">
<div id="directionsHeader">
<span>Pathfinding</span>
<div id="directionsHeaderRight">
<span class="quiet">Click map to set stops</span>
<div id="sidepanel" style="visibility: hidden;">
<div class="panel linkpanel">
<a href="https://github.com/PyroNicampt/DV-Community-Map">Source</a>
<a href="https://github.com/PyroNicampt/DV-Community-Map/issues">Issue Tracker</a>
<a href="https://derailvalley.wiki.gg/wiki/Derail_Valley_Wiki">DV Wiki</a>
<a href="https://wiki.derailvalley.com/wiki/Main_Page">Official Manual</a>
<a href="https://github.com/PyroNicampt/DV-Community-Map/releases">Location Mod</a>
</div>
<div id="tabContents">
<div data-tab="open">
<div class="tabContent">
Welcome to the WIP community map of <a href="https://www.derailvalley.com/" style="white-space:nowrap;">Derail Valley</a>, a resource for documenting and displaying all tracks and points of interest.
<p>
Problems? Suggestions? Head over to <a href="https://github.com/PyroNicampt/DV-Community-Map/issues">the issue tracker</a> and see if it's already there, or submit if not. Or contribute a fix yourself if you really want!
</p>
<p>
Click the tabs to the left to view the Legend, Options, and more.
</p>
<hr style="margin: 5px 0;">
Hover over tracks and objects for detailed information.
<hr style="margin: 5px 0;">
Last update <span id="lastUpdate"></span>:
<div id="changelog"></div>
</div>
<div class="tabButton" title="Info">
<svg class="inlineSvg"><use href="#Info"></use></svg>
</div>
</div>
<div>
<div class="tabContent">
<h3>Key</h3>
<hr>
<div id="legendKey"></div>
<hr>
Track Colors:
<div id="legendKeyColors"></div>
</div>
<div class="tabButton" title="Legend">
<svg class="inlineSvg"><use href="#Key"></use></svg>
</div>
</div>
<div id="directionsTab">
<div class="tabContent">
<h3>Pathfinding</h3>
<hr>
Click map to set stops.
<div id="directionsBody">
<div id="directionsStops"></div>
<div id="directionsControls">
@@ -61,43 +114,21 @@
<div id="directionsSummary" class="quiet"></div>
</div>
</div>
<button id="directionsToggle" class="panel orange directionToggle" title="Collapse or expand pathfinding panel">&lt;</button>
<div class="tabButton" title="Pathfinding">
<svg class="inlineSvg"><use href="#Pathfinding"></use></svg>
</div>
<div id="legend">
<div id="legendContents">
<div class="panel linkpanel">
<a href="https://github.com/PyroNicampt/DV-Community-Map">Source</a>
<a href="https://github.com/PyroNicampt/DV-Community-Map/issues">Issue Tracker</a>
<a href="https://derailvalley.wiki.gg/wiki/Derail_Valley_Wiki">DV Wiki</a>
<a href="https://wiki.derailvalley.com/wiki/Main_Page">Official Manual</a>
<a href="https://github.com/PyroNicampt/DV-Community-Map/releases">Location Mod</a>
</div>
<div class="panel">
Welcome to the WIP community map of <a href="https://www.derailvalley.com/" style="white-space:nowrap;">Derail Valley</a>, a resource for documenting and displaying all tracks and points of interest.
<p>
Problems? Suggestions? Head over to <a href="https://github.com/PyroNicampt/DV-Community-Map/issues">the issue tracker</a> and see if it's already there, or submit if not. Or contribute a fix yourself if you really want!
</p>
<hr style="margin: 5px 0;">
Hover over tracks and objects for detailed information.
<hr style="margin: 5px 0;">
Last update <span id="lastUpdate"></span>:
<div id="changelog"></div>
</div>
<div class="panel">
<h3>Key</h3>
<hr>
<div id="legendKey"></div>
<hr>
Track Colors:
<div id="legendKeyColors"></div>
</div>
<div id="settingsPanel" class="panel">
<div>
<div id="settingsPanel" class="tabContent">
<h3>Map Settings</h3>
<div class="quiet" style="margin: 0 0 5px 0;font-size: 10px;">(Page uses local-only cookies to keep settings)</div>
<hr>
</div>
<div class="tabButton" title="Settings">
<svg class="inlineSvg"><use href="#Settings"></use></svg>
</div>
</div>
</div>
<button id="legendButton" class="panel orange"><svg style="transform:rotate(180deg) translateY(0px)" viewBox="0 0 32 32"><path fill="currentColor" d="M31 16 20 31l-16 0L15 16 4 1l16 0Z"/></svg></button>
</div>
</div>
<div style="display:none">
+7 -1
View File
@@ -254,6 +254,8 @@ async function loadTrackData(file){
}
}
MapData.addPoi({type:'dummy_polyline', position:{x:0,y:0,z:0}});
MapData.sortTracks();
MapData.generateTrackSignage();
}
@@ -311,6 +313,7 @@ function redrawMap(){
mapctx.stroke();
}
}
Directions.drawPolyline(mapctx, trackWidth);
let curSprite;
let spriteSize;
let textMeasure;
@@ -324,6 +327,9 @@ function redrawMap(){
let markerY = MapData.view.convertY(marker.position.z);
if(markerX > mapCanvas.width + Config.viewCullMargin || markerX < -Config.viewCullMargin || markerY > mapCanvas.height + Config.viewCullMargin || markerY < -Config.viewCullMargin) continue; // View Culling
switch(marker.type){
case 'dummy_polyline':
Directions.drawPolyline(mapctx, trackWidth);
break;
case 'junction':
if(!(MapData.layers.junctions && MapData.layers.signage)) break;
marker.visible = true;
@@ -494,6 +500,7 @@ function redrawMap(){
);
}
}
Directions.drawMarkers(mapctx);
MapData.view.dirty = false;
@@ -503,7 +510,6 @@ function redrawMap(){
function redrawDynamics(){
dynCanvas.width = mapCanvas.width;
dynCanvas.height = mapCanvas.height;
Directions.draw(dynctx);
let curSprite;
let spriteSize;
for(const marker of MapData.dynamicMarkers){
+10 -34
View File
@@ -61,7 +61,7 @@ function clearPoints(){
state.route = null;
renderStops();
renderSummary('Click the map to set a start point.');
MapData.view.dynDirty = true;
MapData.view.dirty = true;
}
function stopLabel(index){
@@ -146,7 +146,7 @@ function updateRoute(){
if(!state.graph || state.points.length < 2){
state.route = null;
renderSummary('Click the map to set a start and end point.');
MapData.view.dynDirty = true;
MapData.view.dirty = true;
return;
}
@@ -160,7 +160,7 @@ function updateRoute(){
if(!result){
state.route = null;
renderSummary('No route found with current limits.');
MapData.view.dynDirty = true;
MapData.view.dirty = true;
return;
}
combinedEdges = combinedEdges.concat(result.edges);
@@ -193,7 +193,7 @@ function updateRoute(){
`<span title="Highest effective speed used in estimation after applying limits, grade penalty, and curve caps.">Max speed ${Math.round(stats.maxSpeed)} km/h</span>`,
`<span title="Highest posted track speed limit encountered (with realistic caps for unposted sections).">Max speed limit ${Math.round(stats.maxSpeedLimit)} km/h</span>`,
].join(' | ');
MapData.view.dynDirty = true;
MapData.view.dirty = true;
}
function scheduleRouteUpdate(){
@@ -205,13 +205,13 @@ function scheduleRouteUpdate(){
});
}
function drawPolyline(ctx){
export function drawPolyline(ctx, trackWidth){
if(!state.route || !state.route.polyline || state.route.polyline.length < 2) return;
ctx.save();
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
const width = 6 * MapData.view.pixelRatio;
const outline = 10 * MapData.view.pixelRatio;
const width = trackWidth * 1.3;
const outline = trackWidth * 1.3 + 5;
ctx.beginPath();
for(let i=0; i<state.route.polyline.length; i++){
@@ -231,7 +231,7 @@ function drawPolyline(ctx){
ctx.restore();
}
function drawMarkers(ctx){
export function drawMarkers(ctx){
const radius = Config.routing.markerRadius * MapData.view.pixelRatio;
ctx.save();
ctx.textAlign = 'center';
@@ -257,6 +257,7 @@ function drawMarkers(ctx){
function handleMapPointerUp(e){
if(e.button !== 0) return;
if(state.draggingIndex != null) return;
if(!state.ui.tab.dataset.tab) return;
const down = state.ui.lastPointerDown;
if(!down) return;
const dx = e.clientX - down.x;
@@ -283,8 +284,7 @@ function handleMapPointerUp(e){
export function initialize(){
state.graph = Pathfinder.buildGraph(MapData.railTracks);
state.ui.panel = document.getElementById('directionsPanel');
state.ui.contents = document.getElementById('directionsContents');
state.ui.tab = document.getElementById('directionsTab');
state.ui.stops = document.getElementById('directionsStops');
state.ui.summary = document.getElementById('directionsSummary');
state.ui.addStop = document.getElementById('directionsAddStop');
@@ -323,24 +323,6 @@ export function initialize(){
clearPoints();
});
const updatePanelState = () => {
const width = state.ui.contents.offsetWidth;
if(state.ui.panel.classList.contains('collapsed')){
state.ui.panel.style.transform = `translateX(${-width}px)`;
state.ui.toggle.textContent = '>';
}else{
state.ui.panel.style.transform = 'translateX(0px)';
state.ui.toggle.textContent = '<';
}
};
state.ui.toggle.addEventListener('click', () => {
state.ui.panel.classList.toggle('collapsed');
updatePanelState();
});
window.addEventListener('resize', updatePanelState);
const mapContainer = document.getElementById('mapContainer');
mapContainer.addEventListener('pointerdown', e => {
state.ui.lastPointerDown = {x:e.clientX, y:e.clientY};
@@ -349,12 +331,6 @@ export function initialize(){
renderStops();
renderSummary('Click the map to set a start point.');
updatePanelState();
}
export function draw(ctx){
drawPolyline(ctx);
drawMarkers(ctx);
}
function hitTestMarker(e){
+3
View File
@@ -122,6 +122,8 @@ function markerSortFunction(a, b){
if(b.type == 'station') return -1;
if(a.type == 'speed') return 1;
if(b.type == 'speed') return -1;
if(a.type == 'dummy_polyline') return 1;
if(b.type == 'dummy_polyline') return -1;
if(a.type == 'junction') return 1;
if(b.type == 'junction') return -1;
if(a.type == 'service') return 1;
@@ -306,6 +308,7 @@ export function addPoi(poiData, level = 0){
newPoi.radius = poiData.radius;
break;
default:
if(poiData.type.startsWith('dummy_')) break;
newPoi.tooltip = `<h1>${newPoi.name}</h1>${poiData.description ? '<hr>'+poiData.description : ''}`;
newPoi.minZoom = 0.07;
break;
+46 -10
View File
@@ -316,18 +316,10 @@ export function initialize(){
const legend = document.getElementById('legend');
const legendContents = document.getElementById('legendContents');
const legendButton = document.getElementById('legendButton');
const legendArrow = legendButton.children[0];
let legendState = true;
let legendButtonEvent = e => {
legendArrow.style.transform = `rotate(${legendState ? 180 : 0}deg) translateY(${legendState ? -0 : 0}px)`;
legend.style.transform = `translateX(${legendState ? 0 : -legendContents.clientWidth}px)`;
legendState = !legendState;
};
legendButton.addEventListener('click', legendButtonEvent);
legendButtonEvent();
if(document.body.clientWidth <= 700) legendButtonEvent();
populateKey();
setLastUpdateData();
initTabs();
document.getElementById('sidepanel').style.visibility = '';
};
function addSettingEntry(thisSetting, parent, indent=0){
@@ -551,6 +543,50 @@ async function setLastUpdateData(){
setTimeout(() => {setLastUpdateData()}, 600000);
}
async function initTabs(){
const tabContents = document.getElementById('tabContents');
let tabs = [];
let totalOffset = 5;
let maxButtonWidth = 0;
const updateTabPosition = tabData => {
//tabData.element.style.zIndex = tabData.visible ? 10 : '';
tabData.element.style.transform = `translateX(${tabData.visible ? maxButtonWidth : -(tabData.content.clientWidth + 10)}px)`;
};
for(let tabButton of document.querySelectorAll('#tabContents .tabButton')){
maxButtonWidth = Math.max(maxButtonWidth, tabButton.clientWidth);
}
for(let tabElement of tabContents.children){
let newTab = {
element: tabElement,
button: tabElement.getElementsByClassName('tabButton')[0],
content: tabElement.getElementsByClassName('tabContent')[0],
visible: tabElement.dataset.tab,
};
newTab.button.style.transform = `translateY(${totalOffset}px)`;
totalOffset += newTab.button.clientHeight + 6;
updateTabPosition(newTab);
tabs.push(newTab);
newTab.button.addEventListener('click', () => {
for(let tab of tabs){
if(tab == newTab)
tab.visible = !tab.visible;
else
tab.visible = false;
if(tab.visible)
tab.element.dataset.tab = 'open';
else
delete tab.element.dataset.tab;
updateTabPosition(tab);
}
});
}
await new Promise(requestAnimationFrame);
tabContents.classList.add('active');
}
function populateKey(){
addKeyEntry('gradeArrow', 'Grade Indication')
addKeyEntry('speed_5', 'Speed Limit');
+45 -10
View File
@@ -3,6 +3,9 @@
:root{
--absScale: 1;
--legendButtonSize: 50px;
--accentCol: #e79830;
--bg1: #2c3f58;
--bg2: #191e2e;
}
html *{
@@ -28,11 +31,11 @@ a:visited{
color: #5ea8e4;
}
a:hover{
color: #e79830;
color: var(--accentCol);
}
.orange{
color: #e79830;
.orange, .tabButton{
color: var(--accentCol);
}
.quiet{
@@ -44,9 +47,9 @@ a:hover{
color: #fff;
}
.panel{
.panel, .tabContent, .tabButton{
border: 2px solid #b3b3b396;
background: linear-gradient(120deg, #2c3f58, #191e2e);
background: linear-gradient(120deg, var(--bg1), var(--bg2));
text-shadow: 2px 2px 5px #0003;
margin: 5px;
padding: 8px 13px;
@@ -54,7 +57,7 @@ a:hover{
pointer-events: all;
}
.panel hr {
.panel hr, .tabContent hr {
border: 0px;
border-bottom: 2px solid #b3b3b396;
}
@@ -77,7 +80,7 @@ a:hover{
}
a:hover{
color: #31281b;
background: #e79830;
background: var(--accentCol);
}
}
@@ -88,11 +91,42 @@ a:hover{
pointer-events: none;
height: 100vh;
overflow: hidden;
max-width: min(500px,100vw);
display: flex;
flex-direction: column;
}
#tabContents{
margin-left: 5px;
>div{
position: fixed;
display: flex;
align-items: flex-start;
>div{
max-width: min(500px,100vw);
margin: 0;
}
}
}
#tabContents.active>div{
transition: transform 400ms cubic-bezier(0.445, 0.050, 0.550, 0.950);
}
.tabButton{
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
font-size: 25px;
padding: 8px;
background: var(--bg1);
cursor: pointer;
transition: all 0.1s cubic-bezier(0.250, 0.460, 0.450, 0.940);
}
.tabButton:hover{
background: var(--accentCol);
color: #000;
}
#headerbar{
background: linear-gradient(120deg, #3d5779, #121522);
font-size: 2em;
@@ -164,6 +198,7 @@ a:hover{
#directionsBody{
display: flex;
flex-direction: column;
max-width: 250px;
}
.directionToggle{
@@ -254,9 +289,9 @@ a:hover{
}
.directionButton.active{
border-color: #e79830;
border-color: var(--accentCol);
color: #31281b;
background: #e79830;
background: var(--accentCol);
}
.directionButton:disabled{