Syn-city

Online map editor for Kigard : www.kigard.fr.

This started out as a simple tool to help plan the construction of our city (hence the name), but the ambition was to extend it to include the entire map so that anyone could use it.

tileset.html

A bit of a naming heresy as it doesn't really use tilesets in the strictest definition of the term, what it really does is generate CSS stylesheets from the icons that the map editor uses to parse files into layers.

The main viewport contains all the available icons that you can use. The side viewport shows the custom collections that you can edit and sort.

In order to edit a layer, first select it by clicking the checkbox next to its name (any layer except the top-most one - explained below). This will display the icons in the set and also select them in the main viewport. Click icons in the main viewport to add or remove them from the set. Drag the added icons in the side viewport to re-order them. You can also reorganise layers by dragging their checkbox.

The top-most layer, above the load and save buttons, isn't really a layer but an index that holds the references to the stylesheets that make up the layers (technically it's a series of @import rules if that means anything to you)

Be aware that only the currently selected layer will be saved when you click the save button, it's a little bit wierd but it keeps things simple by avoiding .zip files and the like. The default file location to save the index is the current directory and asset/kigard for layer stylesheets.

map.html

Once you are satisfied with your layers, or are happy using the predefined ones, you can open up the map editor.

developper notes

structure

script

module

grid

Fairly basic Layer and Tile classes that mostly serve as a wrapper for the HTML elements. Changes should get automatically propagated up to the DOM to keep the view up to date.

find()

Being able to quickly retrieve a grid coordinate is essential for real time editing, and search functions have a noticable delay when there are thousands of coordinates to look through. So for performance reasons direct accessors are the only viable method

Incidentally this was the main reason that there is a grid object at all, to have a two dimensional array that stores references we can retrieve with data[x][y]

array.find() method
		   find (gridX,gridY) {
		   	
		   	return Array.from(layer.children).find((child) => {
		   		
		   		return (child.style.gridColumnStart == gridX) && (child.style.gridRowStart == gridY);
		   		
		   	});
		   	
		   }
			
elementFromPoint method
		   find (gridX,gridY) {
		   	
		      const position = (gridX,gridY) => {
		      	
		      	const size = 18;
		      	
		      	const offsetX = (gridX - 0.5) * size;
		      	const offsetY = (gridY - 0.5) * size;
		      	
		      	const pageX = Math.round(offsetX * scale + translateX);
		      	const pageY = Math.round(offsetY * scale + translateY);
		      	
		      	return [pageX,pageY];
		      	
		      }
		      
		   	const {pageX,pageY} = position(gridX,gridY);
		   	
		   	const target = document.elementFromPoint(pageX,pageY);
		   	
		   	if (target.matches(".layer > div")) {
		   		
		   		return target;
		   		
		   	}
				
		   }
			

view

To be honest, this is only a seperate module because pan and zoom are such generically useful functions that they could be used in other projects.

zoom

The transform-origin css property is actually a translation prior to the transform and the inverse translation afterwards.

mouse pan / zoom
			export const mousemove = (event) => {
			   
			   if (event.buttons === 0) {
			      
			      return;
			      
			   }
			   
			   transform.translateX += event.movementX;
			   transform.translateY += event.movementY;
			   
			   // apply
			   
			   update();
			   
			}
			
			export const wheel = (event) => {
				
			   // configuration
			   
			   const step = 0.1;
			   const minimum = 0.5;
			   const maximum = 2.0;
			   
			   // center
			   
				const scale = transform.scale;
				
			   transform.scale -= Math.sign(event.deltaY) * step;
			   
			   transform.scale = Math.max(transform.scale,minimum);
			   transform.scale = Math.min(transform.scale,maximum);
				
				const ratio = 1 - transform.scale / scale;
				
				transform.translateX += (event.pageX - transform.translateX) * ratio;
				transform.translateY += (event.pageY - transform.translateY) * ratio;
			   
			   // apply
			   
			   update();
			   
			}
			
			grid.view.addEventListener("mousemove",mousemove);
			grid.view.addEventListener("wheel",wheel);
			

modulo

The % operator in javascript is the remainder operator, not modulo. This means it can also be a negative number which causes an issue when translating in negative coordinates. The solution is to convert a remainder to modulo with this :

remainder to modulo
			const n = tilesize;
			
			((translation % n) + n) % n;
			

canvas

Just remember that setting the value of either of the canvas dimensions resets its' context.

tileset

0 1 2 3 - 4 5 6 7

bitmask