22 Commits

Author SHA1 Message Date
thallada 4d1ef84568 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.
2018-04-22 18:40:20 -04:00
thallada ad9167528a Add unused Signal. private textStyle for labels 2018-04-21 01:45:00 -04:00
thallada 065a69a475 Keep CNAME on master, copy it on build to deploy 2018-04-16 23:00:50 -04:00
thallada 12b2fa7400 Fix random cases where init would error out 2018-04-16 22:49:29 -04:00
thallada 4ecc93189b Deploy to github remote 2018-04-16 22:48:51 -04:00
thallada f663e23bc9 Improve prod build. Add screenshot to readme. 2018-04-16 17:52:07 -04:00
thallada df9ba6d5ea Rework Line gen, add stats & viewport
Generate 4 separate lines. Trains now follow lines and only spawn on connected
stations.
2018-04-16 16:59:28 -04:00
thallada a79f501c5e Appease linter 2018-04-15 04:06:45 -04:00
thallada 35b4ef25bb Use image sprite for trains 2018-04-15 04:02:54 -04:00
thallada d1d342ee0f Trains have own color, passengers can stay on 2018-04-15 02:54:35 -04:00
thallada 95805184ea Add Direction for 4 colored directional Lines
Trains still do not follow lines. Lines do not reach every station. I need to
redo the Line creation.
2018-04-14 23:26:10 -04:00
thallada 61642cb70f Move couple util funcs to Station, add comments 2018-04-14 18:22:16 -04:00
thallada 7c488cf2ca Proper Station color averaging 2018-04-14 18:09:25 -04:00
thallada 902645d46a Station and train colors 2018-04-10 01:31:49 -04:00
thallada e686a854d8 Colors, exclude origin when choosing destination 2018-04-09 23:56:04 -04:00
thallada 35e3f8f3a3 Continuously traveling trains 2018-04-09 23:41:16 -04:00
thallada 502f15f1ef Add train & station labels, fix train movement 2018-04-09 18:35:30 -04:00
thallada 4b7bf5353c Out of control trains! 2018-04-06 01:07:16 -04:00
thallada edae3f76f4 Add TypeScript, generate stations and a line 2018-04-05 22:41:19 -04:00
thallada 434578cebc Draw random lines, display FPS counter 2018-03-31 01:50:39 -04:00
thallada b51fbf3e7f Bash code snippet highlighting in README 2018-03-31 01:09:51 -04:00
thallada 391182251c Initial commit: webpack building a blank canvas 2018-03-31 01:07:44 -04:00
30 changed files with 14011 additions and 46 deletions
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
"extends": "airbnb-base",
"plugins": [
"import"
],
"env": {
"browser": true,
}
};
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
tags
Session.vim
+1
View File
@@ -0,0 +1 @@
transport.hallada.net
+20
View File
@@ -0,0 +1,20 @@
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
View File
@@ -0,0 +1,35 @@
# 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.

After

Width:  |  Height:  |  Size: 307 KiB

+2 -2
View File
@@ -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" /> -->
<link href="main.css" rel="stylesheet"></head> </head>
<body> <body>
<script type="text/javascript" src="vendors.min.js"></script><script type="text/javascript" src="main.min.js"></script></body> </body>
<!-- Google Analytics --> <!-- Google Analytics -->
<script> <script>
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"main.css","sourceRoot":""}
Vendored
-2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
+13062
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
{
"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",
"worker-loader": "^1.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"
}
}
+41
View File
@@ -0,0 +1,41 @@
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;
+56
View File
@@ -0,0 +1,56 @@
import * as tinycolor from 'tinycolor2';
import Direction, { getPointDirection } from './Direction';
import LineConnection from './LineConnection';
import Station from './Station';
import { distance, randomInt, randomPoint } from './utils';
export default class Line {
public name: string;
public color: ColorFormats.RGBA;
constructor(
name: string,
color: ColorFormats.RGBA,
) {
this.name = name;
this.color = color;
}
public connectStations(
currentStation: Station,
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,
connectionRadius,
);
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,
connectionRadius,
);
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import Line from './Line';
import Signal from './Signal';
import Station from './Station';
export default class LineConnection {
public line: Line;
public signals: Signal[];
public station: Station;
constructor(
station: Station,
line: Line,
) {
this.station = station;
this.line = line;
}
}
+11
View File
@@ -0,0 +1,11 @@
import * as PIXI from 'pixi.js';
export default class Signal {
public location: PIXI.Point;
constructor(
location: PIXI.Point,
) {
this.location = location;
}
}
+94
View File
@@ -0,0 +1,94 @@
import Point from 'pixi.js/lib/core/math/Point';
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: Point,
radius: number): Station[] {
return stations.filter(station => distance(point, station.location) <= radius);
}
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: Point): Station {
return stations.reduce(
(prev, curr) => distance(point, prev.location) < distance(point, curr.location) ? prev : curr,
);
}
public static randomCloseLargeStation(stations: Station[], point: 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: 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,
height: number,
width: number,
): Point | null {
let tries = 100;
while (tries > 0) {
const point = randomPoint(height, width);
if (Station.isPointDistant(point, stations, minDistance)) {
return point;
}
tries -= 1;
}
return null;
}
public location: Point;
public population: number;
public connections: LineConnection[];
public id: number;
public color: ColorFormats.RGBA;
constructor(
location: Point,
population: number,
color: ColorFormats.RGBA,
connections?: LineConnection[],
) {
this.location = location;
this.population = population;
this.color = color;
this.connections = connections || [];
// for debugging
stationCount += 1;
this.id = stationCount;
}
}
+60
View 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 });
}
});
+38
View File
@@ -0,0 +1,38 @@
import Point from 'pixi.js/lib/core/math/Point';
import * as tinycolor from 'tinycolor2';
import Station from './Station';
let trainCount = 0;
export default class Train {
public location: Point;
public speed: number;
public origin: Station;
public destination: Station;
public passengers: number;
public id: number;
public color: ColorFormats.RGBA;
private textStyle: object;
constructor(
location: Point,
speed: number,
passengers: number,
origin: Station,
destination: Station,
color: ColorFormats.RGBA,
) {
this.location = location;
this.speed = speed;
this.origin = origin;
this.destination = destination;
this.passengers = passengers;
this.color = color;
// for debugging
trainCount += 1;
this.id = trainCount;
}
}
+116
View 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 });
}
});

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

-2
View File
@@ -18,5 +18,3 @@ canvas {
bottom: 0 !important; bottom: 0 !important;
top: 0 !important; top: 0 !important;
} }
/*# sourceMappingURL=main.css.map*/
+204
View File
@@ -0,0 +1,204 @@
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 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 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;
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 drawStations = (stations: Station[], stationLabels: PIXI.Text[], graphics: PIXI.Graphics) => {
stations.forEach((station, i) => {
const radius = station.population / 150;
const color = tinycolor(station.color);
graphics.beginFill(parseInt(color.toHex(), 16), 0.5);
graphics.drawCircle(station.location.x, station.location.y, radius);
graphics.endFill();
stationLabels[i].x = station.location.x + radius + 1;
stationLabels[i].y = station.location.y + radius + 1;
});
};
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;
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(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 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);
}
// 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();
trainWorker.postMessage({ moveTrains: {} });
graphics.clear();
graphics.lineStyle(1, 0xFFA500, 1);
drawStations(stations, stationLabels, graphics);
graphics.lineStyle(1, 0xAEAEAE, 1);
drawTrains(trains, trainLabels, trainSprites, graphics);
drawLines(stations, graphics);
stats.end();
});
ticker.start();
viewport.addChild(graphics);
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
View File
@@ -0,0 +1,52 @@
import Point from 'pixi.js/lib/core/math/Point';
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 = (height: number, width: number): Point => (
new Point(randomInt(0, width), randomInt(0, height))
);
export const pointsEqual = (pointA: Point, pointB: Point): boolean => (
(pointA.x === pointB.x && pointA.y === pointB.y)
);
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: Point, pointB: 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: Point, pointB: Point): number => (
Math.atan2(-(pointB.x - pointA.x), pointB.y - pointA.y)
);
export const angleDegrees = (pointA: Point, pointB: Point): number => (
180 + angleRadians(pointA, pointB) * (180 / Math.PI)
);
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "es6",
"target": "es6",
"jsx": "react",
"allowJs": true
},
"include": [
"typings"
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-config-airbnb",
"tslint-eslint-rules"
],
"jsRules": {},
"rules": {},
"rulesDirectory": []
}
+17
View File
@@ -0,0 +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;
}
-37
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+91
View File
@@ -0,0 +1,91 @@
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,
},
};