|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(() => { |
|
const original = { |
|
drawImage: CanvasRenderingContext2D.prototype.drawImage, |
|
getImageData: CanvasRenderingContext2D.prototype.getImageData, |
|
putImageData: CanvasRenderingContext2D.prototype.putImageData, |
|
|
|
|
|
moveTo: CanvasRenderingContext2D.prototype.moveTo, |
|
lineTo: CanvasRenderingContext2D.prototype.lineTo, |
|
|
|
arc: CanvasRenderingContext2D.prototype.arc, |
|
fillRect: CanvasRenderingContext2D.prototype.fillRect, |
|
clearRect: CanvasRenderingContext2D.prototype.clearRect, |
|
}; |
|
|
|
|
|
Object.keys(original).forEach((key) => { |
|
CanvasRenderingContext2D.prototype[key + "Root"] = function (...args) { |
|
return original[key].call(this, ...args); |
|
}; |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "bb", { |
|
get: function () { |
|
return new BoundingBox({ |
|
x: -this.origin.x, |
|
y: -this.origin.y, |
|
w: this.canvas.width, |
|
h: this.canvas.height, |
|
}); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "drawImage", { |
|
value: function (...args) { |
|
switch (args.length) { |
|
case 3: |
|
case 5: |
|
if (this.origin !== undefined) { |
|
args[1] += this.origin.x; |
|
args[2] += this.origin.y; |
|
} |
|
break; |
|
case 9: |
|
|
|
const sctx = args[0].getContext && args[0].getContext("2d"); |
|
if (sctx && sctx.origin !== undefined) { |
|
args[1] += sctx.origin.x; |
|
args[2] += sctx.origin.y; |
|
} |
|
|
|
|
|
if (this.origin !== undefined) { |
|
args[5] += this.origin.x; |
|
args[6] += this.origin.y; |
|
} |
|
break; |
|
} |
|
|
|
return original.drawImage.call(this, ...args); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "getImageData", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.getImageData.call(this, ...args); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "putImageData", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.putImageData.call(this, ...args); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "moveTo", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.moveTo.call(this, ...args); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "lineTo", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.lineTo.call(this, ...args); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "arc", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.arc.call(this, ...args); |
|
}, |
|
}); |
|
|
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "fillRect", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.fillRect.call(this, ...args); |
|
}, |
|
}); |
|
|
|
Reflect.defineProperty(CanvasRenderingContext2D.prototype, "clearRect", { |
|
value: function (...args) { |
|
if (this.origin) { |
|
args[0] += this.origin.x; |
|
args[1] += this.origin.y; |
|
} |
|
|
|
return original.clearRect.call(this, ...args); |
|
}, |
|
}); |
|
})(); |
|
|
|
|
|
const layers = { |
|
_collections: [], |
|
collections: makeWriteOnce({}, "layers.collections"), |
|
|
|
listen: { |
|
oncollectioncreate: new Observer(), |
|
oncollectiondelete: new Observer(), |
|
|
|
onlayercreate: new Observer(), |
|
onlayerdelete: new Observer(), |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
registerCollection: (key, size, options = {}) => { |
|
defaultOpt(options, { |
|
|
|
name: key, |
|
|
|
|
|
initLayer: { |
|
key: "default", |
|
options: {}, |
|
}, |
|
|
|
|
|
inputSizeMultiplier: 9, |
|
|
|
|
|
targetElement: document.getElementById("layer-render"), |
|
|
|
|
|
resolution: size, |
|
}); |
|
|
|
if (options.inputSizeMultiplier % 2 === 0) options.inputSizeMultiplier++; |
|
|
|
|
|
const _logpath = "layers.collections." + key; |
|
|
|
|
|
const id = guid(); |
|
|
|
|
|
const element = document.createElement("div"); |
|
element.id = `collection-${id}`; |
|
element.style.width = `${size.w}px`; |
|
element.style.height = `${size.h}px`; |
|
element.classList.add("collection"); |
|
|
|
|
|
const inputel = document.createElement("div"); |
|
inputel.id = `collection-input-${id}`; |
|
inputel.classList.add("collection-input-overlay"); |
|
element.appendChild(inputel); |
|
|
|
options.targetElement.appendChild(element); |
|
|
|
|
|
const collection = makeWriteOnce( |
|
{ |
|
id, |
|
|
|
_logpath, |
|
|
|
_layers: [], |
|
layers: {}, |
|
|
|
key, |
|
name: options.name, |
|
element, |
|
inputElement: inputel, |
|
_inputOffset: null, |
|
get inputOffset() { |
|
return this._inputOffset; |
|
}, |
|
|
|
_origin: {x: 0, y: 0}, |
|
get origin() { |
|
return {...this._origin}; |
|
}, |
|
|
|
get bb() { |
|
return new BoundingBox({ |
|
x: -this.origin.x, |
|
y: -this.origin.y, |
|
w: this.size.w, |
|
h: this.size.h, |
|
}); |
|
}, |
|
|
|
_resizeInputDiv() { |
|
|
|
const oldOffset = {...this._inputOffset}; |
|
this._inputOffset = { |
|
x: |
|
-Math.floor(options.inputSizeMultiplier / 2) * size.w - |
|
this._origin.x, |
|
y: |
|
-Math.floor(options.inputSizeMultiplier / 2) * size.h - |
|
this._origin.y, |
|
}; |
|
|
|
|
|
this.inputElement.style.left = `${this.inputOffset.x}px`; |
|
this.inputElement.style.top = `${this.inputOffset.y}px`; |
|
this.inputElement.style.width = `${ |
|
size.w * options.inputSizeMultiplier |
|
}px`; |
|
this.inputElement.style.height = `${ |
|
size.h * options.inputSizeMultiplier |
|
}px`; |
|
|
|
|
|
for (const child of this.inputElement.children) { |
|
if (child.style.position === "absolute") { |
|
child.style.left = `${ |
|
parseInt(child.style.left, 10) + |
|
oldOffset.x - |
|
this._inputOffset.x |
|
}px`; |
|
child.style.top = `${ |
|
parseInt(child.style.top, 10) + |
|
oldOffset.y - |
|
this._inputOffset.y |
|
}px`; |
|
} |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expand(left, top, right, bottom) { |
|
this._layers.forEach((layer) => { |
|
if (layer.full) layer._expand(left, top, right, bottom); |
|
}); |
|
|
|
this._origin.x += left; |
|
this._origin.y += top; |
|
|
|
this.size.w += left + right; |
|
this.size.h += top + bottom; |
|
|
|
this._resizeInputDiv(); |
|
|
|
for (const layer of this._layers) { |
|
layer.moveTo(layer.x, layer.y); |
|
} |
|
}, |
|
|
|
size, |
|
resolution: options.resolution, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
registerLayer(key = null, options = {}) { |
|
|
|
const id = options.id ?? guid(); |
|
|
|
defaultOpt(options, { |
|
|
|
id: null, |
|
|
|
|
|
name: key || `Temporary ${id}`, |
|
|
|
|
|
bb: { |
|
x: -collection.origin.x, |
|
y: -collection.origin.y, |
|
w: collection.size.w, |
|
h: collection.size.h, |
|
}, |
|
|
|
|
|
category: null, |
|
|
|
|
|
resolution: null, |
|
|
|
|
|
group: null, |
|
|
|
|
|
after: null, |
|
|
|
|
|
ctxOptions: {}, |
|
}); |
|
|
|
|
|
let full = false; |
|
if ( |
|
options.bb.x === -collection.origin.x && |
|
options.bb.y === -collection.origin.y && |
|
options.bb.w === collection.size.w && |
|
options.bb.h === collection.size.h |
|
) |
|
full = true; |
|
|
|
if (!options.resolution) |
|
|
|
options.resolution = { |
|
w: Math.round( |
|
(collection.resolution.w / collection.size.w) * options.bb.w |
|
), |
|
h: Math.round( |
|
(collection.resolution.h / collection.size.h) * options.bb.h |
|
), |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = document.createElement("canvas"); |
|
canvas.id = `layer-${id}`; |
|
|
|
canvas.style.left = `${options.bb.x}px`; |
|
canvas.style.top = `${options.bb.y}px`; |
|
canvas.style.width = `${options.bb.w}px`; |
|
canvas.style.height = `${options.bb.h}px`; |
|
canvas.width = options.resolution.w; |
|
canvas.height = options.resolution.h; |
|
|
|
if (!options.after) collection.element.appendChild(canvas); |
|
else { |
|
options.after.canvas.after(canvas); |
|
} |
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext("2d", options.ctxOptions); |
|
if (full) { |
|
|
|
ctx.origin = { |
|
get x() { |
|
return collection.origin.x; |
|
}, |
|
get y() { |
|
return collection.origin.y; |
|
}, |
|
}; |
|
} |
|
|
|
|
|
const _layerlogpath = key |
|
? _logpath + ".layers." + key |
|
: _logpath + ".layers[" + id + "]"; |
|
const layer = makeWriteOnce( |
|
{ |
|
_logpath: _layerlogpath, |
|
_collection: collection, |
|
|
|
_bb: new BoundingBox(options.bb), |
|
get bb() { |
|
return new BoundingBox(this._bb); |
|
}, |
|
|
|
resolution: new Size(options.resolution), |
|
id, |
|
key, |
|
name: options.name, |
|
full, |
|
category: options.category, |
|
|
|
state: new Proxy( |
|
{visible: true}, |
|
{ |
|
set(obj, opt, val) { |
|
switch (opt) { |
|
case "visible": |
|
layer.canvas.style.display = val ? "block" : "none"; |
|
break; |
|
} |
|
obj[opt] = val; |
|
}, |
|
} |
|
), |
|
|
|
get x() { |
|
return this._bb.x; |
|
}, |
|
|
|
get y() { |
|
return this._bb.y; |
|
}, |
|
|
|
get width() { |
|
return this._bb.w; |
|
}, |
|
|
|
get height() { |
|
return this._bb.h; |
|
}, |
|
|
|
get w() { |
|
return this._bb.w; |
|
}, |
|
|
|
get h() { |
|
return this._bb.h; |
|
}, |
|
|
|
get origin() { |
|
return this._collection.origin; |
|
}, |
|
|
|
get hidden() { |
|
return !this.state.visible; |
|
}, |
|
|
|
|
|
canvas, |
|
ctx, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_expand(left, top, right, bottom) { |
|
const tmpCanvas = document.createElement("canvas"); |
|
tmpCanvas.width = this.w; |
|
tmpCanvas.height = this.h; |
|
tmpCanvas.getContext("2d").drawImage(this.canvas, 0, 0); |
|
|
|
this.resize(this.w + left + right, this.h + top + bottom); |
|
this.clear(); |
|
this.ctx.drawImageRoot(tmpCanvas, left, top); |
|
|
|
this.moveTo(this.x - left, this.y - top); |
|
}, |
|
|
|
|
|
|
|
|
|
clear() { |
|
this.ctx.clearRectRoot( |
|
0, |
|
0, |
|
this.canvas.width, |
|
this.canvas.height |
|
); |
|
}, |
|
|
|
|
|
|
|
|
|
syncDOM() { |
|
this.moveTo(this.x, this.y); |
|
this.resize(this.w, this.h); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
moveAfter(layer) { |
|
layer.canvas.after(this.canvas); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
moveBefore(layer) { |
|
layer.canvas.before(this.canvas); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
moveTo(x, y) { |
|
this._bb.x = x; |
|
this._bb.y = y; |
|
this.canvas.style.left = `${x}px`; |
|
this.canvas.style.top = `${y}px`; |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resize(w, h) { |
|
canvas.width = Math.round( |
|
options.resolution.w * (w / options.bb.w) |
|
); |
|
canvas.height = Math.round( |
|
options.resolution.h * (h / options.bb.h) |
|
); |
|
this._bb.w = w; |
|
this._bb.h = h; |
|
canvas.style.width = `${w}px`; |
|
canvas.style.height = `${h}px`; |
|
}, |
|
|
|
|
|
hide() { |
|
this.state.visible = false; |
|
}, |
|
|
|
unhide() { |
|
this.state.visible = true; |
|
}, |
|
}, |
|
_layerlogpath |
|
); |
|
|
|
|
|
if (!options.after) collection._layers.push(layer); |
|
else { |
|
const index = collection._layers.findIndex( |
|
(l) => l === options.after |
|
); |
|
collection._layers.splice(index, 0, layer); |
|
} |
|
if (key) collection.layers[key] = layer; |
|
collection.layers[id] = layer; |
|
|
|
if (key === null) |
|
console.debug( |
|
`[layers] Anonymous layer '${layer.name}' registered` |
|
); |
|
else |
|
console.info( |
|
`[layers] Layer '${layer.name}' at ${layer._logpath} registered` |
|
); |
|
|
|
layers.listen.onlayercreate.emit({ |
|
layer, |
|
}); |
|
return layer; |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
deleteLayer: (layer) => { |
|
const lobj = collection._layers.splice( |
|
collection._layers.findIndex( |
|
(l) => l.id === layer || l.id === layer.id |
|
), |
|
1 |
|
)[0]; |
|
if (!lobj) return; |
|
|
|
layers.listen.onlayerdelete.emit({ |
|
layer: lobj, |
|
}); |
|
if (lobj.key) collection.layers[lobj.key] = undefined; |
|
collection.layers[lobj.id] = undefined; |
|
|
|
collection.element.removeChild(lobj.canvas); |
|
|
|
if (lobj.key) console.info(`[layers] Layer '${lobj.key}' deleted`); |
|
else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`); |
|
}, |
|
}, |
|
_logpath, |
|
["_inputOffset"] |
|
); |
|
|
|
collection._resizeInputDiv(); |
|
|
|
layers._collections.push(collection); |
|
layers.collections[key] = collection; |
|
|
|
console.info( |
|
`[layers] Collection '${options.name}' at ${_logpath} registered` |
|
); |
|
|
|
return collection; |
|
}, |
|
}; |
|
|