Merge pull request #50 from seijihariki/quick-patches

Dream and Selection - Resource Manager Interaction and Selected Area saving

Former-commit-id: d361b4731686b52d5dc48a4717d637569a03180a
This commit is contained in:
tim h 2022-11-25 10:28:28 -06:00 committed by GitHub
commit 905a815cf2
5 changed files with 204 additions and 79 deletions

View file

@ -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);
}

View file

@ -205,7 +205,8 @@ function imageAcceptReject(x, y, data, extra = null) {
div.style.width = "200px";
div.style.height = "70px";
div.innerHTML =
'<button onclick="prevImg(this)">&lt;</button><button onclick="nextImg(this)">&gt;</button><span class="strokeText" id="currentImgIndex"></span><span class="strokeText"> of </span><span class="strokeText" id="totalImgIndex"></span><button onclick="accept(this)">Y</button><button onclick="reject(this)">N</button><span class="strokeText" id="estRemaining"></span>';
'<button onclick="prevImg(this)">&lt;</button><button onclick="nextImg(this)">&gt;</button><span class="strokeText" id="currentImgIndex"></span><span class="strokeText"> of </span><span class="strokeText" id="totalImgIndex"></span><button onclick="accept(this)">Y</button><button onclick="reject(this)">N</button><button onclick="resource(this)">RES</button><span class="strokeText" id="estRemaining"></span>';
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)

View file

@ -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",
}

View file

@ -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);
},

View file

@ -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();
}
}