|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Size { |
|
w = 0; |
|
h = 0; |
|
|
|
constructor({w, h} = {w: 0, h: 0}) { |
|
this.w = w; |
|
this.h = h; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class BoundingBox { |
|
x = 0; |
|
y = 0; |
|
w = 0; |
|
h = 0; |
|
|
|
|
|
get tl() { |
|
return {x: this.x, y: this.y}; |
|
} |
|
|
|
|
|
get tr() { |
|
return {x: this.x + this.w, y: this.y}; |
|
} |
|
|
|
|
|
get bl() { |
|
return {x: this.x, y: this.y + this.h}; |
|
} |
|
|
|
|
|
get br() { |
|
return {x: this.x + this.w, y: this.y + this.h}; |
|
} |
|
|
|
|
|
get center() { |
|
return {x: this.x + this.w / 2, y: this.y + this.h / 2}; |
|
} |
|
|
|
constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) { |
|
this.x = x; |
|
this.y = y; |
|
this.w = w; |
|
this.h = h; |
|
} |
|
|
|
contains(x, y) { |
|
return ( |
|
this.x < x && this.y < y && x < this.x + this.w && y < this.y + this.h |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static fromStartEnd(start, end) { |
|
const minx = Math.min(start.x, end.x); |
|
const miny = Math.min(start.y, end.y); |
|
const maxx = Math.max(start.x, end.x); |
|
const maxy = Math.max(start.y, end.y); |
|
|
|
return new BoundingBox({ |
|
x: minx, |
|
y: miny, |
|
w: maxx - minx, |
|
h: maxy - miny, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
transform(transform) { |
|
return BoundingBox.fromStartEnd( |
|
transform.transformPoint({x: this.x, y: this.y}), |
|
transform.transformPoint({x: this.x + this.w, y: this.y + this.h}) |
|
); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
class Observer { |
|
|
|
|
|
|
|
|
|
_handlers = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
on(callback, priority = 0, wait = false) { |
|
this._handlers.push({handler: callback, priority, wait}); |
|
this._handlers.sort((a, b) => b.priority - a.priority); |
|
return callback; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
clear(callback) { |
|
const index = this._handlers.findIndex((v) => v.handler === callback); |
|
if (index === -1) return false; |
|
this._handlers.splice(index, 1); |
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async emit(msg, state = {}) { |
|
const promises = []; |
|
for (const {handler, wait} of this._handlers) { |
|
const run = async () => { |
|
try { |
|
await handler(msg, state); |
|
} catch (e) { |
|
console.warn("Observer failed to run handler"); |
|
console.warn(e); |
|
} |
|
}; |
|
|
|
if (wait) await run(); |
|
else promises.push(run()); |
|
} |
|
|
|
return Promise.all(promises); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class DOM { |
|
static inputTags = new Set(["input", "textarea"]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
static hasActiveInput() { |
|
const active = document.activeElement; |
|
const tag = active.tagName.toLowerCase(); |
|
|
|
const checkTag = this.inputTags.has(tag); |
|
if (!checkTag) return false; |
|
|
|
return tag !== "input" || active.type === "text"; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const guid = (size = 3) => { |
|
const s4 = () => { |
|
return Math.floor((1 + Math.random()) * 0x10000) |
|
.toString(16) |
|
.substring(1); |
|
}; |
|
|
|
let id = ""; |
|
for (var i = 0; i < size - 1; i++) id += s4() + "-"; |
|
id += s4(); |
|
return id; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function defaultOpt(options, defaults) { |
|
Object.keys(defaults).forEach((key) => { |
|
if (options[key] === undefined) options[key] = defaults[key]; |
|
}); |
|
} |
|
|
|
|
|
class ProxyReadOnlySetError extends Error {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function makeReadOnly(obj, name = "read-only object", exceptions = []) { |
|
return new Proxy(obj, { |
|
set: (obj, prop, value) => { |
|
if (!exceptions.some((v) => v === prop)) |
|
throw new ProxyReadOnlySetError( |
|
`Tried setting the '${prop}' property on '${name}'` |
|
); |
|
obj[prop] = value; |
|
}, |
|
}); |
|
} |
|
|
|
|
|
class ProxyWriteOnceSetError extends Error {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function makeWriteOnce(obj, name = "write-once object", exceptions = []) { |
|
return new Proxy(obj, { |
|
set: (obj, prop, value) => { |
|
if (obj[prop] !== undefined && !exceptions.some((v) => v === prop)) |
|
throw new ProxyWriteOnceSetError( |
|
`Tried setting the '${prop}' property on '${name}' after it was already set` |
|
); |
|
obj[prop] = value; |
|
}, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function snap(i, offset = 0, gridSize = config.gridSize) { |
|
let diff = i - offset; |
|
if (diff < 0) { |
|
diff += gridSize * Math.ceil(Math.abs(diff / gridSize)); |
|
} |
|
|
|
const modulus = diff % gridSize; |
|
var snapOffset = modulus; |
|
|
|
if (modulus > gridSize / 2) snapOffset = modulus - gridSize; |
|
|
|
if (snapOffset == 0) { |
|
return snapOffset; |
|
} |
|
return -snapOffset; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) { |
|
const offs = {x: 0, y: 0}; |
|
const box = {x: 0, y: 0}; |
|
|
|
if (gridSnap) { |
|
offs.x = snap(cx, offset, gridSnap); |
|
offs.y = snap(cy, offset, gridSnap); |
|
} |
|
|
|
box.x = Math.round(offs.x + cx); |
|
box.y = Math.round(offs.y + cy); |
|
|
|
return new BoundingBox({ |
|
x: Math.floor(box.x - w / 2), |
|
y: Math.floor(box.y - h / 2), |
|
w: Math.round(w), |
|
h: Math.round(h), |
|
}); |
|
} |
|
|
|
class NoContentError extends Error {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function cropCanvas(sourceCanvas, options = {}) { |
|
defaultOpt(options, {border: 0}); |
|
|
|
const w = sourceCanvas.width; |
|
const h = sourceCanvas.height; |
|
const srcCtx = sourceCanvas.getContext("2d"); |
|
const offset = { |
|
x: (srcCtx.origin && -srcCtx.origin.x) || 0, |
|
y: (srcCtx.origin && -srcCtx.origin.y) || 0, |
|
}; |
|
var imageData = srcCtx.getImageDataRoot(0, 0, w, h); |
|
|
|
const bb = new BoundingBox(); |
|
|
|
let minx = Infinity; |
|
let maxx = -Infinity; |
|
let miny = Infinity; |
|
let maxy = -Infinity; |
|
|
|
for (let y = 0; y < h; y++) { |
|
for (let x = 0; x < w; x++) { |
|
|
|
const index = (y * w + x) * 4; |
|
|
|
if (imageData.data[index + 3] > 0) { |
|
minx = Math.min(minx, x + offset.x); |
|
maxx = Math.max(maxx, x + offset.x); |
|
miny = Math.min(miny, y + offset.y); |
|
maxy = Math.max(maxy, y + offset.y); |
|
} |
|
} |
|
} |
|
|
|
bb.x = minx - options.border; |
|
bb.y = miny - options.border; |
|
bb.w = maxx - minx + 1 + 2 * options.border; |
|
bb.h = maxy - miny + 1 + 2 * options.border; |
|
|
|
if (!Number.isFinite(maxx)) |
|
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}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function downloadCanvas(options = {}) { |
|
defaultOpt(options, { |
|
cropToContent: true, |
|
canvas: uil.getVisible(imageCollection.bb), |
|
filename: |
|
new Date() |
|
.toISOString() |
|
.slice(0, 19) |
|
.replace("T", " ") |
|
.replace(":", " ") + " openOutpaint image.png", |
|
}); |
|
|
|
var link = document.createElement("a"); |
|
link.target = "_blank"; |
|
if (options.filename) link.download = options.filename; |
|
|
|
var croppedCanvas = options.cropToContent |
|
? cropCanvas(options.canvas).canvas |
|
: options.canvas; |
|
|
|
if (croppedCanvas != null) { |
|
croppedCanvas.toBlob((blob) => { |
|
link.href = URL.createObjectURL(blob); |
|
link.click(); |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const makeElement = ( |
|
type, |
|
x, |
|
y, |
|
offset = { |
|
x: -imageCollection.inputOffset.x, |
|
y: -imageCollection.inputOffset.y, |
|
} |
|
) => { |
|
const el = document.createElement(type); |
|
el.style.position = "absolute"; |
|
el.style.left = `${x + offset.x}px`; |
|
el.style.top = `${y + offset.y}px`; |
|
|
|
|
|
imageCollection.inputElement.appendChild(el); |
|
|
|
return el; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const subtractBackground = (canvas, bb, bgImg, blur = 0, threshold = 10) => { |
|
|
|
const bgCanvas = document.createElement("canvas"); |
|
const fgCanvas = document.createElement("canvas"); |
|
const returnCanvas = document.createElement("canvas"); |
|
bgCanvas.width = fgCanvas.width = returnCanvas.width = bb.w; |
|
bgCanvas.height = fgCanvas.height = returnCanvas.height = bb.h; |
|
const bgCtx = bgCanvas.getContext("2d"); |
|
const fgCtx = fgCanvas.getContext("2d"); |
|
const returnCtx = returnCanvas.getContext("2d"); |
|
returnCtx.rect(0, 0, bb.w, bb.h); |
|
returnCtx.fill(); |
|
|
|
bgCtx.drawImage(bgImg, 0, 0, bb.w, bb.h); |
|
bgCtx.filter = "blur(" + blur + "px)"; |
|
|
|
const bgImgData = bgCtx.getImageData(0, 0, bb.w, bb.h); |
|
|
|
fgCtx.drawImage(canvas, 0, 0); |
|
const fgImgData = fgCtx.getImageData(0, 0, bb.w, bb.h); |
|
for (var i = 0; i < bgImgData.data.length; i += 4) { |
|
|
|
|
|
|
|
|
|
var bgr = bgImgData.data[i]; |
|
var bgg = bgImgData.data[i + 1]; |
|
var bgb = bgImgData.data[i + 2]; |
|
|
|
var fgr = fgImgData.data[i]; |
|
var fgb = fgImgData.data[i + 1]; |
|
var fgd = fgImgData.data[i + 2]; |
|
|
|
const dr = Math.abs(bgr - fgr) > threshold ? fgr : 0; |
|
const dg = Math.abs(bgg - fgb) > threshold ? fgb : 0; |
|
const db = Math.abs(bgb - fgd) > threshold ? fgd : 0; |
|
|
|
const pxChanged = dr > 0 && dg > 0 && db > 0; |
|
|
|
fgImgData.data[i + 3] = pxChanged ? 255 : 0; |
|
} |
|
returnCtx.putImageData(fgImgData, 0, 0); |
|
|
|
return returnCanvas; |
|
}; |
|
|