Try initializing and move trains in Web Workers
Unfortunately, all of the train data has to be copied from the train web worker to the main thread on every tick so it knows the new positions of the trains to render. So every few ticks, the GC causes a little lag as it collects ~90+MB of obsolete train arrays. There's no way for the two threads to share the same data thanks to Spectre.
This commit is contained in:
parent
ad9167528a
commit
4d1ef84568
10
package-lock.json
generated
10
package-lock.json
generated
@ -12769,6 +12769,16 @@
|
|||||||
"errno": "0.1.7"
|
"errno": "0.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"worker-loader": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-qJZLVS/jMCBITDzPo/RuweYSIG8VJP5P67mP/71alGyTZRe1LYJFdwLjLalY3T5ifx0bMDRD3OB6P2p1escvlg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "1.1.0",
|
||||||
|
"schema-utils": "0.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||||
|
@ -41,7 +41,8 @@
|
|||||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||||
"webpack": "^4.4.1",
|
"webpack": "^4.4.1",
|
||||||
"webpack-cli": "^2.0.13",
|
"webpack-cli": "^2.0.13",
|
||||||
"webpack-dev-server": "^3.1.1"
|
"webpack-dev-server": "^3.1.1",
|
||||||
|
"worker-loader": "^1.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/pixi.js": "^4.7.2",
|
"@types/pixi.js": "^4.7.2",
|
||||||
|
12
src/Line.ts
12
src/Line.ts
@ -5,17 +5,13 @@ 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(
|
|
||||||
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
|
|
||||||
) / 8);
|
|
||||||
|
|
||||||
export default class Line {
|
export default class Line {
|
||||||
public name: string;
|
public name: string;
|
||||||
public color: tinycolorInstance;
|
public color: ColorFormats.RGBA;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
name: string,
|
name: string,
|
||||||
color: tinycolorInstance,
|
color: ColorFormats.RGBA,
|
||||||
) {
|
) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
@ -26,13 +22,14 @@ export default class Line {
|
|||||||
stations: Station[],
|
stations: Station[],
|
||||||
visitedStations: Station[],
|
visitedStations: Station[],
|
||||||
connectionLimit: number,
|
connectionLimit: number,
|
||||||
|
connectionRadius: number,
|
||||||
) {
|
) {
|
||||||
visitedStations.push(currentStation);
|
visitedStations.push(currentStation);
|
||||||
const otherStations = stations.filter(station => station !== currentStation);
|
const otherStations = stations.filter(station => station !== currentStation);
|
||||||
const closeStations = Station.stationsWithinRadius(
|
const closeStations = Station.stationsWithinRadius(
|
||||||
otherStations,
|
otherStations,
|
||||||
currentStation.location,
|
currentStation.location,
|
||||||
CONNECTION_RADIUS,
|
connectionRadius,
|
||||||
);
|
);
|
||||||
for (let i = 0; i < connectionLimit; i += 1) {
|
for (let i = 0; i < connectionLimit; i += 1) {
|
||||||
if (closeStations.length < 1) {
|
if (closeStations.length < 1) {
|
||||||
@ -51,6 +48,7 @@ export default class Line {
|
|||||||
stations,
|
stations,
|
||||||
visitedStations,
|
visitedStations,
|
||||||
connectionLimit,
|
connectionLimit,
|
||||||
|
connectionRadius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import Line from './Line';
|
import Line from './Line';
|
||||||
|
import Signal from './Signal';
|
||||||
import Station from './Station';
|
import Station from './Station';
|
||||||
|
|
||||||
export default class LineConnection {
|
export default class LineConnection {
|
||||||
public station: Station;
|
|
||||||
public line: Line;
|
public line: Line;
|
||||||
|
public signals: Signal[];
|
||||||
|
public station: Station;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
station: Station,
|
station: Station,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Point from 'pixi.js/lib/core/math/Point';
|
||||||
import * as tinycolor from 'tinycolor2';
|
import * as tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
import Direction, { getPointDirection } from './Direction';
|
import Direction, { getPointDirection } from './Direction';
|
||||||
@ -18,23 +19,23 @@ export default class Station {
|
|||||||
return largest;
|
return largest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static stationsWithinRadius(stations: Station[], point: PIXI.Point,
|
public static stationsWithinRadius(stations: Station[], point: Point,
|
||||||
radius: number): Station[] {
|
radius: number): Station[] {
|
||||||
return stations.filter(station => distance(point, station.location) <= radius);
|
return stations.filter(station => distance(point, station.location) <= radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static stationsInDirection(stations: Station[], point: PIXI.Point,
|
public static stationsInDirection(stations: Station[], point: Point,
|
||||||
direction: Direction): Station[] {
|
direction: Direction): Station[] {
|
||||||
return stations.filter(station => getPointDirection(point, station.location) === direction);
|
return stations.filter(station => getPointDirection(point, station.location) === direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static closestStation(stations: Station[], point: PIXI.Point): Station {
|
public static closestStation(stations: Station[], point: Point): Station {
|
||||||
return stations.reduce(
|
return stations.reduce(
|
||||||
(prev, curr) => distance(point, prev.location) < distance(point, curr.location) ? prev : curr,
|
(prev, curr) => distance(point, prev.location) < distance(point, curr.location) ? prev : curr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static randomCloseLargeStation(stations: Station[], point: PIXI.Point,
|
public static randomCloseLargeStation(stations: Station[], point: Point,
|
||||||
radius: number): Station {
|
radius: number): Station {
|
||||||
const closeStations = Station.stationsWithinRadius(stations, point,
|
const closeStations = Station.stationsWithinRadius(stations, point,
|
||||||
radius);
|
radius);
|
||||||
@ -42,7 +43,7 @@ export default class Station {
|
|||||||
return weightedRandom(closeStations, closeStationWeights);
|
return weightedRandom(closeStations, closeStationWeights);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static isPointDistant(point: PIXI.Point, stations: Station[],
|
public static isPointDistant(point: Point, stations: Station[],
|
||||||
minDistance: number): boolean {
|
minDistance: number): boolean {
|
||||||
for (const station of stations) {
|
for (const station of stations) {
|
||||||
if (distance(point, station.location) < minDistance) {
|
if (distance(point, station.location) < minDistance) {
|
||||||
@ -52,10 +53,15 @@ export default class Station {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static randomDistantPoint(stations: Station[], minDistance: number): PIXI.Point | null {
|
public static randomDistantPoint(
|
||||||
|
stations: Station[],
|
||||||
|
minDistance: number,
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
): Point | null {
|
||||||
let tries = 100;
|
let tries = 100;
|
||||||
while (tries > 0) {
|
while (tries > 0) {
|
||||||
const point = randomPoint();
|
const point = randomPoint(height, width);
|
||||||
if (Station.isPointDistant(point, stations, minDistance)) {
|
if (Station.isPointDistant(point, stations, minDistance)) {
|
||||||
return point;
|
return point;
|
||||||
}
|
}
|
||||||
@ -64,19 +70,16 @@ export default class Station {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public location: PIXI.Point;
|
public location: Point;
|
||||||
public population: number;
|
public population: number;
|
||||||
public connections: LineConnection[];
|
public connections: LineConnection[];
|
||||||
public id: number;
|
public id: number;
|
||||||
public label: PIXI.Text;
|
public color: ColorFormats.RGBA;
|
||||||
public color: tinycolorInstance;
|
|
||||||
|
|
||||||
private textStyle: object;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
location: PIXI.Point,
|
location: Point,
|
||||||
population: number,
|
population: number,
|
||||||
color: tinycolorInstance,
|
color: ColorFormats.RGBA,
|
||||||
connections?: LineConnection[],
|
connections?: LineConnection[],
|
||||||
) {
|
) {
|
||||||
this.location = location;
|
this.location = location;
|
||||||
@ -87,15 +90,5 @@ export default class Station {
|
|||||||
// for debugging
|
// for debugging
|
||||||
stationCount += 1;
|
stationCount += 1;
|
||||||
this.id = stationCount;
|
this.id = stationCount;
|
||||||
this.textStyle = {
|
|
||||||
fill: '#FFA500',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '12px',
|
|
||||||
};
|
|
||||||
this.renderLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderLabel() {
|
|
||||||
this.label = new PIXI.Text(`${this.id}`, this.textStyle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
src/Station.worker.ts
Normal file
60
src/Station.worker.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import * as tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
|
import Line from './Line';
|
||||||
|
import Station from './Station';
|
||||||
|
import { randomInt } from './utils';
|
||||||
|
|
||||||
|
const LINE_CONNECTION_LIMIT = 5;
|
||||||
|
const ctx: Worker = self as any;
|
||||||
|
|
||||||
|
const initStations = (numStations: number, height: number, width: number): Station[] => {
|
||||||
|
const stations: Station[] = [];
|
||||||
|
for (let i = 0; i < numStations; i += 1) {
|
||||||
|
stations.push(new Station(
|
||||||
|
Station.randomDistantPoint(stations, 30, height, width),
|
||||||
|
randomInt(300, 2000),
|
||||||
|
tinycolor.random().toRgb()));
|
||||||
|
}
|
||||||
|
return stations;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initLines = (numLines: number, stations: Station[], connectionRadius: number): Line[] => {
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < numLines; i += 1) {
|
||||||
|
let color = tinycolor.random();
|
||||||
|
while (color.isDark()) {
|
||||||
|
color = tinycolor.random();
|
||||||
|
}
|
||||||
|
const lineColor = color.toRgb();
|
||||||
|
const stationsWithoutConnections = stations.filter(station =>
|
||||||
|
station.connections.length === 0,
|
||||||
|
);
|
||||||
|
let centralHub: Station;
|
||||||
|
if (stationsWithoutConnections.length > 0) {
|
||||||
|
centralHub = Station.largestStation(stationsWithoutConnections);
|
||||||
|
} else {
|
||||||
|
centralHub = stations[randomInt(0, stations.length - 1)];
|
||||||
|
}
|
||||||
|
const line = new Line(`line-${i}`, lineColor);
|
||||||
|
const stationsLeft = stations.slice(0);
|
||||||
|
line.connectStations(centralHub, stationsLeft, [], LINE_CONNECTION_LIMIT, connectionRadius);
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.addEventListener('message', (event: MessageEvent) => {
|
||||||
|
if ('initStations' in event.data) {
|
||||||
|
const { connectionRadius, height, numLines, numStations, width } = event.data.initStations;
|
||||||
|
let stations: Station[] = [];
|
||||||
|
let lines: Line[] = [];
|
||||||
|
let stationsWithConnections: Station[] = [];
|
||||||
|
while (stationsWithConnections.length === 0) {
|
||||||
|
// If all stations are too far away to connect, try generating again
|
||||||
|
stations = initStations(numStations, height, width);
|
||||||
|
lines = initLines(numLines, stations, connectionRadius);
|
||||||
|
stationsWithConnections = stations.filter(station => station.connections.length > 0);
|
||||||
|
}
|
||||||
|
ctx.postMessage({ stations, lines });
|
||||||
|
}
|
||||||
|
});
|
28
src/Train.ts
28
src/Train.ts
@ -1,3 +1,4 @@
|
|||||||
|
import Point from 'pixi.js/lib/core/math/Point';
|
||||||
import * as tinycolor from 'tinycolor2';
|
import * as tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
import Station from './Station';
|
import Station from './Station';
|
||||||
@ -5,25 +6,23 @@ import Station from './Station';
|
|||||||
let trainCount = 0;
|
let trainCount = 0;
|
||||||
|
|
||||||
export default class Train {
|
export default class Train {
|
||||||
public location: PIXI.Point;
|
public location: Point;
|
||||||
public speed: number;
|
public speed: number;
|
||||||
public origin: Station;
|
public origin: Station;
|
||||||
public destination: Station;
|
public destination: Station;
|
||||||
public passengers: number;
|
public passengers: number;
|
||||||
public id: number;
|
public id: number;
|
||||||
public label: PIXI.Text;
|
public color: ColorFormats.RGBA;
|
||||||
public color: tinycolorInstance;
|
|
||||||
public sprite: PIXI.Sprite;
|
|
||||||
|
|
||||||
private textStyle: object;
|
private textStyle: object;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
location: PIXI.Point,
|
location: Point,
|
||||||
speed: number,
|
speed: number,
|
||||||
passengers: number,
|
passengers: number,
|
||||||
origin: Station,
|
origin: Station,
|
||||||
destination: Station,
|
destination: Station,
|
||||||
color: tinycolorInstance,
|
color: ColorFormats.RGBA,
|
||||||
) {
|
) {
|
||||||
this.location = location;
|
this.location = location;
|
||||||
this.speed = speed;
|
this.speed = speed;
|
||||||
@ -32,25 +31,8 @@ export default class Train {
|
|||||||
this.passengers = passengers;
|
this.passengers = passengers;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
|
|
||||||
this.sprite = new PIXI.Sprite(PIXI.loader.resources.nodeImg.texture);
|
|
||||||
|
|
||||||
// for debugging
|
// for debugging
|
||||||
trainCount += 1;
|
trainCount += 1;
|
||||||
this.id = trainCount;
|
this.id = trainCount;
|
||||||
this.textStyle = {
|
|
||||||
fill: '#AEAEAE',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '12px',
|
|
||||||
};
|
|
||||||
this.renderLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boardPassengers() {
|
|
||||||
if (this.location === this.origin.location) { // about to leave a station
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderLabel() {
|
|
||||||
this.label = new PIXI.Text(`${this.id}`, this.textStyle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
116
src/Train.worker.ts
Normal file
116
src/Train.worker.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import Point from 'pixi.js/lib/core/math/Point';
|
||||||
|
import * as tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
|
import Station from './Station';
|
||||||
|
import Train from './Train';
|
||||||
|
import { distance, pointsAlmostEqual, randomInt, weightedRandom } from './utils';
|
||||||
|
|
||||||
|
// TODO: define these in a common constants file
|
||||||
|
const MAX_SPEED = 10.0;
|
||||||
|
const ACCELERATION = 0.025;
|
||||||
|
const APPROACH_DISTANCE = 3.0;
|
||||||
|
const TRAIN_CAPACITY = 50;
|
||||||
|
|
||||||
|
const ctx: Worker = self as any;
|
||||||
|
|
||||||
|
let stations: Station[] = [];
|
||||||
|
let trains: Train[] = [];
|
||||||
|
|
||||||
|
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 = stationsWithConnections[
|
||||||
|
Math.floor(Math.random() * stationsWithConnections.length)
|
||||||
|
];
|
||||||
|
trains.push(new Train(
|
||||||
|
new Point(originStation.location.x, originStation.location.y),
|
||||||
|
0, 0, originStation, undefined, tinycolor('grey').toRgb()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return trains;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = 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,
|
||||||
|
train.origin.population));
|
||||||
|
// set or mix train color with the color of new passenger origin
|
||||||
|
if (train.passengers === 0) {
|
||||||
|
train.color = train.origin.color;
|
||||||
|
} else {
|
||||||
|
train.color = tinycolor.mix(
|
||||||
|
train.color,
|
||||||
|
train.origin.color,
|
||||||
|
Math.round((boardingPassengers / train.passengers) * 100),
|
||||||
|
).toRgb();
|
||||||
|
}
|
||||||
|
train.passengers += boardingPassengers;
|
||||||
|
train.origin.population -= boardingPassengers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// train reached destination, stop moving and let passengers off
|
||||||
|
if (pointsAlmostEqual(train.location, train.destination.location)) {
|
||||||
|
train.speed = 0;
|
||||||
|
|
||||||
|
// average destination color with passenger color weighted by ratio
|
||||||
|
// (a simulation of culture mixing)
|
||||||
|
train.destination.color = tinycolor.mix(
|
||||||
|
train.destination.color,
|
||||||
|
train.origin.color,
|
||||||
|
Math.round((train.passengers / train.destination.population) * 100),
|
||||||
|
).toRgb();
|
||||||
|
|
||||||
|
// transfer passengers to destination
|
||||||
|
const disembarkingPassengers = randomInt(0, train.passengers);
|
||||||
|
train.destination.population += disembarkingPassengers;
|
||||||
|
train.passengers -= disembarkingPassengers;
|
||||||
|
|
||||||
|
// prepare for next journey
|
||||||
|
train.origin = train.destination;
|
||||||
|
train.destination = undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const journeyLeft = distance(train.location, train.destination.location);
|
||||||
|
|
||||||
|
if ((train.speed / ACCELERATION) >= ((journeyLeft / train.speed) - APPROACH_DISTANCE) &&
|
||||||
|
train.speed !== ACCELERATION) {
|
||||||
|
// slowing down
|
||||||
|
train.speed -= ACCELERATION;
|
||||||
|
} else if (train.speed < MAX_SPEED) {
|
||||||
|
// speeding up
|
||||||
|
train.speed += ACCELERATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance train
|
||||||
|
const progress = train.speed / journeyLeft;
|
||||||
|
train.location.x += ((train.destination.location.x - train.location.x) * progress);
|
||||||
|
train.location.y += ((train.destination.location.y - train.location.y) * progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.addEventListener('message', (event: MessageEvent) => {
|
||||||
|
if ('initTrains' in event.data) {
|
||||||
|
const { numTrains } = event.data.initTrains;
|
||||||
|
stations = event.data.initTrains.stations;
|
||||||
|
trains = initTrains(numTrains, stations);
|
||||||
|
ctx.postMessage({ initTrains: trains });
|
||||||
|
} else if ('moveTrains' in event.data) {
|
||||||
|
// trains = event.data.moveTrains.trains;
|
||||||
|
// stations = event.data.moveTrains.stations;
|
||||||
|
moveTrains(trains, stations);
|
||||||
|
ctx.postMessage({ moveTrains: trains });
|
||||||
|
}
|
||||||
|
});
|
260
src/transport.ts
260
src/transport.ts
@ -6,18 +6,20 @@ import * as tinycolor from 'tinycolor2';
|
|||||||
import Direction from './Direction';
|
import Direction from './Direction';
|
||||||
import Line from './Line';
|
import Line from './Line';
|
||||||
import Station from './Station';
|
import Station from './Station';
|
||||||
|
import StationWorker from 'worker-loader!./Station.worker';
|
||||||
import Train from './Train';
|
import Train from './Train';
|
||||||
import { distance, pointsAlmostEqual, pointsEqual, randomInt, randomPoint,
|
import { distance, pointsAlmostEqual, pointsEqual, randomInt, randomPoint,
|
||||||
rangeMap, weightedRandom } from './utils';
|
rangeMap, weightedRandom } from './utils';
|
||||||
|
import TrainWorker from 'worker-loader!./Train.worker';
|
||||||
|
|
||||||
import * as imgNode from './node.png';
|
import * as imgNode from './node.png';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const NODE_RES = 100;
|
const NODE_RES = 100;
|
||||||
|
|
||||||
const MAX_SPEED = 10.0;
|
const CONNECTION_RADIUS = Math.floor(Math.sqrt(
|
||||||
const ACCELERATION = 0.025;
|
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
|
||||||
const APPROACH_DISTANCE = 3.0;
|
) / 8);
|
||||||
const TRAIN_CAPACITY = 50;
|
const TRAIN_CAPACITY = 50;
|
||||||
const LINE_CONNECTION_LIMIT = 5;
|
const LINE_CONNECTION_LIMIT = 5;
|
||||||
const WORLD_WIDTH = 1000;
|
const WORLD_WIDTH = 1000;
|
||||||
@ -29,161 +31,49 @@ const ZOOM_MAX_HEIGHT = 4000;
|
|||||||
|
|
||||||
const trainTexts: PIXI.Text[] = [];
|
const trainTexts: PIXI.Text[] = [];
|
||||||
|
|
||||||
const initStations = (numStations: number): Station[] => {
|
const drawStations = (stations: Station[], stationLabels: PIXI.Text[], graphics: PIXI.Graphics) => {
|
||||||
const stations: Station[] = [];
|
stations.forEach((station, i) => {
|
||||||
for (let i = 0; i < numStations; i += 1) {
|
|
||||||
stations.push(new Station(
|
|
||||||
Station.randomDistantPoint(stations, 30),
|
|
||||||
randomInt(300, 2000),
|
|
||||||
tinycolor.random()));
|
|
||||||
}
|
|
||||||
return stations;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = 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')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
let centralHub: Station;
|
|
||||||
if (stationsWithoutConnections.length > 0) {
|
|
||||||
centralHub = Station.largestStation(stationsWithoutConnections);
|
|
||||||
} else {
|
|
||||||
centralHub = stations[randomInt(0, stations.length - 1)];
|
|
||||||
}
|
|
||||||
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 = 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,
|
|
||||||
train.origin.population));
|
|
||||||
// set or mix train color with the color of new passenger origin
|
|
||||||
if (train.passengers === 0) {
|
|
||||||
train.color = train.origin.color;
|
|
||||||
} else {
|
|
||||||
train.color = tinycolor.mix(
|
|
||||||
train.color,
|
|
||||||
train.origin.color,
|
|
||||||
Math.round((boardingPassengers / train.passengers) * 100),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
train.passengers += boardingPassengers;
|
|
||||||
train.origin.population -= boardingPassengers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// train reached destination, stop moving and let passengers off
|
|
||||||
if (pointsAlmostEqual(train.location, train.destination.location)) {
|
|
||||||
train.speed = 0;
|
|
||||||
|
|
||||||
// average destination color with passenger color weighted by ratio
|
|
||||||
// (a simulation of culture mixing)
|
|
||||||
train.destination.color = tinycolor.mix(
|
|
||||||
train.destination.color,
|
|
||||||
train.origin.color,
|
|
||||||
Math.round((train.passengers / train.destination.population) * 100),
|
|
||||||
);
|
|
||||||
|
|
||||||
// transfer passengers to destination
|
|
||||||
const disembarkingPassengers = randomInt(0, train.passengers);
|
|
||||||
train.destination.population += disembarkingPassengers;
|
|
||||||
train.passengers -= disembarkingPassengers;
|
|
||||||
|
|
||||||
// prepare for next journey
|
|
||||||
train.origin = train.destination;
|
|
||||||
train.destination = undefined;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const journeyLeft = distance(train.location, train.destination.location);
|
|
||||||
|
|
||||||
if ((train.speed / ACCELERATION) >= ((journeyLeft / train.speed) - APPROACH_DISTANCE) &&
|
|
||||||
train.speed !== ACCELERATION) {
|
|
||||||
// slowing down
|
|
||||||
train.speed -= ACCELERATION;
|
|
||||||
} else if (train.speed < MAX_SPEED) {
|
|
||||||
// speeding up
|
|
||||||
train.speed += ACCELERATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
// advance train
|
|
||||||
const progress = train.speed / journeyLeft;
|
|
||||||
train.location.x += ((train.destination.location.x - train.location.x) * progress);
|
|
||||||
train.location.y += ((train.destination.location.y - train.location.y) * progress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawStations = (stations: Station[], graphics: PIXI.Graphics) => {
|
|
||||||
for (const station of stations) {
|
|
||||||
const radius = station.population / 150;
|
const radius = station.population / 150;
|
||||||
graphics.beginFill(parseInt(station.color.toHex(), 16), 0.5);
|
const color = tinycolor(station.color);
|
||||||
|
graphics.beginFill(parseInt(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();
|
||||||
station.label.x = station.location.x + radius + 1;
|
stationLabels[i].x = station.location.x + radius + 1;
|
||||||
station.label.y = station.location.y + radius + 1;
|
stationLabels[i].y = station.location.y + radius + 1;
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
|
const drawTrains = (
|
||||||
for (const train of trains) {
|
trains: Train[],
|
||||||
|
trainLabels: PIXI.Text[],
|
||||||
|
trainSprites: PIXI.Sprite[],
|
||||||
|
graphics: PIXI.Graphics,
|
||||||
|
) => {
|
||||||
|
trains.forEach((train, i) => {
|
||||||
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;
|
const color = tinycolor(train.color);
|
||||||
train.sprite.y = train.location.y;
|
trainSprites[i].x = train.location.x;
|
||||||
train.sprite.scale.x = scale;
|
trainSprites[i].y = train.location.y;
|
||||||
train.sprite.scale.y = scale;
|
trainSprites[i].scale.x = scale;
|
||||||
train.sprite.tint = parseInt(train.color.toHex(), 16);
|
trainSprites[i].scale.y = scale;
|
||||||
train.label.x = train.location.x + scale + 1;
|
trainSprites[i].tint = parseInt(color.toHex(), 16);
|
||||||
train.label.y = train.location.y + scale + 1;
|
trainLabels[i].x = train.location.x + scale + 1;
|
||||||
}
|
trainLabels[i].y = train.location.y + scale + 1;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
|
const drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
|
||||||
for (const station of stations) {
|
for (const station of stations) {
|
||||||
for (const connection of station.connections) {
|
for (const connection of station.connections) {
|
||||||
|
const color = tinycolor(connection.line.color);
|
||||||
let twoWay = false;
|
let twoWay = false;
|
||||||
for (const conn of connection.station.connections) {
|
for (const conn of connection.station.connections) {
|
||||||
if (conn.station === station) {
|
if (conn.station === station) {
|
||||||
twoWay = true;
|
twoWay = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
graphics.lineStyle(twoWay ? 2 : 1, parseInt(connection.line.color.toHex(), 16), 1);
|
graphics.lineStyle(twoWay ? 2 : 1, parseInt(color.toHex(), 16), 1);
|
||||||
graphics.moveTo(station.location.x, station.location.y);
|
graphics.moveTo(station.location.x, station.location.y);
|
||||||
graphics.lineTo(connection.station.location.x, connection.station.location.y);
|
graphics.lineTo(connection.station.location.x, connection.station.location.y);
|
||||||
}
|
}
|
||||||
@ -210,28 +100,85 @@ const run = () => {
|
|||||||
|
|
||||||
let stations: Station[] = [];
|
let stations: Station[] = [];
|
||||||
let lines: Line[] = [];
|
let lines: Line[] = [];
|
||||||
let stationsWithConnections: Station[] = [];
|
let trains: Train[] = [];
|
||||||
while (stationsWithConnections.length === 0) {
|
const stationLabels: PIXI.Text[] = [];
|
||||||
// If all stations are too far away to connect, try generating again
|
const trainLabels: PIXI.Text[] = [];
|
||||||
stations = initStations(30);
|
const trainSprites: PIXI.Sprite[] = [];
|
||||||
lines = initLines(4, stations);
|
const stationWorker = new StationWorker();
|
||||||
stationsWithConnections = stations.filter(station => station.connections.length > 0);
|
const trainWorker = new TrainWorker();
|
||||||
}
|
|
||||||
const trains = initTrains(50, stations);
|
stationWorker.postMessage({ initStations: {
|
||||||
|
connectionRadius: CONNECTION_RADIUS,
|
||||||
|
height: window.innerHeight,
|
||||||
|
numLines: 4,
|
||||||
|
numStations: 30,
|
||||||
|
width: window.innerWidth,
|
||||||
|
}});
|
||||||
|
stationWorker.onmessage = (event: MessageEvent) => {
|
||||||
|
stations = event.data.stations;
|
||||||
|
lines = event.data.lines;
|
||||||
|
console.log(stations);
|
||||||
|
console.log(lines);
|
||||||
|
|
||||||
|
trainWorker.postMessage({ initTrains: {
|
||||||
|
stations,
|
||||||
|
|
||||||
|
numTrains: 50,
|
||||||
|
}});
|
||||||
|
trainWorker.onmessage = (trainEvent: MessageEvent) => {
|
||||||
|
if ('initTrains' in trainEvent.data) {
|
||||||
|
trains = trainEvent.data.initTrains;
|
||||||
|
|
||||||
|
// add train sprites
|
||||||
|
for (const train of trains) {
|
||||||
|
const sprite = new PIXI.Sprite(PIXI.loader.resources.nodeImg.texture);
|
||||||
|
sprite.visible = false;
|
||||||
|
trainSprites.push(sprite);
|
||||||
|
viewport.addChild(sprite);
|
||||||
|
}
|
||||||
|
// Add debug labels
|
||||||
|
for (const train of trains) {
|
||||||
|
const label = new PIXI.Text(
|
||||||
|
`${train.id}`, {
|
||||||
|
fill: '#AEAEAE',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '12px',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
trainLabels.push(label);
|
||||||
|
viewport.addChild(label);
|
||||||
|
}
|
||||||
|
} else if ('moveTrains' in trainEvent.data) {
|
||||||
|
trains = trainEvent.data.moveTrains;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const station of stations) {
|
||||||
|
const label = new PIXI.Text(
|
||||||
|
`${station.id}`, {
|
||||||
|
fill: '#FFA500',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '12px',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
stationLabels.push(label);
|
||||||
|
viewport.addChild(label);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ticker.stop();
|
ticker.stop();
|
||||||
ticker.add((deltaTime) => {
|
ticker.add((deltaTime) => {
|
||||||
stats.begin();
|
stats.begin();
|
||||||
|
|
||||||
moveTrains(trains, stations);
|
trainWorker.postMessage({ moveTrains: {} });
|
||||||
|
|
||||||
graphics.clear();
|
graphics.clear();
|
||||||
|
|
||||||
graphics.lineStyle(1, 0xFFA500, 1);
|
graphics.lineStyle(1, 0xFFA500, 1);
|
||||||
drawStations(stations, graphics);
|
drawStations(stations, stationLabels, graphics);
|
||||||
|
|
||||||
graphics.lineStyle(1, 0xAEAEAE, 1);
|
graphics.lineStyle(1, 0xAEAEAE, 1);
|
||||||
drawTrains(trains, graphics);
|
drawTrains(trains, trainLabels, trainSprites, graphics);
|
||||||
|
|
||||||
drawLines(stations, graphics);
|
drawLines(stations, graphics);
|
||||||
|
|
||||||
@ -240,17 +187,6 @@ const run = () => {
|
|||||||
ticker.start();
|
ticker.start();
|
||||||
|
|
||||||
viewport.addChild(graphics);
|
viewport.addChild(graphics);
|
||||||
// add train sprites
|
|
||||||
for (const train of trains) {
|
|
||||||
viewport.addChild(train.sprite);
|
|
||||||
}
|
|
||||||
// Add debug labels
|
|
||||||
for (const train of trains) {
|
|
||||||
viewport.addChild(train.label);
|
|
||||||
}
|
|
||||||
for (const station of stations) {
|
|
||||||
viewport.addChild(station.label);
|
|
||||||
}
|
|
||||||
document.body.appendChild(app.view);
|
document.body.appendChild(app.view);
|
||||||
app.stage.addChild(viewport);
|
app.stage.addChild(viewport);
|
||||||
viewport.drag().pinch().wheel().clampZoom({
|
viewport.drag().pinch().wheel().clampZoom({
|
||||||
|
16
src/utils.ts
16
src/utils.ts
@ -1,4 +1,4 @@
|
|||||||
import * as PIXI from 'pixi.js';
|
import Point from 'pixi.js/lib/core/math/Point';
|
||||||
|
|
||||||
const EPSILON = 1.0;
|
const EPSILON = 1.0;
|
||||||
|
|
||||||
@ -19,20 +19,20 @@ export const weightedRandom = (choices: any[], weights: number[]): any => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const randomPoint = () => (
|
export const randomPoint = (height: number, width: number): Point => (
|
||||||
new PIXI.Point(randomInt(0, window.innerWidth), randomInt(0, window.innerHeight))
|
new Point(randomInt(0, width), randomInt(0, height))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pointsEqual = (pointA: PIXI.Point, pointB: PIXI.Point): boolean => (
|
export const pointsEqual = (pointA: Point, pointB: Point): boolean => (
|
||||||
(pointA.x === pointB.x && pointA.y === pointB.y)
|
(pointA.x === pointB.x && pointA.y === pointB.y)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pointsAlmostEqual = (pointA: PIXI.Point, pointB: PIXI.Point): boolean => (
|
export const pointsAlmostEqual = (pointA: Point, pointB: Point): boolean => (
|
||||||
Math.abs(pointA.x - pointB.x) < EPSILON &&
|
Math.abs(pointA.x - pointB.x) < EPSILON &&
|
||||||
Math.abs(pointA.y - pointB.y) < EPSILON
|
Math.abs(pointA.y - pointB.y) < EPSILON
|
||||||
);
|
);
|
||||||
|
|
||||||
export const distance = (pointA: PIXI.Point, pointB: PIXI.Point): number => {
|
export const distance = (pointA: Point, pointB: Point): number => {
|
||||||
const distX = pointA.x - pointB.x;
|
const distX = pointA.x - pointB.x;
|
||||||
const distY = pointA.y - pointB.y;
|
const distY = pointA.y - pointB.y;
|
||||||
return Math.sqrt((distX * distX) + (distY * distY));
|
return Math.sqrt((distX * distX) + (distY * distY));
|
||||||
@ -43,10 +43,10 @@ export const rangeMap = (num: number, inMin: number, inMax: number,
|
|||||||
(num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
|
(num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
|
||||||
);
|
);
|
||||||
|
|
||||||
export const angleRadians = (pointA: PIXI.Point, pointB: PIXI.Point): number => (
|
export const angleRadians = (pointA: Point, pointB: Point): number => (
|
||||||
Math.atan2(-(pointB.x - pointA.x), pointB.y - pointA.y)
|
Math.atan2(-(pointB.x - pointA.x), pointB.y - pointA.y)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const angleDegrees = (pointA: PIXI.Point, pointB: PIXI.Point): number => (
|
export const angleDegrees = (pointA: Point, pointB: Point): number => (
|
||||||
180 + angleRadians(pointA, pointB) * (180 / Math.PI)
|
180 + angleRadians(pointA, pointB) * (180 / Math.PI)
|
||||||
);
|
);
|
||||||
|
16
typings/custom.d.ts
vendored
16
typings/custom.d.ts
vendored
@ -1 +1,17 @@
|
|||||||
declare module '*.png';
|
declare module '*.png';
|
||||||
|
|
||||||
|
declare module 'worker-loader!*' {
|
||||||
|
class WebpackWorker extends Worker {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebpackWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'pixi.js/lib/core/math/Point' {
|
||||||
|
export default PIXI.Point;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'pixi.js/lib/core/text/Text' {
|
||||||
|
export default PIXI.Text;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user