Add car rendering
Also split dynamic objects into a separate canvas for faster rendering
This commit is contained in:
1 parent
8daf861088
commit
efa6d7dd1f
5 files changed
+184
-41
No files matched your search
+2
-1
@@ -15,7 +15,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mapContainer">
|
<div id="mapContainer">
|
||||||
<canvas id="mapCanvas" style="width:100%;height:100%"></canvas>
|
<canvas id="mapCanvas"></canvas>
|
||||||
|
<canvas id="dynCanvas"></canvas>
|
||||||
<div id="tooltip" class="panel" style="display:none"></div>
|
<div id="tooltip" class="panel" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import * as Legend from './legend.js';
|
|||||||
|
|
||||||
const mapContainer = document.getElementById('mapContainer');
|
const mapContainer = document.getElementById('mapContainer');
|
||||||
const mapCanvas = document.getElementById('mapCanvas');
|
const mapCanvas = document.getElementById('mapCanvas');
|
||||||
|
const dynCanvas = document.getElementById('dynCanvas');
|
||||||
/** The main map drawing context.
|
/** The main map drawing context.
|
||||||
* @type {CanvasRenderingContext2D} */
|
* @type {CanvasRenderingContext2D} */
|
||||||
const mapctx = mapCanvas.getContext('2d');
|
const mapctx = mapCanvas.getContext('2d');
|
||||||
|
/** The dynamic map drawing context.
|
||||||
|
* @type {CanvasRenderingContext2D} */
|
||||||
|
const dynctx = dynCanvas.getContext('2d');
|
||||||
const mapSprites = document.getElementById('mapSprites');
|
const mapSprites = document.getElementById('mapSprites');
|
||||||
const mapTerrain = document.getElementById('mapTerrain');
|
const mapTerrain = document.getElementById('mapTerrain');
|
||||||
const zoomLevelDisplay = document.getElementById('zoomLevelDisplay');
|
const zoomLevelDisplay = document.getElementById('zoomLevelDisplay');
|
||||||
@@ -23,7 +27,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await loadShopData('shop_items.json');
|
await loadShopData('shop_items.json');
|
||||||
await loadTrackData('trackdata_dv.json');
|
await loadTrackData('trackdata_dv.json');
|
||||||
await loadPoiData('poi_dv.json');
|
await loadPoiData('poi_dv.json');
|
||||||
MapData.markers.push(MapData.playerMarker);
|
|
||||||
MapData.sortMarkers();
|
MapData.sortMarkers();
|
||||||
|
|
||||||
Utils.generateGradeArrows();
|
Utils.generateGradeArrows();
|
||||||
@@ -111,6 +114,9 @@ function mapNavigationSetup(){
|
|||||||
if(ts && MapData.view.dirty){
|
if(ts && MapData.view.dirty){
|
||||||
redrawMap();
|
redrawMap();
|
||||||
}
|
}
|
||||||
|
if(ts && MapData.view.dynDirty){
|
||||||
|
redrawDynamics();
|
||||||
|
}
|
||||||
requestAnimationFrame(navUpdate);
|
requestAnimationFrame(navUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +304,7 @@ function redrawMap(){
|
|||||||
let spriteSize;
|
let spriteSize;
|
||||||
let textMeasure;
|
let textMeasure;
|
||||||
let tempMeasure = {};
|
let tempMeasure = {};
|
||||||
|
//Draw Static Markers
|
||||||
for(const marker of MapData.markers){
|
for(const marker of MapData.markers){
|
||||||
curSprite = null;
|
curSprite = null;
|
||||||
marker.visible = false;
|
marker.visible = false;
|
||||||
@@ -455,29 +462,6 @@ function redrawMap(){
|
|||||||
mapctx.fill();
|
mapctx.fill();
|
||||||
mapctx.globalAlpha = 1.0;
|
mapctx.globalAlpha = 1.0;
|
||||||
break;
|
break;
|
||||||
case 'player':
|
|
||||||
if(!(MapData.layers.poi)) break;
|
|
||||||
marker.visible = true;
|
|
||||||
curSprite = Config.spriteBounds.player;
|
|
||||||
spriteSize = 45;
|
|
||||||
|
|
||||||
mapctx.translate(markerX, markerY);
|
|
||||||
mapctx.rotate(marker.rotation);
|
|
||||||
mapctx.translate(-0.5*spriteSize * MapData.view.pixelRatio, -0.5*spriteSize * MapData.view.pixelRatio);
|
|
||||||
mapctx.drawImage(
|
|
||||||
mapSprites,
|
|
||||||
curSprite.x,
|
|
||||||
curSprite.y,
|
|
||||||
curSprite.width,
|
|
||||||
curSprite.height,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
spriteSize * MapData.view.pixelRatio,
|
|
||||||
spriteSize * MapData.view.pixelRatio
|
|
||||||
);
|
|
||||||
mapctx.resetTransform();
|
|
||||||
curSprite = null;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
if(!MapData.layers.poi) break;
|
if(!MapData.layers.poi) break;
|
||||||
marker.visible = true;
|
marker.visible = true;
|
||||||
@@ -499,5 +483,103 @@ function redrawMap(){
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MapData.view.dirty = false;
|
MapData.view.dirty = false;
|
||||||
|
|
||||||
|
redrawDynamics();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redrawDynamics(){
|
||||||
|
dynCanvas.width = mapCanvas.width;
|
||||||
|
dynCanvas.height = mapCanvas.height;
|
||||||
|
let curSprite;
|
||||||
|
let spriteSize;
|
||||||
|
for(const marker of MapData.dynamicMarkers){
|
||||||
|
curSprite = null;
|
||||||
|
marker.visible = false;
|
||||||
|
if(marker.hidden || (marker.minZoom && MapData.view.scale < marker.minZoom) || (marker.maxZoom && MapData.view.scale > marker.maxZoom)) continue;
|
||||||
|
let markerX = MapData.view.convertX(marker.position.x);
|
||||||
|
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 'player':
|
||||||
|
if(!(MapData.layers.poi)) break;
|
||||||
|
marker.visible = true;
|
||||||
|
curSprite = Config.spriteBounds.player;
|
||||||
|
spriteSize = 45;
|
||||||
|
|
||||||
|
dynctx.translate(markerX, markerY);
|
||||||
|
dynctx.rotate(marker.rotation);
|
||||||
|
dynctx.translate(-0.5 * spriteSize * MapData.view.pixelRatio, -0.5 * spriteSize * MapData.view.pixelRatio);
|
||||||
|
dynctx.drawImage(
|
||||||
|
mapSprites,
|
||||||
|
curSprite.x,
|
||||||
|
curSprite.y,
|
||||||
|
curSprite.width,
|
||||||
|
curSprite.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
spriteSize * MapData.view.pixelRatio,
|
||||||
|
spriteSize * MapData.view.pixelRatio
|
||||||
|
);
|
||||||
|
dynctx.resetTransform();
|
||||||
|
curSprite = null;
|
||||||
|
break;
|
||||||
|
case 'car':
|
||||||
|
if(!(MapData.layers.poi)) break;
|
||||||
|
let scale = MapData.view.pixelRatio * MapData.view.scale;
|
||||||
|
let limitScaleRatio = (MapData.view.pixelRatio * Math.max(MapData.view.scale, 3)) / scale;
|
||||||
|
let width = marker.width * scale;
|
||||||
|
let length = marker.length * scale;
|
||||||
|
|
||||||
|
marker.visible = true;
|
||||||
|
dynctx.beginPath();
|
||||||
|
dynctx.translate(markerX, markerY);
|
||||||
|
dynctx.rotate(marker.rotation);
|
||||||
|
dynctx.translate(-0.5 * width, -0.5 * length);
|
||||||
|
dynctx.rect(0, 0, width, length);
|
||||||
|
dynctx.moveTo(0, width);
|
||||||
|
dynctx.lineTo(0.5 * width, 0);
|
||||||
|
dynctx.lineTo(width, width);
|
||||||
|
dynctx.moveTo(0.5 * width, 0);
|
||||||
|
dynctx.arc(0.5 * width, 0.5 * length, 15, -0.5*Math.PI, 1.5*Math.PI);
|
||||||
|
dynctx.resetTransform();
|
||||||
|
dynctx.strokeStyle = '#fff';
|
||||||
|
dynctx.lineWidth = 0.75 * scale * limitScaleRatio;
|
||||||
|
dynctx.miterLimit = 1.5;
|
||||||
|
dynctx.stroke();
|
||||||
|
|
||||||
|
dynctx.fillStyle = marker.isLoco ? '#e79830aa' : '#559bd4aa';
|
||||||
|
dynctx.fill();
|
||||||
|
dynctx.strokeStyle = '#000';
|
||||||
|
dynctx.lineWidth = 0.3 * scale * limitScaleRatio;
|
||||||
|
dynctx.stroke();
|
||||||
|
if(!marker.tooltipHitzone){
|
||||||
|
marker.tooltipHitzone = {
|
||||||
|
radius: 15,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if(!MapData.layers.poi) break;
|
||||||
|
marker.visible = true;
|
||||||
|
curSprite = Config.spriteBounds.unknown;
|
||||||
|
spriteSize = 30;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(curSprite != null){
|
||||||
|
dynctx.drawImage(
|
||||||
|
mapSprites,
|
||||||
|
curSprite.x,
|
||||||
|
curSprite.y,
|
||||||
|
curSprite.width,
|
||||||
|
curSprite.height,
|
||||||
|
markerX-(0.5*spriteSize)*MapData.view.pixelRatio,
|
||||||
|
markerY-(0.5*spriteSize)*MapData.view.pixelRatio,
|
||||||
|
spriteSize*MapData.view.pixelRatio,
|
||||||
|
spriteSize*MapData.view.pixelRatio
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MapData.view.dynDirty = false;
|
||||||
}
|
}
|
||||||
+69
-15
@@ -8,6 +8,7 @@ import * as Config from './../config.js';
|
|||||||
|
|
||||||
export let railTracks = [];
|
export let railTracks = [];
|
||||||
export let markers = [];
|
export let markers = [];
|
||||||
|
export let dynamicMarkers = [];
|
||||||
|
|
||||||
let visibleMarkers = [];
|
let visibleMarkers = [];
|
||||||
|
|
||||||
@@ -15,10 +16,9 @@ let uniqueTrackNames = {};
|
|||||||
|
|
||||||
export let testPoint = {x:0,y:0};
|
export let testPoint = {x:0,y:0};
|
||||||
|
|
||||||
let playerLocation = null;
|
let locationData = null;
|
||||||
export let playerMarker = {
|
export let playerMarker = {
|
||||||
type: 'player',
|
type: 'player',
|
||||||
name: 'Player',
|
|
||||||
position: {x:8192, y:200, z:8192},
|
position: {x:8192, y:200, z:8192},
|
||||||
rotation: 0.0,
|
rotation: 0.0,
|
||||||
minZoom: 0.0,
|
minZoom: 0.0,
|
||||||
@@ -70,6 +70,7 @@ export let view = {
|
|||||||
view.y = -(mapContainer.clientHeight*0.5 - (matrix.height * 0.5 + matrix.minY)*view.scale);
|
view.y = -(mapContainer.clientHeight*0.5 - (matrix.height * 0.5 + matrix.minY)*view.scale);
|
||||||
}
|
}
|
||||||
view.dirty = false;
|
view.dirty = false;
|
||||||
|
view.dynDirty = false;
|
||||||
|
|
||||||
view.convertX = x => {
|
view.convertX = x => {
|
||||||
return x * view.scale * view.pixelRatio + view.x * view.pixelRatio;
|
return x * view.scale * view.pixelRatio + view.x * view.pixelRatio;
|
||||||
@@ -103,7 +104,11 @@ export function sortTracks(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sortMarkers(){
|
export function sortMarkers(){
|
||||||
markers.sort((a,b) => {
|
markers.sort(markerSortFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markerSortFunction(a, b){
|
||||||
|
(a,b) => {
|
||||||
if(a.type == 'player') return 1;
|
if(a.type == 'player') return 1;
|
||||||
if(b.type == 'player') return -1;
|
if(b.type == 'player') return -1;
|
||||||
if(a.type == b.type){
|
if(a.type == b.type){
|
||||||
@@ -126,7 +131,7 @@ export function sortMarkers(){
|
|||||||
if(a.type == 'demonstratorSpawnHint') return -1;
|
if(a.type == 'demonstratorSpawnHint') return -1;
|
||||||
if(b.type == 'demonstratorSpawnHint') return 1;
|
if(b.type == 'demonstratorSpawnHint') return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortShops(){
|
export function sortShops(){
|
||||||
@@ -511,6 +516,32 @@ export function testTooltip(cursorX, cursorY){
|
|||||||
let radius = 0;
|
let radius = 0;
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
|
for(const marker of dynamicMarkers){
|
||||||
|
if(!marker.visible) continue;
|
||||||
|
if(marker.boundsCheck != null){
|
||||||
|
if(marker.boundsCheck(cursorX, cursorY))
|
||||||
|
finalTooltip = marker.tooltip ?? finalTooltip;
|
||||||
|
}else if(marker.tooltipHitzone && marker.tooltipHitzone.width && marker.tooltipHitzone.height){
|
||||||
|
width = marker.tooltipHitzone.width * 0.5/view.scale;
|
||||||
|
height = marker.tooltipHitzone.height * 0.5/view.scale;
|
||||||
|
if( marker.position.x - cursorX + (marker.tooltipHitzone.offsetX ?? 0)/view.scale <= width
|
||||||
|
&& marker.position.x - cursorX + (marker.tooltipHitzone.offsetX ?? 0)/view.scale >= -width
|
||||||
|
&& marker.position.z - cursorY + (marker.tooltipHitzone.offsetY ?? 0)/view.scale <= height
|
||||||
|
&& marker.position.z - cursorY + (marker.tooltipHitzone.offsetY ?? 0)/view.scale >= -height){
|
||||||
|
finalTooltip = marker.tooltip ?? finalTooltip;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if(marker.tooltipHitzone)
|
||||||
|
radius = (marker.tooltipHitzone.radius ?? Config.tooltipHitzone.default.radius) / view.scale;
|
||||||
|
else
|
||||||
|
radius = Config.tooltipHitzone.default.radius / view.scale;
|
||||||
|
if(Math.pow(marker.position.x - cursorX, 2) + Math.pow(marker.position.z - cursorY, 2) <= radius * radius){
|
||||||
|
finalTooltip = marker.tooltip ?? finalTooltip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(finalTooltip != '') return finalTooltip;
|
||||||
|
|
||||||
for(const marker of markers){
|
for(const marker of markers){
|
||||||
if(!marker.visible) continue;
|
if(!marker.visible) continue;
|
||||||
if(marker.boundsCheck != null){
|
if(marker.boundsCheck != null){
|
||||||
@@ -662,35 +693,58 @@ export function connectPlayerLocation(address, status){
|
|||||||
console.log(`Connecting to game at ${address}`);
|
console.log(`Connecting to game at ${address}`);
|
||||||
let started = false;
|
let started = false;
|
||||||
const endConnect = () => {
|
const endConnect = () => {
|
||||||
playerLocation = null;
|
locationData = null;
|
||||||
status.value = 'Connect';
|
status.value = 'Connect';
|
||||||
playerMarker.hidden = true;
|
playerMarker.hidden = true;
|
||||||
view.dirty = true;
|
view.dynDirty = true;
|
||||||
}
|
}
|
||||||
const pingLocation = async () => {
|
const pingLocation = async () => {
|
||||||
if(playerLocation == null && started){
|
if(locationData == null && started){
|
||||||
endConnect();
|
endConnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
status.value = 'Disconnect...';
|
status.value = 'Disconnect...';
|
||||||
try {
|
try {
|
||||||
const fetchRequest = await fetch(new Request(address), {signal: AbortSignal.timeout(5000)});
|
const fetchRequest = await fetch(new Request(address), {signal: AbortSignal.timeout(5000)});
|
||||||
if(!fetchRequest.ok || (playerLocation == null && started)){
|
if(!fetchRequest.ok || (locationData == null && started)){
|
||||||
endConnect();
|
endConnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
status.value = 'Disconnect ';
|
status.value = 'Disconnect ';
|
||||||
playerLocation = await (fetchRequest).json();
|
locationData = await (fetchRequest).json();
|
||||||
playerMarker.position = {
|
playerMarker.position = {
|
||||||
x: playerLocation.X,
|
x: locationData.x,
|
||||||
y: playerLocation.Y,
|
y: locationData.y,
|
||||||
z: playerLocation.Z,
|
z: locationData.z,
|
||||||
};
|
};
|
||||||
playerMarker.rotation = playerLocation.Rotation;
|
playerMarker.rotation = locationData.rotation;
|
||||||
playerMarker.tooltip = `<h1>Player</h1>X: ${playerMarker.position.x.toFixed(2)}\nY: ${playerMarker.position.y.toFixed(2)}\nZ: ${playerMarker.position.z.toFixed(2)}\nBearing ${(playerMarker.rotation * (180/Math.PI)).toFixed(1)} (${Utils.angleToCardinalDirection(playerMarker.rotation)})`;
|
playerMarker.tooltip = `<h1>Player</h1>X: ${playerMarker.position.x.toFixed(2)}\nY: ${playerMarker.position.y.toFixed(2)}\nZ: ${playerMarker.position.z.toFixed(2)}\nBearing ${(playerMarker.rotation * (180/Math.PI)).toFixed(1)} (${Utils.angleToCardinalDirection(playerMarker.rotation)})`;
|
||||||
playerMarker.hidden = false;
|
playerMarker.hidden = false;
|
||||||
|
|
||||||
|
dynamicMarkers = [];
|
||||||
|
if(!locationData.cars) locationData.cars = [];
|
||||||
|
for(let car of locationData.cars){
|
||||||
|
car.name = car.name.replaceAll('(Clone)', '').replaceAll('_',' ').replaceAll(/(Car|Loco)/g, '');
|
||||||
|
dynamicMarkers.push({
|
||||||
|
type: 'car',
|
||||||
|
name: 'car.id',
|
||||||
|
position: {
|
||||||
|
x: car.x,
|
||||||
|
y: car.y,
|
||||||
|
z: car.z
|
||||||
|
},
|
||||||
|
length: car.length,
|
||||||
|
width: 2.0,
|
||||||
|
rotation: car.rotation,
|
||||||
|
guid: car.carGuid,
|
||||||
|
isLoco: car.isLoco,
|
||||||
|
tooltip: `<h1>${car.id}</h1>Type: ${car.name}\nLength: ${car.length.toFixed(1)}m`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dynamicMarkers.push(playerMarker);
|
||||||
|
|
||||||
started = true;
|
started = true;
|
||||||
view.dirty = true;
|
view.dynDirty = true;
|
||||||
setTimeout(pingLocation, locationUpdateRate);
|
setTimeout(pingLocation, locationUpdateRate);
|
||||||
}catch(e){
|
}catch(e){
|
||||||
endConnect();
|
endConnect();
|
||||||
@@ -702,5 +756,5 @@ export function connectPlayerLocation(address, status){
|
|||||||
|
|
||||||
export function disconnectPlayerLocation(){
|
export function disconnectPlayerLocation(){
|
||||||
console.log('Disconnecting');
|
console.log('Disconnecting');
|
||||||
playerLocation = null;
|
locationData = null;
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ let settingEntries = [
|
|||||||
label: 'Location Update Rate (seconds)',
|
label: 'Location Update Rate (seconds)',
|
||||||
id: 'dropdown_locationFrequency',
|
id: 'dropdown_locationFrequency',
|
||||||
tooltip: 'Lower update intervals may cause poor performance',
|
tooltip: 'Lower update intervals may cause poor performance',
|
||||||
state: '0.5',
|
state: '0.2',
|
||||||
options: [
|
options: [
|
||||||
'0.05',
|
'0.05',
|
||||||
'0.1',
|
'0.1',
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ a:visited{
|
|||||||
background: linear-gradient(#444, #333);
|
background: linear-gradient(#444, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mapCanvas, #dynCanvas{
|
||||||
|
position: fixed;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
.colorKey, .gradientKey{
|
.colorKey, .gradientKey{
|
||||||
border:1px #858585 solid;
|
border:1px #858585 solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
Reference in new issue
Block a user