Rework Line gen, add stats & viewport

Generate 4 separate lines. Trains now follow lines and only spawn on connected
stations.
这个提交包含在:
2018-04-16 16:59:28 -04:00
父节点 a79f501c5e
当前提交 df9ba6d5ea
共有 7 个文件被更改,包括 218 次插入104 次删除

81
package-lock.json 自动生成的
查看文件

@@ -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"
}
}
}
}

查看文件

@@ -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"
}
}

查看文件

@@ -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 普通文件
查看文件

@@ -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;
}
}

查看文件

@@ -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;

查看文件

@@ -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);

查看文件

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