├── .gitignore ├── README.md ├── app.js ├── controllers ├── buildSVG.js ├── handleImage.js └── hueToTerrain.js ├── package.json ├── routes └── index.js ├── src └── conversionHelper.js ├── static ├── css │ └── style.css ├── img │ └── favicon.ico └── js │ └── app.js ├── test └── resources │ ├── example-output.png │ └── map.png └── views ├── pages ├── hexMap.ejs └── index.ejs └── partials ├── footer.ejs ├── head.ejs └── header.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | output 3 | .env 4 | uploads 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Hex Tile Map Generator by Box It Off 2 | 3 | (uses https://github.com/jjmax75/node-image-terrain-array to generate input values from a PNG image) 4 | 5 | Input - array of average hsv values for each hexagon, array of centre points of hexagons, hexagon radius 6 | 7 | Output - svg code for map terrain 8 | 9 | ## Example 10 | 11 | Start with this 12 | 13 | ![a physical map of part of the world][physicalmap] 14 | 15 | Turn it into this - 16 | 17 | [{ h: 91, s: 55, v: 21 }, {....}, .....], [[ 9.804061174918171, 16.9811320754717 ], [ ....... ], ......], 11.320754716981131 18 | 19 | And then into this - 20 | 21 | ![An SVG representation built with hexagons of that part of the world][worldHexGrid] 22 | 23 | ## Usage 24 | browser interface 25 | 26 | ## Further Development 27 | - output code for rendering map 28 | - command line inputs 29 | - Browser gui 30 | 31 | ## Tasks 32 | - [x] take array input 33 | - [x] return svg code 34 | 35 | [physicalmap]: https://github.com/jjmax75/node-hexmap-generator/blob/master/test/resources/map.png "Physical Map" 36 | 37 | [worldHexGrid]: https://github.com/jjmax75/node-hexmap-generator/blob/master/test/resources/example-output.png "World Hex Grid - Asia, Europe, North Africa" 38 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | const morgan = require('morgan'); 6 | const favicon = require('serve-favicon'); 7 | 8 | require('dotenv').config(); 9 | const port = process.env.PORT || 3000; 10 | const path = process.cwd(); 11 | 12 | app.use(favicon(path + '/static/img/favicon.ico')); 13 | app.use(morgan('dev')); 14 | app.set('view-engine', 'ejs'); 15 | 16 | app.use('/css', express.static(path + '/static/css')); 17 | app.use('/js', express.static(path + '/static/js')); 18 | 19 | require(path + '/routes/index.js')(app); 20 | 21 | app.listen(port, function() { 22 | console.log('Hex Generator App listening on port ' + port + '......'); 23 | }); 24 | -------------------------------------------------------------------------------- /controllers/buildSVG.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /controllers/handleImage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function imageHandler(file, cols, cb) { 4 | const terrainFile = file; 5 | const numCols = Number(cols); 6 | 7 | const imageToTerrainFile = require('node-image-terrain-array'); 8 | const imageToTerrain = imageToTerrainFile(terrainFile, numCols); 9 | 10 | imageToTerrain.getTerrainArray() 11 | .then(function(terrain) { 12 | cb(terrain, imageToTerrain.points, imageToTerrain.hexRadius, imageToTerrain.rows); 13 | }); 14 | } 15 | 16 | module.exports = imageHandler; 17 | -------------------------------------------------------------------------------- /controllers/hueToTerrain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function mapColourToTileType(hsvValues) { 4 | let terrain = []; 5 | 6 | function getCellType(cellHue) { 7 | switch (true) { 8 | case (cellHue <= 35): 9 | return 'mountain'; 10 | break; 11 | case (cellHue <= 59): 12 | return 'desert'; 13 | break; 14 | case (cellHue <= 159): 15 | return 'land'; 16 | break; 17 | case (cellHue <= 240): 18 | return 'water'; 19 | break; 20 | default: 21 | return 'wtf'; 22 | } 23 | } 24 | 25 | hsvValues.forEach(function(hsv) { 26 | terrain.push(getCellType(hsv.h)); 27 | }); 28 | 29 | return terrain; 30 | } 31 | 32 | module.exports = mapColourToTileType; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-hex-map-generator", 3 | "version": "1.0.0", 4 | "description": "Generates a Hex Tile Map from an array of terrain types", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js", 9 | "test": "tape test/*.js | tap-dot" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jjmax75/node-hexmap-generator.git" 14 | }, 15 | "keywords": [ 16 | "Node", 17 | "D3", 18 | "Hex Map" 19 | ], 20 | "author": "Sean Behan (http://boxitoff.com)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/jjmax75/node-hexmap-generator/issues" 24 | }, 25 | "homepage": "https://github.com/jjmax75/node-hexmap-generator#readme", 26 | "engines": { 27 | "node": "4.4.4" 28 | }, 29 | "devDependencies": { 30 | "tap-dot": "1.0.5", 31 | "tape": "4.5.1" 32 | }, 33 | "dependencies": { 34 | "body-parser": "1.15.1", 35 | "d3": "3.5.17", 36 | "d3-hexbin": "0.2.0", 37 | "dotenv": "2.0.0", 38 | "ejs": "2.4.1", 39 | "express": "4.13.4", 40 | "morgan": "1.7.0", 41 | "multer": "1.1.0", 42 | "node-image-terrain-array": "1.0.6", 43 | "serve-favicon": "2.3.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = process.cwd(); 4 | 5 | const imageHandler = require(path + '/controllers/handleImage'); 6 | const hsvToTerrain = require(path + '/controllers/hueToTerrain'); 7 | 8 | const multer = require('multer'); 9 | // const storage = multer.diskStorage({ 10 | // destination: function(req, file, cb) { 11 | // cb(null, path + '/uploads/'); 12 | // }, 13 | // filename: function(req, file, cb) { 14 | // cb(null, Date.now() + '_' + file.originalname); 15 | // } 16 | // }); 17 | const storage = multer.memoryStorage(); 18 | const upload = multer({ 19 | storage: storage, 20 | limits: { 21 | fileSize: 1048576 22 | }, 23 | filename: function(req, file, cb) { 24 | cb(null, Date.now() + '.png'); 25 | }, 26 | fileFilter: function(req, file, cb) { 27 | if (file.mimetype !== 'image/png') { 28 | cb(true, null); 29 | } else { 30 | cb(null, true); 31 | } 32 | } 33 | }).single('pngFile'); 34 | 35 | 36 | module.exports = function(app) { 37 | 38 | app.locals.mapData = { 39 | terrain: [], 40 | cols: 0, 41 | rows: 0, 42 | hexRadius: 0, 43 | points: [] 44 | }; 45 | 46 | app.get('/', function(req, res) { 47 | res.render('pages/index.ejs', { 48 | pageTitle: 'Upload PNG' 49 | }); 50 | }); 51 | 52 | app.post('/process-image', function(req, res) { 53 | upload(req, res, function(err) { 54 | if (err) { 55 | res.send('Error - File must be a PNG'); 56 | } else { 57 | const image = imageHandler(req.file.buffer, req.body.numCols, setTerrain); 58 | function setTerrain(hsvValues, points, hexRadius, rows) { 59 | const terrain = hsvToTerrain(hsvValues); 60 | req.app.locals.mapData.terrain = JSON.stringify(terrain); 61 | req.app.locals.mapData.cols = req.body.numCols; 62 | req.app.locals.mapData.rows = rows; 63 | req.app.locals.mapData.hexRadius = hexRadius; 64 | req.app.locals.mapData.points = JSON.stringify(points); 65 | 66 | res.redirect('/map'); 67 | } 68 | } 69 | }); 70 | }); 71 | 72 | app.get('/map', function(req, res) { 73 | res.render('pages/hexMap.ejs', { 74 | pageTitle: 'Your map, Sir', 75 | terrain: req.app.locals.mapData.terrain, 76 | cols: req.app.locals.mapData.cols, 77 | rows: req.app.locals.mapData.rows, 78 | hexRadius: req.app.locals.mapData.hexRadius, 79 | points: req.app.locals.mapData.points 80 | }); 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/conversionHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = process.cwd(); 4 | 5 | const terrainFile = path + '/' + process.argv[2]; 6 | const numCols = Number(process.argv[3]); 7 | const numRows = Number(process.argv[4]); 8 | 9 | // const width & height will be proportional to the cols and rows 10 | 11 | const imageToTerrainFile = require('node-image-terrain-array'); 12 | const imageToTerrain = imageToTerrainFile(terrainFile, numCols, numRows); 13 | 14 | imageToTerrain.getTerrainArray() 15 | .then(function(terrain) { 16 | console.log(terrain[33]); // land on map.png 17 | }); 18 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background-color: #eee; 8 | } 9 | 10 | footer { 11 | margin-top: 40px; 12 | padding: 10px; 13 | text-align: center; 14 | } 15 | 16 | form { 17 | margin-bottom: 5px; 18 | } 19 | 20 | header { 21 | margin-bottom: 40px; 22 | } 23 | 24 | .login, .register { 25 | display: inline-block; 26 | width: 48%; 27 | } 28 | 29 | #map { 30 | text-align: center; 31 | } 32 | 33 | svg { 34 | max-height: 80vh; 35 | max-width: 90vw; 36 | } 37 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjmax75/node-hexmap-generator/0397c21533afaa75421669886b95954cae559446/static/img/favicon.ico -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | function buildMap(terrain, cols, rows, hexRadius, points) { 2 | const width = cols*hexRadius*Math.sqrt(3)-hexRadius; 3 | const height = rows*1.5*hexRadius+0.5*hexRadius-hexRadius; 4 | 5 | const hexbin = d3.hexbin().radius(hexRadius); 6 | 7 | const svg = d3.select("#map").append("svg") 8 | .attr("preserveAspectRatio", "xMinYMin meet") 9 | .attr("viewBox", "0 0 " + width + " " + height); 10 | 11 | ///////////////////////////////////////////////////////////////////////// 12 | //////////////////// Draw hexagons and color them /////////////////////// 13 | ///////////////////////////////////////////////////////////////////////// 14 | var count = 0; 15 | //Start drawing the hexagons 16 | svg.append("g") 17 | .selectAll(".hexagon") 18 | .data(hexbin(points)) 19 | .enter().append("path") 20 | .attr("class", "hexagon") 21 | .attr("d", function (d) { 22 | return "M" + d.x + "," + d.y + hexbin.hexagon(); 23 | }) 24 | .attr("data-pointX", function (p) { 25 | return p.i; 26 | }) 27 | .attr("data-pointY", function(p) { 28 | return p.j; 29 | }) 30 | .attr("data-terrain", function() { 31 | var type = terrain[count]; 32 | return type; 33 | }) 34 | .attr("stroke", function (d,i) { 35 | return "#fff"; 36 | }) 37 | .attr("stroke", "#111") 38 | .attr("stroke-width", "1px") 39 | .style("fill", function() { 40 | switch (terrain[count]) { 41 | case 'water': 42 | count++; 43 | return 'blue'; 44 | break; 45 | case 'land': 46 | count++; 47 | return 'green'; 48 | break; 49 | case 'mountain': 50 | count++; 51 | return 'yellow'; 52 | break; 53 | case 'desert': 54 | count++; 55 | return 'grey'; 56 | break; 57 | default: 58 | count++; 59 | return 'red'; 60 | } 61 | }) 62 | .append("title") 63 | .text(function(p) { 64 | return p.i + ", " + p.j; 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/resources/example-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjmax75/node-hexmap-generator/0397c21533afaa75421669886b95954cae559446/test/resources/example-output.png -------------------------------------------------------------------------------- /test/resources/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjmax75/node-hexmap-generator/0397c21533afaa75421669886b95954cae559446/test/resources/map.png -------------------------------------------------------------------------------- /views/pages/hexMap.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% include ../partials/head %> 6 | 7 | 8 | 9 |
10 | <% include ../partials/header %> 11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /views/pages/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% include ../partials/head %> 6 | 7 | 8 | 9 |
10 | <% include ../partials/header %> 11 |
12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 |

Rows will be calculated based on the proportions of the image and the number of columns you have chosen.

23 |
24 |
25 | 26 | 27 |

Must be a PNG file. Max-size is 1MB

28 |
29 | 30 | 31 |
32 |
33 |
34 |
35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | © Copyright 2016 - Seán Behan, Box It Off 2 | -------------------------------------------------------------------------------- /views/partials/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hex Map Generator - <%= pageTitle %> 5 | 6 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 23 | --------------------------------------------------------------------------------