diff --git a/css/index.css b/css/index.css index 9c6b916..20766f3 100644 --- a/css/index.css +++ b/css/index.css @@ -64,6 +64,11 @@ body { margin-bottom: 5px; } +.button.tool:disabled { + background-color: #666 !important; + cursor: default; +} + .button.tool:hover { background-color: rgb(30, 30, 80); } diff --git a/js/index.js b/js/index.js index f974a75..eb85116 100644 --- a/js/index.js +++ b/js/index.js @@ -205,7 +205,8 @@ function imageAcceptReject(x, y, data, extra = null) { div.style.width = "200px"; div.style.height = "70px"; div.innerHTML = - ' of '; + ' of '; + document.getElementById("tempDiv").appendChild(div); document.getElementById("currentImgIndex").innerText = "1"; document.getElementById("totalImgIndex").innerText = totalImagesReturned; @@ -238,11 +239,28 @@ function reject(evt) { blockNewImages = false; } +function resource(evt) { + // send image to resources + const img = new Image(); + // load the image data after defining the closure + img.src = "data:image/png;base64," + returnedImages[imageIndex]; + + tools.stamp.state.addResource( + prompt("Enter new resource name", "Dream Resource"), + img + ); +} + function newImage(evt) { clearPaintedMask(); clearBackupMask(); clearTargetMask(); - clearImgMask(); + commands.runCommand("eraseImage", "Clear Canvas", { + x: 0, + y: 0, + w: imgCanvas.width, + h: imgCanvas.height, + }); } function prevImg(evt) { @@ -592,65 +610,6 @@ function drawBackground() { } } -function downloadCanvas() { - var link = document.createElement("a"); - link.download = - new Date().toISOString().slice(0, 19).replace("T", " ").replace(":", " ") + - " openOutpaint image.png"; - var croppedCanvas = cropCanvas(imgCanvas); - if (croppedCanvas != null) { - link.href = croppedCanvas.toDataURL("image/png"); - link.click(); - } -} - -function cropCanvas(sourceCanvas) { - var w = sourceCanvas.width; - var h = sourceCanvas.height; - var pix = {x: [], y: []}; - var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h); - var x, y, index; - - for (y = 0; y < h; y++) { - for (x = 0; x < w; x++) { - // lol i need to learn what this part does - index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords - //this part i get, this is checking that 4th RGBA byte for opacity - if (imageData.data[index + 3] > 0) { - pix.x.push(x); - pix.y.push(y); - } - } - } - // ...need to learn what this part does too :badpokerface: - // is this just determining the boundaries of non-transparent pixel data? - pix.x.sort(function (a, b) { - return a - b; - }); - pix.y.sort(function (a, b) { - return a - b; - }); - var n = pix.x.length - 1; - w = pix.x[n] - pix.x[0] + 1; - h = pix.y[n] - pix.y[0] + 1; - // yup sure looks like it - - try { - var cut = sourceCanvas - .getContext("2d") - .getImageData(pix.x[0], pix.y[0], w, h); - var cutCanvas = document.createElement("canvas"); - cutCanvas.width = w; - cutCanvas.height = h; - cutCanvas.getContext("2d").putImageData(cut, 0, 0); - } catch (ex) { - // probably empty image - //TODO confirm edge cases? - cutCanvas = null; - } - return cutCanvas; -} - function checkIfWebuiIsRunning() { var url = document.getElementById("host").value + "/startup-events"; fetch(url) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 409a25d..6bd6a76 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -20,6 +20,8 @@ const selectTransformTool = () => keyboard.onShortcut({ctrl: true, key: "KeyV"}, state.ctrlvcb); keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb); keyboard.onShortcut({ctrl: true, key: "KeyS"}, state.ctrlscb); + + state.selected = null; }, (state, opt) => { mouse.listen.canvas.onmousemove.clear(state.movecb); @@ -50,7 +52,16 @@ const selectTransformTool = () => state.original = null; state.dragging = null; - state.selected = null; + state._selected = null; + Object.defineProperty(state, "selected", { + get: () => state._selected, + set: (v) => { + if (v) state.ctxmenu.enableButtons(); + else state.ctxmenu.disableButtons(); + + return (state._selected = v); + }, + }); state.moving = null; state.lastMouseTarget = null; @@ -503,12 +514,52 @@ const selectTransformTool = () => state.ctxmenu.useClipboardLabel = clipboardCheckbox.label; if (!navigator.clipboard.write) clipboardCheckbox.checkbox.disabled = true; // Disable if not available + + // Some useful actions to do with selection + const actionArray = document.createElement("div"); + actionArray.classList.add("button-array"); + + const saveSelectionButton = document.createElement("button"); + saveSelectionButton.classList.add("button", "tool"); + saveSelectionButton.textContent = "Save"; + saveSelectionButton.title = "Saves Selection"; + saveSelectionButton.onclick = () => { + downloadCanvas({ + cropToContent: false, + canvas: state.selected.image, + }); + }; + + const createResourceButton = document.createElement("button"); + createResourceButton.classList.add("button", "tool"); + createResourceButton.textContent = "Resource"; + createResourceButton.title = "Saves Selection as a Resource"; + createResourceButton.onclick = () => { + const image = document.createElement("img"); + image.src = state.selected.image.toDataURL(); + tools.stamp.state.addResource("Selection Resource", image); + tools.stamp.enable(); + }; + + actionArray.appendChild(saveSelectionButton); + actionArray.appendChild(createResourceButton); + + state.ctxmenu.disableButtons = () => { + saveSelectionButton.disabled = true; + createResourceButton.disabled = true; + }; + state.ctxmenu.enableButtons = () => { + saveSelectionButton.disabled = ""; + createResourceButton.disabled = ""; + }; + state.ctxmenu.actionArray = actionArray; } menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.keepAspectRatioLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.useClipboardLabel); + menu.appendChild(state.ctxmenu.actionArray); }, shortcut: "S", } diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 44efcb3..00e3ce3 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -49,25 +49,49 @@ const stampTool = () => state.selected = null; state.back = null; + const selectResource = (resource) => { + if (state.ctxmenu.uploadButton.disabled) return; + + const resourceWrapper = resource.dom.wrapper; + + const wasSelected = resourceWrapper.classList.contains("selected"); + + Array.from(state.ctxmenu.resourceList.children).forEach((child) => { + child.classList.remove("selected"); + }); + + // Select + if (!wasSelected) { + resourceWrapper.classList.add("selected"); + state.selected = resource; + } + // If already selected, clear selection + else { + resourceWrapper.classList.remove("selected"); + state.selected = null; + } + }; + + // Synchronizes resources array with the DOM const syncResources = () => { + // Creates DOM elements when needed state.resources.forEach((resource) => { - if (!document.getElementById(`resource-${resource.id}`)) { + if ( + !state.ctxmenu.resourceList.querySelector( + `#resource-${resource.id}` + ) + ) { + console.debug( + `Creating resource element 'resource-${resource.id}'` + ); const resourceWrapper = document.createElement("div"); resourceWrapper.id = `resource-${resource.id}`; resourceWrapper.textContent = resource.name; resourceWrapper.classList.add("resource"); - resourceWrapper.addEventListener("click", () => { - if (state.ctxmenu.uploadButton.disabled) return; - state.selected = resource; - Array.from(state.ctxmenu.resourceList.children).forEach( - (child) => { - child.classList.remove("selected"); - } - ); - - resourceWrapper.classList.add("selected"); - }); + resourceWrapper.addEventListener("click", () => + selectResource(resource) + ); resourceWrapper.addEventListener("mouseover", () => { state.ctxmenu.previewPane.style.display = "block"; @@ -78,16 +102,17 @@ const stampTool = () => }); state.ctxmenu.resourceList.appendChild(resourceWrapper); + resource.dom = {wrapper: resourceWrapper}; } }); + // Removes DOM elements when needed const elements = Array.from(state.ctxmenu.resourceList.children); if (elements.length > state.resources.length) elements.forEach((element) => { let remove = true; state.resources.some((resource) => { - console.debug(element.id, resource.id); if (element.id.endsWith(resource.id)) remove = false; }); @@ -95,6 +120,7 @@ const stampTool = () => }); }; + // Adds a image resource (temporary allows only one draw, used for pasting) state.addResource = (name, image, temporary = false) => { const id = guid(); const resource = { @@ -105,8 +131,15 @@ const stampTool = () => }; state.resources.push(resource); syncResources(); + + // Select this resource + selectResource(resource); + return resource; }; + + // Deletes a resource (Yes, functionality is here, but we don't have an UI for this yet) + // Used for temporary images too state.deleteResource = (id) => { state.resources = state.resources.filter((v) => v.id !== id); @@ -175,8 +208,10 @@ const stampTool = () => } } }; - }, - populateContextMenu: (menu, state) => { + + /** + * Creates context menu + */ if (!state.ctxmenu) { state.ctxmenu = {}; // Snap To Grid Checkbox @@ -218,7 +253,7 @@ const stampTool = () => const image = document.createElement("img"); image.src = url.createObjectURL(file); - state.selected = state.addResource(file.name, image, false); + state.addResource(file.name, image, false); } }); @@ -272,7 +307,8 @@ const stampTool = () => state.ctxmenu.resourceManager = resourceManager; state.ctxmenu.resourceList = resourceList; } - + }, + populateContextMenu: (menu, state) => { menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(state.ctxmenu.resourceManager); }, diff --git a/js/util.js b/js/util.js index bb75916..2383e9e 100644 --- a/js/util.js +++ b/js/util.js @@ -92,3 +92,77 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null) { h, }; } + +/** + * Triggers Canvas Download + */ +function cropCanvas(sourceCanvas) { + var w = sourceCanvas.width; + var h = sourceCanvas.height; + var pix = {x: [], y: []}; + var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h); + var x, y, index; + + for (y = 0; y < h; y++) { + for (x = 0; x < w; x++) { + // lol i need to learn what this part does + index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords + //this part i get, this is checking that 4th RGBA byte for opacity + if (imageData.data[index + 3] > 0) { + pix.x.push(x); + pix.y.push(y); + } + } + } + // ...need to learn what this part does too :badpokerface: + // is this just determining the boundaries of non-transparent pixel data? + pix.x.sort(function (a, b) { + return a - b; + }); + pix.y.sort(function (a, b) { + return a - b; + }); + var n = pix.x.length - 1; + w = pix.x[n] - pix.x[0] + 1; + h = pix.y[n] - pix.y[0] + 1; + // yup sure looks like it + + try { + var cut = sourceCanvas + .getContext("2d") + .getImageData(pix.x[0], pix.y[0], w, h); + var cutCanvas = document.createElement("canvas"); + cutCanvas.width = w; + cutCanvas.height = h; + cutCanvas.getContext("2d").putImageData(cut, 0, 0); + } catch (ex) { + // probably empty image + //TODO confirm edge cases? + cutCanvas = null; + } + return cutCanvas; +} + +function downloadCanvas(options) { + defaultOpt(options, { + cropToContent: true, + canvas: imgCanvas, + filename: + new Date() + .toISOString() + .slice(0, 19) + .replace("T", " ") + .replace(":", " ") + " openOutpaint image.png", + }); + + var link = document.createElement("a"); + link.download = options.filename; + + var croppedCanvas = options.cropToContent + ? cropCanvas(options.canvas) + : options.canvas; + if (croppedCanvas != null) { + link.href = croppedCanvas.toDataURL("image/png"); + link.click(); + } +}