Rework Line gen, add stats & viewport

Generate 4 separate lines. Trains now follow lines and only spawn on connected
stations.
This commit is contained in:
Tyler Hallada 2018-04-16 16:59:28 -04:00
parent a79f501c5e
commit df9ba6d5ea
7 changed files with 218 additions and 104 deletions

81
package-lock.json generated
View File

@ -980,6 +980,11 @@
"resolved": "https://registry.npmjs.org/@types/pixi.js/-/pixi.js-4.7.2.tgz", "resolved": "https://registry.npmjs.org/@types/pixi.js/-/pixi.js-4.7.2.tgz",
"integrity": "sha512-ybrqVdncNCa81fCYCqxz/CISyMbXl8usszNv0mwdeYDyfDqmemQHJtf4GtduHva+3suhItPc9Akr/WfV19zWiQ==" "integrity": "sha512-ybrqVdncNCa81fCYCqxz/CISyMbXl8usszNv0mwdeYDyfDqmemQHJtf4GtduHva+3suhItPc9Akr/WfV19zWiQ=="
}, },
"@types/stats.js": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz",
"integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w=="
},
"@types/tinycolor2": { "@types/tinycolor2": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.0.tgz",
@ -4406,6 +4411,11 @@
} }
} }
}, },
"exists": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/exists/-/exists-1.0.1.tgz",
"integrity": "sha1-/8vuKRQvJAVt8Bkk5zJicz2xC0k="
},
"exit-hook": { "exit-hook": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
@ -9205,6 +9215,11 @@
"sha.js": "2.4.11" "sha.js": "2.4.11"
} }
}, },
"penner": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/penner/-/penner-0.1.3.tgz",
"integrity": "sha1-C4tILU6bOa8vPXw3WSIpuKzClwU="
},
"pify": { "pify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@ -9226,11 +9241,46 @@
"pinkie": "2.0.4" "pinkie": "2.0.4"
} }
}, },
"pixi-ease": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/pixi-ease/-/pixi-ease-0.18.0.tgz",
"integrity": "sha512-qC9ofPKHblNlkdKDFgXDcw1vTPg4zDTLBudk32lScafOB59QKE5duElA+XwaS5kEpumVfnlKtg3k4gCcGcat3Q==",
"requires": {
"eventemitter3": "3.0.1",
"penner": "0.1.3",
"yy-angle": "1.2.0",
"yy-color": "1.0.7"
},
"dependencies": {
"eventemitter3": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.0.1.tgz",
"integrity": "sha512-QOCPu979MMWX9XNlfRZoin+Wm+bK1SP7vv3NGUniYwuSJK/+cPA10blMaeRgzg31RvoSFk6FsCDVa4vNryBTGA=="
}
}
},
"pixi-gl-core": { "pixi-gl-core": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/pixi-gl-core/-/pixi-gl-core-1.1.4.tgz", "resolved": "https://registry.npmjs.org/pixi-gl-core/-/pixi-gl-core-1.1.4.tgz",
"integrity": "sha1-i0tcQzsx5Bm8N53FZc4bg1qRs3I=" "integrity": "sha1-i0tcQzsx5Bm8N53FZc4bg1qRs3I="
}, },
"pixi-viewport": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-1.5.0.tgz",
"integrity": "sha512-hMPtka90PulpBLXBhE3RZvKaB1VTPFoXe4dSuqsYYBQeo8b1G3FTy7WAfgqkqj1ibrblEf0MmTzO9PzoXKLKXA==",
"requires": {
"eventemitter3": "3.0.1",
"exists": "1.0.1",
"pixi-ease": "0.18.0"
},
"dependencies": {
"eventemitter3": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.0.1.tgz",
"integrity": "sha512-QOCPu979MMWX9XNlfRZoin+Wm+bK1SP7vv3NGUniYwuSJK/+cPA10blMaeRgzg31RvoSFk6FsCDVa4vNryBTGA=="
}
}
},
"pixi.js": { "pixi.js": {
"version": "4.7.1", "version": "4.7.1",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-4.7.1.tgz", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-4.7.1.tgz",
@ -10595,6 +10645,11 @@
"integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=", "integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=",
"dev": true "dev": true
}, },
"seedrandom": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
"integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw="
},
"select-hose": { "select-hose": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -11159,6 +11214,11 @@
} }
} }
}, },
"stats.js": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
"integrity": "sha1-scPcRtlEmLV4t/05hbgaznExzH0="
},
"statuses": { "statuses": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
@ -12966,6 +13026,27 @@
"dev": true "dev": true
} }
} }
},
"yy-angle": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/yy-angle/-/yy-angle-1.2.0.tgz",
"integrity": "sha512-Sf311F5zlItZA0dH/mD3MMG6mIIo1b+9XsFqpLhKCTqFFUL4ip+XWTaQLWLRDkh/Ag0GzAsGt6rWcq61OU9+zQ=="
},
"yy-color": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/yy-color/-/yy-color-1.0.7.tgz",
"integrity": "sha1-P5IxiCQ/q8zqJNNy9Li24wk8Zak=",
"requires": {
"yy-random": "1.6.0"
}
},
"yy-random": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/yy-random/-/yy-random-1.6.0.tgz",
"integrity": "sha512-oMIO8eo4BVed+o8NDMGj9scboHWBFtOdip1oDpXhxYw3A03lYqDqtlwPLg8SOK0q95Fsdzj9EdCQMWFWRk3p7A==",
"requires": {
"seedrandom": "2.4.3"
}
} }
} }
} }

View File

@ -44,8 +44,11 @@
}, },
"dependencies": { "dependencies": {
"@types/pixi.js": "^4.7.2", "@types/pixi.js": "^4.7.2",
"@types/stats.js": "^0.17.0",
"@types/tinycolor2": "^1.4.0", "@types/tinycolor2": "^1.4.0",
"pixi-viewport": "^1.5.0",
"pixi.js": "^4.7.1", "pixi.js": "^4.7.1",
"stats.js": "^0.17.0",
"tinycolor2": "^1.4.1" "tinycolor2": "^1.4.1"
} }
} }

View File

@ -1,72 +1,58 @@
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import Direction, { getPointDirection } from './Direction'; import Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import Station from './Station'; import Station from './Station';
import { distance, randomInt, randomPoint } from './utils'; import { distance, randomInt, randomPoint } from './utils';
const CONNECTION_RADIUS = Math.floor(Math.sqrt( const CONNECTION_RADIUS = Math.floor(Math.sqrt(
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2), Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
) / 4); ) / 8);
export default class Line { export default class Line {
public static getLinesWithStation(lines: Line[], station: Station): Line[] { public name: string;
return lines.filter(line => line.stations.indexOf(station) >= 0);
}
public stations: Station[];
public color: tinycolorInstance; public color: tinycolorInstance;
constructor( constructor(
stations: Station[], name: string,
start: PIXI.Point,
startDirection: Direction,
numStations: number,
color: tinycolorInstance, color: tinycolorInstance,
) { ) {
this.name = name;
this.color = color; this.color = color;
this.stations = []; }
let stationsLeft = stations.slice();
let currentStation = Station.randomCloseLargeStation(stationsLeft, start, CONNECTION_RADIUS);
stationsLeft = stationsLeft.filter(s => s !== currentStation);
this.stations.push(currentStation);
let direction = startDirection; public connectStations(
while (this.stations.length < numStations) { currentStation: Station,
const previousStation = this.stations[this.stations.length - 1]; stations: Station[],
let candidateStations = Station.stationsWithinRadius( visitedStations: Station[],
stationsLeft, connectionLimit: number,
) {
visitedStations.push(currentStation);
const otherStations = stations.filter(station => station !== currentStation);
const closeStations = Station.stationsWithinRadius(
otherStations,
currentStation.location, currentStation.location,
CONNECTION_RADIUS, CONNECTION_RADIUS,
); );
for (let i = 0; i < connectionLimit; i += 1) {
if (this.stations.length > 1) { if (closeStations.length < 1) {
const secondPreviousStation = this.stations[this.stations.length - 2];
direction = getPointDirection(secondPreviousStation.location,
previousStation.location);
}
const straightStations = Station.stationsInDirection(
candidateStations, previousStation.location, direction,
);
const leftStations = Station.stationsInDirection(
candidateStations, previousStation.location, (direction - 1) % 7,
);
const rightStations = Station.stationsInDirection(
candidateStations, previousStation.location, (direction + 1) % 7,
);
candidateStations = [
...straightStations,
...leftStations,
...rightStations,
];
currentStation = Station.randomCloseLargeStation(candidateStations, previousStation.location,
CONNECTION_RADIUS);
if (currentStation === null || currentStation === undefined) {
break; break;
} }
stationsLeft = stationsLeft.filter(s => s !== currentStation); const largest = Station.largestStation(closeStations);
this.stations.push(currentStation); currentStation.connections.push(
new LineConnection(largest, this),
);
closeStations.splice(closeStations.indexOf(largest), 1);
}
for (const connectedStation of currentStation.connections) {
if (visitedStations.indexOf(connectedStation.station) === -1) {
this.connectStations(
connectedStation.station,
stations,
visitedStations,
connectionLimit,
);
}
} }
} }
} }

15
src/LineConnection.ts Normal file
View File

@ -0,0 +1,15 @@
import Line from './Line';
import Station from './Station';
export default class LineConnection {
public station: Station;
public line: Line;
constructor(
station: Station,
line: Line,
) {
this.station = station;
this.line = line;
}
}

View File

@ -1,6 +1,7 @@
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import Direction, { getPointDirection } from './Direction'; import Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import { distance, randomPoint, weightedRandom } from './utils'; import { distance, randomPoint, weightedRandom } from './utils';
let stationCount = 0; let stationCount = 0;
@ -65,7 +66,7 @@ export default class Station {
public location: PIXI.Point; public location: PIXI.Point;
public population: number; public population: number;
public connections: Station[]; public connections: LineConnection[];
public id: number; public id: number;
public label: PIXI.Text; public label: PIXI.Text;
public color: tinycolorInstance; public color: tinycolorInstance;
@ -74,12 +75,12 @@ export default class Station {
location: PIXI.Point, location: PIXI.Point,
population: number, population: number,
color: tinycolorInstance, color: tinycolorInstance,
connections?: Station[], connections?: LineConnection[],
) { ) {
this.location = location; this.location = location;
this.population = population; this.population = population;
this.color = color; this.color = color;
this.connections = connections; this.connections = connections || [];
// for debugging // for debugging
stationCount += 1; stationCount += 1;

View File

@ -1,4 +1,6 @@
import * as Viewport from 'pixi-viewport';
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import * as Stats from 'stats.js';
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import Direction from './Direction'; import Direction from './Direction';
@ -16,10 +18,14 @@ const NODE_RES = 100;
const MAX_SPEED = 10.0; const MAX_SPEED = 10.0;
const ACCELERATION = 0.025; const ACCELERATION = 0.025;
const APPROACH_DISTANCE = 3.0; const APPROACH_DISTANCE = 3.0;
const MAX_JOURNEY = Math.floor(Math.sqrt(
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
) / 4);
const TRAIN_CAPACITY = 50; const TRAIN_CAPACITY = 50;
const LINE_CONNECTION_LIMIT = 5;
const WORLD_WIDTH = 1000;
const WORLD_HEIGHT = 1000;
const ZOOM_MIN_WIDTH = 100;
const ZOOM_MIN_HEIGHT = 100;
const ZOOM_MAX_WIDTH = 4000;
const ZOOM_MAX_HEIGHT = 4000;
const trainTexts: PIXI.Text[] = []; const trainTexts: PIXI.Text[] = [];
@ -36,8 +42,11 @@ const initStations = (numStations: number): Station[] => {
const initTrains = (numTrains: number, stations: Station[]): Train[] => { const initTrains = (numTrains: number, stations: Station[]): Train[] => {
const trains = []; const trains = [];
const stationsWithConnections = stations.filter(station => station.connections.length > 0);
for (let i = 0; i < numTrains; i += 1) { for (let i = 0; i < numTrains; i += 1) {
const originStation = stations[Math.floor(Math.random() * stations.length)]; const originStation = stationsWithConnections[
Math.floor(Math.random() * stationsWithConnections.length)
];
trains.push(new Train( trains.push(new Train(
new PIXI.Point(originStation.location.x, originStation.location.y), new PIXI.Point(originStation.location.x, originStation.location.y),
0, 0, originStation, undefined, tinycolor('grey')), 0, 0, originStation, undefined, tinycolor('grey')),
@ -46,15 +55,36 @@ const initTrains = (numTrains: number, stations: Station[]): Train[] => {
return trains; return trains;
}; };
const initLines = (numLines: number, stations: Station[]): Line[] => {
const lines = [];
for (let i = 0; i < numLines; i += 1) {
let color = tinycolor.random();
while (color.isDark()) {
color = tinycolor.random();
}
const stationsWithoutConnections = stations.filter(station =>
station.connections.length === 0,
);
const centralHub = Station.largestStation(stationsWithoutConnections);
const line = new Line(`line-${i}`, tinycolor.random());
const stationsLeft = stations.slice(0);
line.connectStations(centralHub, stationsLeft, [], LINE_CONNECTION_LIMIT);
lines.push(line);
}
return lines;
};
const moveTrains = (trains: Train[], stations: Station[]) => { const moveTrains = (trains: Train[], stations: Station[]) => {
for (const train of trains) { for (const train of trains) {
if (train.origin.connections.length === 0) {
// train is stuck at an orphaned station
continue;
}
// choose a destination randomly with a bias towards larger stations // choose a destination randomly with a bias towards larger stations
if (train.destination === undefined) { if (train.destination === undefined) {
const otherStations = stations.filter(station => station !== train.origin); const otherStations = train.origin.connections.map(conn => conn.station);
const closeStations = Station.stationsWithinRadius(otherStations, train.location, const closeStationWeights = otherStations.map(station => station.population);
MAX_JOURNEY); train.destination = weightedRandom(otherStations, closeStationWeights);
const closeStationWeights = closeStations.map(station => station.population);
train.destination = weightedRandom(closeStations, closeStationWeights);
// board passengers // board passengers
const boardingPassengers = randomInt(0, Math.min(TRAIN_CAPACITY - train.passengers, const boardingPassengers = randomInt(0, Math.min(TRAIN_CAPACITY - train.passengers,
@ -116,7 +146,7 @@ const moveTrains = (trains: Train[], stations: Station[]) => {
const drawStations = (stations: Station[], graphics: PIXI.Graphics) => { const drawStations = (stations: Station[], graphics: PIXI.Graphics) => {
for (const station of stations) { for (const station of stations) {
const radius = station.population / 60; const radius = station.population / 150;
graphics.beginFill(parseInt(station.color.toHex(), 16), 0.5); graphics.beginFill(parseInt(station.color.toHex(), 16), 0.5);
graphics.drawCircle(station.location.x, station.location.y, radius); graphics.drawCircle(station.location.x, station.location.y, radius);
graphics.endFill(); graphics.endFill();
@ -127,10 +157,6 @@ const drawStations = (stations: Station[], graphics: PIXI.Graphics) => {
const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => { const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
for (const train of trains) { for (const train of trains) {
// graphics.beginFill(parseInt(train.color.toHex(), 16), 0.8);
// graphics.drawCircle(train.location.x, train.location.y,
// rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5));
// graphics.endFill();
const trainSize = rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5); const trainSize = rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5);
const scale = trainSize / NODE_RES; const scale = trainSize / NODE_RES;
train.sprite.x = train.location.x; train.sprite.x = train.location.x;
@ -143,13 +169,18 @@ const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
} }
}; };
const drawLines = (lines: Line[], graphics: PIXI.Graphics) => { const drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
for (const line of lines) { for (const station of stations) {
graphics.lineStyle(1, parseInt(line.color.toHex(), 16), 1); for (const connection of station.connections) {
const start = line.stations[0].location; let twoWay = false;
graphics.moveTo(start.x, start.y); for (const conn of connection.station.connections) {
for (const station of line.stations.slice(1)) { if (conn.station === station) {
graphics.lineTo(station.location.x, station.location.y); twoWay = true;
}
}
graphics.lineStyle(twoWay ? 2 : 1, parseInt(connection.line.color.toHex(), 16), 1);
graphics.moveTo(station.location.x, station.location.y);
graphics.lineTo(connection.station.location.x, connection.station.location.y);
} }
} }
}; };
@ -160,41 +191,29 @@ const run = () => {
height: window.innerHeight, height: window.innerHeight,
width: window.innerWidth, width: window.innerWidth,
}); });
const viewport = new Viewport({
screenHeight: window.innerHeight,
screenWidth: window.innerWidth,
worldHeight: WORLD_HEIGHT,
worldWidth: WORLD_WIDTH,
});
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
const ticker = new PIXI.ticker.Ticker(); const ticker = new PIXI.ticker.Ticker();
const graphics = new PIXI.Graphics(); const graphics = new PIXI.Graphics();
const fpsText = new PIXI.Text('', { fontSize: '25px', fontFamily: 'monospace', fill: 'yellow' });
fpsText.anchor = new PIXI.ObservablePoint(null, 0, 1);
fpsText.x = window.innerWidth;
fpsText.y = 0;
const stations = initStations(30); const stations = initStations(30);
const lines = initLines(4, stations);
const trains = initTrains(50, stations); const trains = initTrains(50, stations);
const lines = [
new Line(
stations, new PIXI.Point(0, 0),
Direction.Southeast, 12, tinycolor('red'),
),
new Line(
stations, new PIXI.Point(window.innerWidth, 0),
Direction.Southwest, 12, tinycolor('darkcyan'),
),
new Line(
stations, new PIXI.Point(window.innerWidth, window.innerHeight),
Direction.Northwest, 12, tinycolor('yellow'),
),
new Line(
stations, new PIXI.Point(0, window.innerHeight),
Direction.Northeast, 12, tinycolor('green'),
),
];
ticker.stop(); ticker.stop();
ticker.add((deltaTime) => { ticker.add((deltaTime) => {
stats.begin();
moveTrains(trains, stations); moveTrains(trains, stations);
graphics.clear(); graphics.clear();
fpsText.text = `${Math.round(ticker.FPS)}`;
graphics.lineStyle(1, 0xFFA500, 1); graphics.lineStyle(1, 0xFFA500, 1);
drawStations(stations, graphics); drawStations(stations, graphics);
@ -202,24 +221,32 @@ const run = () => {
graphics.lineStyle(1, 0xAEAEAE, 1); graphics.lineStyle(1, 0xAEAEAE, 1);
drawTrains(trains, graphics); drawTrains(trains, graphics);
drawLines(lines, graphics); drawLines(stations, graphics);
stats.end();
}); });
ticker.start(); ticker.start();
app.stage.addChild(graphics); viewport.addChild(graphics);
app.stage.addChild(fpsText);
// add train sprites // add train sprites
for (const train of trains) { for (const train of trains) {
app.stage.addChild(train.sprite); viewport.addChild(train.sprite);
} }
// Add debug labels // Add debug labels
for (const train of trains) { for (const train of trains) {
app.stage.addChild(train.label); viewport.addChild(train.label);
} }
for (const station of stations) { for (const station of stations) {
app.stage.addChild(station.label); viewport.addChild(station.label);
} }
document.body.appendChild(app.view); document.body.appendChild(app.view);
app.stage.addChild(viewport);
viewport.drag().pinch().wheel().clampZoom({
maxHeight: ZOOM_MAX_HEIGHT,
maxWidth: ZOOM_MAX_WIDTH,
minHeight: ZOOM_MIN_HEIGHT,
minWidth: ZOOM_MIN_WIDTH,
}).decelerate();
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
app.renderer.resize(window.innerWidth, window.innerHeight); app.renderer.resize(window.innerWidth, window.innerHeight);

View File

@ -10,6 +10,7 @@ module.exports = {
filename: 'transport.js', filename: 'transport.js',
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
}, },
mode: env === 'production' ? 'production' : 'development',
module: { module: {
rules: [ rules: [
{ {