import {
Vector2,
Vector3,
ImageLoader,
Texture,
VideoTexture,
CanvasTexture,
LinearFilter,
AlphaFormat,
RedFormat,
RGBAFormat,
RGFormat,
RGBFormat,
DepthFormat,
MeshBasicMaterial
} from "three";
/*LinearSRGBColorSpace,
SRGBColorSpace,
CubeUVRefractionMapping,*/
/**
* @alias pxlUtils
* @class
* @description Utility class providing various helper functions for pxlNav.
* @property {number} curMS - Read-Only; Current time in milliseconds since the last frame update.
*/
export class Utils{
/**
* Create a Utils instance.
* @param {string} [assetRoot="images/assets/"] - Root path for assets.
* @param {boolean} [mobile] - Whether running on a mobile device.
*/
constructor(assetRoot="images/assets/", mobile ){
this.assetRoot=assetRoot;
this.mobile=mobile;
this.pxlTimer=null;
this.pxlEnums=null;
this.verboseLoading=false;
this.texLoader=new ImageLoader();
this.textLoaderArray=[];
// Texture formats, use as needed
// ImageLoader's defauls; images load as RGBAFormat by default, and JPG as RGBAFormat
this.channelFormats=[ AlphaFormat, RedFormat, RGFormat, RGBFormat, RGBAFormat, DepthFormat ];
}
get curMS(){
return this.pxlTimer.curMS;
}
setDependencies( pxlNav ){
this.pxlTimer=pxlNav.pxlTimer;
this.pxlEnums=pxlNav.pxlEnums;
}
/**
* @method
* @memberof pxlUtils
* @description Update the browser URL using history API.
* @param {string} url - The new URL.
* @param {Object} [state={}] - State object for history.
* @param {string} [title=""] - Title for the history entry.
*/
updateUrl(url,state={},title=""){
if (window.history.replaceState) {
window.history.replaceState(state, title, url);
}else{
window.history.pushState(state, title, url);
}
}
/**
* @method
* @memberof pxlUtils
* @description Copy the current room URL to the clipboard.
* @returns {boolean|string} Status of the copy operation.
*/
copyRoomUrl(){
let url=window.location;
let tmpFocus=document.activeElement;
let tmpText = document.createElement("textarea");
tmpText.value = url;
document.body.appendChild(tmpText);
tmpText.focus();
tmpText.select();
let status=false;
try{
let callback = document.execCommand('copy');
status = callback ? 'successful' : 'unsuccessful';
}catch(err){}
document.body.removeChild(tmpText);
tmpFocus.focus();
return status;
}
/**
* @method
* @memberof pxlUtils
* @description Check if a value is an integer.
* @param {number} val - The value to check.
* @returns {boolean} True if integer, false otherwise.
*/
checkInt(val){
return (val%1)===0;
}
/**
* @method
* @memberof pxlUtils
* @description Convert degrees to radians.
* @param {number} deg - Degrees.
* @returns {number} Radians.
*/
degToRad(deg){
return deg*(Math.PI/180);
}
/**
* @method
* @memberof pxlUtils
* @description Round a number to the nearest hundredths (2 decimal places).
* @param {number} val - Value to round.
* @returns {number} Rounded value.
*/
toHundreths(val){ // int(val*100)*.01 returns an erronious float on semi ussual basis...
if(!val) return 0;
if( Number.isInteger(val) ){
return val;
}else{
let sp=(val+"").split(".");
let ret=parseFloat(sp[0]+"."+sp[1].substr(0,2));
return ret;
}
}
/**
* @method
* @memberof pxlUtils
* @description Round a number to the nearest tenths (1 decimal place).
* @param {number} val - Value to round.
* @returns {number} Rounded value.
*/
toTenths(val){ // int(val*100)*.01 returns an erronious float on semi ussual basis...
if(!val) return 0;
if( Number.isInteger(val) ){
return val;
}else{
let sp=(val+"").split(".");
let ret=parseFloat(sp[0]+"."+sp[1].substr(0,1));
return ret;
}
}
/**
* @method
* @memberof pxlUtils
* @description Get the current date and time as an array.
* @returns {Array<string>} [date, time] in "YYYY-MM-DD" and "HH:MM:SS" format.
*/
getDateTime(){
let d=new Date();
let date=(d.getFullYear()+"").padStart(2,'0')+"-"+((d.getMonth()+1)+"").padStart(2,'0')+"-"+(d.getDate()+"").padStart(2,'0');
let time=(d.getHours()+"").padStart(2,'0')+":"+(d.getMinutes()+"").padStart(2,'0')+":"+(d.getSeconds()+"").padStart(2,'0');
return [date, time];
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Strictly clean a string from HTML and non-alphanumeric characters.
* @param {string} messageString - String to clean.
* @returns {string} Cleaned string.
*/
cleanStrict( messageString ){
let strip=document.createElement( "div" );
strip.innerHTML=messageString;
strip=strip.innerText;
let matcher=strip.match(/([a-zA-Z0-9])\w+/g);
if(matcher){
strip=matcher.join(" ");
}
return strip;
}
/**
* @method
* @memberof pxlUtils
* @description Basic clean of a string, allowing some symbols.
* @param {string} messageString - String to clean.
* @returns {string} Cleaned string.
*/
cleanBasic( messageString ){
let strip=document.createElement( "div" );
strip.innerHTML=messageString;
strip=strip.innerText;
let matcher=strip.match(/([a-zA-Z0-9\s\w-+()[\]])+/g);
if(matcher){
strip=matcher.join("");
}
return strip;
}
/**
* @method
* @memberof pxlUtils
* @description Remove HTML from a string.
* @param {string} messageString - String to clean.
* @returns {string} Cleaned string.
*/
cleanString( messageString ){
let strip=document.createElement( "div" );
strip.innerHTML=messageString;
strip=strip.innerText;
return strip;
}
// Round to nearest
/**
* @method
* @memberof pxlUtils
* @description Round a value to a fixed precision and return as string.
* @param {number} val - Value to round.
* @param {number} [precision=2] - Number of decimal places.
* @returns {string} Rounded value as string.
*/
toNearestStr( val, precision=2 ){
let retVal = val.toFixed(precision);
return retVal;
}
// Round array elements to nearest
/**
* @method
* @memberof pxlUtils
* @description Round each element of an array to a fixed precision and return as array of strings.
* @param {Array<number>} arr - Array of numbers.
* @param {number} [precision=2] - Number of decimal places.
* @returns {Array<string>} Array of rounded strings.
*/
arrayToStr( arr, precision=2 ){
let retArr = [];
arr.forEach( (val)=>{
retArr.push( this.toNearestStr(val, precision) );
});
return retArr;
}
// Flatten array to joined string
/**
* @method
* @memberof pxlUtils
* @description Flatten an array to a joined string with specified precision and delimiter.
* @param {Array<number>} arr - Array of numbers.
* @param {number} [precision=2] - Number of decimal places.
* @param {string} [delimiter=","] - Delimiter for joining.
* @returns {string} Joined string.
*/
flattenArrayToStr( arr, precision=2, delimiter="," ){
return this.arrayToStr( arr, precision ).join(delimiter);
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Generate a random float between min and max.
* @param {number} min - Minimum value.
* @param {number} max - Maximum value.
* @returns {number} Random float.
*/
randomFloat(min,max){
return Math.random()*(max-min)+min;
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Convert screen coordinates to normalized device coordinates (NDC).
* @param {number} x - X coordinate.
* @param {number} y - Y coordinate.
* @param {number} width - Screen width.
* @param {number} height - Screen height.
* @returns {Vector2} NDC coordinates.
*/
screenToNDC( x, y, width, height ){
let ndcX = ( x / width ) * 2 - 1;
let ndcY = - ( y / height ) * 2 + 1;
return new Vector2( ndcX, ndcY );
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Convert a color component to hexadecimal string.
* @param {number} c - Color component (0-255).
* @returns {string} Hexadecimal string.
*/
componentToHex(c) {
var hex = c.toString(16);
return hex.padStart(2,'0');
}
/**
* @method
* @memberof pxlUtils
* @description Convert RGB values to a hexadecimal color string.
* @param {number} r - Red (0-255).
* @param {number} g - Green (0-255).
* @param {number} b - Blue (0-255).
* @returns {string} Hex color string.
*/
rgbToHex(r, g, b) {
return "#" + this.componentToHex(Math.min(255, Math.max(0,Math.round(r)))) + this.componentToHex(Math.min(255, Math.max(0,Math.round(g)))) + this.componentToHex(Math.min(255, Math.max(0,Math.round(b))));
}
/**
* @method
* @memberof pxlUtils
* @description Convert a hex color string to RGB array.
* @param {string} hex - Hex color string.
* @returns {Array<number>} [r, g, b] array.
*/
hexToRgb( hex ) {
let buffer=hex[0];
if(buffer==="#"){
hex=hex.substr( 1, 6 );
}else{
hex=hex.substr( 0, 6 );
}
let r,g,b;
if(hex.length===3){
r=hex[0]+hex[0];
g=hex[1]+hex[1];
b=hex[2]+hex[2];
}else{
r=hex[0]+hex[1];
g=hex[2]+hex[3];
b=hex[4]+hex[5];
}
r=parseInt(r,16);
g=parseInt(g,16);
b=parseInt(b,16);
return [r,g,b];
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Generate an RGB color from a string.
* @param {string} string - Input string.
* @param {number|null} [boost=null] - Optional boost factor.
* @param {boolean} [zoFit=false] - If true, normalize to [0,1].
* @returns {Array<number>} RGB array.
*/
stringToRgb( string, boost=null, zoFit=false ){
let stringColor=[255,0,0];
if( string ){
let sLength=string.length;
let charCode="";
for(let x=0; x<sLength; ++x){
charCode+=string[ (sLength-1-x) ].charCodeAt(0).toString(16);
}
let ccLength=charCode.length;
if(ccLength>6){
let offset=1;
if(string==="tussin"){
offset=0;
}else if(string==="fexofenadine"){
offset=-1;
}
let reader=Math.max(0,parseInt((ccLength-6)/2+offset));
charCode=charCode.substr(reader,6);
}
stringColor=this.hexToRgb( charCode );
}
if( boost != null){
let maxCd=Math.max(...stringColor);
let minCd=Math.min(...stringColor);
let boostCd=maxCd*boost;
stringColor[0]= parseInt( Math.min(255, ((stringColor[0]-minCd) / (maxCd-minCd))*255+boostCd) );
stringColor[1]= parseInt( Math.min(255, ((stringColor[1]-minCd) / (maxCd-minCd))*255+boostCd) );
stringColor[2]= parseInt( Math.min(255, ((stringColor[2]-minCd) / (maxCd-minCd))*255+boostCd) );
/*
stringColor[0]= Math.max(0, Math.min(255, (stringColor[0]-100)*boost+100));
stringColor[1]= Math.max(0, Math.min(255, (stringColor[1]-100)*boost+100));
stringColor[2]= Math.max(0, Math.min(255, (stringColor[2]-100)*boost+100));
*/
}
if( zoFit===true ){
stringColor[0]=stringColor[0]/255;
stringColor[1]=stringColor[1]/255;
stringColor[2]=stringColor[2]/255;
}
return stringColor;
}
// -- -- -- //
// Convert Color/Vector3 to sRGB Color Space
/**
* @method
* @memberof pxlUtils
* @description Convert a color or Vector3 to sRGB color space.
* @param {Object} color - Color or Vector3 object.
* @returns {Object} Converted color.
*/
colorTosRGB( color ){
// Check if the colorue is a color object
if( typeof color === "object" ){
// Color Object
if( color.hasOwnProperty && color.hasOwnProperty("r") ){
color.r = this.tosRGB(color.r);
color.g = this.tosRGB(color.g);
color.b = this.tosRGB(color.b);
}
if( color.hasOwnProperty && color.hasOwnProperty("x") ){
color.x = this.tosRGB(color.x);
color.y = this.tosRGB(color.y);
color.z = this.tosRGB(color.z);
}
return color;
}
return color;
}
// Convert Linear to sRGB
/**
* @method
* @memberof pxlUtils
* @description Convert a linear value to sRGB.
* @param {number} val - Linear value.
* @returns {number} sRGB value.
*/
tosRGB( val ){
// Convert the value per channel
if( val <= 0.0031308 ){
val *= 12.92;
}else{
val = 1.055 * Math.pow(val, this.oneTwoPFour) - 0.055;
}
return val;
}
// -- -- --
// Convert Color/Vector3 to Linear Color Space
/**
* @method
* @memberof pxlUtils
* @description Convert a color or Vector3 to linear color space.
* @param {Object} color - Color or Vector3 object.
* @returns {Object} Converted color.
*/
colorToLinear( color ){
// Check if the colorue is a color object
if( typeof color === "object" ){
// Color Object
if( color.hasOwnProperty && color.hasOwnProperty("r") ){
color.r = this.toLinear(color.r);
color.g = this.toLinear(color.g);
color.b = this.toLinear(color.b);
}
if( color.hasOwnProperty && color.hasOwnProperty("x") ){
color.x = this.toLinear(color.x);
color.y = this.toLinear(color.y);
color.z = this.toLinear(color.z);
}
return color;
}
return color;
}
// Convert sRGB to Linear
/**
* @method
* @memberof pxlUtils
* @description Convert an sRGB value to linear.
* @param {number} val - sRGB value.
* @returns {number} Linear value.
*/
toLinear( val ){
if( val <= 0.04045 ){
val *= this.twelvePNineTwoDiv;
}else{
val = Math.pow((val + 0.055) * this.onePOFiveFiveDiv, 2.4);
}
return val;
}
// -- -- --
/**
* @method
* @memberof pxlUtils
* @description Apply gamma correction to a color or Vector3.
* @param {Object} color - Color or Vector3 object.
* @param {number|string} [gammaIn="2.2"] - Input gamma.
* @param {number|string} [gammaOut="1.8"] - Output gamma.
* @returns {Object} Gamma-corrected color.
*/
gammaCorrectColor( color, gammaIn="2.2", gammaOut="1.8" ){
// Check if the colorue is a color object
if( typeof color === "object" ){
// Color Object
if( color.hasOwnProperty && color.hasOwnProperty("r") ){
color.r = this.gammaCorrect(color.r, gammaIn, gammaOut);
color.g = this.gammaCorrect(color.g, gammaIn, gammaOut);
color.b = this.gammaCorrect(color.b, gammaIn, gammaOut);
}
if( color.hasOwnProperty && color.hasOwnProperty("x") ){
color.x = this.gammaCorrect(color.x, gammaIn, gammaOut);
color.y = this.gammaCorrect(color.y, gammaIn, gammaOut);
color.z = this.gammaCorrect(color.z, gammaIn, gammaOut);
}
return color;
}
return color;
}
/**
* @method
* @memberof pxlUtils
* @description Apply gamma correction to a single channel.
* @param {number} channel - Channel value.
* @param {number|string} [gammaIn="2.2"] - Input gamma.
* @param {number|string} [gammaOut="1.8"] - Output gamma.
* @returns {number} Gamma-corrected channel.
*/
gammaCorrection( channel, gammaIn="2.2", gammaOut="1.8" ){
// Linearize the color
let linearChannel = Math.pow( channel, gammaIn );
// Apply the gamma correction
let channelShift = Math.pow( linearChannel, 1.0 / gammaOut );
return channelShift;
}
// -- -- --
// TODO : Prep & re-implement THREE.GammaFactor -> pxlNav.pxlDevice.GammaFactor
// TODO : pxlDevice OS detect needs to be implement for color conversion between known OS color spaces
/**
* @method
* @memberof pxlUtils
* @description Convert a color between color spaces.
* @param {Object} color - Color object.
* @param {number} [space=this.pxlEnums.COLOR_SHIFT.KEEP] - Target color space.
* @returns {Object} Converted color.
*/
convertColor( color, space=this.pxlEnums.COLOR_SHIFT.KEEP ){
if( space === this.pxlEnums.COLOR_SHIFT.KEEP ){
return color;
}
// Notes on OS Gamma levels --
// Linear Gamma is 1.0
// Windows default Gamma is 2.2
// Mac, Linux, & Androids default Gamma is 1.8
// Other Unix systems are 2.3-2.5
// Notes on Color Spaces --
// sRGB & Rec.709 are the same
// sRGB is more in line with how the human eye percieves color
// Linear is an uncorrected color space, not data is lost between programs or devices
// Linear Colors should then be converted to sRGB for display,
// But not converted if the color is for Data needs
let retColor = color.clone();
switch (space) {
// Assuming color adjustments have been made with shader math --
case this.pxlEnums.COLOR_SHIFT.sRGB_TO_LINEAR:
retColor = this.colorTosRGB(retColor);
break;
case this.pxlEnums.COLOR_SHIFT.LINEAR_TO_sRGB:
retColor = this.colorToLinear(retColor);
break;
// TODO : These need to be checked, added for completeness, not tested
case this.pxlEnums.COLOR_SHIFT.WINDOWS_TO_UNIX:
retColor = this.gammaCorrectColor(retColor, "2.2", "1.8");
break;
case this.pxlEnums.COLOR_SHIFT.UNIX_TO_WINDOWS:
retColor = this.gammaCorrectColor(retColor, "1.8", "2.2");
break;
case this.pxlEnums.COLOR_SHIFT.LINEAR_TO_WINDOWS:
retColor = this.gammaCorrectColor(retColor, "1.0", "2.2");
break;
case this.pxlEnums.COLOR_SHIFT.WINDOWS_TO_LINEAR:
retColor = this.gammaCorrectColor(retColor, "2.2", "1.0");
break;
case this.pxlEnums.COLOR_SHIFT.LINEAR_TO_UNIX:
retColor = this.gammaCorrectColor(retColor, "1.0", "1.8");
break;
case this.pxlEnums.COLOR_SHIFT.UNIX_TO_LINEAR:
retColor = this.gammaCorrectColor(retColor, "1.8", "1.0");
break;
default:
break;
}
return retColor;
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Randomize the order of elements in an array.
* @param {Array} inputArr - Input array.
* @returns {Array} Randomized array.
*/
randomizeArray(inputArr){
let tmpArr=[...inputArr];
let retArr=[];
while( tmpArr.length > 0){
let rand=tmpArr.length===1 ? 0 : parseInt(Math.random()*21*tmpArr.length)%tmpArr.length;
retArr.push( tmpArr.splice( rand, 1 )[0] );
}
return retArr;
}
/**
* @method
* @memberof pxlUtils
* @description Get a random element from a list.
* @param {Array} list - List of elements.
* @param {number} [seed=1.14] - Optional seed.
* @returns {*} Random element.
*/
getRandom( list, seed=1.14 ){
let randEl= Math.floor( Math.random( seed ) * list.length);
return list[ randEl ];
}
/**
* @method
* @memberof pxlUtils
* @description Apply a transformation list to an object (position, rotation, scale).
* @param {Object} curObj - The object to transform.
* @param {Object} transList - Transformation list with keys "r", "t", "s", and optional "rOrder".
*/
applyTransformList(curObj,transList){
var rotate=transList["r"];
curObj.rotateX(rotate[0]);
curObj.rotateY(rotate[1]);
curObj.rotateZ(rotate[2]);
if(typeof(transList["rOrder"]) !== "undefined" ){
curObj.rotation.order=transList["rOrder"];
}
var pos=transList["t"];
curObj.position.set(pos[0],pos[1],pos[2]);
var scale=transList["s"];
curObj.scale.set(scale[0],scale[1],scale[2]);
curObj.matrixAutoUpdate=false;
curObj.updateMatrix();
}
vec2(x=null,y=null){
return new Vector2(x,y);
}
vec3(x=0,y=0,z=0){
return new Vector3(x,y,z);
}
//this.channelFormats=[ AlphaFormat, RedFormat, RGFormat, RGBFormat, RGBAFormat, DepthFormat ];
/**
* @method
* @memberof pxlUtils
* @description Load a texture from an image path.
* @param {string} imgPath - Image path.
* @param {number|null} [channels=null] - Channel format index.
* @param {Object} [mods={}] - Texture modifications.
* @returns {Texture} Loaded texture.
*/
loadTexture(imgPath,channels=null,mods={}){
// ## Check how textLoaderArray textures are being handled after being disposed
// No path, default to the asset root
if( !imgPath.includes( "/") ){
imgPath = this.assetRoot + imgPath;
}
let texture=null;
if(typeof(this.textLoaderArray[imgPath]) != "undefined"){
texture=this.textLoaderArray[imgPath];
}else{
//var texLoader=new ImageLoader(verboseLoading);
texture=new Texture();
let modKeys = Object.keys(mods);
this.texLoader.load(imgPath,
(tex)=>{
if(channels!=null){
texture.format = this.channelFormats[ channels ];
}
texture.image=tex;
texture.needsUpdate=true;
// Apply texture mods passed from the user
if(modKeys.length>0){
modKeys.forEach((x)=>{
texture[x]=mods[x];
});
}
},
undefined,
(err)=>{
console.error(" TextureLoader :: Error loading texture :: "+imgPath);
console.error(err);
}
);
this.textLoaderArray[imgPath]=texture;
}
return texture;
}
/**
* @method
* @memberof pxlUtils
* @description Create a VideoTexture from a video element.
* @param {HTMLVideoElement} videoObject - Video element.
* @returns {VideoTexture} Video texture.
*/
getVideoTexture( videoObject ){
let videoTexture=new VideoTexture(videoObject);
videoTexture.minFilter=LinearFilter;
videoTexture.magFilter=LinearFilter; // faster, lower samples, NearestFilter
videoTexture.format=RGBFormat;
return videoTexture;
}
/**
* @method
* @memberof pxlUtils
* @description Create a CanvasTexture and MeshBasicMaterial from a canvas.
* @param {HTMLCanvasElement} canvas - Canvas element.
* @returns {{texture: CanvasTexture, material: MeshBasicMaterial}} Texture and material.
*/
getCanvasTexture( canvas ){
const texture = new CanvasTexture(canvas);
const material = new MeshBasicMaterial({
map: texture,
});
return {texture, material};
}
// -- -- -- //
/**
* @method
* @memberof pxlUtils
* @description Duplicate an array a specified number of times.
* @param {Array} val - The array to duplicate.
* @param {number} count - Number of times to duplicate.
* @returns {Array} Duplicated array.
* @example
* // Duplicate an array
* import { pxlNav } from 'pxlNav.js';
* const pxlParticleBase = pxlNav.pxlEffects.pxlParticles.pxlParticle
*
* build(){
* pxlParticleBase.dupeArray( [0.0,0.75], 4 );
* // Output: [0.0,0.75], [0.0,0.75], [0.0,0.75], [0.0,0.75]
* }
*/
dupeArray( val, count ){
return Array.from({length:count}).fill(val);
}
}