Compare commits
1 Commits
master
...
webworkers
| Author | SHA1 | Date | |
|---|---|---|---|
| 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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user