|
|
|
|
|
|
|
|
|
function unzoom() { |
|
viewport.zoom = 1; |
|
viewport.transform(imageCollection.element); |
|
|
|
notifications.notify("Zoom reset to 1x"); |
|
} |
|
|
|
const uil = { |
|
|
|
onactive: new Observer(), |
|
|
|
_ui_layer_list: document.getElementById("layer-list"), |
|
layers: [], |
|
layerIndex: {}, |
|
_active: null, |
|
set active(v) { |
|
this.onactive.emit({ |
|
uilayer: v, |
|
}); |
|
|
|
Array.from(this._ui_layer_list.children).forEach((child) => { |
|
child.classList.remove("active"); |
|
}); |
|
|
|
v.entry.classList.add("active"); |
|
|
|
this._active = v; |
|
}, |
|
get active() { |
|
return this._active; |
|
}, |
|
|
|
|
|
get layer() { |
|
return this.active && this.active.layer; |
|
}, |
|
|
|
get canvas() { |
|
return this.layer && this.active.layer.canvas; |
|
}, |
|
|
|
get ctx() { |
|
return this.layer && this.active.layer.ctx; |
|
}, |
|
|
|
get w() { |
|
return imageCollection.size.w; |
|
}, |
|
get h() { |
|
return imageCollection.size.h; |
|
}, |
|
|
|
|
|
|
|
|
|
_syncLayers() { |
|
const layersEl = document.getElementById("layer-list"); |
|
|
|
const copy = this.layers.map((i) => i); |
|
copy.reverse(); |
|
|
|
copy.forEach((uiLayer, index) => { |
|
|
|
if ( |
|
layersEl.children[index] && |
|
layersEl.children[index].id === `ui-layer-${uiLayer.id}` |
|
) |
|
return; |
|
|
|
|
|
if (!uiLayer.entry) { |
|
uiLayer.entry = document.createElement("div"); |
|
uiLayer.entry.id = `ui-layer-${uiLayer.id}`; |
|
uiLayer.entry.classList.add("ui-layer"); |
|
uiLayer.entry.addEventListener("click", () => { |
|
this.active = uiLayer; |
|
}); |
|
|
|
|
|
const titleEl = document.createElement("input"); |
|
titleEl.classList.add("title"); |
|
titleEl.value = uiLayer.name; |
|
titleEl.style.pointerEvents = "none"; |
|
|
|
const deselect = () => { |
|
titleEl.style.pointerEvents = "none"; |
|
titleEl.setSelectionRange(0, 0); |
|
}; |
|
|
|
titleEl.addEventListener("blur", deselect); |
|
uiLayer.entry.appendChild(titleEl); |
|
|
|
uiLayer.entry.addEventListener("change", () => { |
|
const name = titleEl.value.trim(); |
|
titleEl.value = name; |
|
uiLayer.entry.title = name; |
|
|
|
uiLayer.name = name; |
|
|
|
this._syncLayers(); |
|
|
|
titleEl.blur(); |
|
}); |
|
uiLayer.entry.addEventListener("dblclick", () => { |
|
titleEl.style.pointerEvents = "auto"; |
|
titleEl.focus(); |
|
titleEl.select(); |
|
}); |
|
|
|
|
|
const actionArray = document.createElement("div"); |
|
actionArray.classList.add("actions"); |
|
|
|
if (uiLayer.deletable) { |
|
const deleteButton = document.createElement("button"); |
|
deleteButton.addEventListener( |
|
"click", |
|
(evn) => { |
|
evn.stopPropagation(); |
|
commands.runCommand( |
|
"deleteLayer", |
|
"Deleted Layer", |
|
{ |
|
layer: uiLayer, |
|
}, |
|
{ |
|
extra: { |
|
log: `Deleted Layer ${uiLayer.name} [${uiLayer.id}]`, |
|
}, |
|
} |
|
); |
|
}, |
|
{passive: false} |
|
); |
|
|
|
deleteButton.addEventListener( |
|
"dblclick", |
|
(evn) => { |
|
evn.stopPropagation(); |
|
}, |
|
{passive: false} |
|
); |
|
deleteButton.title = "Delete Layer"; |
|
deleteButton.appendChild(document.createElement("div")); |
|
deleteButton.classList.add("delete-btn"); |
|
|
|
actionArray.appendChild(deleteButton); |
|
} |
|
|
|
const hideButton = document.createElement("button"); |
|
hideButton.addEventListener( |
|
"click", |
|
(evn) => { |
|
evn.stopPropagation(); |
|
uiLayer.hidden = !uiLayer.hidden; |
|
}, |
|
{passive: false} |
|
); |
|
hideButton.addEventListener( |
|
"dblclick", |
|
(evn) => { |
|
evn.stopPropagation(); |
|
}, |
|
{passive: false} |
|
); |
|
hideButton.title = "Hide/Unhide Layer"; |
|
hideButton.appendChild(document.createElement("div")); |
|
hideButton.classList.add("hide-btn"); |
|
|
|
actionArray.appendChild(hideButton); |
|
uiLayer.entry.appendChild(actionArray); |
|
|
|
if (layersEl.children[index]) |
|
layersEl.children[index].before(uiLayer.entry); |
|
else layersEl.appendChild(uiLayer.entry); |
|
} else if (!layersEl.querySelector(`#ui-layer-${uiLayer.id}`)) { |
|
|
|
if (index === 0) layersEl.children[0].before(uiLayer.entry); |
|
else layersEl.children[index - 1].after(uiLayer.entry); |
|
} else { |
|
|
|
layersEl.children[index].before(uiLayer.entry); |
|
} |
|
}); |
|
|
|
|
|
for (var i = 0; i < layersEl.children.length; i++) { |
|
if (!copy.find((l) => layersEl.children[i].id === `ui-layer-${l.id}`)) { |
|
layersEl.children[i].remove(); |
|
} |
|
} |
|
|
|
|
|
const ids = this.layers.map((l) => l.id); |
|
ids.forEach((id, index) => { |
|
if (index === 0) this.layerIndex[id].layer.moveAfter(bgLayer); |
|
else |
|
this.layerIndex[id].layer.moveAfter( |
|
this.layerIndex[ids[index - 1]].layer |
|
); |
|
}); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_addLayer(group, name) { |
|
const layer = imageCollection.registerLayer(null, { |
|
name, |
|
category: "user", |
|
after: |
|
(this.layers.length > 0 && this.layers[this.layers.length - 1].layer) || |
|
bgLayer, |
|
}); |
|
|
|
const uiLayer = { |
|
id: layer.id, |
|
group, |
|
name, |
|
_hidden: false, |
|
set hidden(v) { |
|
if (v) { |
|
this._hidden = true; |
|
this.layer.hide(v); |
|
this.entry && this.entry.classList.add("hidden"); |
|
} else { |
|
this._hidden = false; |
|
this.layer.unhide(v); |
|
this.entry && this.entry.classList.remove("hidden"); |
|
} |
|
}, |
|
get hidden() { |
|
return this._hidden; |
|
}, |
|
entry: null, |
|
layer, |
|
}; |
|
this.layers.push(uiLayer); |
|
|
|
this._syncLayers(); |
|
|
|
this.active = uiLayer; |
|
|
|
return uiLayer; |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_moveLayerTo(layer, position) { |
|
if (position < 0 || position >= this.layers.length) |
|
throw new RangeError("Position out of bounds"); |
|
|
|
const index = this.layers.indexOf(layer); |
|
if (index !== -1) { |
|
if (this.layers.length < 2) return; |
|
|
|
this.layers.splice(index, 1); |
|
this.layers.splice(position, 0, layer); |
|
|
|
this._syncLayers(); |
|
|
|
return; |
|
} |
|
throw new ReferenceError("Layer could not be found"); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_moveLayerUp(layer = uil.active) { |
|
const index = this.layers.indexOf(layer); |
|
if (index === -1) throw new ReferenceError("Layer could not be found"); |
|
try { |
|
this._moveLayerTo(layer, index + 1); |
|
} catch (e) {} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_moveLayerDown(layer = uil.active) { |
|
const index = this.layers.indexOf(layer); |
|
if (index === -1) throw new ReferenceError("Layer could not be found"); |
|
try { |
|
this._moveLayerTo(layer, index - 1); |
|
} catch (e) {} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getVisible(bb, options = {}) { |
|
defaultOpt(options, { |
|
includeBg: false, |
|
categories: ["user", "image"], |
|
}); |
|
|
|
const canvas = document.createElement("canvas"); |
|
const ctx = canvas.getContext("2d"); |
|
|
|
canvas.width = bb.w; |
|
canvas.height = bb.h; |
|
|
|
const categories = new Set(options.categories); |
|
if (options.includeBg) categories.add("background"); |
|
const layers = imageCollection._layers; |
|
|
|
layers.reduceRight((_, layer) => { |
|
if (categories.has(layer.category) && !layer.hidden) |
|
ctx.drawImage(layer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); |
|
}); |
|
|
|
return canvas; |
|
}, |
|
}; |
|
|
|
class UILayer { |
|
|
|
id; |
|
|
|
|
|
name; |
|
|
|
|
|
layer; |
|
|
|
|
|
key; |
|
|
|
|
|
group; |
|
|
|
|
|
deletable; |
|
|
|
|
|
entry; |
|
|
|
|
|
_hidden; |
|
|
|
|
|
set hidden(v) { |
|
if (v) { |
|
this._hidden = true; |
|
this.layer.hide(v); |
|
this.entry && this.entry.classList.add("hidden"); |
|
} else { |
|
this._hidden = false; |
|
this.layer.unhide(v); |
|
this.entry && this.entry.classList.remove("hidden"); |
|
} |
|
} |
|
get hidden() { |
|
return this._hidden; |
|
} |
|
|
|
|
|
get ctx() { |
|
return this.layer.ctx; |
|
} |
|
|
|
|
|
get canvas() { |
|
return this.layer.canvas; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(name, extra = {}) { |
|
defaultOpt(extra, { |
|
id: null, |
|
group: null, |
|
key: null, |
|
deletable: true, |
|
}); |
|
|
|
this.layer = imageCollection.registerLayer(extra.key, { |
|
id: extra.id, |
|
name, |
|
category: "user", |
|
after: |
|
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) || |
|
bgLayer, |
|
}); |
|
|
|
this.name = name; |
|
this.id = this.layer.id; |
|
this.key = extra.key; |
|
this.group = extra.group; |
|
this.deletable = extra.deletable; |
|
|
|
this.hidden = false; |
|
} |
|
|
|
|
|
|
|
|
|
register() { |
|
uil.layers.push(this); |
|
uil.layerIndex[this.id] = this; |
|
uil.layerIndex[this.key] = this; |
|
} |
|
|
|
|
|
|
|
|
|
unregister() { |
|
const index = uil.layers.findIndex((v) => v === this); |
|
|
|
if (index === -1) throw new ReferenceError("Layer could not be found"); |
|
|
|
if (uil.active === this) |
|
uil.active = uil.layers[index + 1] || uil.layers[index - 1]; |
|
uil.layers.splice(index, 1); |
|
uil.layerIndex[this.id] = undefined; |
|
uil.layerIndex[this.key] = undefined; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
commands.createCommand( |
|
"addLayer", |
|
(title, opt, state) => { |
|
const options = Object.assign({}, opt) || {}; |
|
const id = guid(); |
|
defaultOpt(options, { |
|
id, |
|
group: null, |
|
name: id, |
|
key: null, |
|
deletable: true, |
|
}); |
|
|
|
if (!state.layer) { |
|
let {id, name, group, key, deletable} = state; |
|
|
|
if (!state.imported) { |
|
id = options.id; |
|
name = options.name; |
|
group = options.group; |
|
key = options.key; |
|
deletable = options.deletable; |
|
|
|
state.name = name; |
|
state.group = group; |
|
state.key = key; |
|
state.deletable = deletable; |
|
} |
|
|
|
state.layer = new UILayer(name, { |
|
id, |
|
group, |
|
key: key, |
|
deletable: deletable, |
|
}); |
|
|
|
if (state.hidden !== undefined) state.layer.hidden = state.hidden; |
|
|
|
state.id = state.layer.id; |
|
} |
|
|
|
state.layer.register(); |
|
|
|
uil._syncLayers(); |
|
|
|
uil.active = state.layer; |
|
}, |
|
(title, state) => { |
|
state.layer.unregister(); |
|
|
|
uil._syncLayers(); |
|
}, |
|
{ |
|
exportfn(state) { |
|
return { |
|
id: state.layer.id, |
|
hidden: state.layer.hidden, |
|
|
|
name: state.layer.name, |
|
group: state.group, |
|
key: state.key, |
|
deletable: state.deletable, |
|
}; |
|
}, |
|
importfn(value, state) { |
|
state.id = value.id; |
|
state.hidden = value.hidden; |
|
|
|
state.name = value.name; |
|
state.group = value.group; |
|
state.key = value.key; |
|
state.deletable = value.deletable; |
|
}, |
|
} |
|
); |
|
|
|
|
|
|
|
|
|
commands.createCommand( |
|
"moveLayer", |
|
(title, opt, state) => { |
|
const options = opt || {}; |
|
defaultOpt(options, { |
|
layer: null, |
|
to: null, |
|
delta: null, |
|
}); |
|
|
|
if (!state.layer) { |
|
if (options.to === null && options.delta === null) |
|
throw new Error( |
|
"[layers.moveLayer] Options must contain one of {to?, delta?}" |
|
); |
|
|
|
const layer = options.layer || uil.active; |
|
|
|
const index = uil.layers.indexOf(layer); |
|
if (index === -1) throw new ReferenceError("Layer could not be found"); |
|
|
|
let position = options.to; |
|
|
|
if (position === null) position = index + options.delta; |
|
|
|
state.layer = layer; |
|
state.oldposition = index; |
|
state.position = position; |
|
} |
|
|
|
uil._moveLayerTo(state.layer, state.position); |
|
}, |
|
(title, state) => { |
|
uil._moveLayerTo(state.layer, state.oldposition); |
|
}, |
|
{ |
|
exportfn(state) { |
|
return { |
|
layer: state.layer.id, |
|
position: state.position, |
|
oldposition: state.oldposition, |
|
}; |
|
}, |
|
importfn(value, state) { |
|
state.layer = uil.layerIndex[value.layer]; |
|
state.position = value.position; |
|
state.oldposition = value.oldposition; |
|
}, |
|
} |
|
); |
|
|
|
|
|
|
|
|
|
commands.createCommand( |
|
"deleteLayer", |
|
(title, opt, state) => { |
|
const options = opt || {}; |
|
defaultOpt(options, { |
|
layer: null, |
|
}); |
|
|
|
if (!state.layer) { |
|
const layer = options.layer || uil.active; |
|
|
|
if (!layer.deletable) |
|
throw new TypeError("[layer.deleteLayer] Layer is not deletable"); |
|
|
|
const index = uil.layers.indexOf(layer); |
|
if (index === -1) |
|
throw new ReferenceError( |
|
"[layer.deleteLayer] Layer could not be found" |
|
); |
|
|
|
state.layer = layer; |
|
state.position = index; |
|
} |
|
|
|
if (uil.active === state.layer) |
|
uil.active = |
|
uil.layers[state.position - 1] || uil.layers[state.position + 1]; |
|
uil.layers.splice(state.position, 1); |
|
|
|
uil._syncLayers(); |
|
|
|
state.layer.hidden = true; |
|
}, |
|
(title, state) => { |
|
uil.layers.splice(state.position, 0, state.layer); |
|
uil.active = state.layer; |
|
|
|
uil._syncLayers(); |
|
|
|
state.layer.hidden = false; |
|
}, |
|
{ |
|
exportfn(state) { |
|
return { |
|
layer: state.layer.id, |
|
position: state.position, |
|
}; |
|
}, |
|
importfn(value, state) { |
|
state.layer = uil.layerIndex[value.layer]; |
|
state.position = value.position; |
|
}, |
|
} |
|
); |
|
|
|
|
|
|
|
|
|
commands.createCommand( |
|
"mergeLayer", |
|
async (title, opt, state) => { |
|
const options = opt || {}; |
|
defaultOpt(options, { |
|
layerS: null, |
|
layerD: null, |
|
}); |
|
|
|
if (state.imported) { |
|
state.layerS = uil.layerIndex[state.layerSID]; |
|
state.layerD = uil.layerIndex[state.layerDID]; |
|
} |
|
|
|
if (!state.layerS) { |
|
const layerS = options.layer || uil.active; |
|
|
|
if (!layerS.deletable) |
|
throw new TypeError( |
|
"[layer.mergeLayer] Layer is a undeletable layer and cannot be merged" |
|
); |
|
|
|
const index = uil.layers.indexOf(layerS); |
|
if (index === -1) |
|
throw new ReferenceError("[layer.mergeLayer] Layer could not be found"); |
|
|
|
if (index === 0 && !options.layerD) |
|
throw new ReferenceError( |
|
"[layer.mergeLayer] No layer below source layer exists" |
|
); |
|
|
|
|
|
const layerD = options.layerD || uil.layers[index - 1]; |
|
|
|
state.layerS = layerS; |
|
state.layerD = layerD; |
|
} |
|
|
|
|
|
|
|
state.drawCommand = await commands.runCommand( |
|
"drawImage", |
|
"Merge Layer Draw", |
|
{ |
|
image: state.layerS.layer.canvas, |
|
x: 0, |
|
y: 0, |
|
layer: state.layerD.layer, |
|
}, |
|
{recordHistory: false} |
|
); |
|
state.delCommand = await commands.runCommand( |
|
"deleteLayer", |
|
"Merge Layer Delete", |
|
{layer: state.layerS}, |
|
{recordHistory: false} |
|
); |
|
}, |
|
(title, state) => { |
|
state.drawCommand.undo(); |
|
state.delCommand.undo(); |
|
}, |
|
{ |
|
redo: (title, options, state) => { |
|
state.drawCommand.redo(); |
|
state.delCommand.redo(); |
|
}, |
|
exportfn(state) { |
|
return { |
|
layerS: state.layerS.id, |
|
layerD: state.layerD.id, |
|
}; |
|
}, |
|
importfn(value, state) { |
|
state.layerSID = value.layerS; |
|
state.layerDID = value.layerD; |
|
}, |
|
} |
|
); |
|
|
|
commands.runCommand( |
|
"addLayer", |
|
"Initial Layer Creation", |
|
{name: "Default Image Layer", key: "default", deletable: false}, |
|
{recordHistory: false} |
|
); |
|
|