diff --git a/package-lock.json b/package-lock.json index d1b37b5..793cf11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -980,6 +980,11 @@ "resolved": "https://registry.npmjs.org/@types/pixi.js/-/pixi.js-4.7.2.tgz", "integrity": "sha512-ybrqVdncNCa81fCYCqxz/CISyMbXl8usszNv0mwdeYDyfDqmemQHJtf4GtduHva+3suhItPc9Akr/WfV19zWiQ==" }, + "@types/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==" + }, "@types/tinycolor2": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.0.tgz", @@ -4406,6 +4411,11 @@ } } }, + "exists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exists/-/exists-1.0.1.tgz", + "integrity": "sha1-/8vuKRQvJAVt8Bkk5zJicz2xC0k=" + }, "exit-hook": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", @@ -9205,6 +9215,11 @@ "sha.js": "2.4.11" } }, + "penner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/penner/-/penner-0.1.3.tgz", + "integrity": "sha1-C4tILU6bOa8vPXw3WSIpuKzClwU=" + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -9226,11 +9241,46 @@ "pinkie": "2.0.4" } }, + "pixi-ease": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/pixi-ease/-/pixi-ease-0.18.0.tgz", + "integrity": "sha512-qC9ofPKHblNlkdKDFgXDcw1vTPg4zDTLBudk32lScafOB59QKE5duElA+XwaS5kEpumVfnlKtg3k4gCcGcat3Q==", + "requires": { + "eventemitter3": "3.0.1", + "penner": "0.1.3", + "yy-angle": "1.2.0", + "yy-color": "1.0.7" + }, + "dependencies": { + "eventemitter3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.0.1.tgz", + "integrity": "sha512-QOCPu979MMWX9XNlfRZoin+Wm+bK1SP7vv3NGUniYwuSJK/+cPA10blMaeRgzg31RvoSFk6FsCDVa4vNryBTGA==" + } + } + }, "pixi-gl-core": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/pixi-gl-core/-/pixi-gl-core-1.1.4.tgz", "integrity": "sha1-i0tcQzsx5Bm8N53FZc4bg1qRs3I=" }, + "pixi-viewport": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-1.5.0.tgz", + "integrity": "sha512-hMPtka90PulpBLXBhE3RZvKaB1VTPFoXe4dSuqsYYBQeo8b1G3FTy7WAfgqkqj1ibrblEf0MmTzO9PzoXKLKXA==", + "requires": { + "eventemitter3": "3.0.1", + "exists": "1.0.1", + "pixi-ease": "0.18.0" + }, + "dependencies": { + "eventemitter3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.0.1.tgz", + "integrity": "sha512-QOCPu979MMWX9XNlfRZoin+Wm+bK1SP7vv3NGUniYwuSJK/+cPA10blMaeRgzg31RvoSFk6FsCDVa4vNryBTGA==" + } + } + }, "pixi.js": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-4.7.1.tgz", @@ -10595,6 +10645,11 @@ "integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=", "dev": true }, + "seedrandom": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", + "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -11159,6 +11214,11 @@ } } }, + "stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha1-scPcRtlEmLV4t/05hbgaznExzH0=" + }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", @@ -12966,6 +13026,27 @@ "dev": true } } + }, + "yy-angle": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yy-angle/-/yy-angle-1.2.0.tgz", + "integrity": "sha512-Sf311F5zlItZA0dH/mD3MMG6mIIo1b+9XsFqpLhKCTqFFUL4ip+XWTaQLWLRDkh/Ag0GzAsGt6rWcq61OU9+zQ==" + }, + "yy-color": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/yy-color/-/yy-color-1.0.7.tgz", + "integrity": "sha1-P5IxiCQ/q8zqJNNy9Li24wk8Zak=", + "requires": { + "yy-random": "1.6.0" + } + }, + "yy-random": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yy-random/-/yy-random-1.6.0.tgz", + "integrity": "sha512-oMIO8eo4BVed+o8NDMGj9scboHWBFtOdip1oDpXhxYw3A03lYqDqtlwPLg8SOK0q95Fsdzj9EdCQMWFWRk3p7A==", + "requires": { + "seedrandom": "2.4.3" + } } } } diff --git a/package.json b/package.json index 817e1da..95954d6 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,11 @@ }, "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" } } diff --git a/src/Line.ts b/src/Line.ts index 84bc017..a45a314 100644 --- a/src/Line.ts +++ b/src/Line.ts @@ -1,72 +1,58 @@ 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), -) / 4); +) / 8); export default class Line { - public static getLinesWithStation(lines: Line[], station: Station): Line[] { - return lines.filter(line => line.stations.indexOf(station) >= 0); - } - - public stations: Station[]; + public name: string; public color: tinycolorInstance; constructor( - stations: Station[], - start: PIXI.Point, - startDirection: Direction, - numStations: number, + name: string, color: tinycolorInstance, ) { + this.name = name; this.color = color; - this.stations = []; - let stationsLeft = stations.slice(); - let currentStation = Station.randomCloseLargeStation(stationsLeft, start, CONNECTION_RADIUS); - stationsLeft = stationsLeft.filter(s => s !== currentStation); - this.stations.push(currentStation); + } - let direction = startDirection; - while (this.stations.length < numStations) { - const previousStation = this.stations[this.stations.length - 1]; - let candidateStations = Station.stationsWithinRadius( - stationsLeft, - currentStation.location, - CONNECTION_RADIUS, - ); - - if (this.stations.length > 1) { - const secondPreviousStation = this.stations[this.stations.length - 2]; - direction = getPointDirection(secondPreviousStation.location, - previousStation.location); - } - - const straightStations = Station.stationsInDirection( - candidateStations, previousStation.location, direction, - ); - const leftStations = Station.stationsInDirection( - candidateStations, previousStation.location, (direction - 1) % 7, - ); - const rightStations = Station.stationsInDirection( - candidateStations, previousStation.location, (direction + 1) % 7, - ); - candidateStations = [ - ...straightStations, - ...leftStations, - ...rightStations, - ]; - - currentStation = Station.randomCloseLargeStation(candidateStations, previousStation.location, - CONNECTION_RADIUS); - if (currentStation === null || currentStation === undefined) { + 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; } - stationsLeft = stationsLeft.filter(s => s !== currentStation); - this.stations.push(currentStation); + 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, + ); + } } } } diff --git a/src/LineConnection.ts b/src/LineConnection.ts new file mode 100644 index 0000000..33db81c --- /dev/null +++ b/src/LineConnection.ts @@ -0,0 +1,15 @@ +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; + } +} diff --git a/src/Station.ts b/src/Station.ts index 2dc063e..ce01e1c 100644 --- a/src/Station.ts +++ b/src/Station.ts @@ -1,6 +1,7 @@ import * as tinycolor from 'tinycolor2'; import Direction, { getPointDirection } from './Direction'; +import LineConnection from './LineConnection'; import { distance, randomPoint, weightedRandom } from './utils'; let stationCount = 0; @@ -65,7 +66,7 @@ export default class Station { public location: PIXI.Point; public population: number; - public connections: Station[]; + public connections: LineConnection[]; public id: number; public label: PIXI.Text; public color: tinycolorInstance; @@ -74,12 +75,12 @@ export default class Station { location: PIXI.Point, population: number, color: tinycolorInstance, - connections?: Station[], + connections?: LineConnection[], ) { this.location = location; this.population = population; this.color = color; - this.connections = connections; + this.connections = connections || []; // for debugging stationCount += 1; diff --git a/src/transport.ts b/src/transport.ts index 944ca93..cb7f7af 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -1,4 +1,6 @@ +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'; @@ -16,10 +18,14 @@ const NODE_RES = 100; const MAX_SPEED = 10.0; const ACCELERATION = 0.025; const APPROACH_DISTANCE = 3.0; -const MAX_JOURNEY = Math.floor(Math.sqrt( - Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2), -) / 4); 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[] = []; @@ -36,8 +42,11 @@ const initStations = (numStations: number): Station[] => { 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 = stations[Math.floor(Math.random() * stations.length)]; + 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')), @@ -46,15 +55,36 @@ const initTrains = (numTrains: number, stations: Station[]): Train[] => { 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, + ); + const centralHub = Station.largestStation(stationsWithoutConnections); + 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 = stations.filter(station => station !== train.origin); - const closeStations = Station.stationsWithinRadius(otherStations, train.location, - MAX_JOURNEY); - const closeStationWeights = closeStations.map(station => station.population); - train.destination = weightedRandom(closeStations, closeStationWeights); + 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, @@ -116,7 +146,7 @@ const moveTrains = (trains: Train[], stations: Station[]) => { const drawStations = (stations: Station[], graphics: PIXI.Graphics) => { for (const station of stations) { - const radius = station.population / 60; + 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(); @@ -127,10 +157,6 @@ const drawStations = (stations: Station[], graphics: PIXI.Graphics) => { const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => { for (const train of trains) { - // graphics.beginFill(parseInt(train.color.toHex(), 16), 0.8); - // graphics.drawCircle(train.location.x, train.location.y, - // rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5)); - // graphics.endFill(); const trainSize = rangeMap(train.passengers, 0, TRAIN_CAPACITY, 1, 5); const scale = trainSize / NODE_RES; train.sprite.x = train.location.x; @@ -143,13 +169,18 @@ const drawTrains = (trains: Train[], graphics: PIXI.Graphics) => { } }; -const drawLines = (lines: Line[], graphics: PIXI.Graphics) => { - for (const line of lines) { - graphics.lineStyle(1, parseInt(line.color.toHex(), 16), 1); - const start = line.stations[0].location; - graphics.moveTo(start.x, start.y); - for (const station of line.stations.slice(1)) { - graphics.lineTo(station.location.x, station.location.y); +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); } } }; @@ -160,41 +191,29 @@ const run = () => { 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(); - const fpsText = new PIXI.Text('', { fontSize: '25px', fontFamily: 'monospace', fill: 'yellow' }); - - fpsText.anchor = new PIXI.ObservablePoint(null, 0, 1); - fpsText.x = window.innerWidth; - fpsText.y = 0; const stations = initStations(30); + const lines = initLines(4, stations); const trains = initTrains(50, stations); - const lines = [ - new Line( - stations, new PIXI.Point(0, 0), - Direction.Southeast, 12, tinycolor('red'), - ), - new Line( - stations, new PIXI.Point(window.innerWidth, 0), - Direction.Southwest, 12, tinycolor('darkcyan'), - ), - new Line( - stations, new PIXI.Point(window.innerWidth, window.innerHeight), - Direction.Northwest, 12, tinycolor('yellow'), - ), - new Line( - stations, new PIXI.Point(0, window.innerHeight), - Direction.Northeast, 12, tinycolor('green'), - ), - ]; ticker.stop(); ticker.add((deltaTime) => { + stats.begin(); + moveTrains(trains, stations); graphics.clear(); - fpsText.text = `${Math.round(ticker.FPS)}`; graphics.lineStyle(1, 0xFFA500, 1); drawStations(stations, graphics); @@ -202,24 +221,32 @@ const run = () => { graphics.lineStyle(1, 0xAEAEAE, 1); drawTrains(trains, graphics); - drawLines(lines, graphics); + drawLines(stations, graphics); + + stats.end(); }); ticker.start(); - app.stage.addChild(graphics); - app.stage.addChild(fpsText); + viewport.addChild(graphics); // add train sprites for (const train of trains) { - app.stage.addChild(train.sprite); + viewport.addChild(train.sprite); } // Add debug labels for (const train of trains) { - app.stage.addChild(train.label); + viewport.addChild(train.label); } for (const station of stations) { - app.stage.addChild(station.label); + 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); diff --git a/webpack.config.js b/webpack.config.js index 20b8f8c..73ddd97 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,6 +10,7 @@ module.exports = { filename: 'transport.js', path: path.resolve(__dirname, 'dist'), }, + mode: env === 'production' ? 'production' : 'development', module: { rules: [ {