You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

474 lines
12 KiB

function makeLayer({height=600, width=800}={}) {
const layer = {};
layer.resize = function({height, width}) { // {{{
layer.height = height;
layer.width = width;
layer.drawCanvas.resize({height, width});
layer.selectCanvas.resize({height, width});
return layer;
}; // }}}
layer.refreshPreviewElement = function() { // {{{
layer.previewElement.src = layer.drawCanvas.toDataUrl();
return layer;
}; // }}}
layer.fill = function({color}) { // {{{
layer.drawCanvas.fill({color});
return layer;
} // }}}
layer.add = function({layer2}) { // {{{
layer.drawCanvas.add({canvas2: layer2.drawCanvas});
return layer;
} // }}}
// init
layer.destruct = function() { // {{{
if (layer.drawCanvas && layer.drawCanvas.parentNode) {
layer.drawCanvas.parentNode.removeChild(layer.drawCanvas);
}
if (layer.selectCanvas && layer.selectCanvas.parentNode) {
layer.selectCanvas.parentNode.removeChild(layer.selectCanvas);
}
}; // }}}
layer.init = function() { // {{{
layer.height = height;
layer.width = width;
layer.drawCanvas = makeCanvas({height, width});
layer.selectCanvas = makeCanvas({height, width});
layer.selectCanvas.style.mixBlendMode = 'difference';
layer.selectCanvas.style.pointerEvents = 'none';
layer.selectCanvas.style.zIndex = 5;
const previewElement = document.createElement("img");
previewElement.className = "layer-preview";
layer.previewElement = previewElement;
return layer;
} // }}}
layer.init();
return layer;
}
function makeLayers({easelElement, controllerElement, height=600, width=800, backgroundColor=makeColor({r: 255, g: 255, b: 255})}) {
if (!easelElement || !controllerElement) {
throw new Error("easelElement and controllerElement are required to make layers");
}
const layers = [];
// state
layers._serialize = function() { // {{{
return {
height: layers.height,
width: layers.width,
layersData: layers.map(layer => ({
drawCanvasData: layer.drawCanvas.toDataUrl()
}))
};
}; // }}}
layers._deserialize = function({state}) { // {{{
layers.clear();
layers.height = state.height;
layers.width = state.width;
layers.easelElement.style.height = `${state.height + 2}px`;
layers.easelElement.style.width = `${state.width + 2}px`;
const loadPromises = state.layersData.map(layerState => {
const layer = makeLayer({ height: layers.height, width: layers.width });
layers.push(layer);
return layer.drawCanvas.fromDataUrl({ dataUrl: layerState.drawCanvasData });
});
Promise.all(loadPromises).then(() => {
layers.updateActive();
layers.refreshPreviews();
});
}; // }}}
layers.saveToLocalStorage = function() { // {{{
const historyData = JSON.stringify(historyStack);
const redoData = JSON.stringify(redoStack);
localStorage.setItem('layersHistory', historyData);
localStorage.setItem('layersRedo', redoData);
return layers;
}; // }}}
layers.loadFromLocalStorage = function() { // {{{
const historyData = localStorage.getItem('layersHistory');
const redoData = localStorage.getItem('layersRedo');
if (historyData) {
historyStack = JSON.parse(historyData);
}
if (redoData) {
redoStack = JSON.parse(redoData);
}
if (historyStack.length > 0) {
console.log("Restoring history from local storage");
const lastState = historyStack[historyStack.length - 1];
layers._deserialize({state: lastState});
}
return layers;
}; // }}}
layers.save = function() { // {{{
const state = layers._serialize();
historyStack.push(state);
redoStack = [];
layers.saveToLocalStorage();
return layers;
}; // }}}
layers.undo = function() { // {{{
if (historyStack.length > 1) {
const currentState = historyStack.pop();
redoStack.push(currentState);
const previousState = historyStack[historyStack.length - 1];
layers._deserialize({state: previousState});
layers.saveToLocalStorage();
}
return layers;
}; // }}}
layers.redo = function() { // {{{
if (redoStack.length > 0) {
const nextState = redoStack.pop();
historyStack.push(nextState);
layers._deserialize({state: nextState});
layers.saveToLocalStorage();
}
return layers;
}; // }}}
// active layer
layers.getActive = function() { // {{{
return layers[layers.activeIndex];
}; // }}}
layers.activate = function({layer}) { // {{{
layers.activeIndex = layers.indexOf(layer);
return layers;
}; // }}}
layers.updateActive = function() { // {{{
if (layers.activeIndex < 0 || layers.activeIndex >= layers.length) {
layers.activeIndex = layers.length - 1;
}
return layers;
}; // }}}
// layer management
layers.add = function() { // {{{
const layer = makeLayer({height: layers.height, width: layers.width});
layers.push(layer);
layers.activate({layer});
return layers;
}; // }}}
layers.remove = function({ layer}) { // {{{
const index = layers.indexOf(layer);
if (index > 1 && index < layers.length) {
layer.destruct();
layers.splice(index, 1);
layers.updateActive();
}
return layers;
}; // }}}
layers.clear = function() { // {{{
while (layers.length > 0) {
const layer = layers.pop();
if (layer.drawCanvas && layer.drawCanvas.parentNode) {
layer.drawCanvas.parentNode.removeChild(layer.drawCanvas);
}
if (layer.selectCanvas && layer.selectCanvas.parentNode) {
layer.selectCanvas.parentNode.removeChild(layer.selectCanvas);
}
}
return layers;
}; // }}}
layers.switchIndex = function({index1, index2}) { // {{{
const temp = layers[index1];
layers[index1] = layers[index2];
layers[index2] = temp;
return layers;
}; // }}}
layers.mergeIndex = function({index1, index2}) { // {{{
layers[index1].add({layer2: layers[index2]});
layers.remove({layer: layers[index2]});
return layers;
}; // }}}
layers.moveUp = function({layer}) { // {{{
const index = layers.indexOf(layer);
if (index === 0 || index === layers.length - 1) return;
layers.switchIndex({index1: index, index2: index + 1});
layers.activate({layer: layers[index]});
return layers;
}; // }}}
layers.moveDown = function({layer}) { // {{{
const index = layers.indexOf(layer);
if (index === 0 || index === 1) return;
layers.switchIndex({index1: index, index2: index - 1});
layers.activate({layer: layers[index]});
return layers;
}; // }}}
layers.mergeUp = function({layer}) { // {{{
const index = layers.indexOf(layer);
if (index === 0 || index === layers.length - 1) return;
layers.mergeIndex({index1: index, index2: index + 1});
layers.activate({layer: layers[index]});
return layers;
}; // }}}
layers.mergeDown = function({layer}) { // {{{
const index = layers.indexOf(layer);
if (index === 0 || index === 1) return;
layers.mergeIndex({index2: index, index1: index - 1});
layers.activate({layer: layers[index - 1]});
return layers;
}; // }}}
// resize
layers.resize = function({height, width}) { // {{{
layers.height = height;
layers.width = width;
layers.easelElement.style.height = `${height + 2}px`;
layers.easelElement.style.width = `${width + 2}px`;
layers.forEach(layer => {
layer.resize({height, width});
});
if (layers.length > 0) {
layers[0].fill({color: layers.backgroundColor});
}
return layers;
}; // }}}
// export and import
layers.exportPng = function() { // {{{
const mergedCanvas = document.createElement('canvas');
mergedCanvas.width = layers.width;
mergedCanvas.height = layers.height;
const mergedCtx = mergedCanvas.getContext('2d');
layers.forEach(layer => {
if (layer === layers[0]) return;
mergedCtx.drawImage(layer.drawCanvas, 0, 0);
});
const pngDataUrl = mergedCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = pngDataUrl;
link.download = 'layers.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
return layers;
}; // }}}
// refresh
layers.refreshEasel = function() { // {{{
layers.easelElement.innerHTML = "";
layers.forEach(layer => {
layers.easelElement.appendChild(layer.drawCanvas);
layers.easelElement.appendChild(layer.selectCanvas);
});
return layers;
}; // }}}
layers.refreshController = function() { // {{{
layers.controllerElement.innerHTML = "";
layers.forEach(layer => {
const controllerElement = document.createElement("div");
controllerElement.className = "layer";
if (layer === layers.getActive()) {
controllerElement.classList.add("active");
}
const previewElement = layer.previewElement;
controllerElement.appendChild(previewElement);
if(layer !== layers[0]) {
previewElement.addEventListener("click", () => {
layers.activate({layer}).refreshController().refreshEasel();
});
const moveButtons = document.createElement("div");
moveButtons.classList.add("layer-move-buttons");
moveButtons.className = "layer-move-buttons";
const moveUpButton = document.createElement("div");
moveUpButton.classList.add("button");
moveUpButton.classList.add("mini-button");
moveUpButton.classList.add("layer-move-button");
moveUpButton.innerHTML = `<img src="icons/solid/arrow-up.svg" alt="move up">`;
moveUpButton.addEventListener("click", () => {
layers.moveUp({layer}).save().refreshController().refreshEasel();
});
moveButtons.appendChild(moveUpButton);
const moveDownButton = document.createElement("div");
moveDownButton.classList.add("button");
moveDownButton.classList.add("mini-button");
moveDownButton.classList.add("layer-move-button");
moveDownButton.innerHTML = `<img src="icons/solid/arrow-down.svg" alt="move down">`;
moveDownButton.addEventListener("click", () => {
layers.moveDown({layer}).save().refreshController().refreshEasel();
});
moveButtons.appendChild(moveDownButton);
controllerElement.appendChild(moveButtons);
const mergeButtons = document.createElement("div");
mergeButtons.classList.add("layer-merge-buttons");
mergeButtons.className = "layer-merge-buttons";
const mergeDownButton = document.createElement("div");
mergeDownButton.classList.add("button");
mergeDownButton.classList.add("mini-button");
mergeDownButton.classList.add("layer-merge-button");
mergeDownButton.innerHTML = `<img src="icons/solid/angles-down.svg" alt="merge up">`;
mergeDownButton.addEventListener("click", () => {
layers.mergeDown({layer}).save().refreshController().refreshEasel();
});
mergeButtons.appendChild(mergeDownButton);
const deleteButton = document.createElement("div");
deleteButton.classList.add("button");
deleteButton.classList.add("mini-button");
deleteButton.classList.add("layer-delete-button");
deleteButton.innerHTML = `<img src="icons/regular/trash-can.svg" alt="delete">`;
deleteButton.addEventListener("click", () => {
layers.remove({layer}).save().refreshController().refreshEasel();
});
mergeButtons.appendChild(deleteButton);
controllerElement.appendChild(mergeButtons);
}
layers.controllerElement.appendChild(controllerElement);
});
const addLayerButton = document.createElement("div");
addLayerButton.classList.add("layer-add-button");
addLayerButton.classList.add("button");
addLayerButton.innerHTML = `<img src="icons/solid/plus.svg" alt="add layer">`;
addLayerButton.addEventListener("click", () => {
layers.add().save().refresh();
});
layers.controllerElement.appendChild(addLayerButton)
return layers;
}; // }}}
layers.refreshPreviews = function() { // {{{
layers.forEach(layer => {
layer.refreshPreviewElement();
});
return layers;
}; // }}}
layers.refresh = function() { // {{{
layers
.refreshEasel()
.refreshController()
.refreshPreviews();
return layers;
}; // }}}
// init
layers.destruct = function() { // {{{
layers.clear();
layers.easelElement.innerHTML = "";
layers.controllerElement.innerHTML = "";
}; // }}}
layers.reset = function() { // {{{
layers.backgroundColor = backgroundColor;
layers.height = height;
layers.width = width;
layers.easelElement = easelElement;
layers.controllerElement = controllerElement;
layers.activeIndex = 1;
layers.clear();
layers.push(makeLayer({height: layers.height, width: layers.width}).fill({color: layers.backgroundColor}));
layers.push(makeLayer({height: layers.height, width: layers.width}));
return layers;
} // }}}
layers.init = function() { // {{{
layers.undoStack = [];
layers.redoStack = [];
layers.reset();
return layers;
}; // }}}
layers.init();
return layers;
}