Source: sg2d-utils.js

/**
 * SG2D Utilities
 * https://github.com/VediX/sg2d.github.io
 * (c) Kalashnikov Ilya
 */

"use strict";

import SG2DConsts from './sg2d-consts.js';
import SG2DMath from './sg2d-math.js';

/**
 * @namespace SG2D.Utils
 */
var SG2DUtils = {
	
	DEFAULT_WIDTH: 32,
	DEFAULT_HEIGHT: 32,
	
	/**
	 * Флаг добавления прозрачной границы в спрайте.
	 * @see SG2D.Utils.createInBetweenTextures
	 * @memberof SG2D.Utils
	 */
	FLAG_ADD_BORDER_ALPHA: 0b00000001,
	
	/**
	 * Получить время с точностью до мс. Распознаёт браузер и платформу NodeJS
	 * @memberof SG2D.Utils
	 * @returns {Number}
	 */
	getTime: function() {
		var sResult;
		if (typeof process !== "undefined" && process.hrtime) {
			var diff = process.hrtime(),
			sResult = diff[0] + '.' + ('00' + Math.round(diff[1]/1e6)).substr(-3,3);
		} else if (typeof performance !== "undefined" && performance.now) {
			sResult = (performance.now() / 1000).toFixed(3);
		} else {
			var d = new Date();
			sResult = (d.getTime()/1000).toFixed(3);
		}
		return +sResult;
	},
	
	/**
	 * Динамическая загрузка и выполнение скриптов
	 * @memberof SG2D.Utils
	 * @param {string} src - Путь
	 * @param {function} [onload] - Вызывается после загрузки скрипта, но перед его выполнением
	 * @return {Promise}
	 */
	loadJS: function(src, onload = void 0) {
		return new Promise((resolve, reject)=>{
			var script = document.createElement('script');
			script.onload = (event)=>{
				onload && onload(event);
				resolve();
			};
			script.onerror = reject;
			script.src = src;
			document.head.append(script);
		});
	},
	
	/**
	 * Получить текстуру в виде отдельного CANVAS с нарисованной в нём текстурой. При повторном вызове возвращается созданный ранее CANVAS для текстуры
	 * @memberof SG2D.Utils
	 * @param {string|PIXI.Texture} mTexture
	 * @return {HTMLCanvasElement}
	 */
	getTextureAsCanvas: function(mTexture) {

		var texture = typeof mTexture === "object" ? mTexture : PIXI.Texture.from(mTexture);

		if (texture.canvas) return texture.canvas;

		var canvas = this.createCanvas(texture.frame.width, texture.frame.height);
		var ctx = canvas.getContext("2d");
		ctx.drawImage(texture.baseTexture.resource.source, texture.frame.x, texture.frame.y, texture.frame.width, texture.frame.height, 0, 0, texture.frame.width, texture.frame.height);

		canvas.texture = texture.textureCacheIds[0] || "error";
		texture.canvas = canvas;

		return canvas;
	},
	
	/**
	 * Получить текстуру как blob-объект
	 * @memberof SG2D.Utils
	 * @async
	 * @param {string|PIXI.Texture} mTexture
	 * @return {Promise<Blob>}
	 */
	getTextureAsBlob: async function(mTexture) {

		if (typeof mTexture === "string" && ! PIXI.utils.TextureCache[mTexture]) {
			console.error("Texture \"" + mTexture + "\" does not exist!");
			return false;
		}

		var texture = typeof mTexture === "object" ? mTexture : PIXI.Texture.from(mTexture);

		if (texture.blob) return texture.blob;

		var canvas = this.createCanvas(texture.frame.width, texture.frame.height);
		var ctx = canvas.getContext("2d");
		ctx.drawImage(texture.baseTexture.resource.source, texture.frame.x, texture.frame.y, texture.frame.width, texture.frame.height, 0, 0, texture.frame.width, texture.frame.height);

		if (canvas.convertToBlob) {
			texture.blob = await canvas.convertToBlob({type: "image/png"});
		} else {
			texture.blob = await new Promise(resolve=>canvas.toBlob(blob=>resolve(blob)));
		}

		return texture.blob;
	},
	
	/**
	 * Получить URL текстуры с base64 кодированием
	 * @memberof SG2D.Utils
	 * @async
	 * @param {string|PIXI.Texture} mTexture
	 * @param {boolean} [flagOpenWindow=false] - Если **true**, то ссылка откроется в новом окне
	 * @return {Promise<string>|false}
	 */
	getTextureUrl: async function(mTexture, flagOpenWindow = false) {
		if (! mTexture) return null;
		var blob = await SG2DUtils.getTextureAsBlob(mTexture);
		if (blob) {
			blob.url = blob.url || URL.createObjectURL(blob);
			if (flagOpenWindow) {
				var win = window.open("", "_blank");
				win.document.write("<html><head><script>window.location = '"+blob.url+"';</script></head></html>");
				win.document.close();
			} else {
				return blob.url;
			}
		} else {
			return false;
		}
	},

	/** @private */
	_options: {x: void 0, y: void 0},
	
	/**
	 * Отрисовка текстуры в произвольный канвас
	 * @memberof SG2D.Utils
	 * @param {string|PIXI.Texture} texture
	 * @param {HTMLElementCanvas} canvas
	 * @param {object} options
	 * @param {Number} [options.x=0]
	 * @param {Number} [options.y=0]
	 */
	drawTextureToCanvas: function(texture, canvas, options) {
		options = options || this._options;
		var ctx = canvas.getContext("2d");
		var canvas = this.getTextureAsCanvas(texture);
		ctx.drawImage(canvas, options.x === void 0 ? 0 : options.x, options.y === void 0 ? 0 : options.y);
	},

	/**
	 * Создать спрайт-маску
	 * @memberof SG2D.Utils
	 * @param {object} config
	 * @param {Number} [config.width=SG2D.Utils.DEFAULT_WIDTH]
	 * @param {Number} [config.height=SG2D.Utils.DEFAULT_HEIGHT]
	 * @param {function} config.iterate - Итератор
	 * @param {function} config.name - Имя текстуры
	 * @return {HTMLCanvasElement}
	 */
	addMask: function(config) {
		var w = config.width || SG2DUtils.DEFAULT_WIDTH;
		var h = config.height || SG2DUtils.DEFAULT_HEIGHT;

		var canvasMask = this.createCanvas(w, h);

		var ctxMask = canvasMask.getContext("2d");
		var imageData = ctxMask.getImageData(0, 0, w, h);
		var p05 = w>>1;
		var l = w * h * 4;
		for (var i = 0; i < l; i+=4) {
			var p = i >> 2,
				y = Math.floor(p / h) - p05,
				x = p - (y + p05) * w - p05,
				r = SG2DMath.distance_d(x + 0.5, y + 0.5);
			var rgba = config.iterate(x, y, r, p);
			imageData.data[i] = rgba.r;
			imageData.data[i+1] = rgba.g;
			imageData.data[i+2] = rgba.b;
			imageData.data[i+3] = rgba.a;
		}
		ctxMask.putImageData(imageData, 0, 0);

		canvasMask._pixiId = config.name;
		var texture = PIXI.Texture.from(canvasMask);
		texture.canvas = canvasMask;
		texture._isFinalTexture = true;

		//(new Image()).src = canvasMask.toDataURL();

		return canvasMask;
	},

	/** @private */
	_optionsBAT: {},

	/**
	 * Сделать границу спрайтов полупрозрачной (чтобы исключить артефакты в виде полос по краям тайлов ландшафта)
	 * @memberof SG2D.Utils
	 * @param {object} options
	 * @param {string[]|PIXI.Texture[]} [options.textures=void 0] Список текстур. Если не задан, то берутся все текстуры с флагом _isFinalTexture=true
	 * @param {number} [options.alpha=0.995] Прозрачность самой внешней границы (1 пиксель от края)
	 * @param {number} [options.alpha2=1] Прозрачность границы 2 пикселя от края
	 */
	setBorderAlphaTextures: function(options={}) {
		options.textures = options.textures || [];
		if (! options.textures.length) {
			this._getFinalTextures(options.textures);
		}
		for (var i = 0; i < options.textures.length; i++) {
			var texture = typeof options.textures[i] === "object" ? options.textures[i] : PIXI.utils.TextureCache[options.textures[i]];
			if (texture) {
				this._optionsBAT.texture = texture;
				this._optionsBAT.alpha = options.alpha;
				this._optionsBAT.alpha2 = options.alpha2;
				this.setBorderAlphaTexture(this._optionsBAT);
			} else {
				console.warn("Texture with name \"" + options.textures[i] + "\" not found!");
			}
		}
	},

	/**
	 * Сделать границу заданного спрайта полупрозрачной (чтобы исключить артефакты в виде полос по краям тайлов ландшафта)
	 * @memberof SG2D.Utils
	 * @param {object} options
	 * @param {PIXI.Texture|string} options.texture
	 * @param {number} [options.alpha=0.995] Прозрачность самой внешней границы (1 пиксель от края)
	 * @param {number} [options.alpha2=1] Прозрачность границы 2 пикселя от края
	 */
	setBorderAlphaTexture: function(options={}) {

		if (options.alpha === void 0) options.alpha = 0.995;
		if (options.alpha2 === void 0) options.alpha2 = 1;

		options.alpha = ~~(options.alpha * 256);
		options.alpha2 = ~~(options.alpha2 * 256);

		let texture_src = typeof options.texture === "object" ? options.texture : PIXI.utils.TextureCache(options.texture);
		let canvas = this.getTextureAsCanvas(texture_src);
		var ctx = canvas.getContext("2d");
		var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
		var l = canvas.width * canvas.height * 4;
		for (var i = 0; i < l; i+=4) {
			var p = i >>2;
			var y = Math.floor(p / canvas.width);
			var x = p - y * canvas.width;
			if (x === 0 || y === 0 || x === canvas.width-1 || y === canvas.height-1) {
				imageData.data[i+3] = Math.min(imageData.data[i+3], options.alpha);
			}
			if (options.alpha2 < 255 && (x === 1 || y === 1 || x === canvas.width-2 || y === canvas.height-2)) {
				imageData.data[i+3] = Math.min(imageData.data[i+3], options.alpha2);
			}
		}
		ctx.putImageData(imageData, 0, 0);

		PIXI.Texture.removeFromCache(canvas.texture);
		PIXI.BaseTexture.removeFromCache(canvas.texture);

		canvas._pixiId = canvas.texture;
		var texture_dest = PIXI.Texture.from(canvas);
		texture_dest.canvas = canvas;
		texture_dest._isFinalTexture = texture_src._isFinalTexture;
		texture_dest._qBorderAlpha = texture_src._qBorderAlpha;
	},

	/**
	 * Добавляет прозрачную границу к спрайту в 1 пиксель, благодаря чему PIXI сглаживает края спрайта при его вращении. Ширина и высота спрайта увеличиваются на 2 пикселя!
	 * @memberof SG2D.Utils
	 * @param {object} options
	 * @param {string[]|PIXI.Texture[]} [options.textures=void 0] - Список текстур. Если не задан, то берутся все текстуры с флагом _isFinalTexture=true
	 * @param {number} [options.qborder=1] Ограничитель допустимой толщины прозрачной рамки
	 */
	addBorderAlphaTextures: function(options) {
		options.textures = options.textures || [];
		options.qborder = options.qborder || 1;
		if (! options.textures.length) {
			this._getFinalTextures(options.textures);
		}
		for (var i = 0; i < options.textures.length; i++) {
			var texture = typeof options.textures[i] === "object" ? options.textures[i] : PIXI.utils.TextureCache[options.textures[i]];
			if (texture) {
				if (options.qborder > this.ifUndefined(texture._qBorderAlpha, 0)) {
					var srcCanvas = SG2DUtils.getTextureAsCanvas(texture);
					var destCanvas = SG2DUtils.createCanvas(texture.width + 2, texture.height + 2);
					var destCtx = destCanvas.getContext("2d");
					destCtx.drawImage(srcCanvas, 1, 1);
					destCanvas._pixiId = texture.textureCacheIds[0] || texture.canvas._pixiId;
					PIXI.Texture.removeFromCache(destCanvas._pixiId);
					PIXI.BaseTexture.removeFromCache(destCanvas._pixiId);
					var textureDest = PIXI.Texture.from(destCanvas);
					textureDest.canvas = destCanvas;
					textureDest._isFinalTexture = texture._isFinalTexture;
					textureDest._qBorderAlpha = texture._qBorderAlpha ? texture._qBorderAlpha + 1 : 1;
				}
			} else {
				console.warn("Texture with name \"" + options.textures[i] + "\" not found!");
			}
		}
	},

	/** @private */
	_getFinalTextures: function(textures) {
		for (var name in PIXI.utils.TextureCache) {
			var texture = PIXI.utils.TextureCache[name];
			if (texture._isFinalTexture) {
				textures.push(texture);
			}
		}
	},

	// TODO DEL: (не исп-ся?)
	/*_rgba: {r:0,g:0,b:0,a:0},
	imageDataSetPixel: function(imageData, x, y, r, g, b, a) {
		var index = (x + y * imageData.width) * 4;
		imageData.data[index] = r;
		imageData.data[index + 1] = g;
		imageData.data[index + 2] = b;
		imageData.data[index + 3] = a;
	},
	imageDataGetPixel: function(imageData, x, y) {
		var index = (x + y * imageData.width) * 4;
		this._rgba.r = imageData.data[index];
		this._rgba.g = imageData.data[index+1];
		this._rgba.b = imageData.data[index+2];
		this._rgba.a = imageData.data[index+3];
		return this._rgba;
	},*/
	// /TODO DEL.

	/** @private */
	_ibt: [],

	/**
	 * Создание промежуточных текстур для постепенного перехода одной текстуры в другую (без использования альфа-канала)
	 * @memberof SG2D.Utils
	 * @param {object} config
	 * @param {PIXI.Texture|string} config.start
	 * @param {PIXI.Texture|string} config.end
	 * @param {string} config.name - Имя промежуточной текстуры с подстановкой индекса вместо "%"
	 * @param {Number} [config.count=2]
	 * @param {Number} [config.flags] Флаги: {@link SG2D.Utils.FLAG_ADD_BORDER_ALPHA}
	 */
	createInBetweenTextures: function(config) {
		if (! config.start) throw "Error 7020181!";
		if (! config.end) throw "Error 7020182!";
		config.count = Math.max(2, config.count || 2);
		if (! config.name) throw "Error 7020184!";
		config.flags = config.flags || 0;

		if (config.count <= 2) return;

		let canvasStart = this.getTextureAsCanvas(config.start);
		let canvasEnd = this.getTextureAsCanvas(config.end);
		let w = canvasStart.width, h = canvasStart.height, wh = w * h;

		for (var i = 0; i < wh; i++) this._ibt[i] = false;

		let dataStart = canvasStart.getContext("2d").getImageData(0,0,w,h).data;
		let dataEnd = canvasEnd.getContext("2d").getImageData(0,0,w,h).data;

		let blocksize = config.count - 1; // 5-1=4
		// count	     blocks
		// 2-1=1	S-...	e-...
		// 3-1=2	S-S-...	S-e- S-e- S-e- S-e- S-e- S-e-...	e-e-...
		// 4-1=3	S-S-S-...	S-S-e- S-S-e- S-S-e- S-S-e-...		S-e-e- S-e-e- S-e-e- S-e-e-...		e-e-e-...
		// 5-1=4	S-S-S-S-...	S-S-S-e- S-S-S-e- S-S-S-e-...		S-S-e-e- S-S-e-e- S-S-e-e-...		S-e-e-e- S-e-e-e- S-e-e-e-...		e-e-e-e-...
		// +random within one block (S-S-x-S)

		let qblocks = Math.ceil(wh / blocksize);
		let dataPrev = null;

		let texturesCreated = [];
		
		for (var j = 2; j < config.count; j++) {

			var jc = (j-1)/(config.count-1);
			var m = config.count - j + 1; // number of remaining unreplaced pixels

			var imageData = new ImageData(Uint8ClampedArray.from(dataPrev || dataStart), w, h);
			var data = imageData.data;

			for (var b = 0; b < qblocks; b++) {
				var pm = Math.floor(Math.random() * m); // Random position in the remaining unreplaced pixels
				var p0 = b * blocksize;
				for (var p = p0; p < p0 + blocksize; p++) {
					if (this._ibt[p]) continue;
					if (pm === 0) {
						this._ibt[p] = true;
						var index = p<<2;

						var brightnessStart = dataStart[index]+dataStart[index+1]+dataStart[index+2];
						var brightnessEnd = dataEnd[index]+dataEnd[index+1]+dataEnd[index+2];
						var brightnessDelta = (brightnessEnd - brightnessStart) * jc / 3 / 256;
						for (var c = 0; c < 3; c++) {
							var ic = index+c;
							data[ic] = dataEnd[ic];
							if (brightnessDelta < 0) {
								data[ic] = ~~(data[ic] * (1 + brightnessDelta));
							} else {
								data[ic] = ~~(data[ic] + (256 - data[ic]) * brightnessDelta);
							}
						}

						var ic = index+3;
						var alphaDelta = (dataEnd[ic] - dataStart[ic]) * jc / 256;
						data[ic] = dataEnd[ic];
						if (alphaDelta) {
							if (alphaDelta < 0) {
								data[ic] = ~~(data[ic] * (1 + alphaDelta));
							} else {
								data[ic] = ~~(data[ic] + (256 - data[ic]) * alphaDelta);
							}
						}

						break;
					}
					pm--;
				}
			}

			var canvas = this.createCanvas(w, h);
			var ctx = canvas.getContext("2d");
			ctx.putImageData(imageData, 0, 0);
			
			canvas._pixiId = config.name.replace("%", j);
			var texture = PIXI.Texture.from(canvas);
			texture.canvas = canvas;
			texture._isFinalTexture = true;
			
			texturesCreated.push(texture);

			dataPrev = data;
		}
		
		if (config.flags & this.FLAG_ADD_BORDER_ALPHA) {
			this.addBorderAlphaTextures({
				textures: texturesCreated
			});
		}

		var nameStart = config.name.replace("%", 1);
		if (! PIXI.utils.TextureCache[nameStart]) {
			canvasStart._pixiId = nameStart;
			var texture = PIXI.Texture.from(canvasStart);
			texture._isFinalTexture = true;
		}

		var nameEnd = config.name.replace("%", config.count);
		if (! PIXI.utils.TextureCache[nameEnd]) {
			canvasEnd._pixiId = nameEnd;
			var texture = PIXI.Texture.from(canvasEnd);
			texture._isFinalTexture = true;
		}
	},

	/**
	 * Текстуры, в названии которых указан размер по шаблону "_%x%", автоматически распиливаются на составные текстуры с добавлением индекса.
	 * @memberof SG2D.Utils
	 * @param {object} resources - Объект, выдаваемый движком PIXI при загрузке ресурсов (см. PIXI.Loader.load(loader, resources)=>{...})
	 */
	parseTexturePack: function(resources) {
		for (var r in resources) {
			if (r.substr(-6, 6) === "_image") continue;
			var resource = resources[r];
			for (var code in resource.textures) {

				var baseTexture = resource.textures[code];

				var m = code.match(/(.+)_(\d+)x(\d+)(\w)?(\.\w+)?$/); // Search for pictures with a specific name, for example: "explosion_64x64.png"
				if (m) {
					baseTexture._isFinalTexture = false;
					//SG2DUtils.getTextureUrl(code).then((res)=>{ (new Image()).src = res; }); // debugging

					var name = m[1];
					var w = +m[2];
					var h = +m[3];
					var dir = m[4] || "h";
					var sizex = Math.floor(baseTexture.width / w);
					var sizey = Math.floor(baseTexture.height / h);

					if (dir === "h") {
						var index = 0;
						for (var y = 0; y < sizey; y++) {
							for (var x = 0; x < sizex; x++) {
								var texture_name = name + "_" + (++index);
								if (PIXI.utils.TextureCache[texture_name]) {
									console.error("Texture \"" + texture_name + "\" already exists!");
									continue;
								}
								var px = x * w, py = y * h;
								var rect = new PIXI.Rectangle(0, 0, 0, 0);
								rect.width = w;
								rect.height = h;
								rect.x = baseTexture.frame.x + px;
								rect.y = baseTexture.frame.y + py;
								var texture = new PIXI.Texture(baseTexture, rect);
								PIXI.Texture.addToCache(texture, texture_name);
								texture._isFinalTexture = true;
								//SG2DUtils.getTextureUrl(name + "_" + index).then((res)=>{ (new Image()).src = res; }); // debugging
							}
						}
					} else {
						throw "dir vertical no supported! (TODO)";
					}

					PIXI.Texture.removeFromCache(baseTexture);
					PIXI.BaseTexture.removeFromCache(baseTexture);
				} else {
					baseTexture._isFinalTexture = true;
				}
			}
		}
	},

	/** @private */
	_loadSystemTextures: function() {
		let promise = new Promise((resolve, failed)=>{
			if (typeof PIXI === "undefined") {
				resolve("PIXI is undefined! Perhaps we are working in server mode!");
			} else {
				const loader = new PIXI.Loader()
				loader.add(SG2DConsts.SYSTEM_TILES).load((loader, resources)=>{
					for (var i = 0; i < SG2DConsts.SYSTEM_TILES.length; i++) {
						PIXI.Texture.removeFromCache( SG2DConsts.SYSTEM_TILES[i].url );
						PIXI.BaseTexture.removeFromCache( SG2DConsts.SYSTEM_TILES[i].url );
						PIXI.utils.TextureCache[ SG2DConsts.SYSTEM_TILES[i].name ]._isFinalTexture = true;
					}
					resolve();
				});
			}
		});
		return promise;
	},

	FLAG_CANVAS_OFFSCREEN: 0,
	FLAG_CANVAS_ELEMENT: 1,

	/**
	 * Создаёт новый канвас. По умолчанию создаёт OffscreenCanvas
	 * @memberof SG2D.Utils
	 * @param {Number} [width=SG2D.Utils.DEFAULT_WIDTH]
	 * @param {Number} [height=SG2D.Utils.DEFAULT_HEIGHT]
	 * @param {Number} [flag=0] **SG2D.Utils.FLAG_CANVAS_OFFSCREEN** | **SG2D.Utils.FLAG_CANVAS_ELEMENT**
	 */
	createCanvas: function(width = void 0, height = void 0, flag = 0) {
		//flag = this.FLAG_CANVAS_ELEMENT; // FOR DEBUGGING
		if (flag === this.FLAG_CANVAS_ELEMENT || typeof OffscreenCanvas === "undefined") {
			var canvas = document.createElement("CANVAS");
			canvas.width = width || SG2DUtils.DEFAULT_WIDTH;
			canvas.height = height || SG2DUtils.DEFAULT_HEIGHT;
			return canvas;
		} else {
			return new OffscreenCanvas(width || SG2DUtils.DEFAULT_WIDTH, height || SG2DUtils.DEFAULT_HEIGHT);
		}
	},
	
	drawCircle: function(x, y, r, c) {
		var graphics = new PIXI.Graphics();
		graphics.beginFill(c || 0xff2222);
		graphics.drawCircle(x || 0, y || 0, r || 3);
		graphics.endFill();
		graphics.zIndex = 99999;
		SG2D.Application.drawSprite(graphics);
	},

	/**
	 * Преобразовать пиксели в кластеры (единица измерения)
	 * @memberof SG2D.Utils
	 * @param {Number} x_or_y
	 * @returns {Number}
	 */
	PXtoCX: function(x_or_y) {
		return 1 + (x_or_y >> SG2DConsts.CELLSIZELOG2);
	},

	/**
	 * Вернёт функцию, которая выполнится только после истечения заданного промежутка времени за который не было обращений к ней
	 * @memberof SG2D.Utils
	 * @param {function} func
	 * @param {Number} delay
	 * @returns {function}
	 */
	debounce: function(func, delay) {
		let clearTimer;
		return function() {
			const context = this;
			const args = arguments;
			clearTimeout(clearTimer);
			clearTimer = setTimeout(() => func.apply(context, args), delay);
		}
	},

	/**
	 * Вернёт функцию, которая будет выполнятся не чаще одного раза в заданный промежуток времени
	 * @memberof SG2D.Utils
	 * @param {function} func
	 * @param {type} limit - Время задержки
	 * @param {object} [_context=this]
	 * @returns {function}
	 */
	throttle: function(func, limit, _context) {
		let lastFunc;
		let lastRan;
		return function() {
			const context = _context || this;
			const args = arguments;
			if (!lastRan) {
				func.apply(context, args);
				lastRan = Date.now();
			} else {
				clearTimeout(lastFunc);
				lastFunc = setTimeout(function() {
					if ((Date.now() - lastRan) >= limit) {
						func.apply(context, args);
						lastRan = Date.now();
					}
				}, limit - (Date.now() - lastRan));
			}
		}
	},

	/** @private */
	_p: void 0,

	/**
	 * Проверить - объект пустой или нет
	 * @memberof SG2D.Utils
	 * @param {object} o
	 * @returns {Boolean}
	 */
	isEmpty: function(o) {
		for (this._p in o) return false;
		return true;
	},

	/**
	 * forEach для объекта
	 * @memberof SG2D.Utils
	 * @param {object} obj
	 * @param {function} callback
	 * @param {object} context
	 * @returns {undefined}
	 */
	objectForEach: function(obj, callback, context = this) {
		for (var name in obj) {
			if (callback.call(context, obj[name])===false) break;
		}
	},
	
	/**
	 * Количество собственных свойст в объекте
	 * @memberof SG2D.Utils
	 * @param {object} o
	 * @returns {Number}
	 */
	propertiesCount: function(o) {
		let q = 0;
		for (var p in o) q++;
		return q;
	},

	/**
	 * Вернёт valueIfUndefined, если value===undefined
	 * @memberof SG2D.Utils
	 * @param {mixed} value
	 * @param {mixed} valueIfUndefined
	 * @returns {value|valueIfUndefined}
	 */
	ifUndefined: function(value, valueIfUndefined) {
		return (value === void 0 ? valueIfUndefined : value);
	}
};

export default SG2DUtils;