2022-11-20 20:03:07 -06:00
|
|
|
/**
|
2022-11-28 16:48:42 -06:00
|
|
|
* Some type definitions before the actual code
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* Represents a simple bounding box
|
|
|
|
*
|
|
|
|
* @typedef BoundingBox
|
|
|
|
* @type {Object}
|
|
|
|
* @property {number} x - Leftmost coordinate of the box
|
|
|
|
* @property {number} y - Topmost coordinate of the box
|
|
|
|
* @property {number} w - The bounding box Width
|
|
|
|
* @property {number} h - The bounding box Height
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A simple implementation of the Observer programming pattern
|
|
|
|
* @template [T=any] Message type
|
2022-11-20 20:03:07 -06:00
|
|
|
*/
|
2022-11-28 09:17:07 -06:00
|
|
|
class Observer {
|
|
|
|
/**
|
|
|
|
* List of handlers
|
2022-11-28 16:48:42 -06:00
|
|
|
* @type {Set<(msg: T) => void | Promise<void>>}
|
2022-11-28 09:17:07 -06:00
|
|
|
*/
|
|
|
|
_handlers = new Set();
|
2022-11-20 20:03:07 -06:00
|
|
|
|
2022-11-28 09:17:07 -06:00
|
|
|
/**
|
|
|
|
* Adds a observer to the events
|
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {(msg: T) => void | Promise<void>} callback The function to run when receiving a message
|
|
|
|
* @returns {(msg:T) => void | Promise<void>} The callback we received
|
2022-11-28 09:17:07 -06:00
|
|
|
*/
|
2022-11-21 20:19:41 -06:00
|
|
|
on(callback) {
|
2022-11-28 09:17:07 -06:00
|
|
|
this._handlers.add(callback);
|
2022-11-21 20:19:41 -06:00
|
|
|
return callback;
|
2022-11-28 09:17:07 -06:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Removes a observer
|
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {(msg: T) => void | Promise<void>} callback The function used to register the callback
|
2022-11-28 09:17:07 -06:00
|
|
|
* @returns {boolean} Whether the handler existed
|
|
|
|
*/
|
2022-11-21 20:19:41 -06:00
|
|
|
clear(callback) {
|
2022-11-28 09:17:07 -06:00
|
|
|
return this._handlers.delete(callback);
|
|
|
|
}
|
|
|
|
/**
|
2022-11-28 16:48:42 -06:00
|
|
|
* Sends a message to all observers
|
2022-11-28 09:17:07 -06:00
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {T} msg The message to send to the observers
|
2022-11-28 09:17:07 -06:00
|
|
|
*/
|
|
|
|
async emit(msg) {
|
2022-11-28 09:25:31 -06:00
|
|
|
return Promise.all(
|
|
|
|
Array.from(this._handlers).map(async (handler) => {
|
2022-11-28 09:17:07 -06:00
|
|
|
try {
|
|
|
|
await handler(msg);
|
|
|
|
} catch (e) {
|
|
|
|
console.warn("Observer failed to run handler");
|
|
|
|
console.warn(e);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-11-21 20:19:41 -06:00
|
|
|
|
|
|
|
/**
|
2022-11-28 09:11:19 -06:00
|
|
|
* Generates a simple UID in the format xxxx-xxxx-...-xxxx, with x being [0-9a-f]
|
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {number} [size] Number of quartets of characters to generate
|
2022-11-28 09:11:19 -06:00
|
|
|
* @returns {string} The new UID
|
2022-11-21 20:19:41 -06:00
|
|
|
*/
|
|
|
|
const guid = (size = 3) => {
|
|
|
|
const s4 = () => {
|
|
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
|
|
.toString(16)
|
|
|
|
.substring(1);
|
|
|
|
};
|
2022-11-26 19:35:16 -06:00
|
|
|
// returns id of format 'aaaa'-'aaaa'-'aaaa' by default
|
2022-11-21 20:19:41 -06:00
|
|
|
let id = "";
|
|
|
|
for (var i = 0; i < size - 1; i++) id += s4() + "-";
|
|
|
|
id += s4();
|
|
|
|
return id;
|
2022-11-20 20:03:07 -06:00
|
|
|
};
|
2022-11-22 16:24:55 -06:00
|
|
|
|
2022-12-12 15:25:32 -06:00
|
|
|
/**
|
|
|
|
* Returns a hash code from a string
|
|
|
|
*
|
|
|
|
* From https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
|
|
|
|
*
|
|
|
|
* @param {String} str The string to hash
|
|
|
|
* @return {Number} A 32bit integer
|
|
|
|
*/
|
|
|
|
const hashCode = (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) ^
|
|
|
|
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
|
|
h2 =
|
|
|
|
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
|
|
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
|
|
|
|
|
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
|
|
};
|
|
|
|
|
2022-11-23 22:53:14 -06:00
|
|
|
/**
|
2022-11-28 09:11:19 -06:00
|
|
|
* Assigns defaults to an option object passed to the function.
|
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @template T Object Type
|
|
|
|
*
|
|
|
|
* @param {T} options Original options object
|
|
|
|
* @param {T} defaults Default values to assign
|
2022-11-23 22:53:14 -06:00
|
|
|
*/
|
|
|
|
function defaultOpt(options, defaults) {
|
|
|
|
Object.keys(defaults).forEach((key) => {
|
|
|
|
if (options[key] === undefined) options[key] = defaults[key];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-28 09:11:19 -06:00
|
|
|
/** Custom error for attempt to set read-only objects */
|
|
|
|
class ProxyReadOnlySetError extends Error {}
|
2022-11-27 06:49:02 -06:00
|
|
|
/**
|
2022-11-28 09:11:19 -06:00
|
|
|
* Makes a given object read-only; throws a ProxyReadOnlySetError exception if modification is attempted
|
|
|
|
*
|
2022-11-28 09:17:07 -06:00
|
|
|
* @template T Object Type
|
|
|
|
*
|
|
|
|
* @param {T} obj Object to be proxied
|
2022-11-28 09:11:19 -06:00
|
|
|
* @param {string} name Name for logging purposes
|
2022-11-28 09:17:07 -06:00
|
|
|
* @param {string[]} exceptions Parameters excepted from this restriction
|
|
|
|
* @returns {T} Proxied object, intercepting write attempts
|
2022-11-27 06:49:02 -06:00
|
|
|
*/
|
2022-11-28 09:17:07 -06:00
|
|
|
function makeReadOnly(obj, name = "read-only object", exceptions = []) {
|
2022-11-27 06:49:02 -06:00
|
|
|
return new Proxy(obj, {
|
|
|
|
set: (obj, prop, value) => {
|
2022-11-28 09:17:07 -06:00
|
|
|
if (!exceptions.some((v) => v === prop))
|
|
|
|
throw new ProxyReadOnlySetError(
|
|
|
|
`Tried setting the '${prop}' property on '${name}'`
|
|
|
|
);
|
|
|
|
obj[prop] = value;
|
2022-11-27 06:49:02 -06:00
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-28 09:11:19 -06:00
|
|
|
/** Custom error for attempt to set write-once objects a second time */
|
|
|
|
class ProxyWriteOnceSetError extends Error {}
|
|
|
|
/**
|
|
|
|
* Makes a given object write-once; Attempts to overwrite an existing prop in the object will throw a ProxyWriteOnceSetError exception
|
|
|
|
*
|
2022-11-28 09:17:07 -06:00
|
|
|
* @template T Object Type
|
|
|
|
* @param {T} obj Object to be proxied
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {string} [name] Name for logging purposes
|
|
|
|
* @param {string[]} [exceptions] Parameters excepted from this restriction
|
2022-11-28 09:17:07 -06:00
|
|
|
* @returns {T} Proxied object, intercepting write attempts
|
2022-11-28 09:11:19 -06:00
|
|
|
*/
|
2022-11-28 09:17:07 -06:00
|
|
|
function makeWriteOnce(obj, name = "write-once object", exceptions = []) {
|
2022-11-27 06:49:02 -06:00
|
|
|
return new Proxy(obj, {
|
|
|
|
set: (obj, prop, value) => {
|
2022-11-28 09:17:07 -06:00
|
|
|
if (obj[prop] !== undefined && !exceptions.some((v) => v === prop))
|
2022-11-27 06:49:02 -06:00
|
|
|
throw new ProxyWriteOnceSetError(
|
|
|
|
`Tried setting the '${prop}' property on '${name}' after it was already set`
|
|
|
|
);
|
|
|
|
obj[prop] = value;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-22 16:24:55 -06:00
|
|
|
/**
|
2022-11-28 09:11:19 -06:00
|
|
|
* Snaps a single value to an infinite grid
|
|
|
|
*
|
|
|
|
* @param {number} i Original value to be snapped
|
2022-12-03 06:53:12 -06:00
|
|
|
* @param {number} [offset=0] Value to offset the grid. Should be in the rande [0, gridSize[
|
|
|
|
* @param {number} [gridSize=64] Size of the grid
|
2022-11-28 09:11:19 -06:00
|
|
|
* @returns an offset, in which [i + offset = (a location snapped to the grid)]
|
2022-11-22 16:24:55 -06:00
|
|
|
*/
|
2022-12-03 06:53:12 -06:00
|
|
|
function snap(i, offset = 0, gridSize = 64) {
|
|
|
|
const modulus = (i - offset) % gridSize;
|
|
|
|
var snapOffset = modulus;
|
|
|
|
|
2022-11-24 09:30:13 -06:00
|
|
|
if (modulus > gridSize / 2) snapOffset = modulus - gridSize;
|
|
|
|
|
2022-11-22 16:24:55 -06:00
|
|
|
if (snapOffset == 0) {
|
|
|
|
return snapOffset;
|
|
|
|
}
|
|
|
|
return -snapOffset;
|
|
|
|
}
|
|
|
|
|
2022-11-28 09:11:19 -06:00
|
|
|
/**
|
|
|
|
* Gets a bounding box centered on a given set of coordinates. Supports grid snapping
|
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {number} cx - x-coordinate of the center of the box
|
|
|
|
* @param {number} cy - y-coordinate of the center of the box
|
|
|
|
* @param {number} w - the width of the box
|
|
|
|
* @param {height} h - the height of the box
|
2022-12-03 06:53:12 -06:00
|
|
|
* @param {?number} gridSnap - The size of the grid to snap to
|
|
|
|
* @param {number} [offset=0] - How much to offset the grid by
|
2022-11-28 16:48:42 -06:00
|
|
|
* @returns {BoundingBox} - A bounding box object centered at (cx, cy)
|
2022-11-28 09:11:19 -06:00
|
|
|
*/
|
2022-12-03 06:53:12 -06:00
|
|
|
function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) {
|
|
|
|
const offs = {x: 0, y: 0};
|
2022-11-22 16:24:55 -06:00
|
|
|
const box = {x: 0, y: 0};
|
|
|
|
|
|
|
|
if (gridSnap) {
|
2022-12-03 06:53:12 -06:00
|
|
|
offs.x = snap(cx, offset, gridSnap);
|
|
|
|
offs.y = snap(cy, offset, gridSnap);
|
2022-11-22 16:24:55 -06:00
|
|
|
}
|
2022-12-07 15:38:23 -06:00
|
|
|
|
|
|
|
box.x = Math.round(offs.x + cx);
|
|
|
|
box.y = Math.round(offs.y + cy);
|
2022-11-22 16:24:55 -06:00
|
|
|
|
|
|
|
return {
|
|
|
|
x: Math.floor(box.x - w / 2),
|
|
|
|
y: Math.floor(box.y - h / 2),
|
2022-12-07 15:38:23 -06:00
|
|
|
w: Math.round(w),
|
|
|
|
h: Math.round(h),
|
2022-11-22 16:24:55 -06:00
|
|
|
};
|
|
|
|
}
|
2022-11-25 10:16:22 -06:00
|
|
|
|
2022-12-01 15:10:30 -06:00
|
|
|
class NoContentError extends Error {}
|
|
|
|
|
2022-11-28 09:11:19 -06:00
|
|
|
/**
|
|
|
|
* Crops a given canvas to content, returning a new canvas object with the content in it.
|
|
|
|
*
|
|
|
|
* @param {HTMLCanvasElement} sourceCanvas Canvas to get a content crop from
|
2022-12-01 15:10:30 -06:00
|
|
|
* @param {object} options Extra options
|
|
|
|
* @param {number} [options.border=0] Extra border around the content
|
|
|
|
* @returns {{canvas: HTMLCanvasElement, bb: BoundingBox}} A new canvas with the cropped part of the image
|
2022-11-28 09:11:19 -06:00
|
|
|
*/
|
2022-12-01 15:10:30 -06:00
|
|
|
function cropCanvas(sourceCanvas, options = {}) {
|
|
|
|
defaultOpt(options, {border: 0});
|
|
|
|
|
|
|
|
const w = sourceCanvas.width;
|
|
|
|
const h = sourceCanvas.height;
|
2022-11-25 10:16:22 -06:00
|
|
|
var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h);
|
2022-12-01 15:10:30 -06:00
|
|
|
/** @type {BoundingBox} */
|
|
|
|
const bb = {x: 0, y: 0, w: 0, h: 0};
|
2022-11-25 10:16:22 -06:00
|
|
|
|
2022-12-01 15:10:30 -06:00
|
|
|
let minx = w;
|
|
|
|
let maxx = -1;
|
|
|
|
let miny = h;
|
|
|
|
let maxy = -1;
|
|
|
|
|
|
|
|
for (let y = 0; y < h; y++) {
|
|
|
|
for (let x = 0; x < w; x++) {
|
2022-11-25 10:16:22 -06:00
|
|
|
// lol i need to learn what this part does
|
2022-12-01 15:10:30 -06:00
|
|
|
const index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords
|
2022-11-25 10:16:22 -06:00
|
|
|
//this part i get, this is checking that 4th RGBA byte for opacity
|
|
|
|
if (imageData.data[index + 3] > 0) {
|
2022-12-01 15:10:30 -06:00
|
|
|
minx = Math.min(minx, x);
|
|
|
|
maxx = Math.max(maxx, x);
|
|
|
|
miny = Math.min(miny, y);
|
|
|
|
maxy = Math.max(maxy, y);
|
2022-11-25 10:16:22 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-01 15:10:30 -06:00
|
|
|
bb.x = minx - options.border;
|
|
|
|
bb.y = miny - options.border;
|
2022-12-12 07:54:26 -06:00
|
|
|
bb.w = maxx - minx + 1 + 2 * options.border;
|
|
|
|
bb.h = maxy - miny + 1 + 2 * options.border;
|
2022-12-01 15:10:30 -06:00
|
|
|
|
|
|
|
if (maxx < 0) throw new NoContentError("Canvas has no content to crop");
|
|
|
|
|
|
|
|
var cutCanvas = document.createElement("canvas");
|
|
|
|
cutCanvas.width = bb.w;
|
|
|
|
cutCanvas.height = bb.h;
|
|
|
|
cutCanvas
|
|
|
|
.getContext("2d")
|
|
|
|
.drawImage(sourceCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
|
|
|
return {canvas: cutCanvas, bb};
|
2022-11-25 10:16:22 -06:00
|
|
|
}
|
|
|
|
|
2022-11-28 09:11:19 -06:00
|
|
|
/**
|
2022-11-28 09:32:31 -06:00
|
|
|
* Downloads the content of a canvas to the disk, or opens it
|
2022-11-28 09:11:19 -06:00
|
|
|
*
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {Object} options - Optional Information
|
|
|
|
* @param {boolean} [options.cropToContent] - If we wish to crop to content first (default: true)
|
2022-12-04 13:22:35 -06:00
|
|
|
* @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: visible)
|
2022-11-28 16:48:42 -06:00
|
|
|
* @param {string} [options.filename] - The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\
|
2022-11-28 09:11:19 -06:00
|
|
|
* If null, opens image in new tab.
|
|
|
|
*/
|
2022-11-25 12:22:16 -06:00
|
|
|
function downloadCanvas(options = {}) {
|
2022-11-25 10:16:22 -06:00
|
|
|
defaultOpt(options, {
|
|
|
|
cropToContent: true,
|
2022-12-04 13:22:35 -06:00
|
|
|
canvas: uil.getVisible({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
w: imageCollection.size.w,
|
|
|
|
h: imageCollection.size.h,
|
|
|
|
}),
|
2022-11-25 10:16:22 -06:00
|
|
|
filename:
|
|
|
|
new Date()
|
|
|
|
.toISOString()
|
|
|
|
.slice(0, 19)
|
|
|
|
.replace("T", " ")
|
|
|
|
.replace(":", " ") + " openOutpaint image.png",
|
|
|
|
});
|
|
|
|
|
|
|
|
var link = document.createElement("a");
|
2022-11-28 09:11:19 -06:00
|
|
|
link.target = "_blank";
|
|
|
|
if (options.filename) link.download = options.filename;
|
2022-11-25 10:16:22 -06:00
|
|
|
|
|
|
|
var croppedCanvas = options.cropToContent
|
2022-12-01 15:10:30 -06:00
|
|
|
? cropCanvas(options.canvas).canvas
|
2022-11-25 10:16:22 -06:00
|
|
|
: options.canvas;
|
|
|
|
if (croppedCanvas != null) {
|
|
|
|
link.href = croppedCanvas.toDataURL("image/png");
|
|
|
|
link.click();
|
|
|
|
}
|
|
|
|
}
|