df5dc32eee
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 <victorseijih@gmail.com> Former-commit-id: 963312115d4827de1c8d4fac88a97dd64db7fa15
566 lines
15 KiB
JavaScript
566 lines
15 KiB
JavaScript
const selectTransformTool = () =>
|
|
toolbar.registerTool(
|
|
"res/icons/box-select.svg",
|
|
"Select Image",
|
|
(state, opt) => {
|
|
// Draw new cursor immediately
|
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
|
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
|
|
|
|
mouse.listen.canvas.onmousemove.on(state.movecb);
|
|
mouse.listen.canvas.left.onclick.on(state.clickcb);
|
|
mouse.listen.canvas.left.ondragstart.on(state.dragstartcb);
|
|
mouse.listen.canvas.left.ondragend.on(state.dragendcb);
|
|
|
|
mouse.listen.canvas.right.onclick.on(state.cancelcb);
|
|
|
|
keyboard.listen.onkeyclick.on(state.keyclickcb);
|
|
keyboard.listen.onkeydown.on(state.keydowncb);
|
|
keyboard.onShortcut({ctrl: true, key: "KeyC"}, state.ctrlccb);
|
|
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);
|
|
mouse.listen.canvas.left.onclick.clear(state.clickcb);
|
|
mouse.listen.canvas.left.ondragstart.clear(state.dragstartcb);
|
|
mouse.listen.canvas.left.ondragend.clear(state.dragendcb);
|
|
|
|
mouse.listen.canvas.right.onclick.clear(state.cancelcb);
|
|
|
|
keyboard.listen.onkeyclick.clear(state.keyclickcb);
|
|
keyboard.listen.onkeydown.clear(state.keydowncb);
|
|
keyboard.deleteShortcut(state.ctrlccb, "KeyC");
|
|
keyboard.deleteShortcut(state.ctrlvcb, "KeyV");
|
|
keyboard.deleteShortcut(state.ctrlxcb, "KeyX");
|
|
keyboard.deleteShortcut(state.ctrlscb, "KeyS");
|
|
|
|
state.reset();
|
|
|
|
ovCanvas.style.cursor = "auto";
|
|
},
|
|
{
|
|
init: (state) => {
|
|
state.clipboard = {};
|
|
|
|
state.snapToGrid = true;
|
|
state.keepAspectRatio = true;
|
|
state.useClipboard = !!navigator.clipboard.write; // Use it by default if supported
|
|
|
|
state.original = null;
|
|
state.dragging = 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;
|
|
state.lastMouseMove = null;
|
|
|
|
const redraw = () => {
|
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
|
state.movecb(state.lastMouseMove);
|
|
};
|
|
|
|
state.reset = () => {
|
|
if (state.selected)
|
|
imgCtx.drawImage(
|
|
state.selected.image,
|
|
state.selected.original.x,
|
|
state.selected.original.y
|
|
);
|
|
|
|
if (state.dragging) state.dragging = null;
|
|
else state.selected = null;
|
|
|
|
redraw();
|
|
};
|
|
|
|
const selectionBB = (x1, y1, x2, y2) => {
|
|
return {
|
|
original: {
|
|
x: Math.min(x1, x2),
|
|
y: Math.min(y1, y2),
|
|
w: Math.abs(x1 - x2),
|
|
h: Math.abs(y1 - y2),
|
|
},
|
|
x: Math.min(x1, x2),
|
|
y: Math.min(y1, y2),
|
|
w: Math.abs(x1 - x2),
|
|
h: Math.abs(y1 - y2),
|
|
updateOriginal() {
|
|
this.original.x = this.x;
|
|
this.original.y = this.y;
|
|
this.original.w = this.w;
|
|
this.original.h = this.h;
|
|
},
|
|
contains(x, y) {
|
|
return (
|
|
this.x <= x &&
|
|
x <= this.x + this.w &&
|
|
this.y <= y &&
|
|
y <= this.y + this.h
|
|
);
|
|
},
|
|
handles() {
|
|
const _createHandle = (x, y, originOffset = null, size = 10) => {
|
|
return {
|
|
x: x - size / 2,
|
|
y: y - size / 2,
|
|
w: size,
|
|
h: size,
|
|
contains(x, y) {
|
|
return (
|
|
this.x <= x &&
|
|
x <= this.x + this.w &&
|
|
this.y <= y &&
|
|
y <= this.y + this.h
|
|
);
|
|
},
|
|
scaleTo: (tx, ty, keepAspectRatio = true) => {
|
|
const origin = {
|
|
x: this.original.x + this.original.w / 2,
|
|
y: this.original.y + this.original.h / 2,
|
|
};
|
|
let nx = tx;
|
|
let ny = ty;
|
|
|
|
let xRatio = (nx - origin.x) / (x - origin.x);
|
|
let yRatio = (ny - origin.y) / (y - origin.y);
|
|
if (keepAspectRatio)
|
|
xRatio = yRatio = Math.min(xRatio, yRatio);
|
|
|
|
if (Number.isFinite(xRatio)) {
|
|
let left = this.original.x;
|
|
let right = this.original.x + this.original.w;
|
|
|
|
left = (left - origin.x) * xRatio + origin.x;
|
|
right = (right - origin.x) * xRatio + origin.x;
|
|
|
|
this.x = left;
|
|
this.w = right - left;
|
|
}
|
|
|
|
if (Number.isFinite(yRatio)) {
|
|
let top = this.original.y;
|
|
let bottom = this.original.y + this.original.h;
|
|
|
|
top = (top - origin.y) * yRatio + origin.y;
|
|
bottom = (bottom - origin.y) * yRatio + origin.y;
|
|
|
|
this.y = top;
|
|
this.h = bottom - top;
|
|
}
|
|
},
|
|
};
|
|
};
|
|
return [
|
|
_createHandle(this.x, this.y),
|
|
_createHandle(this.x + this.w, this.y),
|
|
_createHandle(this.x, this.y + this.h),
|
|
_createHandle(this.x + this.w, this.y + this.h),
|
|
];
|
|
},
|
|
};
|
|
};
|
|
|
|
state.movecb = (evn) => {
|
|
ovCanvas.style.cursor = "auto";
|
|
state.lastMouseTarget = evn.target;
|
|
state.lastMouseMove = evn;
|
|
if (evn.target.id === "overlayCanvas") {
|
|
let x = evn.x;
|
|
let y = evn.y;
|
|
if (state.snapToGrid) {
|
|
x += snap(evn.x, true, 64);
|
|
y += snap(evn.y, true, 64);
|
|
}
|
|
|
|
// Update scale
|
|
if (state.scaling) {
|
|
state.scaling.scaleTo(x, y, state.keepAspectRatio);
|
|
}
|
|
|
|
// Update position
|
|
if (state.moving) {
|
|
state.selected.x = x - state.moving.offset.x;
|
|
state.selected.y = y - state.moving.offset.y;
|
|
state.selected.updateOriginal();
|
|
}
|
|
|
|
// Draw dragging box
|
|
if (state.dragging) {
|
|
ovCtx.setLineDash([2, 2]);
|
|
ovCtx.lineWidth = 1;
|
|
ovCtx.strokeStyle = "#FFF";
|
|
|
|
const ix = state.dragging.ix;
|
|
const iy = state.dragging.iy;
|
|
|
|
const bb = selectionBB(ix, iy, x, y);
|
|
|
|
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
|
|
ovCtx.setLineDash([]);
|
|
}
|
|
|
|
if (state.selected) {
|
|
ovCtx.lineWidth = 1;
|
|
ovCtx.strokeStyle = "#FFF";
|
|
|
|
const bb = {
|
|
x: state.selected.x,
|
|
y: state.selected.y,
|
|
w: state.selected.w,
|
|
h: state.selected.h,
|
|
};
|
|
|
|
// Draw Image
|
|
ovCtx.drawImage(
|
|
state.selected.image,
|
|
0,
|
|
0,
|
|
state.selected.image.width,
|
|
state.selected.image.height,
|
|
state.selected.x,
|
|
state.selected.y,
|
|
state.selected.w,
|
|
state.selected.h
|
|
);
|
|
|
|
// Draw selection box
|
|
ovCtx.setLineDash([4, 2]);
|
|
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
|
|
ovCtx.setLineDash([]);
|
|
|
|
// Draw Scaling/Rotation Origin
|
|
ovCtx.beginPath();
|
|
ovCtx.arc(
|
|
state.selected.x + state.selected.w / 2,
|
|
state.selected.y + state.selected.h / 2,
|
|
5,
|
|
0,
|
|
2 * Math.PI
|
|
);
|
|
ovCtx.stroke();
|
|
|
|
// Draw Scaling Handles
|
|
let cursorInHandle = false;
|
|
state.selected.handles().forEach((handle) => {
|
|
if (handle.contains(evn.x, evn.y)) {
|
|
cursorInHandle = true;
|
|
ovCtx.strokeRect(
|
|
handle.x - 1,
|
|
handle.y - 1,
|
|
handle.w + 2,
|
|
handle.h + 2
|
|
);
|
|
} else {
|
|
ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h);
|
|
}
|
|
});
|
|
|
|
// Change cursor
|
|
if (cursorInHandle || state.selected.contains(evn.x, evn.y))
|
|
ovCanvas.style.cursor = "pointer";
|
|
}
|
|
|
|
// Draw current cursor location
|
|
ovCtx.lineWidth = 3;
|
|
ovCtx.strokeStyle = "#FFF";
|
|
|
|
ovCtx.beginPath();
|
|
ovCtx.moveTo(x, y + 10);
|
|
ovCtx.lineTo(x, y - 10);
|
|
ovCtx.moveTo(x + 10, y);
|
|
ovCtx.lineTo(x - 10, y);
|
|
ovCtx.stroke();
|
|
}
|
|
};
|
|
|
|
state.clickcb = (evn) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
if (state.selected) {
|
|
imgCtx.drawImage(
|
|
state.selected.image,
|
|
state.original.x,
|
|
state.original.y
|
|
);
|
|
commands.runCommand(
|
|
"eraseImage",
|
|
"Image Transform Erase",
|
|
state.original
|
|
);
|
|
commands.runCommand(
|
|
"drawImage",
|
|
"Image Transform Draw",
|
|
state.selected
|
|
);
|
|
state.original = null;
|
|
state.selected = null;
|
|
|
|
redraw();
|
|
}
|
|
}
|
|
};
|
|
state.dragstartcb = (evn) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
let ix = evn.ix;
|
|
let iy = evn.iy;
|
|
if (state.snapToGrid) {
|
|
ix += snap(evn.ix, true, 64);
|
|
iy += snap(evn.iy, true, 64);
|
|
}
|
|
|
|
if (state.selected) {
|
|
const handles = state.selected.handles();
|
|
|
|
const activeHandle = handles.find((v) =>
|
|
v.contains(evn.ix, evn.iy)
|
|
);
|
|
if (activeHandle) {
|
|
state.scaling = activeHandle;
|
|
} else if (state.selected.contains(ix, iy)) {
|
|
state.moving = {
|
|
offset: {x: ix - state.selected.x, y: iy - state.selected.y},
|
|
};
|
|
}
|
|
} else {
|
|
state.dragging = {ix, iy};
|
|
}
|
|
}
|
|
};
|
|
|
|
state.dragendcb = (evn) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
let x = evn.x;
|
|
let y = evn.y;
|
|
if (state.snapToGrid) {
|
|
x += snap(evn.x, true, 64);
|
|
y += snap(evn.y, true, 64);
|
|
}
|
|
|
|
if (state.scaling) {
|
|
state.selected.updateOriginal();
|
|
state.scaling = null;
|
|
} else if (state.moving) {
|
|
state.moving = null;
|
|
} else if (state.dragging) {
|
|
state.original = selectionBB(
|
|
state.dragging.ix,
|
|
state.dragging.iy,
|
|
x,
|
|
y
|
|
);
|
|
state.selected = selectionBB(
|
|
state.dragging.ix,
|
|
state.dragging.iy,
|
|
x,
|
|
y
|
|
);
|
|
|
|
// Cut out selected portion of the image for manipulation
|
|
const cvs = document.createElement("canvas");
|
|
cvs.width = state.selected.w;
|
|
cvs.height = state.selected.h;
|
|
const ctx = cvs.getContext("2d");
|
|
|
|
ctx.drawImage(
|
|
imgCanvas,
|
|
state.selected.x,
|
|
state.selected.y,
|
|
state.selected.w,
|
|
state.selected.h,
|
|
0,
|
|
0,
|
|
state.selected.w,
|
|
state.selected.h
|
|
);
|
|
|
|
imgCtx.clearRect(
|
|
state.selected.x,
|
|
state.selected.y,
|
|
state.selected.w,
|
|
state.selected.h
|
|
);
|
|
state.selected.image = cvs;
|
|
state.original.image = cvs;
|
|
|
|
if (state.selected.w === 0 || state.selected.h === 0)
|
|
state.selected = null;
|
|
|
|
state.dragging = null;
|
|
}
|
|
redraw();
|
|
}
|
|
};
|
|
|
|
state.cancelcb = (evn) => {
|
|
if (evn.target.id === "overlayCanvas") {
|
|
state.reset();
|
|
}
|
|
};
|
|
|
|
// Keyboard callbacks
|
|
state.keydowncb = (evn) => {};
|
|
|
|
state.keyclickcb = (evn) => {
|
|
if (state.lastMouseTarget.id === "overlayCanvas") {
|
|
switch (evn.code) {
|
|
case "Delete":
|
|
state.selected &&
|
|
commands.runCommand(
|
|
"eraseImage",
|
|
"Erase Area",
|
|
state.selected
|
|
);
|
|
state.selected = null;
|
|
redraw();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Register Ctrl-C/V Shortcut
|
|
state.ctrlccb = (evn) => {
|
|
if (state.selected && state.lastMouseTarget.id === "overlayCanvas") {
|
|
state.clipboard.copy = document.createElement("canvas");
|
|
|
|
state.clipboard.copy.width = state.selected.w;
|
|
state.clipboard.copy.height = state.selected.h;
|
|
|
|
const ctx = state.clipboard.copy.getContext("2d");
|
|
|
|
ctx.clearRect(0, 0, state.selected.w, state.selected.h);
|
|
ctx.drawImage(state.selected.image, 0, 0);
|
|
|
|
// Because firefox needs manual activation of the feature
|
|
if (state.useClipboard) {
|
|
state.clipboard.copy.toBlob((blob) => {
|
|
const item = new ClipboardItem({"image/png": blob});
|
|
navigator.clipboard.write([item]).catch((e) => {
|
|
console.warn("Error sending to clipboard");
|
|
console.warn(e);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
state.ctrlvcb = (evn) => {
|
|
if (state.useClipboard) {
|
|
navigator.clipboard.read().then((items) => {
|
|
console.info(items[0]);
|
|
for (const item of items) {
|
|
for (const type of item.types) {
|
|
if (type.startsWith("image/")) {
|
|
item.getType(type).then((blob) => {
|
|
// Converts blob to image
|
|
const url = window.URL || window.webkitURL;
|
|
const image = document.createElement("img");
|
|
image.src = url.createObjectURL(file);
|
|
tools.stamp.enable({
|
|
image,
|
|
back: tools.selecttransform.enable,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else if (state.clipboard.copy) {
|
|
const image = document.createElement("img");
|
|
image.src = state.clipboard.copy.toDataURL();
|
|
|
|
tools.stamp.enable({
|
|
image,
|
|
back: tools.selecttransform.enable,
|
|
});
|
|
}
|
|
};
|
|
state.ctrlxcb = (evn) => {};
|
|
state.ctrlscb = (evn) => {
|
|
evn.evn.preventDefault();
|
|
};
|
|
},
|
|
populateContextMenu: (menu, state) => {
|
|
if (!state.ctxmenu) {
|
|
state.ctxmenu = {};
|
|
// Snap To Grid Checkbox
|
|
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"snapToGrid",
|
|
"Snap To Grid"
|
|
).label;
|
|
// Keep Aspect Ratio
|
|
state.ctxmenu.keepAspectRatioLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"keepAspectRatio",
|
|
"Keep Aspect Ratio"
|
|
).label;
|
|
// Use Clipboard
|
|
const clipboardCheckbox = _toolbar_input.checkbox(
|
|
state,
|
|
"useClipboard",
|
|
"Use clipboard"
|
|
);
|
|
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",
|
|
}
|
|
);
|