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 = `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 = `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 = `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 = `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 = `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; }