src/utils.mjs
import { Logger } from './vendor/logger.min.mjs';
/**
* A utilities class
* @class UtilsSingleton
* @license Utils is free software, available under the terms of a MIT style License.
* @author https://github.com/doubleactii
*/
class UtilsSingleton {
/**
* Object storing all color objects being transitioned at the moment
* @private
* @type {Object}
*/
transitions = {};
/**
* An array storing all the reserved unique IDS
* @private
* @type {Array}
*/
storedIDs = [];
/**
* The version of the module.
*/
version = "VERSION_REPLACE_ME";
constructor() {
// Create a logger
/** The logger module this module uses to log errors / logs
* @private
* @type {Object}
*/
this.logger = new Logger();
this.logger.registerType('Utils-Module', '#ff6600');
}
/**
* Generates a random decimal number between two numbers with a specified number of decimal places.
*
* @param {number} pNum1 - The first number to use for generating the random decimal number.
* @param {number} pNum2 - The second number to use for generating the random decimal number.
* @param {number} [pPlaces=1] - The number of decimal places to include in the generated random decimal number. Defaults to 1 if not provided.
* @returns {number} A random decimal number between the two numbers with the specified number of decimal places.
*/
decimalRand(pNum1, pNum2, pPlaces = 1) {
const result = Number((Math.random() * (pNum1 - pNum2) + pNum2).toFixed(pPlaces));
return result;
}
/**
* Generates a random decimal number between two numbers with a specified number of decimal places.
*
* @param {number} pNum1 - The first number to use for generating the random decimal number.
* @param {number} pNum2 - The second number to use for generating the random decimal number.
* @returns {number} A random decimal number between the two numbers with the specified number of decimal places.
*/
rand(pNum1, pNum2) {
const result = Number((Math.random() * (pNum1 - pNum2) + pNum2));
return Math.round(result);
}
/**
* Calculates the percentage of a value relative to a total value.
*
* @param {number} pValue - The value to calculate the percentage of.
* @param {number} pTotalValue - The total value to calculate the percentage relative to.
* @returns {number} The percentage of the value relative to the total value.
*/
getPercentage(pValue, pTotalValue) {
return (100 * pValue) / pTotalValue;
}
/**
* Clamps a number between a minimum and maximum value.
*
* @param {number} pNumber - The number to clamp.
* @param {number} [pMin=0] - The minimum value to clamp the number to. Defaults to 0 if not provided.
* @param {number} [pMax=1] - The maximum value to clamp the number to. Defaults to 1 if not provided.
* @returns {number} The clamped number between the minimum and maximum values.
*/
clamp(pNumber, pMin = 0, pMax = 1) {
return Math.max(pMin, Math.min(pNumber, pMax));
}
/**
* Linearly interpolates between two values by a specified amount.
*
* @param {number} pStart - The start value to interpolate from.
* @param {number} pEnd - The end value to interpolate to.
* @param {number} pAmount - The amount to interpolate between the start and end values.
* @returns {number} The interpolated value between the start and end values based on the specified amount.
*/
lerp(pStart, pEnd, pAmount) {
return (1-pAmount)*pStart+pAmount*pEnd;
}
/**
* Linearly interpolates between two values by a specified amount and returns the result as a floored integer.
*
* @param {number} pStart - The start value to interpolate from.
* @param {number} pEnd - The end value to interpolate to.
* @param {number} pAmount - The amount to interpolate between the start and end values.
* @returns {number} The interpolated value between the start and end values based on the specified amount, rounded down to the nearest integer.
*/
flooredLerp(pStart, pEnd, pAmount) {
return Math.floor(this.lerp(pStart, pEnd, pAmount));
}
/**
* Rounds a number to a specified number of decimal places.
*
* @param {number} pNumber - The number to round.
* @param {number} [pPlace=1] - The number of decimal places to round to. Defaults to 1 if not provided.
* @returns {number} The rounded number to the specified number of decimal places.
*/
round(pNumber, pPlace=1) {
return Math.round(pPlace * pNumber) / pPlace;
}
/**
* Normalizes a value between a minimum and maximum value.
*
* @param {number} pVal - The value to normalize.
* @param {number} pMin - The minimum value for normalization.
* @param {number} pMax - The maximum value for normalization.
* @returns {number} The normalized value between 0 and 1 based on the input value's position between the minimum and maximum values.
* If the difference between pMax and pMin is 0, returns 1 to avoid dividing by zero.
*/
normalize(pVal, pMin, pMax) {
if (pMax - pMin === 0) return 1;
return (pVal - pMin) / (pMax - pMin);
}
/**
* Normalizes a value between a minimum and maximum value, clamped to the range of -1 to 1.
*
* @param {number} pVal - The value to normalize.
* @param {number} pMin - The minimum value for normalization.
* @param {number} pMax - The maximum value for normalization.
* @returns {number} The normalized and clamped value between -1 and 1 based on the input value's
* position between the minimum and maximum values. If the difference between pMax and pMin is 0,
* returns 1 to avoid dividing by zero.
*/
normalizeRanged(pVal, pMin, pMax) {
if (pMax - pMin === 0) return 1;
const normalizedValue = -((2 * this.normalize(pVal, pMin, pMax)) - 1);
// Clamp the normalized value to the range of -1 to 1
return this.clamp(normalizedValue, -1, 1);
};
/**
* Checks if a value is within a range of minimum and maximum values (inclusive).
*
* @param {number} pVal - The value to check.
* @param {number} pMin - The minimum value of the range to check against.
* @param {number} pMax - The maximum value of the range to check against.
* @returns {boolean} True if the value is within the range (inclusive), false otherwise.
*/
within(pVal, pMin, pMax) {
return pVal >= pMin && pVal <= pMax;
}
/**
* Formats a number by rounding it to the nearest integer and adding commas to separate thousands places.
*
* @param {number} pNum - The number to format.
* @returns {string} A string representation of the formatted number.
*/
formatIntegerWithCommas(pNum) {
return pNum.toFixed().toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}
/**
* Converts degrees to radians.
*
* @param {number} pDegrees - The angle in degrees.
* @returns {number} The angle in radians.
*/
toRadians(pDegrees) {
return pDegrees * (Math.PI / 180);
}
/**
* Converts radians to degrees.
*
* @param {number} pRadians - The angle in radians.
* @returns {number} The angle in degrees.
*/
toDegrees(pRadians) {
return pRadians * (180 / Math.PI);
}
/**
* Returns a random element from the given array.
*
* @param {Array} pArray - The input array.
* @returns {*} A random element from the array.
*/
pick(pArray) {
const randomIndex = Math.floor(Math.random() * pArray.length);
return pArray[randomIndex];
}
/**
* Removes properties from an object except those listed in the exclude array.
*
* @param {object} pObject - The object to remove properties from.
* @param {Array} pExclude - The array of property names to exclude from removal.
*/
removeProperties(pObject, pExclude) {
if (typeof(pObject) === 'object') {
for (const prop in pObject) {
// Do not reset these properties.
if (Array.isArray(pExclude) && pExclude.includes(prop)) continue;
if (pObject.hasOwnProperty(prop)) {
delete pObject[prop];
}
}
}
}
/**
* Returns true with probability proportional to the given number.
* The higher the number, the higher the chance of returning true.
*
* @param {number} pChance - The probability value, between 0 and 100 (inclusive).
* @returns {boolean} - Returns true or false, based on the probability value.
*/
prob(pChance) {
if (pChance <= 0) {
return false;
}
if (pChance >= 100) {
return true;
}
const randomNumber = Math.floor(Math.random() * 100) + 1;
return randomNumber <= pChance;
}
/**
* Gets the inverse direction of the direction passed
*
* @param {string} pDirection - The direction to get the inverse of.
* @returns {string} The inverse direction
*/
getInverseDirection(pDirection) {
switch (pDirection) {
case 'north':
return 'south';
case 'south':
return 'north';
case 'east':
return 'west';
case 'west':
return 'east';
case 'northeast':
return 'southwest';
case 'northwest':
return 'southeast';
case 'southeast':
return 'northwest';
case 'southwest':
return 'northeast';
default:
this.logger.prefix('Utils-Module').error(`The direction ${pDirection} is not supported.`);
}
}
/**
* Calculates the angle (in radians) from a given direction.
*
* @param {string} pDirection - The direction to calculate the angle from.
* @returns {number} The angle (in radians) associated with the given direction.
* @throws {Error} Throws an error if the direction is not recognized.
*/
getAngleFromDirection(pDirection) {
switch (pDirection) {
case 'north':
return Math.PI / 2;
case 'south':
return (Math.PI * 3) / 2; // Corrected to 270 degrees in radians
case 'east':
return 0;
case 'west':
return Math.PI;
case 'northwest':
return (Math.PI * 3) / 4;
case 'northeast':
return Math.PI / 4;
case 'southwest':
return (Math.PI * 5) / 4;
case 'southeast':
return (Math.PI * 7) / 4;
default:
this.logger.prefix('Utils-Module').error(`The direction ${pDirection} is not supported.`);
}
}
/**
* Centers a rectangle (defined by its dimensions) within a parent rectangle.
*
* @param {number} pChildWidth - The width of the child rectangle.
* @param {number} pChildHeight - The height of the child rectangle.
* @param {number} pParentWidth - The width of the parent rectangle.
* @param {number} pParentHeight - The height of the parent rectangle.
* @param {number} pParentX - The x-coordinate of the parent rectangle.
* @param {number} pParentY - The y-coordinate of the parent rectangle.
* @returns {Object} An object representing the new coordinates of the centered rectangle: { x: centerX, y: centerY }.
*
* @example
* const childWidth = 50;
* const childHeight = 30;
* const parentWidth = 100;
* const parentHeight = 80;
* const parentX = 20;
* const parentY = 10;
* const centeredCoordinates = centerRectangleOnParent(childWidth, childHeight, parentWidth, parentHeight, parentX, parentY);
* // Returns {x: 45, y: 35}
*/
centerRectangleOnParent(pChildWidth, pChildHeight, pParentWidth, pParentHeight, pParentX, pParentY) {
const centerX = pParentX + ((pParentWidth - pChildWidth) / 2);
const centerY = pParentY + ((pParentHeight - pChildHeight) / 2);
return { x: centerX, y: centerY };
}
/**
* Generates a random angle in radians.
* @returns {number} A random angle in radians.
*/
getRandomAngle() {
return Math.random() * (Math.PI * 2); // Random value between 0 and 2*pi (360 degrees)
}
/**
* Gets the angle between two points
*
* @param {Object} pStartPoint - The starting point
* @param {Object} pEndPoint - The ending point
* @returns {number} The angle between the starting point and the ending point
*/
getAngle(pStartPoint, pEndPoint) {
const y = pStartPoint.y - pEndPoint.y;
const x = pStartPoint.x - pEndPoint.x;
return Math.atan2(y, x);
}
/**
* Gets the angle between two points but in VYLO / PIXI coordinate space. Removes 180 degrees from a raw angle
*
* @param {Object} pStartPoint - The starting point
* @param {Object} pEndPoint - The ending point
* @returns {number} The angle between the starting point and the ending point
*/
getAngle2(pStartPoint, pEndPoint) {
const y = pStartPoint.y - pEndPoint.y;
const x = pStartPoint.x - pEndPoint.x;
return (Math.atan2(y, x) - Math.PI) * -1;
}
/**
* Converts a raw angle to be the proper angle in Vylocity. By removing 180 degrees
* @param {number} pAngle - The angle to convert.
* @returns The converted angle
*/
convertRaWAngleToVyloCoords(pAngle) {
return (pAngle - Math.PI) * -1;
}
/**
* Calculates the Euclidean distance between two points in a two-dimensional space.
*
* @param {Object} pStartPoint - The starting point with x and y coordinates.
* @param {number} pStartPoint.x - The x-coordinate of the starting point.
* @param {number} pStartPoint.y - The y-coordinate of the starting point.
* @param {Object} pEndPoint - The ending point with x and y coordinates.
* @param {number} pEndPoint.x - The x-coordinate of the ending point.
* @param {number} pEndPoint.y - The y-coordinate of the ending point.
* @returns {number} The Euclidean distance between the two points.
*
* @example
* const startPoint = { x: 1, y: 2 };
* const endPoint = { x: 4, y: 6 };
* const distance = getDistance(startPoint, endPoint); // 5
* // Returns the Euclidean distance between the points (1, 2) and (4, 6).
*/
getDistance(pStartPoint, pEndPoint) {
const y = (pStartPoint.y - pEndPoint.y);
const x = (pStartPoint.x - pEndPoint.x);
return Math.sqrt((x * x) + (y * y));
}
/**
* Calculates the new position of a point based on distance and angle.
*
* @param {Object} pPoint - The initial position of the point with x and y coordinates.
* @param {number} pPoint.x - The initial x-coordinate of the point.
* @param {number} pPoint.y - The initial y-coordinate of the point.
* @param {number} pDistance - The distance by which to move the point.
* @param {number} pAngle - The angle (in radians) at which to move the point.
* @returns {Object} The new position of the point after moving by the specified distance and angle.
*
* @example
* const initialPosition = { x: 10, y: 20 };
* const distance = 5;
* const angleInRadians = 0.785398; // 45 degrees
* const newPosition = calculateNewPositionFromDistanceAndAngle(initialPosition, distance, angleInDegrees);
* // Returns the new position of the point after moving by 5 units at a 45-degree angle.
*/
calculateNewPositionFromDistanceAndAngle(pPoint, pDistance, pAngle) {
const newPosition = { x: 0, y: 0 };
newPosition.x = pPoint.x - pDistance * Math.cos(pAngle);
newPosition.y = pPoint.y - pDistance * Math.sin(pAngle);
return newPosition;
};
/**
* Calculates the proportional length based on a current value, a maximum value, and a specified total length.
*
* @param {number} pCurrent - The current value to be scaled.
* @param {number} pMax - The maximum value for scaling.
* @param {number} pTotalLength - The specified total length.
* @returns {number} The proportional length based on the current value, maximum value, and total length.
*
* @example
* const current = 25;
* const max = 50;
* const totalLength = 100;
* const proportionalLength = calculateProportionalLength(current, max, totalLength); // 50
* // Returns the proportional length based on the current value, maximum value, and total length.
*/
calculateProportionalLength(pCurrent, pMax, pTotalLength) {
return (pCurrent / pMax) * pTotalLength;
}
/**
* Calculates the compass direction based on the given angle.
*
* @param {number} pAngle - The angle in radians.
* @returns {string} The compass direction (e.g., 'east', 'southeast', 'south', etc.).
*
* @example
* const angle = Math.PI / 4; // 45 degrees in radians
* const direction = getDirection(angle); // Returns 'northeast'
*/
getDirection(pAngle) {
const degree = Math.abs(Math.floor(((pAngle * (180 / Math.PI)) / 45) + 0.5));
// 0 or 360 degrees: 'east'
// 45 degrees: 'northeast'
// 90 degrees: 'north'
// 135 degrees: 'northwest'
// 180 degrees: 'west'
// 225 degrees: 'southwest'
// 270 degrees: 'south'
// 315 degrees: 'southeast'
const compassDirections = ['east', 'northeast', 'north', 'northwest', 'west', 'southwest', 'south', 'southeast'];
return compassDirections[(degree % 8)];
}
/**
* Calculates the linear decay of a variable over time.
*
* @param {number} pInitialValue - The initial value of the variable.
* @param {number} pCurrentTime - The current time at which to calculate the variable value.
* @param {number} pMaxTime - The maximum time for the decay process.
* @param {number} [pDecayRate=0.5] - The decay rate (default is 0.5).
* @returns {number} The remaining value of the variable after linear decay.
*
* @example
* const initialValue = 100;
* const currentTime = 50;
* const maxTime = 1000;
* const decayRate = 0.3;
* const remainingValue = linearDecay(initialValue, currentTime, maxTime, decayRate);
* // Returns the remaining value after linear decay.
*/
linearDecay(pInitialValue, pCurrentTime, pMaxTime, pDecayRate = 0.5) {
// Calculate the variable value at the current time
const proportionOfTimePassed = pCurrentTime / pMaxTime;
const remainingValue = Math.max(pInitialValue * (1 - (proportionOfTimePassed * pDecayRate)), 1);
return remainingValue;
}
/**
* Generates a unique id
*
* @param {string} pIDLength - The length of the ID to create
* @returns A unique ID
*/
generateID(pIDLength = 7) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const makeID = function() {
let ID = '';
for (let i = 0; i < pIDLength; i++) {
ID += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ID;
}
let ID = makeID();
while(this.storedIDs.includes(ID)) {
ID = makeID();
}
this.storedIDs.push(ID);
return ID;
}
/**
* Converts a color in decimal format into hex format
*
* @param {number} pDecimal - The color in decimal format
* @param {number} pChars - The length to make the hex string
* @returns The decimal color converted into hex format
*/
decimalToHex(pDecimal, pChars = 6) {
return '#' + (pDecimal + Math.pow(16, pChars)).toString(16).slice(-pChars).toUpperCase();
}
/**
* Add intensity to this color to get a brighter or dimmer effect
*
* @param {string|number} pColor - Color in hex format or decimal format
* @param {number} pPercent - The percent of brightness to add to this color
* @returns
*/
addIntensity(pColor, pPercent) {
const rgb = this.grabColor(pColor).rgbArray;
const r = rgb[0];
const g = rgb[1];
const b = rgb[2];
let rr = 0;
let rg = 0;
let rb = 0;
const black = (r === 0 && g === 0 && b === 0) ? true : false;
if (r || black) rr = r + Math.floor((255 * pPercent) / 100);
if (g || black) rg = g + Math.floor((255 * pPercent) / 100);
if (b || black) rb = b + Math.floor((255 * pPercent) / 100);
return this.grabColor(this.clamp(rr, 0, 255), this.clamp(rg, 0, 255), this.clamp(rb, 0, 255)).hex
}
/**
* Converts an RGB color value to a hexadecimal color value.
*
* @param {number} pR - The red component of the RGB color value (0-255).
* @param {number} pG - The green component of the RGB color value (0-255).
* @param {number} pB - The blue component of the RGB color value (0-255).
*/
rgbToHex(pR, pG, pB) {
const r = this.clamp(pR, 0, 255);
const g = this.clamp(pG, 0, 255);
const b = this.clamp(pB, 0, 255);
const craftString = function(pColor) {
return pColor.toString(16).padStart(2, '0');
}
const hex = '#' + [r, g, b].map(craftString).join('');
return hex;
}
/**
* Converts a hexadecimal color value to an RGB color value.
*
* @param {string} pHex - The hexadecimal color value to convert (e.g. "#FF0000" for red).
* @returns {Array} - An array containing the red, green, and blue components of the RGB color value.
*/
hexToRgb(pHex) {
pHex = pHex.replace('#', '');
if (pHex.length === 3) {
pHex = pHex.replace(new RegExp('(.)', 'g'), '$1$1');
}
pHex = pHex.match(new RegExp('..', 'g'));
const r = this.clamp(parseInt(pHex[0], 16), 0, 255);
const g = this.clamp(parseInt(pHex[1], 16), 0, 255);
const b = this.clamp(parseInt(pHex[2], 16), 0, 255);
return [r, g, b];
}
/**
* Converts RGB color values to a decimal value.
*
* @param {number} pR - The red component of the RGB color value (0-255).
* @param {number} pG - The green component of the RGB color value (0-255).
* @param {number} pB - The blue component of the RGB color value (0-255).
*/
rgbToDecimal(pR, pG, pB) {
return (pR << 16 | pG << 8 | pB);
}
/**
* Converts a hexadecimal color value to a decimal value.
*
* @param {string} pHex - The hexadecimal color value to convert (e.g. "#FF0000" for red).
* @returns {number} - The decimal representation of the hexadecimal color value.
*/
hexToDecimal(pHex) {
pHex = pHex.replace('#', '');
return parseInt(pHex, 16);
}
/**
* Convert a color to different formats or get a random color
*
* @param {string|number} pSwitch - A hex string representing a color (with or without the tag)
* A color formatted in the decimal format. Or the r value of a rgb color.
* @param {number} [g] g value of a rgb color
* @param {number} [b] b value of a rgb color
* @returns {ColorObject} A color object with various different export options.
* hex, hexTagless, rgb, rgbArray, rgbObject, rgbNormal, decimal formats.
*/
grabColor(pSwitch = this.getRandomColor(), pG, pB) {
let hex, rgb;
// Convert rgb to hex
if (typeof(pSwitch) === 'number' && typeof(pG) === 'number' && typeof(pB) === 'number') {
hex = this.rgbToHex(pSwitch, pG, pB);
} else {
// Convert decimal to hex
if (typeof(pSwitch) === 'number') {
pSwitch = this.decimalToHex(pSwitch);
}
hex = pSwitch;
// Convert hex to rgb
rgb = this.hexToRgb(hex);
}
return {
'hex': hex.toLowerCase(),
'hexTagless': hex.replace('#', '').toLowerCase(),
'rgb': 'rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')',
'rgbArray': rgb,
'rgbObject': { 'r': rgb[0], 'g': rgb[1], 'b': rgb[2] },
'rgbNormal': [Math.round(rgb[0]/255 * 100) / 100, Math.round(rgb[1]/255 * 100) / 100, Math.round(rgb[2]/255 * 100) / 100],
'decimal': this.hexToDecimal(hex)
};
}
/**
* Gets a random color
*
* @returns {string} A random color in the hex format
*/
getRandomColor() {
const chars = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += chars[Math.floor(Math.random() * 16)];
}
return color;
}
/**
* Gets a random color between two colors
*
* @param {number|string} pColor1 - The first color to get a color between
* @param {number|string} pColor2 - The second color to get a color between
* @param {number} [pAmount=0.5] - The closer the random color will be to either input colors on a range of 0-1
* 0 to 0.5 (closer to pColor1)
* 0.5 to 1 (closer to pColor2)
* @returns {string} A random color in the decimal format
*/
getRandomColorBetween(pColor1, pColor2, pAmount = 0.5) {
// u is the amount of the lerp 0-1
return this.flooredLerp(this.grabColor(pColor1).decimal, this.grabColor(pColor2).decimal, pAmount);
}
/**
* Transition a color to another color in pDuration time.
*
* @param {Object} pInstance - The instance to transition it's color property.
* pInstance's color will be transitioned either via pInstance.color = newColor
* or
* pInstance.color.tint = newColor (if the color is defined as an object)
* @param {string|number} pStartColor - The start color
* @param {string|number} pEndColor - The end color
* @param {number} pDuration - The duration of the transition
* @param {Function} pIterativeCallback - Callback to call every tick of the transition
* @param {Function} pEndCallback - Callback to call at the end of the transition
* @returns An ID that references this transition to be passed to cancelTransition to stop an ongoing transition.
*/
transitionColor(pInstance, pStartColor='#000', pEndColor='#fff', pDuration=1000, pIterativeCallback, pEndCallback) {
// Cannot use this API on the server
if (!globalThis.window) return;
const iterativeCallback = typeof(pIterativeCallback) === 'function' ? pIterativeCallback : null;
const endCallback = typeof(pEndCallback) === 'function' ? pEndCallback : null;
let id;
let isParticle;
let isTintObject;
if (pInstance) {
id = pInstance.id ? pInstance.id : this.generateID();
isParticle = (pInstance.type === 'GeneratedParticle');
isTintObject = (typeof(pInstance.color) === 'object' && pInstance.color.constructor === Object ? true : false);
if (this.transitions[id]) this.cancelTransitionColor(id);
} else {
id = this.generateID();
}
this.transitions[id] = {
'duration': pDuration,
'timeTracker': isParticle ? pInstance.info.lifetime : 0
};
const rgbStartColor = this.grabColor(pStartColor).rgbArray;
const rgbEndColor = this.grabColor(pEndColor).rgbArray;
const self = this;
this.transitions[id].step = (pTimeStamp) => {
if (self.transitions[id]) {
if (isParticle) {
if (pInstance.info) {
if (pInstance.info.owner) {
if (pInstance.info.owner.settings.paused) {
return;
}
}
} else {
if (self.transitions[id]) this.cancelTransitionColor(id);
return;
}
}
const now = pTimeStamp;
if (!self.transitions[id].lastTime) self.transitions[id].lastTime = now;
const elapsed = now - self.transitions[id].lastTime;
// Time tracker is used rather than lastStamp - startStamp because this currently takes into account particles passed in (this will be removed in the future and use the former method)
self.transitions[id].timeTracker += elapsed;
// The max value of percentage is 1, so we clamp it at 1
const percentage = Math.min(self.transitions[id].timeTracker / self.transitions[id].duration, 1);
const r = parseInt(self.lerp(rgbStartColor[0], rgbEndColor[0], percentage), 10);
const g = parseInt(self.lerp(rgbStartColor[1], rgbEndColor[1], percentage), 10);
const b = parseInt(self.lerp(rgbStartColor[2], rgbEndColor[2], percentage), 10);
const color = self.grabColor(r, g, b);
if (iterativeCallback) iterativeCallback(color);
if (pInstance) {
if (isTintObject) {
pInstance.color.tint = color.decimal;
pInstance.color = pInstance.color;
} else {
pInstance.color = color.hex;
}
}
if (percentage >= 1 || self.transitions[id].timeTracker >= pDuration) {
if (self.transitions[id]) this.cancelTransitionColor(id);
if (endCallback) endCallback(color);
return;
}
self.transitions[id].req = globalThis.requestAnimationFrame(self.transitions[id].step);
self.transitions[id].lastTime = now;
}
}
this.transitions[id].req = globalThis.requestAnimationFrame(this.transitions[id].step);
return id;
}
/**
* Cancels an ongoing transition
*
* @param {string} pID - The ID of the ongoing transition to cancel
*/
cancelTransitionColor(pID) {
if (this.transitions[pID]) {
globalThis.cancelAnimationFrame(this.transitions[pID].req);
delete this.transitions[pID];
}
}
/**
* Calculates the position of a point after rotating it around a center point by a given angle.
*
* @param {object} pRect - The rectangle object to rotate the point around.
* pRect.anchor.x and pRecent.anchor.y is used to control the "center" of the rectangle.
* @param {number} pTheta - The angle (in radians) to rotate the point by.
* @param {object} pPoint - The point object to rotate around the center of the rectangle.
* @param {number} pPoint.x - The x-coordinate of the point to rotate.
* @param {number} pPoint.y - The y-coordinate of the point to rotate.
* @returns {object} An object with the rotated point's new x and y coordinates.
*/
getPointRotated(pRect, pTheta, pPoint) {
// cx, cy - center of square coordinates
// x, y - coordinates of a corner point of the square
// theta is the angle of rotation
const cx = pRect.x + pRect.width * (typeof(pRect.anchor) === 'object' && pRect.anchor.x ? pRect.anchor.x : 0.5);
const cy = pRect.y + pRect.height * (typeof(pRect.anchor) === 'object' && pRect.anchor.y ? pRect.anchor.y : 0.5);
// translate point to origin
const tempX = pPoint.x - cx;
const tempY = pPoint.y - cy;
// now apply rotation
const rotatedX = tempX*Math.cos(pTheta) - tempY*(-Math.sin(pTheta));
const rotatedY = tempX*(-Math.sin(pTheta)) + tempY*Math.cos(pTheta);
// translate back
const x = rotatedX + cx;
const y = rotatedY + cy;
return { 'x': x, 'y': y };
}
/**
* Calculates the position of a rectangle's corner points and center point after rotating it around a center point by a given angle.
*
* @param {object} pRect - The rectangle object to rotate the point around.
* pRect.anchor.x and pRecent.anchor.y is used to control the "center" of the rectangle.
* @param {number} pTheta - The angle (in radians) to rotate the point by.
* @returns {object} An object with the rotated rectangle's new corner points and center points.
*/
getPointsOfRotatedRect(pRect, pTheta) {
const tl = this.getPointRotated(pRect, pTheta, { 'x': pRect.x, 'y': pRect.y });
const tr = this.getPointRotated(pRect, pTheta, { 'x': pRect.x + pRect.width, 'y': pRect.y });
const bl = this.getPointRotated(pRect, pTheta, { 'x': pRect.x, 'y': pRect.y + pRect.height });
const br = this.getPointRotated(pRect, pTheta, { 'x': pRect.x + pRect.width, 'y': pRect.y + pRect.height });
const center = this.getPointRotated(pRect, pTheta, { 'x': pRect.x + pRect.width / 2, 'y': pRect.y + pRect.height / 2 });
return { 'tl': tl, 'tr': tr, 'bl': bl, 'br': br, 'center': center };
}
/**
* Calculate the icon offset to compensate for a non-zero anchor.
*
* @param {Object} [pIconSize] - The size of the icon with properties `.x` and `.y`.
* @param {number} [pIconSize.width=32] - The size of the icon's width.
* @param {number} [pIconSize.height=32] - The size of the icon's height'.
* @param {Object} [pAnchor] - The anchor point with properties `.x` and `.y`.
* @param {number} [pAnchor.x=0.5] - The anchor's x value.
* @param {number} [pAnchor.y=0.5] - The anchor's y value.
* @param {Object} [pScale] - The scale factor applied to the object with properties `.x` and `.y`.
* @param {number} [pScale.x=1] - The scale's y value.
* @param {number} [pScale.y=1] - The scale's y value.
* @returns {Object} - The calculated icon offset with properties `.x` and `.y`.
*/
calculateIconOffset(pIconSize = { width: 32, height: 32 }, pAnchor = { x: 0.5, y: 0.5 }, pScale= { x: 1, y: 1}) {
const scaledSize = {
x: pIconSize.width * pScale.x,
y: pIconSize.height * pScale.y,
};
const offset = {
x: pAnchor.x * (scaledSize.x - pIconSize.width),
y: pAnchor.y * (scaledSize.y - pIconSize.height),
};
return {
x: offset.x,
y: offset.y,
};
}
}
export const Utils = new UtilsSingleton();