///////////////////////////////////// const UUID = function() { let allowedIdChars = "0123456789abcdef"; // fallback in case we cannot crypto const notUniqueId = (t = 16) => { let out = ""; for (let i = 0; i < t; i++) { out += allowedIdChars[(Math.random() * allowedIdChars.length + Math.floor(performance.now())) % allowedIdChars.length]; } return out; } // from https://github.com/ai/nanoid/blob/main/nanoid.js const uniqueId = (t = 16) => { const indices = crypto.getRandomValues(new Uint8Array(t)); let out = ""; for (var i = 0; i < t; i++) { out += allowedIdChars[indices[i] % allowedIdChars.length]; } return out; } this.getUuid = () => { return typeof crypto === 'object' && typeof crypto.getRandomValues === 'function' ? uniqueId() : notUniqueId(); } } const uuid = new UUID(); const getUuid = () => { return uuid.getUuid(); } const makeEven = (n) => { const nr = Math.round(n); return nr - nr % 2; } const getMix = (before_s, after_s, time_s, clamp = true) => { const diff = after_s - before_s; const travel = time_s - before_s; if (diff === 0 || travel === 0) { return 0; } else if (clamp) { return Math.min(Math.max(travel / diff)); } else { return travel / diff; } } const mix = (a, b, m, t) => { if (Math.abs(a - b) < t) { return b; } else { return a * (1.0 - m) + b * m; } }; const mixObject = (a, b, m) => { const out = JSON.parse(JSON.stringify(a)); const a_keys = Object.keys(a); const b_keys = Object.keys(b); let keys = [...new Set([...a_keys, ...b_keys])]; keys.forEach((key) => { if (!a.hasOwnProperty(key)) { out[key] = b[key]; } else if (!b.hasOwnProperty(key)) { out[key] = a[key]; } else { if (typeof a[key] === 'object') { out[key] = mixObject(a[key], b[key], m); } else if (typeof a[key] === 'number') { out[key] = a[key] * (1.0 - m) + b[key] * m; } else { out[key] = b[key]; } } }); return out; }; ///////////////////////////////////// const htmlToElement = (html) => { var template = document.createElement('template'); html = html.trim(); // Never return a text node of whitespace as the result template.innerHTML = html; return template.content.firstChild; } ///////////////////////////////////// // download(textData, 'lol.txt', 'text/plain'); // download(jsonData, 'lol.json', 'application/json'); function downloadFile(content, fileName, contentType) { var a = document.createElement("a"); var file = new Blob([content], { type: contentType }); a.href = URL.createObjectURL(file); a.download = fileName; a.click(); } function uploadFile(expectedType = 'application/json') { return new Promise((resolve, reject) => { var input = document.createElement('input'); input.type = 'file'; input.addEventListener('change', () => { let json; let files = input.files; if (files.length == 0) return; const file = files[0]; console.log('file', file); let reader = new FileReader(); if (expectedType === 'application/zip' || file.type === 'application/zip' || file.type.indexOf('audio') === 0) { reader.onload = (e) => { const f = e.target.result; console.log(e, file.name, file.size, file.type, f); resolve({ name: file.name, size: file.size, type: file.type, arrayBuffer: f, }); }; reader.onerror = (e) => reject(e.target.error.name); reader.readAsArrayBuffer(file); } else if (expectedType === 'application/json') { reader.onload = (e) => { console.log(e); const f = e.target.result; // This is a regular expression to identify carriage // Returns and line breaks //const lines = file.split(/\r\n|\n/); if (file.type === expectedType) { try { json = JSON.parse(f); resolve(json); } catch (e) { reject("Caught: " + e.message) } } }; reader.onerror = (e) => reject(e.target.error.name); reader.readAsText(file); } else if (expectedType.indexOf('font') >= 0) { console.log('expect font'); reader.onload = (e) => { console.log(e); const f = e.target.result; if (file.type.indexOf('font') >= 0) { console.log('is font'); //var uint8View = new Uint8Array(f); //console.log('trying to save the font file, file, uint8View', file, uint8View); //FS.createDataFile(config.fs.idbfsFontDir, file.name, uint8View, true, true); resolve({ name: file.name, size: file.size, type: file.type, arrayBuffer: f }); } else { const extension = file.name.split('.').reverse().shift() const fileType = `font/${extension}`; if(confirm(`${file.name} has type ${file.type} instead of the expected ${fileType}. are you sure this is a font?`)) { const outputFile = { isFont: true, name: file.name, size: file.size, type: file.type, arrayBuffer: f }; console.log({outputFile}); resolve(outputFile); } else { reject('not a font'); } } }; reader.onerror = (e) => reject(e.target.error.name); reader.readAsArrayBuffer(file); } else { alert(`unknown filetype ${file.type}, what are you uploading?`); resolve(false); } }); input.click(); //var a = document.createElement('a'); //a.onclick = () => { //var e = document.createEvent('MouseEvents'); //e.initEvent('click', true, false); //input.dispatchEvent(e); //}; //a.click(); }); } const makeDraggable = (elmnt, draggedCallback = false) => { var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; if (elmnt.querySelector('.header .move')) { // if present, the header is where you move the DIV from: elmnt.querySelector('.header .move').onmousedown = dragMouseDown; } else { // otherwise, move the DIV from anywhere inside the DIV: elmnt.onmousedown = dragMouseDown; } function dragMouseDown(e) { e = e || window.event; e.preventDefault(); // get the mouse cursor position at startup: pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; // call a function whenever the cursor moves: document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); // calculate the new cursor position: pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; // set the element's new position: elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; } function closeDragElement() { // stop moving when mouse button is released: document.onmouseup = null; document.onmousemove = null; if (typeof draggedCallback === 'function') { draggedCallback(); } } } const cyrb53 = (str, seed = 0) => { let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); return 4294967296 * (2097151 & h2) + (h1 >>> 0); }; const hashFromString = (str, prefix = 'hash') => { return `${prefix}${cyrb53(str)}`; }; function getBaseName(filePath) { return filePath.substring(filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.')) } function verifyVariableTimeProject(vt_project) { const exampleProject = { projectId: 'exampleProject', variable_time_version: VARIABLE_TIME_VERSION, theatre: 'complete theatre saveFile', layerOrder: ['layer-0', 'layer-4', 'layer-2'], }; if (!vt_project) { console.error('Utils::verifyVariableTimeProject::couldNotVerify', 'project equals false', 'this is what we received', vt_project, 'compare to following example', exampleProject); return false; } if (typeof vt_project === 'string') { console.error('Utils::verifyVariableTimeProject::couldNotVerify', 'project is a string, please first parse json ', 'this is what we received', vt_project, 'compare to following example', exampleProject); return false; // do not allow strings //try { //vt_project = JSON.parse(vt_project); //} catch (e) { //console.error('Utils::verifyVariableTimeProject::couldNotVerify', //'project is a string, //but could not parse json ', //'this is what we received', //vt_project, //'compare to following example', //exampleProject); //return false; //} } if (typeof vt_project !== 'object') { console.error('Utils::verifyVariableTimeProject::couldNotVerify', 'project is not an object', 'this is what we received', vt_project, 'compare to following example', exampleProject); return false; } const exampleKeys = Object.keys(exampleProject); for (let i = 0; i < exampleKeys.length; i++) { if (!vt_project.hasOwnProperty(exampleKeys[i])) { console.error('Utils::verifyVariableTimeProject::couldNotVerify', `${exampleKeys[i]} missing`, 'this is what we received', vt_project, 'compare to following example', exampleProject); return false; } } return true; } function clone(a) { return JSON.parse(JSON.stringify(a)); } function getParents(elem, until = null) { const parents = []; let done = false; while (!done) { elem = elem.parentNode; if (elem === until) { done = true; } else if (elem === null) { // until is not a parent return null; } else { parents.push(elem); } } return parents; } function arraysEqual(a, b, sortingMatters = false) { if (a === b) return true; if (a == null || b == null) return false; if (a.length !== b.length) return false; if (!Array.isArray(a) || !Array.isArray(b)) return false; let _a = sortingMatters ? a : a.toSorted(); let _b = sortingMatters ? b : b.toSorted(); for (var i = 0; i < _a.length; ++i) { if (_a[i] !== _b[i]) return false; } return true; } const mapValue = (value, low1, high1, low2, high2, clamp=false) => { const mapped = low2 + (high2 - low2) * (value - low1) / (high1 - low1); return clamp ? Math.min(high2 > low2 ? high2 : low2, Math.max(low2 < high2 ? low2 : high2, mapped)) : mapped; } const isMobile = () => { if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { return true; } else if (navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform)) { return true; } return false; }; // NOTE: // promises must be delivered inside a function like: // // const promises = []; // // promises.push(() => { return new Promise((resolve) => { console.log('lalala ONE'); resolve() }); }); // promises.push(() => { return new Promise((resolve) => { console.log('lalala TWO'); resolve() }); }); // promises.push(() => { return new Promise((resolve) => { console.log('lalala THREE'); resolve() }); }); // // sequencialPromises(promises, () => { console.log('i am done'); }); const sequencialPromises = async (iterable, callback = false) => { for (const x of iterable) { await x(); } if (typeof callback === 'function') { callback(); } }; // NOTE: this is not perfect, // but good enough for our use case // theoretically we would have to get // rid of all special characters const toCssClass = (text, prefix = '') => { return prefix + 'vt_' + text .replaceAll('.','-dot-') .replaceAll('#','-hash-') ; }; const renameProperty = (o, old_key, new_key) => { Object.defineProperty(o, new_key, Object.getOwnPropertyDescriptor(o, old_key)); delete o[old_key]; }; const flattenObject = (o, ignoreKeys = [], pathSymbol = '.') => { if (typeof o !== 'object') { return o; } let doItAgain = false; Object.keys(o).forEach((k) => { if (typeof o[k] === 'object' && ignoreKeys.indexOf(k) < 0) { doItAgain = true; Object.keys(o[k]).forEach((sk) => { const nk = `${k}${pathSymbol}${sk}`; o[nk] = o[k][sk]; }); delete o[k]; } }); if (doItAgain) { flattenObject(o, ignoreKeys, pathSymbol); } }; const deFlattenObject = (o, ignoreKeys = [], pathSymbol = '.') => { Object.keys(o).forEach((k) => { if (ignoreKeys.indexOf(k) < 0) { const ks = k.split(pathSymbol); if (ks.length > 1) { let sos = o[k]; for (let i = ks.length - 1; i > 0; i--) { sos = {[ks[i]]: sos}; } if (typeof o[ks[0]] === 'object') { o[ks[0]] = {...o[ks[0]], ...sos}; } else { o[ks[0]] = sos; } delete o[k]; } } }); }; ///////////////////////////////////// // you can test these functions in // the browser like so: /* import("./web/js/utils.js").then((module) => { const toast = module.toCssClass('lol.lol'); console.log(toast); }); */ // ///////////////////////////////////// export { getUuid, htmlToElement, downloadFile, uploadFile, makeDraggable, getBaseName, hashFromString, verifyVariableTimeProject, makeEven, mix, getMix, mixObject, clone, getParents, arraysEqual, mapValue, isMobile, sequencialPromises, toCssClass, renameProperty, flattenObject, deFlattenObject, }