Source: plugins/sg2d-transitions.js

"use strict";

/**
 * SG2DTransitions v1.0.0.
 * Генератор плавных переходов между разными типами местности.
 * В тайлах или их конструкторе (класс тайла) можно задать индекс слоя (от 0 до 31) и тип перехода (TRANSITIONS_STANDARD, ...).
 * Плагин обрабатывает плитки только со статическим свойством **useTransition=true** и использует текстуру из статического свойства плитки!
 * @see https://github.com/VediX/sg2d.github.io
 * @license SG2DTransitions may be freely distributed under the MIT license
 * @copyright Kalashnikov Ilya
 */
class SG2DTransitions extends SG2D.PluginBase {
		
	static code = "transitions";
	
	static TRANSITIONS_STANDARD = 1;
	static TRANSITIONS_STRICT = 2;
	
	static K_STRICT = 1.1875;
	
	/**
	 * Конструктор выполняется автоматически движком SG2D при подключении плагина
	 * @protected
	 */
	constructor(...args) {
		
		super(...args);
		
		class VirtualTransitionsTile extends SG2D.PluginBase.SG2DTile {
			static char = " ";
			static texture = "vtt";
			static useTransitions = true;
			static altitude = -999;
			static _uid = 900000000;
			static isVirtualTransitionTile = true;
		}
		
		SG2DTransitions.VirtualTransitionsTile = VirtualTransitionsTile;
		
		SG2DTransitions.createMask();
		
		SG2DTransitions.success();
		//SG2DTransitions.failed("Test error: message error!");
	}
	
	/**
	 * Генерация графических масок
	 * @protected
	 */
	static createMask() {
		this.masks = [];
		this.masks[this.TRANSITIONS_STANDARD] = SG2D.PluginBase.SG2DUtils.addMask({
			name: "mask_transitions_" + this.TRANSITIONS_STANDARD,
			iterate: (x, y, radius, p_index)=>{
				return { r: 0, g: 0, b: 0, a: (radius >= SG2D.PluginBase.SG2DConsts.CELLSIZEPIX05 ? 255 : 0) };
			},
			width: SG2D.PluginBase.SG2DConsts.CELLSIZEPIX,
			height: SG2D.PluginBase.SG2DConsts.CELLSIZEPIX
		});
		
		var rStrict = this.K_STRICT * SG2D.PluginBase.SG2DConsts.CELLSIZEPIX05;
		this.masks[this.TRANSITIONS_STRICT] = SG2D.PluginBase.SG2DUtils.addMask({
			name: "mask_transitions_" + this.TRANSITIONS_STRICT,
			iterate: (x, y, radius, p_index)=>{
				return { r: 0, g: 0, b: 0, a: (radius >= rStrict ? 255 : 0) };
			},
			width: SG2D.PluginBase.SG2DConsts.CELLSIZEPIX,
			height: SG2D.PluginBase.SG2DConsts.CELLSIZEPIX
		});
		
		SG2D.PluginBase.SG2DUtils.addMask({
			name: this.VirtualTransitionsTile.texture,
			iterate: (x, y, radius, p_index)=>{
				return { r: 0, g: 0, b: 0, a: 0 };
			},
			width: SG2D.PluginBase.SG2DConsts.CELLSIZEPIX,
			height: SG2D.PluginBase.SG2DConsts.CELLSIZEPIX
		}).virtualTile = true;
	}
	
	static transitionsCoord = [
		[{dx:0,dy:-1},{dx:1,dy:-1},{dx:1,dy:0}], // 45
		[{dx:-1,dy:0},{dx:-1,dy:-1},{dx:0,dy:-1}], // 135
		[{dx:0,dy:1},{dx:-1,dy:1},{dx:-1,dy:0}], // 225
		[{dx:1,dy:0},{dx:1,dy:1},{dx:0,dy:1}] // 315
	];
	
	static nsca = [[],[],[],[]]; // Nearest Sprites Class for 45 corners
	
	/**
	 * Просканировать карту и создать переходные тайлы
	 * @param {SG2D.Clusters} area
	 */
	static run(area) {
		
		this.area = area;
		
		this.canvasTemp = this.canvasTemp || SG2D.PluginBase.SG2DUtils.createCanvas(
			SG2D.PluginBase.SG2DConsts.CELLSIZEPIX05,
			SG2D.PluginBase.SG2DConsts.CELLSIZEPIX05
		);
		this.ctxTemp = this.canvasTemp.getContext("2d");
		
		var tile, layer;
		
		// 1. Automatic detection of layers where there are tiles with transitions
		var layers = {};
		this.area.each((cluster)=>{
			for (tile of cluster.tiles) {
				if (tile.constructor.useTransitions) {
					layers[ tile.properties.layer === void 0 ? tile.constructor.layer : tile.properties.layer ] = true;
				}
			}
		});
		
		// 2. Creating virtual tiles around real tiles
		var tiles = [];
		this.area.each((cluster)=>{
			for (layer in layers) {
				cluster.getTilesInLayer(layer, tiles);
				if (tiles.length === 0 && SG2D.PluginBase.SG2DClusters.nearestClusters45(cluster, (nc)=>{
					nc.getTilesInLayer(layer, tiles);
					for (var i = 0; i < tiles.length; i++) {
						if (tiles[i].constructor !== this.VirtualTransitionsTile) return true;
					}
				})) {
					tile = new this.VirtualTransitionsTile({id: ++this.VirtualTransitionsTile._uid, position: SG2D.PluginBase.SGModel.clone(cluster.position), layer: layer });
				}
			}
		});
		
		// 3. Detect transitions
		var nc, nsc;
		this.area.each((cluster)=>{
			for (var tileMain of cluster.tiles) {
				if (! tileMain.constructor.texture || ! tileMain.constructor.useTransitions) continue;
				layer = tileMain.properties.layer === void 0 ? tileMain.constructor.layer : tileMain.properties.layer;
				var texture_name = tileMain.constructor.texture;
				for (var a = 0; a < 4; a++) {
					nsc = this.nsca[a];
					nsc[0] = void 0;
					nsc[1] = void 0;
					nsc[2] = void 0;
					for (var i = 0; i < 3; i++) {
						if (nc = this.area.getCluster(cluster.x + this.transitionsCoord[a][i].dx, cluster.y + this.transitionsCoord[a][i].dy)) {
							nc.getTilesInLayer(layer, tiles);
							for (var tile of tiles) {
								if (tile.constructor.useTransitions) {
									nsc[i] = tile.constructor;
									nsc[i].altitude = nsc[i].altitude || 0;
									nsc[i].transitionsMask = nsc[i].transitionsMask || this.TRANSITIONS_STANDARD;
									nsc[i].char = nsc[i].char || nsc[i].name[0];
								}
							}
						}
					}

					if (! nsc[0] || ! nsc[1] || ! nsc[2]) continue;

					if (
						tileMain.constructor !== nsc[0] && nsc[0] === nsc[2] && nsc[0] === nsc[1] || // самый распростарненный случай
						tileMain.constructor !== nsc[0] && nsc[0] === nsc[2] && nsc[1] !== nsc[0] && tileMain.constructor.altitude <= nsc[0].altitude || // пример: песчанная коса идет по диагонали
						tileMain.constructor !== nsc[0] && nsc[0] === nsc[2] && nsc[1] !== nsc[0] && tileMain.constructor.altitude > nsc[1].altitude
					) {
						if (! tileMain.transitions) tileMain.transitions = [];
						tileMain.transitions[a] = nsc[0];
						texture_name += "_"+nsc[0].char[0]+a;
					}
				}

				if (tileMain.transitions && tileMain.transitions.length) {
					var texture = PIXI.utils.TextureCache[texture_name];
					if (! texture) {
						texture = this.genTransitions(tileMain, texture_name);
					}
					tileMain.set("texture", texture_name);

					// For simple cases when the tile does not have transitions with different alpha landscape!
					if (tileMain.constructor === this.VirtualTransitionsTile) {
						for (var a = 0; a < 4; a++) {
							if (tileMain.transitions[a] && tileMain.transitions[a].alpha !== void 0) {
								tileMain.alpha = tileMain.transitions[a].alpha;
								break;
							}
						}
					}
				}
			}
		});
		
		// 4. Destroy unnecessary virtual tiles
		this.area.each((cluster)=>{
			for (tile of cluster.tiles) {
				if (tile.constructor === SG2DTransitions.VirtualTransitionsTile && tile.texture_current === "vtt") {
					tile.destroy();
				}
			}
		});
	}
	
	static quarters = [[0.5,0],[0,0],[0,0.5],[0.5,0.5]];
	
	/**
	 * Генератор переходных спрайтов
	 * @param {SG2D.Tile} tileMain
	 * @param {string} texture_name
	 * @returns {PIXI.Texture}
	 * @protected
	 */
	static genTransitions(tileMain, texture_name) {
	
		var mainClass = tileMain.constructor;
		var imgMain = SG2D.PluginBase.SG2DUtils.getTextureAsCanvas(mainClass.texture);
		var w = imgMain.width, h = imgMain.height;
		var w05 = Math.round(w/2), h05 = Math.round(h/2);
		var outClass;
		var canvasResult = SG2D.PluginBase.SG2DUtils.createCanvas(w, h);
		var ctxResult = canvasResult.getContext("2d");

		ctxResult.drawImage(imgMain, 0, 0);
		
		for (var a = 0; a < 4; a++) {
			if ((outClass = tileMain.transitions[a]) && outClass.texture) {
				var smx = Math.round(w * this.quarters[a][0]);
				var smy = Math.round(h * this.quarters[a][1]);
				var outCanvas = SG2D.PluginBase.SG2DUtils.getTextureAsCanvas(outClass.texture);
				var transitionsMask = (outClass.altitude > mainClass.altitude ? outClass.transitionsMask : mainClass.transitionsMask) || this.TRANSITIONS_STANDARD;
				if (outClass === this.VirtualTransitionsTile) {
					transitionsMask = mainClass.transitionsMask;
				}
				var mask = this.masks[transitionsMask];
				
				if (outClass === this.VirtualTransitionsTile) {
					ctxResult.globalCompositeOperation = "destination-out";
					ctxResult.drawImage(mask, smx, smy, w05, h05, smx, smy, w05, h05);
				} else if (mainClass === this.VirtualTransitionsTile) {
					this.canvasTemp.width = w05;
					this.canvasTemp.height = w05;
					this.ctxTemp.globalCompositeOperation = "source-over";
					this.ctxTemp.drawImage(outCanvas, smx, smy, w05, h05, 0, 0, w05, h05);
					this.ctxTemp.globalCompositeOperation = "destination-in";
					this.ctxTemp.drawImage(mask, smx, smy, w05, h05, 0, 0, w05, h05);
					ctxResult.drawImage(this.canvasTemp, 0, 0, w05, h05, smx, smy, w05, h05);
				} else {
					
					ctxResult.globalCompositeOperation = "destination-out";
					try {
					ctxResult.drawImage(mask, smx, smy, w05, h05, smx, smy, w05, h05);
					} catch(e) {
						debugger;
					}
					
					//(new Image()).src = canvasResult.toDataURL();
					
					// We work pixel by pixel due to alpha channel support, because standard modes (destination-out, destination-in, etc.) do not work with the alpha channel as required!
					var len = w05 * h05 * 4;
					var mainData = ctxResult.getImageData(smx, smy, w05, h05);
					var outData = (outCanvas.getContext("2d")).getImageData(smx, smy, w05, h05);
					for (var i = 0; i < len; i+=4) { // TODO: сделать через WebGL
						if (mainData.data[i+3]===0) {
							mainData.data[i] = outData.data[i];
							mainData.data[i+1] = outData.data[i+1];
							mainData.data[i+2] = outData.data[i+2];
							mainData.data[i+3] = outData.data[i+3];
						}
					}
					ctxResult.putImageData(mainData, smx, smy);
				}
			}
		}

		//(new Image()).src = canvasResult.toDataURL();
		
		var texture = PIXI.Texture.from((canvasResult._pixiId = texture_name, canvasResult));

		return texture;
	}
}

export default SG2DTransitions;