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",
"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": {
"version": "1.4.0",
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
@ -9205,6 +9215,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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@ -9226,11 +9241,46 @@
"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": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/pixi-gl-core/-/pixi-gl-core-1.1.4.tgz",
"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": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-4.7.1.tgz",
@ -10595,6 +10645,11 @@
"integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=",
"dev": true
},
"seedrandom": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
"integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw="
},
"select-hose": {
"version": "2.0.0",
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
@ -12966,6 +13026,27 @@
"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": {
"@types/pixi.js": "^4.7.2",
"@types/stats.js": "^0.17.0",
"@types/tinycolor2": "^1.4.0",
"pixi-viewport": "^1.5.0",
"pixi.js": "^4.7.1",
"stats.js": "^0.17.0",
"tinycolor2": "^1.4.1"
}
}

View File

@ -1,72 +1,58 @@
import * as tinycolor from 'tinycolor2';
import Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import Station from './Station';
import { distance, randomInt, randomPoint } from './utils';
const CONNECTION_RADIUS = Math.floor(Math.sqrt(
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
) / 4);
) / 8);
export default class Line {
public static getLinesWithStation(lines: Line[], station: Station): Line[] {
return lines.filter(line => line.stations.indexOf(station) >= 0);
}
public stations: Station[];
public name: string;
public color: tinycolorInstance;
constructor(
stations: Station[],
start: PIXI.Point,
startDirection: Direction,
numStations: number,
name: string,
color: tinycolorInstance,
) {
this.name = name;
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;
while (this.stations.length < numStations) {
const previousStation = this.stations[this.stations.length - 1];
let candidateStations = Station.stationsWithinRadius(
stationsLeft,
currentStation.location,
CONNECTION_RADIUS,
);
if (this.stations.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) {
public connectStations(
currentStation: Station,
stations: Station[],
visitedStations: Station[],
connectionLimit: number,
) {
visitedStations.push(currentStation);
const otherStations = stations.filter(station => station !== currentStation);
const closeStations = Station.stationsWithinRadius(
otherStations,
currentStation.location,
CONNECTION_RADIUS,
);
for (let i = 0; i < connectionLimit; i += 1) {
if (closeStations.length < 1) {
break;
}
stationsLeft = stationsLeft.filter(s => s !== currentStation);
this.stations.push(currentStation);
const largest = Station.largestStation(closeStations);
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 Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import { distance, randomPoint, weightedRandom } from './utils';
let stationCount = 0;
@ -65,7 +66,7 @@ export default class Station {
public location: PIXI.Point;
public population: number;
public connections: Station[];
public connections: LineConnection[];
public id: number;
public label: PIXI.Text;
public color: tinycolorInstance;
@ -74,12 +75,12 @@ export default class Station {
location: PIXI.Point,
population: number,
color: tinycolorInstance,
connections?: Station[],
connections?: LineConnection[],
) {
this.location = location;
this.population = population;
this.color = color;
this.connections = connections;
this.connections = connections || [];
// for debugging
stationCount += 1;

View File

@ -1,4 +1,6 @@
import * as Viewport from 'pixi-viewport';
import * as PIXI from 'pixi.js';
import * as Stats from 'stats.js';
import * as tinycolor from 'tinycolor2';
import Direction from './Direction';
@ -16,10 +18,14 @@ const NODE_RES = 100;
const MAX_SPEED = 10.0;
const ACCELERATION = 0.025;
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 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[] = [];
@ -36,8 +42,11 @@ const initStations = (numStations: number): Station[] => {
const initTrains = (numTrains: number, stations: Station[]): Train[] => {
const trains = [];
const stationsWithConnections = stations.filter(station => station.connections.length > 0);
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(
new PIXI.Point(originStation.location.x, originStation.location.y),
0, 0, originStation, undefined, tinycolor('grey')),
@ -46,15 +55,36 @@ const initTrains = (numTrains: number, stations: Station[]): Train[] => {
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[]) => {
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
if (train.destination === undefined) {
const otherStations = stations.filter(station => station !== train.origin);
const closeStations = Station.stationsWithinRadius(otherStations, train.location,
MAX_JOURNEY);
const closeStationWeights = closeStations.map(station => station.population);
train.destination = weightedRandom(closeStations, closeStationWeights);
const otherStations = train.origin.connections.map(conn => conn.station);
const closeStationWeights = otherStations.map(station => station.population);
train.destination = weightedRandom(otherStations, closeStationWeights);
// board 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) => {
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.drawCircle(station.location.x, station.location.y, radius);
graphics.endFill();
@ -127,10 +157,6 @@ const drawStations = (stations: Station[], graphics: PIXI.Graphics) => {
const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
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 scale = trainSize / NODE_RES;
train.sprite.x = train.location.x;
@ -143,13 +169,18 @@ const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
}
};
const drawLines = (lines: Line[], graphics: PIXI.Graphics) => {
for (const line of lines) {
graphics.lineStyle(1, parseInt(line.color.toHex(), 16), 1);
const start = line.stations[0].location;
graphics.moveTo(start.x, start.y);
for (const station of line.stations.slice(1)) {
graphics.lineTo(station.location.x, station.location.y);
const drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
for (const station of stations) {
for (const connection of station.connections) {
let twoWay = false;
for (const conn of connection.station.connections) {
if (conn.station === station) {
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,
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 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 lines = initLines(4, 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.add((deltaTime) => {
stats.begin();
moveTrains(trains, stations);
graphics.clear();
fpsText.text = `${Math.round(ticker.FPS)}`;
graphics.lineStyle(1, 0xFFA500, 1);
drawStations(stations, graphics);
@ -202,24 +221,32 @@ const run = () => {
graphics.lineStyle(1, 0xAEAEAE, 1);
drawTrains(trains, graphics);
drawLines(lines, graphics);
drawLines(stations, graphics);
stats.end();
});
ticker.start();
app.stage.addChild(graphics);
app.stage.addChild(fpsText);
viewport.addChild(graphics);
// add train sprites
for (const train of trains) {
app.stage.addChild(train.sprite);
viewport.addChild(train.sprite);
}
// Add debug labels
for (const train of trains) {
app.stage.addChild(train.label);
viewport.addChild(train.label);
}
for (const station of stations) {
app.stage.addChild(station.label);
viewport.addChild(station.label);
}
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', () => {
app.renderer.resize(window.innerWidth, window.innerHeight);

View File

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