Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"extends": "airbnb-base",
|
|
||||||
"plugins": [
|
|
||||||
"import"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
}
|
|
||||||
};
|
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
tags
|
|
||||||
Session.vim
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
20
LICENSE.txt
20
LICENSE.txt
@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2017 Tyler Hallada
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
35
README.md
35
README.md
@ -1,35 +0,0 @@
|
|||||||
# Transport
|
|
||||||
|
|
||||||
Work-in-progress procedurally generated train network simulation written in
|
|
||||||
Typescript with PixiJs.
|
|
||||||
|
|
||||||
![Screenshot of simulation](img/screenshot.png)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Clone the repo and then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Then visit `http://localhost:8888` in your browser to view the development
|
|
||||||
build. Edit files in `/src` and see the changes reflected in the browser.
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
No tests right now. But, to run lint checks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
npm run deploy
|
|
||||||
```
|
|
Binary file not shown.
Before Width: | Height: | Size: 307 KiB |
@ -12,9 +12,9 @@
|
|||||||
<!-- <meta property="og:image:height" content="300" /> -->
|
<!-- <meta property="og:image:height" content="300" /> -->
|
||||||
<!-- <meta property="og:image:alt" content="Screenshot of the animation in action" /> -->
|
<!-- <meta property="og:image:alt" content="Screenshot of the animation in action" /> -->
|
||||||
<!-- <meta property="og:description" content="A procedurally generated and interactive animation created with PixiJS" /> -->
|
<!-- <meta property="og:description" content="A procedurally generated and interactive animation created with PixiJS" /> -->
|
||||||
</head>
|
<link href="main.css" rel="stylesheet"></head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
<script type="text/javascript" src="vendors.min.js"></script><script type="text/javascript" src="main.min.js"></script></body>
|
||||||
|
|
||||||
<!-- Google Analytics -->
|
<!-- Google Analytics -->
|
||||||
<script>
|
<script>
|
||||||
|
@ -18,3 +18,5 @@ canvas {
|
|||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=main.css.map*/
|
1
main.css.map
Normal file
1
main.css.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"main.css","sourceRoot":""}
|
2
main.min.js
vendored
Normal file
2
main.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
main.min.js.map
Normal file
1
main.min.js.map
Normal file
File diff suppressed because one or more lines are too long
13052
package-lock.json
generated
13052
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "transport",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"start": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --mode development",
|
|
||||||
"build": "NODE_ENV=production ./node_modules/.bin/webpack --mode production && cp CNAME dist/",
|
|
||||||
"deploy": "./node_modules/.bin/gh-pages -d dist --remote github",
|
|
||||||
"lint": "./node_modules/.bin/tslint src/",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git@git.hallada.net:pixi.git"
|
|
||||||
},
|
|
||||||
"author": "Tyler Hallada",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.0.0-beta.42",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.0.0-beta.42",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.42",
|
|
||||||
"@babel/preset-env": "^7.0.0-beta.42",
|
|
||||||
"babel-loader": "^8.0.0-beta.2",
|
|
||||||
"css-loader": "^0.28.11",
|
|
||||||
"eslint": "^3.17.0",
|
|
||||||
"eslint-config-airbnb-base": "^11.1.1",
|
|
||||||
"eslint-config-standard": "^7.0.0",
|
|
||||||
"eslint-plugin-import": "^2.2.0",
|
|
||||||
"eslint-plugin-promise": "^3.5.0",
|
|
||||||
"eslint-plugin-standard": "^2.1.1",
|
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
|
||||||
"file-loader": "^1.1.11",
|
|
||||||
"gh-pages": "^1.1.0",
|
|
||||||
"html-webpack-plugin": "^3.1.0",
|
|
||||||
"style-loader": "^0.20.3",
|
|
||||||
"ts-loader": "^4.1.0",
|
|
||||||
"tslint": "^5.9.1",
|
|
||||||
"tslint-config-airbnb": "^5.8.0",
|
|
||||||
"typescript": "^2.8.1",
|
|
||||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
|
||||||
"webpack": "^4.4.1",
|
|
||||||
"webpack-cli": "^2.0.13",
|
|
||||||
"webpack-dev-server": "^3.1.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/pixi.js": "^4.7.2",
|
|
||||||
"@types/stats.js": "^0.17.0",
|
|
||||||
"@types/tinycolor2": "^1.4.0",
|
|
||||||
"pixi-viewport": "^1.5.0",
|
|
||||||
"pixi.js": "^4.7.1",
|
|
||||||
"stats.js": "^0.17.0",
|
|
||||||
"tinycolor2": "^1.4.1"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import * as PIXI from 'pixi.js';
|
|
||||||
|
|
||||||
import { angleDegrees } from './utils';
|
|
||||||
|
|
||||||
enum Direction {
|
|
||||||
North, // 0
|
|
||||||
Northeast, // 1
|
|
||||||
East, // 2
|
|
||||||
Southeast, // 3
|
|
||||||
South, // 4
|
|
||||||
Southwest, // 5
|
|
||||||
West, // 6
|
|
||||||
Northwest, // 7
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getPointDirection = (pointA: PIXI.Point, pointB: PIXI.Point): Direction => {
|
|
||||||
const angle = angleDegrees(pointA, pointB);
|
|
||||||
let direction = null;
|
|
||||||
if (angle >= 337.5 || angle < 22.5) {
|
|
||||||
direction = Direction.North;
|
|
||||||
} else if (angle >= 22.5 && angle < 67.5) {
|
|
||||||
direction = Direction.Northeast;
|
|
||||||
} else if (angle >= 67.5 && angle < 112.5) {
|
|
||||||
direction = Direction.East;
|
|
||||||
} else if (angle >= 112.5 && angle < 157.5) {
|
|
||||||
direction = Direction.Southeast;
|
|
||||||
} else if (angle >= 157.5 && angle < 202.5) {
|
|
||||||
direction = Direction.South;
|
|
||||||
} else if (angle >= 202.5 && angle < 247.5) {
|
|
||||||
direction = Direction.Southwest;
|
|
||||||
} else if (angle >= 247.5 && angle < 292.5) {
|
|
||||||
direction = Direction.West;
|
|
||||||
} else if (angle >= 292.5 && angle < 337.5) {
|
|
||||||
direction = Direction.Northwest;
|
|
||||||
} else {
|
|
||||||
throw Error('Angle between points is not a valid degree');
|
|
||||||
}
|
|
||||||
return direction;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Direction;
|
|
58
src/Line.ts
58
src/Line.ts
@ -1,58 +0,0 @@
|
|||||||
import * as tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
import Direction, { getPointDirection } from './Direction';
|
|
||||||
import LineConnection from './LineConnection';
|
|
||||||
import Station from './Station';
|
|
||||||
import { distance, randomInt, randomPoint } from './utils';
|
|
||||||
|
|
||||||
const CONNECTION_RADIUS = Math.floor(Math.sqrt(
|
|
||||||
Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2),
|
|
||||||
) / 8);
|
|
||||||
|
|
||||||
export default class Line {
|
|
||||||
public name: string;
|
|
||||||
public color: tinycolorInstance;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
name: string,
|
|
||||||
color: tinycolorInstance,
|
|
||||||
) {
|
|
||||||
this.name = name;
|
|
||||||
this.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public connectStations(
|
|
||||||
currentStation: Station,
|
|
||||||
stations: Station[],
|
|
||||||
visitedStations: Station[],
|
|
||||||
connectionLimit: number,
|
|
||||||
) {
|
|
||||||
visitedStations.push(currentStation);
|
|
||||||
const otherStations = stations.filter(station => station !== currentStation);
|
|
||||||
const closeStations = Station.stationsWithinRadius(
|
|
||||||
otherStations,
|
|
||||||
currentStation.location,
|
|
||||||
CONNECTION_RADIUS,
|
|
||||||
);
|
|
||||||
for (let i = 0; i < connectionLimit; i += 1) {
|
|
||||||
if (closeStations.length < 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const largest = Station.largestStation(closeStations);
|
|
||||||
currentStation.connections.push(
|
|
||||||
new LineConnection(largest, this),
|
|
||||||
);
|
|
||||||
closeStations.splice(closeStations.indexOf(largest), 1);
|
|
||||||
}
|
|
||||||
for (const connectedStation of currentStation.connections) {
|
|
||||||
if (visitedStations.indexOf(connectedStation.station) === -1) {
|
|
||||||
this.connectStations(
|
|
||||||
connectedStation.station,
|
|
||||||
stations,
|
|
||||||
visitedStations,
|
|
||||||
connectionLimit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import Line from './Line';
|
|
||||||
import Station from './Station';
|
|
||||||
|
|
||||||
export default class LineConnection {
|
|
||||||
public station: Station;
|
|
||||||
public line: Line;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
station: Station,
|
|
||||||
line: Line,
|
|
||||||
) {
|
|
||||||
this.station = station;
|
|
||||||
this.line = line;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import * as PIXI from 'pixi.js';
|
|
||||||
|
|
||||||
export default class Signal {
|
|
||||||
public location: PIXI.Point;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
location: PIXI.Point,
|
|
||||||
) {
|
|
||||||
this.location = location;
|
|
||||||
}
|
|
||||||
}
|
|
101
src/Station.ts
101
src/Station.ts
@ -1,101 +0,0 @@
|
|||||||
import * as tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
import Direction, { getPointDirection } from './Direction';
|
|
||||||
import LineConnection from './LineConnection';
|
|
||||||
import { distance, randomPoint, weightedRandom } from './utils';
|
|
||||||
|
|
||||||
let stationCount = 0;
|
|
||||||
|
|
||||||
export default class Station {
|
|
||||||
// Utility methods for working with arrays of Stations
|
|
||||||
public static largestStation(stations: Station[]): Station {
|
|
||||||
let largest: Station = null;
|
|
||||||
for (const station of stations) {
|
|
||||||
if (largest === null || station.population > largest.population) {
|
|
||||||
largest = station;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return largest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static stationsWithinRadius(stations: Station[], point: PIXI.Point,
|
|
||||||
radius: number): Station[] {
|
|
||||||
return stations.filter(station => distance(point, station.location) <= radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static stationsInDirection(stations: Station[], point: PIXI.Point,
|
|
||||||
direction: Direction): Station[] {
|
|
||||||
return stations.filter(station => getPointDirection(point, station.location) === direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static closestStation(stations: Station[], point: PIXI.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,
|
|
||||||
radius: number): Station {
|
|
||||||
const closeStations = Station.stationsWithinRadius(stations, point,
|
|
||||||
radius);
|
|
||||||
const closeStationWeights = closeStations.map(station => station.population);
|
|
||||||
return weightedRandom(closeStations, closeStationWeights);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isPointDistant(point: PIXI.Point, stations: Station[],
|
|
||||||
minDistance: number): boolean {
|
|
||||||
for (const station of stations) {
|
|
||||||
if (distance(point, station.location) < minDistance) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static randomDistantPoint(stations: Station[], minDistance: number): PIXI.Point | null {
|
|
||||||
let tries = 100;
|
|
||||||
while (tries > 0) {
|
|
||||||
const point = randomPoint();
|
|
||||||
if (Station.isPointDistant(point, stations, minDistance)) {
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
tries -= 1;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public location: PIXI.Point;
|
|
||||||
public population: number;
|
|
||||||
public connections: LineConnection[];
|
|
||||||
public id: number;
|
|
||||||
public label: PIXI.Text;
|
|
||||||
public color: tinycolorInstance;
|
|
||||||
|
|
||||||
private textStyle: object;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
location: PIXI.Point,
|
|
||||||
population: number,
|
|
||||||
color: tinycolorInstance,
|
|
||||||
connections?: LineConnection[],
|
|
||||||
) {
|
|
||||||
this.location = location;
|
|
||||||
this.population = population;
|
|
||||||
this.color = color;
|
|
||||||
this.connections = connections || [];
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
57
src/Train.ts
57
src/Train.ts
@ -1,57 +0,0 @@
|
|||||||
import * as tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
import Station from './Station';
|
|
||||||
|
|
||||||
let trainCount = 0;
|
|
||||||
|
|
||||||
export default class Train {
|
|
||||||
public location: PIXI.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;
|
|
||||||
|
|
||||||
private textStyle: object;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
location: PIXI.Point,
|
|
||||||
speed: number,
|
|
||||||
passengers: number,
|
|
||||||
origin: Station,
|
|
||||||
destination: Station,
|
|
||||||
color: tinycolorInstance,
|
|
||||||
) {
|
|
||||||
this.location = location;
|
|
||||||
this.speed = speed;
|
|
||||||
this.origin = origin;
|
|
||||||
this.destination = destination;
|
|
||||||
this.passengers = passengers;
|
|
||||||
this.color = color;
|
|
||||||
|
|
||||||
this.sprite = new PIXI.Sprite(PIXI.loader.resources.nodeImg.texture);
|
|
||||||
this.sprite.visible = false;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
269
src/transport.ts
269
src/transport.ts
@ -1,269 +0,0 @@
|
|||||||
import * as Viewport from 'pixi-viewport';
|
|
||||||
import * as PIXI from 'pixi.js';
|
|
||||||
import * as Stats from 'stats.js';
|
|
||||||
import * as tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
import Direction from './Direction';
|
|
||||||
import Line from './Line';
|
|
||||||
import Station from './Station';
|
|
||||||
import Train from './Train';
|
|
||||||
import { distance, pointsAlmostEqual, pointsEqual, randomInt, randomPoint,
|
|
||||||
rangeMap, weightedRandom } from './utils';
|
|
||||||
|
|
||||||
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 TRAIN_CAPACITY = 50;
|
|
||||||
const LINE_CONNECTION_LIMIT = 5;
|
|
||||||
const WORLD_WIDTH = 1000;
|
|
||||||
const WORLD_HEIGHT = 1000;
|
|
||||||
const ZOOM_MIN_WIDTH = 100;
|
|
||||||
const ZOOM_MIN_HEIGHT = 100;
|
|
||||||
const ZOOM_MAX_WIDTH = 4000;
|
|
||||||
const ZOOM_MAX_HEIGHT = 4000;
|
|
||||||
|
|
||||||
const trainTexts: PIXI.Text[] = [];
|
|
||||||
|
|
||||||
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 radius = station.population / 150;
|
|
||||||
graphics.beginFill(parseInt(station.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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => {
|
|
||||||
for (const train of trains) {
|
|
||||||
const trainSize = rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5);
|
|
||||||
const scale = trainSize / NODE_RES;
|
|
||||||
train.sprite.visible = true;
|
|
||||||
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 drawLines = (stations: Station[], graphics: PIXI.Graphics) => {
|
|
||||||
for (const station of stations) {
|
|
||||||
for (const connection of station.connections) {
|
|
||||||
let twoWay = false;
|
|
||||||
for (const conn of connection.station.connections) {
|
|
||||||
if (conn.station === station) {
|
|
||||||
twoWay = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graphics.lineStyle(twoWay ? 2 : 1, parseInt(connection.line.color.toHex(), 16), 1);
|
|
||||||
graphics.moveTo(station.location.x, station.location.y);
|
|
||||||
graphics.lineTo(connection.station.location.x, connection.station.location.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const run = () => {
|
|
||||||
const app = new PIXI.Application({
|
|
||||||
antialias: true,
|
|
||||||
height: window.innerHeight,
|
|
||||||
width: window.innerWidth,
|
|
||||||
});
|
|
||||||
const viewport = new Viewport({
|
|
||||||
screenHeight: window.innerHeight,
|
|
||||||
screenWidth: window.innerWidth,
|
|
||||||
worldHeight: WORLD_HEIGHT,
|
|
||||||
worldWidth: WORLD_WIDTH,
|
|
||||||
});
|
|
||||||
const stats = new Stats();
|
|
||||||
stats.showPanel(0);
|
|
||||||
document.body.appendChild(stats.dom);
|
|
||||||
const ticker = new PIXI.ticker.Ticker();
|
|
||||||
const graphics = new PIXI.Graphics();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
const trains = initTrains(50, stations);
|
|
||||||
|
|
||||||
ticker.stop();
|
|
||||||
ticker.add((deltaTime) => {
|
|
||||||
stats.begin();
|
|
||||||
|
|
||||||
moveTrains(trains, stations);
|
|
||||||
|
|
||||||
graphics.clear();
|
|
||||||
|
|
||||||
graphics.lineStyle(1, 0xFFA500, 1);
|
|
||||||
drawStations(stations, graphics);
|
|
||||||
|
|
||||||
graphics.lineStyle(1, 0xAEAEAE, 1);
|
|
||||||
drawTrains(trains, graphics);
|
|
||||||
|
|
||||||
drawLines(stations, graphics);
|
|
||||||
|
|
||||||
stats.end();
|
|
||||||
});
|
|
||||||
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({
|
|
||||||
maxHeight: ZOOM_MAX_HEIGHT,
|
|
||||||
maxWidth: ZOOM_MAX_WIDTH,
|
|
||||||
minHeight: ZOOM_MIN_HEIGHT,
|
|
||||||
minWidth: ZOOM_MIN_WIDTH,
|
|
||||||
}).decelerate();
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
app.renderer.resize(window.innerWidth, window.innerHeight);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
PIXI.loader.add('nodeImg', imgNode).load(run);
|
|
52
src/utils.ts
52
src/utils.ts
@ -1,52 +0,0 @@
|
|||||||
import * as PIXI from 'pixi.js';
|
|
||||||
|
|
||||||
const EPSILON = 1.0;
|
|
||||||
|
|
||||||
export const randomInt = (min: number, max: number): number => (
|
|
||||||
// inclusive of min and max
|
|
||||||
Math.floor(Math.random() * (max - (min + 1))) + min
|
|
||||||
);
|
|
||||||
|
|
||||||
export const weightedRandom = (choices: any[], weights: number[]): any => {
|
|
||||||
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
||||||
const rand = randomInt(0, totalWeight);
|
|
||||||
let cumulWeight = 0;
|
|
||||||
for (let i = 0; i < weights.length; i += 1) {
|
|
||||||
cumulWeight += weights[i];
|
|
||||||
if (rand < cumulWeight) {
|
|
||||||
return choices[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const randomPoint = () => (
|
|
||||||
new PIXI.Point(randomInt(0, window.innerWidth), randomInt(0, window.innerHeight))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const pointsEqual = (pointA: PIXI.Point, pointB: PIXI.Point): boolean => (
|
|
||||||
(pointA.x === pointB.x && pointA.y === pointB.y)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const pointsAlmostEqual = (pointA: PIXI.Point, pointB: PIXI.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 => {
|
|
||||||
const distX = pointA.x - pointB.x;
|
|
||||||
const distY = pointA.y - pointB.y;
|
|
||||||
return Math.sqrt((distX * distX) + (distY * distY));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rangeMap = (num: number, inMin: number, inMax: number,
|
|
||||||
outMin: number, outMax: number): number => (
|
|
||||||
(num - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
|
|
||||||
);
|
|
||||||
|
|
||||||
export const angleRadians = (pointA: PIXI.Point, pointB: PIXI.Point): number => (
|
|
||||||
Math.atan2(-(pointB.x - pointA.x), pointB.y - pointA.y)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const angleDegrees = (pointA: PIXI.Point, pointB: PIXI.Point): number => (
|
|
||||||
180 + angleRadians(pointA, pointB) * (180 / Math.PI)
|
|
||||||
);
|
|
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist/",
|
|
||||||
"sourceMap": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"module": "es6",
|
|
||||||
"target": "es6",
|
|
||||||
"jsx": "react",
|
|
||||||
"allowJs": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"typings"
|
|
||||||
]
|
|
||||||
}
|
|
11
tslint.json
11
tslint.json
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"defaultSeverity": "error",
|
|
||||||
"extends": [
|
|
||||||
"tslint:recommended",
|
|
||||||
"tslint-config-airbnb",
|
|
||||||
"tslint-eslint-rules"
|
|
||||||
],
|
|
||||||
"jsRules": {},
|
|
||||||
"rules": {},
|
|
||||||
"rulesDirectory": []
|
|
||||||
}
|
|
1
typings/custom.d.ts
vendored
1
typings/custom.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
declare module '*.png';
|
|
37
vendors.min.js
vendored
Normal file
37
vendors.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendors.min.js.map
Normal file
1
vendors.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,91 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
const env = process.env.NODE_ENV;
|
|
||||||
|
|
||||||
const prodPlugins = env === 'production' ? [
|
|
||||||
new UglifyJsPlugin({
|
|
||||||
sourceMap: true,
|
|
||||||
}),
|
|
||||||
] : [];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './src/transport.ts',
|
|
||||||
output: {
|
|
||||||
filename: env === 'production' ? '[name].min.js' : '[name].js',
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
},
|
|
||||||
mode: env === 'production' ? 'production' : 'development',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: [
|
|
||||||
'@babel/preset-env',
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
'@babel/proposal-class-properties',
|
|
||||||
'@babel/proposal-object-rest-spread',
|
|
||||||
],
|
|
||||||
cacheDirectory: env === 'development',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: env === 'production'
|
|
||||||
? ExtractTextPlugin.extract({
|
|
||||||
fallback: 'style-loader',
|
|
||||||
use: ['css-loader'],
|
|
||||||
})
|
|
||||||
: ['style-loader', 'css-loader'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.png$/,
|
|
||||||
use: 'file-loader',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.ts', '.js'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: path.join(__dirname, 'index.html'),
|
|
||||||
}),
|
|
||||||
new ExtractTextPlugin({
|
|
||||||
disable: env === 'development',
|
|
||||||
filename: '[name].css',
|
|
||||||
}),
|
|
||||||
...prodPlugins,
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
cacheGroups: {
|
|
||||||
commons: {
|
|
||||||
test: /[\\/]node_modules[\\/]/,
|
|
||||||
name: 'vendors',
|
|
||||||
chunks: 'all',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
devtool: env === 'production' ? 'source-map' : 'cheap-module-eval-source-map',
|
|
||||||
devServer: {
|
|
||||||
contentBase: path.join(__dirname, 'dist'),
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 8888,
|
|
||||||
disableHostCheck: true,
|
|
||||||
},
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user