src/utils/Utils.js
import * as constants from './Constants';
/**
* Obtains the pixel ratio of the device. Used to scale properly the canvas for high resolution devices.
* @public
* @returns {number} The pixel ratio of the device.
*/
export function getPixelRatio() {
return window.devicePixelRatio || 1;
}
/**
* Makes sure that a HTML id allways has a # at the beginning.
* If the provided id already has a # then it returns the same id, otherwise it is added.
* @public
* @param {string} id Id of an HTML object.
* @returns {string} Id with a # prepended.
*/
export function fixId(id) {
return id[0] === '#' ? id : `#${id}`;
}
/**
* Round a number to a given amount of decimal places.
* @public
* @param {number} value Value to round.
* @param {number} precision Amount of decimal places required.
* @returns {number} Rounded number.
*/
export function round(value, precision) {
const multiplier = Math.pow(10, precision || 0);
return Math.round(value * multiplier) / multiplier;
}
/**
* Constrain a value within a range of values.
* If value > max then value = max;
* If value < min then value = min;
* @public
* @param {number} val Value to clamp within range.
* @param {number} min Minimum acceptable value.
* @param {number} max Maximum acceptable value.
* @returns {number} Constrained value to the given range.
*/
export function clamp(val, min, max) {
if (val > max) return max;
if (val < min) return min;
return val;
}
/**
* Cosine function that can accept angles in radians or degrees.
* @public
* @param {number} val Angle for calculating the cosine.
* @param {number} type Determines if the angle provided is in radians or in degrees. See {@link ANGLE_STYLE}.
* @returns {number} Cosine of the angle.
*/
export function cos(val, type) {
return Math.cos(
type === constants.ANGLE_STYLE.DEG ? val * constants.DEG_TO_RAD : val
);
}
/**
* Sine function that can accept angles in radians or degrees.
* @public
* @param {number} val Angle for calculating the sine.
* @param {number} type Determines if the angle provided is in radians or in degrees. See {@link ANGLE_STYLE}.
* @returns {number} Sine of the angle.
*/
export function sin(val, type) {
return Math.sin(
type === constants.ANGLE_STYLE.DEG ? val * constants.DEG_TO_RAD : val
);
}
/**
* Test if an object is a function.
* @public
* @param {object} f Object to test.
* @returns {boolean} True if the object is a function, false otherwise.
*/
export function isFunction(f) {
return typeof f === 'function';
}
/**
* Test if an object is an object.
* @public
* @param {object} o Object to test.
* @returns {boolean} True if the object is an object, false otherwise.
*/
export function isObject(o) {
return typeof o === 'object';
}
/**
* Test if an object is a string.
* @public
* @param {object} s Object to test.
* @returns {boolean} True if the object is a string, false otherwise.
*/
export function isString(s) {
return typeof s === 'string';
}
/**
* Given a set of two points the squared distance is calculated.
* This is faster than calculating the Euclidean distance between them, since no square root is calculated.
* @public
* @param {number} x0 Initial -x coordinate.
* @param {number} y0 Initial -y coordinate.
* @param {number} x1 Final -x coordinate.
* @param {number} y1 Final -y coordinate.
* @returns {number} The squared distance between points.
*/
export function distSquared(x0, y0, x1, y1) {
return Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2);
}
/**
* Given an angle makes sure that it is in radians.
* @public
* @param {number} angle Angle to convert to radians.
* @param {number} type Determines if the angle provided is in radians or in degrees. See {@link ANGLE_STYLE}.
* @returns {number} Angle in radians.
*/
export function rad(angle, type) {
return type === constants.ANGLE_STYLE.DEG
? angle * constants.DEG_TO_RAD
: angle;
}
/**
* Determines if a point is inside of a box.
* Often used to test is the mouse is over an element.
* @public
* @param {number} x -x coordinate to test.
* @param {number} y -y coordinate to test.
* @param {number} bx -x center coordinate of the box.
* @param {number} by -y center coordinate of the box.
* @param {number} bw Half of the box's width.
* @param {number} bh Half of the box's height.
* @returns {boolean} True if point is within the box, false otherwise.
*/
export function isCoordInside(x, y, bx, by, bw, bh) {
return (
Math.pow(bx - x, 2) < Math.pow(bw, 2) &&
Math.pow(y - by, 2) < Math.pow(bh, 2)
);
}
/**
* Format a number to have a certain number of decimal places and/or fixed places and
* add a unit label. Used to format the strings of labels and sliders.
* If the input value is a {@link SYMBOL} then return the same symbol.
* @public
* @param {number|string} val Input number. Can also be a string or a symbol such as in {@link SYMBOL}.
* @param {string} units Units of the input number.
* @param {number} decPlaces Amount of decimal places to round.
* @param {boolean} fixPlaces Fix the amount of decimal places. If the number doesn't have enough, zeros will be added.
* @returns {string} Formated number.
*/
export function formatValue(val, units, decPlaces, fixPlaces) {
if (val === constants.SYMBOL.BLANK || val === constants.SYMBOL.INF)
return val;
let result =
Number.isNaN(val) && val !== undefined ? 0 : round(val, decPlaces);
if (fixPlaces) result = result.toFixed(decPlaces);
return units === '°' || units === 'º' ? `${result}°` : `${result} ${units}`;
}
/**
* Assign the matching properties from the args object to the obj object.
* This allows for settings to be passed in single line and set on the receiving object.
* Almost all World Element objects accept such settings on their constructors.
* @example
* loadOptions(font, { face: "Helvetica", size: 12 });
* @public
* @param {object} obj Object where the settings will be loaded.
* @param {object} args Object with matching properties from obj.
*/
export function loadOptions(obj, args) {
if (args) {
const keys = Object.keys(args);
for (let i = 0; i < keys.length; i += 1) {
if (Object.prototype.hasOwnProperty.call(obj, keys[i])) {
obj[keys[i]] = args[keys[i]];
}
}
}
}
/**
* Finds the best fitting scale for a given range. Used for simulations where the scale of the data
* changes dramatically.
* @param {number} value Corresponds to the range between the minimum required value to be displayed and the maximum required value to be displayed.
* @param {number} stepAmount Desired amount of steps within the range.
* @returns {number} Magnitude of the step size.
*/
export function calcStepSize(value, stepAmount) {
// Calculate initial guess at step size
const tempStep = value / stepAmount;
// Get the magnitude of the step size
const mag = Math.floor(Math.log(tempStep) / constants.LOG10);
const magPow = Math.pow(10, mag);
// Calculate the most significant digit of the new step size
let magMsd = Math.round(tempStep / magPow + 0.5);
// Promote the MSD to either 1, 2, or 5
if (magMsd > 5.0) {
magMsd = 10.0;
} else if (magMsd > 2.0) {
magMsd = 5.0;
} else if (magMsd > 1) {
magMsd = 2.0;
}
return magMsd * magPow;
}
/**
* Generate a random number following a gaussian distribuition.
* @public
* @param {number} n Amount of iterations.
* @returns {number} Random number that follows a gaussian distribuition.
*/
export function gaussian(n) {
let sum = 0;
for (let i = 0; i < n; i += 1) {
sum += Math.random();
}
return sum / n;
}
/**
* Adds an alpha channel to an rgb color.
* Must have the pattern #AABBCC.
* @public
* @param {string} color Color to add alpha.
* @param {number} alpha Alpha channel added to the color.
*/
export function rgbToRgba(color, alpha) {
const alphaP = alpha > 1 ? 1 : alpha < 0 ? 0 : alpha;
const r = parseInt(color.substr(1, 2), 16);
const g = parseInt(color.substr(3, 2), 16);
const b = parseInt(color.substr(5, 2), 16);
return `rgba(${r}, ${g}, ${b}, ${alphaP})`;
}