// SPDX-License-Identifier: GPL-3.0-only
/*
---------------------------------------------------------
General Purpose Utility Functions
---------------------------------------------------------
*/
export var padString = function (str, pad) {
var str = "" + str,
return_str = str;
if (str.length < pad.length) {
return_str = pad.substr(0, pad.length - str.length) + str;
}
///logMsg(pad + " || " + str + " --> " + return_str);
return return_str;
};
export var removeChildFromParent = function (el) {
if (el?.parentNode) {
el.parentNode.removeChild(el);
}
};
export var isEmpty = function (str) {
return !str || str.length === 0;
};
/*
This function has an unusual structure whereby if window.CustomEvent
already exists it dispatches the event then returns. This is because
wrapping the polyfill `function CustomEvent` definition inside an if/else
block is disallowed by `use: strict`, causing an error in Safari
*/
export var manualEvent = function (el, eventName, detail_ob) {
if (typeof window.CustomEvent === "function") {
el.dispatchEvent(
new CustomEvent(eventName, {
bubbles: true,
detail: detail_ob,
})
);
return;
}
// polyfill for IE11 etc
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent("CustomEvent");
evt.initCustomEvent(
event,
params.bubbles,
params.cancelable,
params.detail
);
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
el.dispatchEvent(
new CustomEvent(eventName, {
bubbles: true,
detail: detail_ob,
})
);
};
export var pointIsInRect = function (point, rect) {
if (
point.x >= rect.left &&
point.y >= rect.top &&
point.x <= rect.right &&
point.y <= rect.bottom
) {
return true;
} else {
return false;
}
};
/*
---------------------------------------------------------
Graphics
---------------------------------------------------------
*/
export var getStripesGradientStopArray = function (_color_ar, _repetitions) {
var rep,
col,
currentPos = 0,
stop_ar = [],
totalStops = _color_ar.length * _repetitions,
stopSize = 1 / totalStops;
for (rep = 0; rep < _repetitions; rep++) {
for (col = 0; col < _color_ar.length; col++) {
stop_ar.push({
pos: currentPos,
color: _color_ar[col],
});
currentPos += stopSize;
stop_ar.push({
pos: currentPos,
color: _color_ar[col],
});
}
}
return stop_ar;
};
export var scaleImageData = function (imageData, scale, ctx) {
var row,
col,
sourcePixel,
x,
y,
destRow,
destCol,
scaledImageData = ctx.createImageData(
imageData.width * scale,
imageData.height * scale
),
currentLineImageData = ctx.createImageData(scale, 1).data;
for (row = 0; row < imageData.height; row++) {
for (col = 0; col < imageData.width; col++) {
sourcePixel = imageData.data.subarray(
(row * imageData.width + col) * 4,
(row * imageData.width + col) * 4 + 4
);
for (x = 0; x < scale; x++) {
currentLineImageData.set(sourcePixel, x * 4);
}
for (y = 0; y < scale; y++) {
destRow = row * scale + y;
destCol = col * scale;
scaledImageData.data.set(
currentLineImageData,
(destRow * scaledImageData.width + destCol) * 4
);
}
}
}
return scaledImageData;
};
export var getRandomTrianglePointsWithinCircle = function (
_radius,
_numTriangles
) {
// Give each point as degrees and distance from centre
var i,
j,
trianglePoints = 3,
allTriangles_ar = [],
currentTriangle_ar;
for (i = 0; i < _numTriangles; i++) {
currentTriangle_ar = [];
for (j = 0; j < trianglePoints; j++) {
currentTriangle_ar.push({
degrees: randomIntBetween(0, 359),
distance: randomFloatBetween(0, _radius),
});
}
allTriangles_ar.push(currentTriangle_ar);
}
return allTriangles_ar;
};
export var getOffscreenOrNormalCanvas = function () {
return "OffscreenCanvas" in window
? document.createElement("canvas").transferControlToOffscreen()
: document.createElement("canvas");
};
/*
---------------------------------------------------------
Trigonometry / Pythagoras
---------------------------------------------------------
*/
export var circleAreaFromRadius = function (_radius) {
return Math.PI * _radius * _radius;
};
export var vectorToDegrees = function (vector) {
var radians = Math.atan2(vector.y, vector.x),
degrees = radiansToDegrees(radians);
return modulo(Math.round(degrees), 360);
};
export var degreesToVector = function (degrees) {
var radians = degreesToRadians(degrees);
return {
x: Math.cos(radians),
y: Math.sin(radians),
};
};
export var degreesToRadians = function (degrees) {
return degrees * (Math.PI / 180);
};
export var radiansToDegrees = function (radians) {
return radians * (180 / Math.PI);
};
export var getDistanceBetweenPoints = function (p1, p2) {
var a = Math.abs(p1.x - p2.x),
b = Math.abs(p1.y - p2.y);
return Math.sqrt(a * a + b * b);
};
export var getSquaredDistanceBetweenPoints = function (p1, p2) {
var a = Math.abs(p1.x - p2.x),
b = Math.abs(p1.y - p2.y);
return a * a + b * b;
};
/*
---------------------------------------------------------
Vectors
---------------------------------------------------------
*/
export var vectorRotateByDegrees = function (vector, degrees) {
var currentDegrees = vectorToDegrees(vector);
return degreesToVector(currentDegrees + degrees);
};
export var vectorGetDotProduct = function (v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
};
export var vectorAdd = function (v1, v2) {
return {
x: v1.x + v2.x,
y: v1.y + v2.y,
};
};
export var vectorSubtract = function (v1, v2) {
return {
x: v1.x - v2.x,
y: v1.y - v2.y,
};
};
export var vectorMultiply = function (v1, v2) {
return {
x: v1.x * v2.x,
y: v1.y * v2.y,
};
};
export var vectorScalarMultiply = function (vector, scalar) {
return {
x: vector.x * scalar,
y: vector.y * scalar,
};
};
export var vectorGetUnitNormal = function (vector) {
return {
x: (-1 * vector.y) / Math.sqrt(vector.x * vector.x + vector.y * vector.y),
y: vector.x / Math.sqrt(vector.x * vector.x + vector.y * vector.y),
};
};
export const vectorGetMagnitude = function (vector) {
return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
};
/*
---------------------------------------------------------
Other number stuff
---------------------------------------------------------
*/
export var modulo = function (n, mod) {
return ((n % mod) + mod) % mod;
};
export var roundToPlaces = function (number, places) {
return Number(Math.round(number + "e" + places) + "e-" + places);
};
export var randomIntBetween = function (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export var randomFloatBetween = function (min, max) {
return Math.random() * (max - min) + min;
};
export var getRandomItemFromArray = function (ar) {
return ar[Math.floor(Math.random() * ar.length)];
};
export var getNearestEvenNumber = function (num) {
return 2 * Math.round(num / 2);
};
export var isEvenNumber = function (num) {
return num / 2 === Math.floor(num / 2);
};
/*
---------------------------------------------------------
Colours
---------------------------------------------------------
*/
export const toHex = (x) => {
const hex = Math.round(x).toString(16);
//logMsg('x: ' + x);
//logMsg('hex: ' + hex);
return hex.length === 1 ? "0" + hex : hex;
};
export var rgbToHex = function (r, g, b) {
return "#" + toHex(r) + toHex(g) + toHex(b);
};
export var hexOpacityToRGBA = function (hexColor, opacity) {
var r, g, b;
// skip first character '#'
r = parseInt(hexColor.substring(1, 3), 16);
g = parseInt(hexColor.substring(3, 5), 16);
b = parseInt(hexColor.substring(5, 7), 16);
return "rgba(" + r + ", " + g + ", " + b + ", " + opacity + ")";
};
export var hexToRGB_ar = function (hex) {
var bigint;
// trim leading hash
if (hex.substr(0, 1) === "#") {
hex = hex.substr(1);
}
bigint = parseInt(hex, 16);
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
};
export var getFadeStepBetweenRGBColorsArray = function (color1, color2, steps) {
var rgb1_ar = rgbToRGB_ar(color1),
rgb2_ar = rgbToRGB_ar(color2);
//__("steps: " + steps);
//__("Number(rgb2_ar[0]): " + Number(rgb2_ar[0]));
//__("Number(rgb1_ar[0]): " + Number(rgb1_ar[0]));
// __("Number(rgb2_ar[0]) - Number(rgb1_ar[0]) / steps: " + (( Number(rgb2_ar[0]) - Number(rgb1_ar[0])) / steps ));
return [
(Number(rgb2_ar[0]) - Number(rgb1_ar[0])) / steps,
(Number(rgb2_ar[1]) - Number(rgb1_ar[1])) / steps,
(Number(rgb2_ar[2]) - Number(rgb1_ar[2])) / steps,
];
};
export var addFadeStepToRGB = function (rgb, rgbStep_ar) {
var rgb_ar = rgbToRGB_ar(rgb),
returnRgb_ar = [
Number(rgb_ar[0]) + Number(rgbStep_ar[0]),
Number(rgb_ar[1]) + Number(rgbStep_ar[1]),
Number(rgb_ar[2]) + Number(rgbStep_ar[2]),
];
return (
"rgb(" +
returnRgb_ar[0] +
", " +
returnRgb_ar[1] +
", " +
returnRgb_ar[2] +
")"
);
};
export var rgbToRGB_ar = function (rgb) {
return rgb.replace(/[^\d,.]/g, "").split(",");
};
export var getBrightnessFromRGBAr = function (ar) {
//return ((299 * ar[0]) + (587 * ar[1]) + (114 * ar[2])) / 1000;
return (ar[0] + ar[0] + ar[2] + ar[1] + ar[1] + ar[1]) / 6;
};
export var hslToHex = function (h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = (x) => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
export var getRandomHSLColorArray = function (tone) {
var saturation,
lightness,
hue = Math.floor(Math.random() * 360);
if (tone === undefined) {
saturation = Math.floor(Math.random() * 100);
lightness = Math.floor(Math.random() * 100);
} else if (tone.toUpperCase() === "LIGHT") {
saturation = Math.floor(Math.random() * 100);
lightness = 80 + Math.floor(Math.random() * 20);
} else if (tone.toUpperCase() === "DARK") {
saturation = 30 + Math.floor(Math.random() * 40);
lightness = Math.floor(Math.random() * 50);
}
return [hue, saturation, lightness];
};
export var getRandomHexColor = function (tone) {
var hsl_ar = getRandomHSLColorArray(tone);
return hslToHex.apply(null, hsl_ar);
};
export var getRandomContrastingHexColor = function (hexColor, minContrast) {
var brightness1,
brightness2,
c1RGB_ar,
hexContrasting,
c2RGB_ar = hexToRGB_ar(hexColor),
brightness2 = getBrightnessFromRGBAr(c2RGB_ar);
do {
hexContrasting = getRandomHexColor();
c1RGB_ar = hexToRGB_ar(hexContrasting);
brightness1 = getBrightnessFromRGBAr(c1RGB_ar);
//logMsg("brightness1: " + brightness1);
//logMsg((brightness1 + 0.05) / brightness2 + 0.05);
} while (Math.abs(brightness1 - brightness2) < minContrast);
//logMsg("hexContrasting: " + hexContrasting);
return hexContrasting;
};
/*
---------------------------------------------------------
Hash/URL parameter handling
---------------------------------------------------------
*/
/**
* @function importHashParamsTo
*
* @description
* #### Process the URL `location.hash` to import variables into the app
*
* @param { Object } _ob Info about how to process the query string
* @param { Object } _ob.recipient_ob Object which will receive the parameters
* @param { String[] } [ _ob.filter_ar ] List of parameters to be accepted (any others are ignored)
*/
export const importHashParamsTo = function (_ob) {
const paramPairs = window.location.hash.slice(1).split("&");
// Iterate the search parameters
for (const pair of paramPairs) {
const pair_ar = pair.split("=");
let paramName = pair_ar[0];
let paramValue = pair_ar[1];
if (_ob.forceLowerCase) {
if (paramName) {
paramName = paramName.toLowerCase();
}
if (paramValue) {
paramValue = paramValue.toLowerCase();
}
}
if (Array.isArray(_ob.filter_ar)) {
// Only accept parameters in the filter list
for (const filterParam of _ob.filter_ar) {
if (paramName === filterParam) {
// Treat an existing but empty param as `true`, so eg `&item` is
// imported as `&item=true`. If you want a truly empty param, it
// should just not be in the hash at all. `setHashParam()` handles
// this automatically.
if (paramValue === undefined) {
paramValue = true;
}
_ob.recipient_ob[paramName] = paramValue;
}
}
}
}
};
/**
* @function setHashParam
*
* @description
* #### Add or remove a parameter in the `location.hash` portion of the URL
*
* @param { String } _fullParams_ar An array of all params accepted by the app
* (same as passed to `importHashParamsTo()`). This will be used to order the
* parameters in the re-written hash.
* @param { String } _name The parameter name
* @param { String } [ _value ] The parameter value, if missing or `undefined` the parameter will be omitted
*
* `_value`s of `true` or `'true'` are treated as a special case, and in the
* re-written hash they will be left empty eg `&item=true` becomes `&item`.
* This is to keep the hash short and friendly, as in this game people may want
* to share URLs or edit them eg to jump to a specific level.
*
*
* To reiterate the above:
*
* Although the **re-written** hash will omit `true`s, when calling this
* function if you want true you must pass `_value` as true. An omitted
* `_value` is taken to mean the parameter should be deleted.
*/
export const setHashParam = function (_fullParams_ar, _name, _value) {
let currentHash = window.delayedLocationHash || window.location.hash,
hash_str = "#";
// Get existing parameters from the URL hash
const existingParams = {};
for (const pair of currentHash.slice(1).split("&")) {
const pair_ar = pair.split("=");
if (!pair_ar[1]) {
pair_ar[1] = true;
}
existingParams[pair_ar[0]] = pair_ar[1];
}
// Add/overwrite or delete the param (_name) we're setting
if (_value !== undefined) {
existingParams[_name] = _value;
} else {
delete existingParams[_name];
}
// Rewrite hash string, filling in the params in the order of `_fullParams_ar`
for (let i = 0; i < _fullParams_ar.length; i++) {
const checkedParamName = _fullParams_ar[i];
// Treat `true`/`'true'` as a special case and just use an empty param
if (existingParams[checkedParamName]) {
if (
existingParams[checkedParamName] === true ||
existingParams[checkedParamName] === "true"
) {
hash_str += checkedParamName + "&";
} else {
hash_str +=
checkedParamName + "=" + existingParams[checkedParamName] + "&";
}
}
}
// Remove trailing ampersand
hash_str = hash_str.replace(/&$/, "");
// Sometimes this function might be called several times consecutively, the intention being to instantly update the hash in several steps. This pollutes the history as every change to `window.location.hash` updates live.
// Here we use a timeout to prevent this. Cache the changes to a global variable, check that first when reading the hash. Make sure the variable is deleted after the update so as not to pollute global scope.
// Use a zero timeout, meaning the callback will happen as soon as the rest of the current event loop has finished, with no added delay.
window.delayedLocationHash = hash_str;
clearTimeout(window.locationHashChangeTimeout);
window.locationHashChangeTimeout = window.setTimeout(function () {
window.location.hash = hash_str;
window.delayedLocationHash = undefined;
}, 0);
};
/*
---------------------------------------------------------
Logging
---------------------------------------------------------
*/
/**
* @function __
*
* @description
* #### Logging function. **To be used everywhere instead of `console.log()`**.
*
* The name "__" is chosen to be short and clean. When visually scanning code it looks
* a bit like indentation so can help differentiate from normal non-logging lines of code.
*
* With this function logging can be enabled/disabled via a query string parameter in the URL hash:
* - `?debug=true` displays console output
* - `?debug=overlay` displays console output and also displays output in a floating element
* (useful for logging to eg mobile browsers when we can't access developer tools easily)
* - If `?debug` is missing or not set to any of the above values, logging is disabled
* @param { String } _msg Message to be logged to console and/or printed to screen
* @param { String } [ _format ] CSS style to apply to message
*/
export const __ = function (_msg, _format) {
if (window.PipeDream.hashParams && window.PipeDream.hashParams.debug) {
// `debug` param comes from the query string, so is a string, **not** a boolean
if (
window.PipeDream.hashParams.debug === true ||
window.PipeDream.hashParams.debug === "true" ||
window.PipeDream.hashParams.debug === "overlay"
) {
if (_format) {
console.log("%c" + _msg, _format);
} else {
console.log(_msg);
}
}
if (window.PipeDream.hashParams.debug === "overlay") {
// Create debug element in DOM if it doesn't already exist
if (!window.debug_el) {
window.debug_el = document.createElement("div");
window.debug_el.id = "debug";
document.body.insertBefore(window.debug_el, document.body.firstChild);
}
// Create element and style it if necessary
const el = document.createElement("p");
el.innerText = _msg;
if (_format) {
el.style = _format;
}
// Add element to debug area and scroll to the latest item
window.debug_el.appendChild(el);
window.debug_el.scrollTop = window.debug_el.scrollHeight;
}
}
};