2023-09-24 18:39:52 +02:00
|
|
|
/////////////////////////////////////
|
|
|
|
|
|
|
|
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') {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-09-24 21:36:18 +02:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2023-10-07 12:21:36 +02:00
|
|
|
// 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'); });
|
2023-10-02 10:05:20 +02:00
|
|
|
const sequencialPromises = async (iterable, callback = false) => {
|
|
|
|
for (const x of iterable) {
|
|
|
|
await x();
|
|
|
|
}
|
2023-10-07 12:21:36 +02:00
|
|
|
if (typeof callback === 'function') {
|
2023-10-02 10:05:20 +02:00
|
|
|
callback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-10-07 18:01:00 +02:00
|
|
|
// 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};
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
*/
|
|
|
|
//
|
2023-09-24 18:39:52 +02:00
|
|
|
/////////////////////////////////////
|
|
|
|
|
|
|
|
export {
|
|
|
|
getUuid,
|
|
|
|
htmlToElement,
|
|
|
|
downloadFile,
|
|
|
|
uploadFile,
|
|
|
|
makeDraggable,
|
|
|
|
getBaseName,
|
|
|
|
hashFromString,
|
|
|
|
verifyVariableTimeProject,
|
|
|
|
makeEven,
|
|
|
|
mix,
|
|
|
|
getMix,
|
|
|
|
mixObject,
|
|
|
|
clone,
|
|
|
|
getParents,
|
|
|
|
arraysEqual,
|
|
|
|
mapValue,
|
2023-09-24 21:48:53 +02:00
|
|
|
isMobile,
|
2023-10-02 10:05:20 +02:00
|
|
|
sequencialPromises,
|
2023-10-07 18:01:00 +02:00
|
|
|
toCssClass,
|
|
|
|
renameProperty,
|
|
|
|
flattenObject,
|
|
|
|
deFlattenObject,
|
2023-09-24 18:39:52 +02:00
|
|
|
}
|