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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||
|
@ -41,7 +41,8 @@
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"webpack": "^4.4.1",
|
||||
"webpack-cli": "^2.0.13",
|
||||
"webpack-dev-server": "^3.1.1"
|
||||
"webpack-dev-server": "^3.1.1",
|
||||
"worker-loader": "^1.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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 { 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 {
|
||||
public name: string;
|
||||
public color: tinycolorInstance;
|
||||
public color: ColorFormats.RGBA;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
color: tinycolorInstance,
|
||||
color: ColorFormats.RGBA,
|
||||
) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
@ -26,13 +22,14 @@ export default class Line {
|
||||
stations: Station[],
|
||||
visitedStations: Station[],
|
||||
connectionLimit: number,
|
||||
connectionRadius: number,
|
||||
) {
|
||||
visitedStations.push(currentStation);
|
||||
const otherStations = stations.filter(station => station !== currentStation);
|
||||
const closeStations = Station.stationsWithinRadius(
|
||||
otherStations,
|
||||
currentStation.location,
|
||||
CONNECTION_RADIUS,
|
||||
connectionRadius,
|
||||
);
|
||||
for (let i = 0; i < connectionLimit; i += 1) {
|
||||
if (closeStations.length < 1) {
|
||||
@ -51,6 +48,7 @@ export default class Line {
|
||||
stations,
|
||||
visitedStations,
|
||||
connectionLimit,
|
||||
connectionRadius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import Line from './Line';
|
||||
import Signal from './Signal';
|
||||
import Station from './Station';
|
||||
|
||||
export default class LineConnection {
|
||||
public station: Station;
|
||||
public line: Line;
|
||||
public signals: Signal[];
|
||||
public station: Station;
|
||||
|
||||
constructor(
|
||||
station: Station,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import Point from 'pixi.js/lib/core/math/Point';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
|
||||
import Direction, { getPointDirection } from './Direction';
|
||||
@ -18,23 +19,23 @@ export default class Station {
|
||||
return largest;
|
||||
}
|
||||
|
||||
public static stationsWithinRadius(stations: Station[], point: PIXI.Point,
|
||||
public static stationsWithinRadius(stations: Station[], point: Point,
|
||||
radius: number): Station[] {
|
||||
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[] {
|
||||
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(
|
||||
(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 {
|
||||
const closeStations = Station.stationsWithinRadius(stations, point,
|
||||
radius);
|
||||
@ -42,7 +43,7 @@ export default class Station {
|
||||
return weightedRandom(closeStations, closeStationWeights);
|
||||
}
|
||||
|
||||
public static isPointDistant(point: PIXI.Point, stations: Station[],
|
||||
public static isPointDistant(point: Point, stations: Station[],
|
||||
minDistance: number): boolean {
|
||||
for (const station of stations) {
|
||||
if (distance(point, station.location) < minDistance) {
|
||||
@ -52,10 +53,15 @@ export default class Station {
|
||||
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;
|
||||
while (tries > 0) {
|
||||
const point = randomPoint();
|
||||
const point = randomPoint(height, width);
|
||||
if (Station.isPointDistant(point, stations, minDistance)) {
|
||||
return point;
|
||||
}
|
||||
@ -64,19 +70,16 @@ export default class Station {
|
||||
return null;
|
||||
}
|
||||
|
||||
public location: PIXI.Point;
|
||||
public location: Point;
|
||||
public population: number;
|
||||
public connections: LineConnection[];
|
||||
public id: number;
|
||||
public label: PIXI.Text;
|
||||
public color: tinycolorInstance;
|
||||
|
||||
private textStyle: object;
|
||||
public color: ColorFormats.RGBA;
|
||||
|
||||
constructor(
|
||||
location: PIXI.Point,
|
||||
location: Point,
|
||||
population: number,
|
||||
color: tinycolorInstance,
|
||||
color: ColorFormats.RGBA,
|
||||
connections?: LineConnection[],
|
||||
) {
|
||||
this.location = location;
|
||||
@ -87,15 +90,5 @@ export default class Station {
|
||||
// for debugging
|
||||
stationCount += 1;
|
||||
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 Station from './Station';
|
||||
@ -5,25 +6,23 @@ import Station from './Station';
|
||||
let trainCount = 0;
|
||||
|
||||
export default class Train {
|
||||
public location: PIXI.Point;
|
||||
public location: Point;
|
||||
public speed: number;
|
||||
public origin: Station;
|
||||
public destination: Station;
|
||||
public passengers: number;
|
||||
public id: number;
|
||||
public label: PIXI.Text;
|
||||
public color: tinycolorInstance;
|
||||
public sprite: PIXI.Sprite;
|
||||
public color: ColorFormats.RGBA;
|
||||
|
||||
private textStyle: object;
|
||||
|
||||
constructor(
|
||||
location: PIXI.Point,
|
||||
location: Point,
|
||||
speed: number,
|
||||
passengers: number,
|
||||
origin: Station,
|
||||
destination: Station,
|
||||
color: tinycolorInstance,
|
||||
color: ColorFormats.RGBA,
|
||||
) {
|
||||
this.location = location;
|
||||
this.speed = speed;
|
||||
@ -32,25 +31,8 @@ export default class Train {
|
||||
this.passengers = passengers;
|
||||
this.color = color;
|
||||
|
||||
this.sprite = new PIXI.Sprite(PIXI.loader.resources.nodeImg.texture);
|
||||
|
||||
// for debugging
|
||||
trainCount += 1;
|
||||
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 });
|
||||
}
|
||||
});
|
258
src/transport.ts
258
src/transport.ts
@ -6,18 +6,20 @@ import * as tinycolor from 'tinycolor2';
|
||||
import Direction from './Direction';
|
||||
import Line from './Line';
|
||||
import Station from './Station';
|
||||
import StationWorker from 'worker-loader!./Station.worker';
|
||||
import Train from './Train';
|
||||
import { distance, pointsAlmostEqual, pointsEqual, randomInt, randomPoint,
|
||||
rangeMap, weightedRandom } from './utils';
|
||||
import TrainWorker from 'worker-loader!./Train.worker';
|
||||
|
||||
import * as imgNode from './node.png';
|
||||
import './style.css';
|
||||
|
||||
const NODE_RES = 100;
|
||||
|
||||
const MAX_SPEED = 10.0;
|
||||
const ACCELERATION = 0.025;
|
||||
const APPROACH_DISTANCE = 3.0;
|
||||
const CONNECTION_RADIUS = Math.floor(Math.sqrt(
|
||||
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
|
||||
) / 8);
|
||||
const TRAIN_CAPACITY = 50;
|
||||
const LINE_CONNECTION_LIMIT = 5;
|
||||
const WORLD_WIDTH = 1000;
|
||||
@ -29,161 +31,49 @@ const ZOOM_MAX_HEIGHT = 4000;
|
||||
|
||||
const trainTexts: PIXI.Text[] = [];
|
||||
|
||||
const initStations = (numStations: number): Station[] => {
|
||||
const stations: Station[] = [];
|
||||
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 drawStations = (stations: Station[], stationLabels: PIXI.Text[], graphics: PIXI.Graphics) => {
|
||||
stations.forEach((station, i) => {
|
||||
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.endFill();
|
||||
station.label.x = station.location.x + radius + 1;
|
||||
station.label.y = station.location.y + radius + 1;
|
||||
}
|
||||
stationLabels[i].x = station.location.x + radius + 1;
|
||||
stationLabels[i].y = station.location.y + radius + 1;
|
||||
});
|
||||
};
|
||||
|
||||
const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
|
||||
for (const train of trains) {
|
||||
const drawTrains = (
|
||||
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 scale = trainSize / NODE_RES;
|
||||
train.sprite.x = train.location.x;
|
||||
train.sprite.y = train.location.y;
|
||||
train.sprite.scale.x = scale;
|
||||
train.sprite.scale.y = scale;
|
||||
train.sprite.tint = parseInt(train.color.toHex(), 16);
|
||||
train.label.x = train.location.x + scale + 1;
|
||||
train.label.y = train.location.y + scale + 1;
|
||||
}
|
||||
const color = tinycolor(train.color);
|
||||
trainSprites[i].x = train.location.x;
|
||||
trainSprites[i].y = train.location.y;
|
||||
trainSprites[i].scale.x = scale;
|
||||
trainSprites[i].scale.y = scale;
|
||||
trainSprites[i].tint = parseInt(color.toHex(), 16);
|
||||
trainLabels[i].x = train.location.x + scale + 1;
|
||||
trainLabels[i].y = train.location.y + scale + 1;
|
||||
});
|
||||
};
|
||||
|
||||
const drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
|
||||
for (const station of stations) {
|
||||
for (const connection of station.connections) {
|
||||
const color = tinycolor(connection.line.color);
|
||||
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.lineStyle(twoWay ? 2 : 1, parseInt(color.toHex(), 16), 1);
|
||||
graphics.moveTo(station.location.x, station.location.y);
|
||||
graphics.lineTo(connection.station.location.x, connection.station.location.y);
|
||||
}
|
||||
@ -210,28 +100,85 @@ const run = () => {
|
||||
|
||||
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(30);
|
||||
lines = initLines(4, stations);
|
||||
stationsWithConnections = stations.filter(station => station.connections.length > 0);
|
||||
let trains: Train[] = [];
|
||||
const stationLabels: PIXI.Text[] = [];
|
||||
const trainLabels: PIXI.Text[] = [];
|
||||
const trainSprites: PIXI.Sprite[] = [];
|
||||
const stationWorker = new StationWorker();
|
||||
const trainWorker = new TrainWorker();
|
||||
|
||||
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);
|
||||
}
|
||||
const trains = initTrains(50, stations);
|
||||
// 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.add((deltaTime) => {
|
||||
stats.begin();
|
||||
|
||||
moveTrains(trains, stations);
|
||||
trainWorker.postMessage({ moveTrains: {} });
|
||||
|
||||
graphics.clear();
|
||||
|
||||
graphics.lineStyle(1, 0xFFA500, 1);
|
||||
drawStations(stations, graphics);
|
||||
drawStations(stations, stationLabels, graphics);
|
||||
|
||||
graphics.lineStyle(1, 0xAEAEAE, 1);
|
||||
drawTrains(trains, graphics);
|
||||
drawTrains(trains, trainLabels, trainSprites, graphics);
|
||||
|
||||
drawLines(stations, graphics);
|
||||
|
||||
@ -240,17 +187,6 @@ const run = () => {
|
||||
ticker.start();
|
||||
|
||||
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);
|
||||
app.stage.addChild(viewport);
|
||||
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;
|
||||
|
||||
@ -19,20 +19,20 @@ export const weightedRandom = (choices: any[], weights: number[]): any => {
|
||||
}
|
||||
};
|
||||
|
||||
export const randomPoint = () => (
|
||||
new PIXI.Point(randomInt(0, window.innerWidth), randomInt(0, window.innerHeight))
|
||||
export const randomPoint = (height: number, width: number): Point => (
|
||||
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)
|
||||
);
|
||||
|
||||
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.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 distY = pointA.y - pointB.y;
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
16
typings/custom.d.ts
vendored
16
typings/custom.d.ts
vendored
@ -1 +1,17 @@
|
||||
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