Source: CrossBase/general/CB_others.js

/**
 * @file Miscellaneous code.
 *  @author Joan Alba Maldonado <workindalian@gmail.com>
 *  @license Creative Commons Attribution 4.0 International. See more at {@link https://crossbrowdy.com/about#what_is_the_crossbrowdy_copyright_and_license}.
 *  @todo Think about a 'CB_symmetricInterval' function, similar to {@link CB_symmetricCall} but calling the callback function automatically.
 */


/**
 * Callback that is called by {@link CB_symmetricCall}.
 *  @callback CB_symmetricCall_CALLBACK
 *  @param {integer} expectedCallingTime - The timestamp in milliseconds that represents when the callback should have been called (it will be more or less accurate depending on many factors as the platform used, code performance, etc.).
 */

var CB_symmetricCallLastTimes = {}; //Array that stores the last times of every function.
/**
 * Calls the given function once through the native [setTimeout]{@link https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout} function internally but having in mind the time taken when the function was called previously so it can be called multiple times and respect a symmetric interval between each call (simulates [requestAnimationFrame]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}).
 *  @function
 *  @param {CB_symmetricCall_CALLBACK} callbackFunction - Function that will be called every time, receiving as the unique parameter the time (timestamp in milliseconds returned by the [performance.now]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance/now} method, which could have been polyfilled automatically by CrossBrowdy) when it is called, being "this" the same "this" of the scope where it was called.
 *  @param {integer} timeMs - Milliseconds between one call to the function and the next one. The accuracy will depend on many factors as the platform used, code performance, etc.
 *  @param {string} [id=callbackFunction.toString()] - String that will identify this symmetric interval. Recommended to avoid possible problems.
 *  @returns {number|null} Returns a numeric identifier generated by an internal call to the native [setTimeout]{@link https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout} function (can be cleared/cancelled with [clearTimeout]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout}). Returns null if the given "callbackFunction" is not a valid function.
 */
//* Source: Based on requestAnimationFrame polyfill by Erik Möller.
function CB_symmetricCall(callbackFunction, timeMs, id)
{
	if (typeof(callbackFunction) !== "function") { return null; }
	
	var now = window.performance.now(); //Can be polyfilled (https://gist.github.com/jalbam/cc805ac3cfe14004ecdf323159ecf40e)

	id = id || callbackFunction;
	
	if (typeof(CB_symmetricCallLastTimes[id]) === "undefined" || CB_symmetricCallLastTimes[id] === null)
	{
		var nextTime = now + timeMs; //First time, it lasts the given milliseconds.
		CB_symmetricCallLastTimes[id] = 0;
	}
	else
	{
		//var lastTime = CB_symmetricCallLastTimes[id];
		
		/*
		var timeToCall = Math.max(lastTime + timeMs, now);//Math.max(0, timeMs - (now - lastTime));
		var id = setTimeout(callbackFunction, timeToCall - now);//, timeToCall);
		lastTime = timeToCall;//now + timeToCall;
		*/
		
		//var timeToCall = Math.max(0, timeMs - (now - lastTime));
		//var timeToCall = Math.max(0, timeMs - (now - CB_symmetricCallLastTimes[id]));
		var nextTime = Math.max(CB_symmetricCallLastTimes[id] + timeMs, now);
	}
	
	var that = this;
	return setTimeout
	(
		function()
		{
			//callbackFunction.call(that, CB_symmetricCallLastTimes[id] = now + timeToCall);
			callbackFunction.call(that, CB_symmetricCallLastTimes[id] = nextTime);
		},
		//timeToCall
		nextTime - now
	);
}


/**
 * Clears the stored last time used by {@link CB_symmetricCall} for a given symmetric interval identifier.
 *  @function
 *  @param {string} id - String that identifies this symmetric interval.
 *  @returns {boolean|null} Returns null if the given "id" is not a valid string. Returns false if the stored time did not exist for the given "id" or it was cleared already. Returns true otherwise, after clearing it.
 */
function CB_symmetricCallClear(id)
{
	id = id + "";
	if (id === "") { return null; }
	if (typeof(CB_symmetricCallLastTimes[id]) === "undefined" || CB_symmetricCallLastTimes[id] === null) { return false; }
	CB_symmetricCallLastTimes[id] = null;
	return true;
}