From df5dc32eee79aadcd7dca1137c75ce967d3588ac Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Fri, 25 Nov 2022 13:16:22 -0300 Subject: [PATCH 1/2] Some quick lunch patches Allows for selections and dreams to be sent to the resource manager, allows resource deselection when clicking on the currently selected item, put some extra comments and allow saving of a selected canvas area Signed-off-by: Victor Seiji Hariki Former-commit-id: 963312115d4827de1c8d4fac88a97dd64db7fa15 --- css/index.css | 5 +++ js/index.js | 74 +++++++++----------------------------------- js/ui/tool/select.js | 53 ++++++++++++++++++++++++++++++- js/ui/tool/stamp.js | 70 +++++++++++++++++++++++++++++++---------- js/util.js | 74 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 78 deletions(-) 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 8bd17b0..af22566 100644 --- a/js/index.js +++ b/js/index.js @@ -204,7 +204,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; @@ -237,6 +238,18 @@ 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(); @@ -576,65 +589,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(); + } +} From 7bb22380563866522e5d8f057cc8a206ce5acdef Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Fri, 25 Nov 2022 13:25:16 -0300 Subject: [PATCH 2/2] clear canvas now has history Signed-off-by: Victor Seiji Hariki Former-commit-id: 6ec970f947ee6a7472a6e376c236885d26f5b276 --- js/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index af22566..e1df930 100644 --- a/js/index.js +++ b/js/index.js @@ -254,7 +254,12 @@ 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) {