a big commit with the selection and stamping tools

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>

Former-commit-id: b3f849f3fdb334916f1a4ee6c81ac6bdf42a8f52
This commit is contained in:
Victor Seiji Hariki 2022-11-25 00:34:34 -03:00
parent 42144ea577
commit 70d1fda23d
12 changed files with 830 additions and 43 deletions

View file

@ -30,20 +30,27 @@ body {
grid-row-gap: 5px;
}
.toolbar {
.button-array {
display: flex;
justify-content: space-between;
justify-content: stretch;
}
.toolbar > .tool {
.button-array > .button.tool {
flex: 1;
border-radius: 0;
}
.toolbar > .tool:not(:last-child) {
margin-right: 10px;
.button-array > .button.tool:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
button.tool {
.button-array > .button.tool:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.button.tool {
background-color: rgb(0, 0, 50);
color: rgb(255, 255, 255);
border-radius: 5px;
@ -57,8 +64,11 @@ button.tool {
margin-bottom: 5px;
}
button.tool:hover {
background-color: #667;
.button.tool:hover {
background-color: rgb(30, 30, 80);
}
.button.tool:active {
background-color: rgb(60, 60, 130);
}
.collapsible {
@ -100,7 +110,7 @@ button.tool:hover {
font-size: medium;
text-align: left;
max-height: fit-content;
overflow: auto;
overflow: visible;
cursor: auto;
}

77
css/ui/tool/stamp.css Normal file
View file

@ -0,0 +1,77 @@
.resource-manager {
position: relative;
border-radius: 5px;
}
.resource-manager {
user-select: none;
}
.resource-manager > .preview-pane {
display: none;
position: absolute;
height: 200px;
width: 200px;
right: -200px;
top: 0px;
background-color: white;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
border-radius: 2px;
}
.resource-manager > .resource-list {
height: 200px;
background-color: #ffffff66;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
overflow-y: scroll;
overflow-x: hidden;
}
.resource-manager > .resource-list > * {
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-manager > .resource-list > *:hover {
background-color: #ffff;
}
.resource-manager > .resource-list > .selected {
background-color: #fff6;
}
.resource-manager > .resource-list.dragging {
background-color: #ffffff88;
transition-duration: 0.3s;
}
.resource-manager > .upload-button {
display: block;
width: 100%;
padding-left: 0;
padding-right: 0;
margin-top: 0;
text-align: center;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border: 0px;
}

View file

@ -32,6 +32,29 @@
filter: invert(90%);
}
/* Toolbar lock indicator */
#ui-toolbar .lock-indicator {
position: absolute;
display: none;
padding: 0;
background-color: var(--c-text);
right: 2px;
top: 10px;
width: 15px;
height: 15px;
background-color: red;
-webkit-mask-image: url("/res/icons/lock.svg");
-webkit-mask-size: contain;
mask-image: url("/res/icons/lock.svg");
mask-size: contain;
}
/* The separator */
#ui-toolbar .separator {
width: 80%;

View file

@ -12,6 +12,9 @@
<link href="css/ui/history.css" rel="stylesheet" />
<link href="css/ui/toolbar.css" rel="stylesheet" />
<!-- Tool Specific CSS -->
<link href="css/ui/tool/stamp.css" rel="stylesheet" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
@ -165,11 +168,11 @@ people, person, humans, human, divers, diver, glitch, error, text, watermark, ba
<div class="draggable floating-window-title">History</div>
<div class="menu-container" style="min-width: 200px">
<div id="history" class="history"></div>
<div class="toolbar" style="padding: 10px">
<button type="button" onclick="commands.undo()" class="tool">
<div class="button-array" style="padding: 10px">
<button type="button" onclick="commands.undo()" class="button tool">
undo
</button>
<button type="button" onclick="commands.redo()" class="tool">
<button type="button" onclick="commands.redo()" class="button tool">
redo
</button>
</div>
@ -179,11 +182,12 @@ people, person, humans, human, divers, diver, glitch, error, text, watermark, ba
<!-- Toolbar -->
<div
id="ui-toolbar"
class="floating-window"
class="floating-window toolbar"
style="right: 10px; top: 350px">
<div class="draggable handle">
<span class="line"></span>
</div>
<div class="lock-indicator" id="toolbar-lock-indicator"></div>
<div class="toolbar-section"></div>
</div>
@ -296,6 +300,7 @@ people, person, humans, human, divers, diver, glitch, error, text, watermark, ba
<script src="js/ui/tool/dream.js" type="text/javascript"></script>
<script src="js/ui/tool/maskbrush.js" type="text/javascript"></script>
<script src="js/ui/tool/select.js" type="text/javascript"></script>
<script src="js/ui/tool/stamp.js" type="text/javascript"></script>
<script src="js/ui/toolbar.js" type="text/javascript"></script>
</body>

View file

@ -133,7 +133,7 @@ commands.createCommand(
options.x === undefined ||
options.y === undefined
)
throw "Command drawImage requires options in the format: {image, x, y, ctx?}";
throw "Command drawImage requires options in the format: {image, x, y, w?, h?, ctx?}";
// Check if we have state
if (!state.context) {
@ -144,14 +144,14 @@ commands.createCommand(
const imgData = context.getImageData(
options.x,
options.y,
options.image.width,
options.image.height
options.w || options.image.width,
options.h || options.image.height
);
state.box = {
x: options.x,
y: options.y,
w: options.image.width,
h: options.image.height,
w: options.w || options.image.width,
h: options.h || options.image.height,
};
// Create Image
const cutout = document.createElement("canvas");
@ -163,7 +163,17 @@ commands.createCommand(
}
// Apply command
state.context.drawImage(options.image, state.box.x, state.box.y);
state.context.drawImage(
options.image,
0,
0,
options.image.width,
options.image.height,
state.box.x,
state.box.y,
state.box.w,
state.box.h
);
},
(title, state) => {
// Clear destination area

View file

@ -392,7 +392,13 @@ const keyboard = {
callback,
});
},
deleteShortcut(id) {
deleteShortcut(id, key = null) {
if (key) {
this.shortcuts[key] = this.shortcuts[key].filter(
(v) => v.id !== id && v.callback !== id
);
return;
}
this.shortcuts.keys().forEach((key) => {
this.shortcuts[key] = this.shortcuts[key].filter(
(v) => v.id !== id && v.callback !== id

View file

@ -17,3 +17,9 @@ keyboard.onShortcut({key: "KeyM"}, () => {
keyboard.onShortcut({key: "KeyI"}, () => {
tools.img2img.enable();
});
keyboard.onShortcut({key: "KeyS"}, () => {
tools.selecttransform.enable();
});
keyboard.onShortcut({key: "KeyU"}, () => {
tools.stamp.enable();
});

View file

@ -8,33 +8,152 @@ const selectTransformTool = () =>
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, 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;
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.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),
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;
@ -43,6 +162,17 @@ const selectTransformTool = () =>
y += snap(evn.y, true, 64);
}
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y);
}
// Update position
if (state.moving) {
state.selected.x = x - state.moving.offset.x;
state.selected.y = y - state.moving.offset.y;
}
// Draw dragging box
if (state.dragging) {
ovCtx.setLineDash([2, 2]);
@ -58,22 +188,68 @@ const selectTransformTool = () =>
ovCtx.setLineDash([]);
}
// Draw selection box
if (state.selected) {
ovCtx.lineWidth = 1;
ovCtx.strokeStyle = "#FFF";
ovCtx.setLineDash([4, 2]);
ovCtx.strokeRect(
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 cuttent cursor location
// Draw current cursor location
ovCtx.lineWidth = 3;
ovCtx.strokeStyle = "#FFF";
@ -86,6 +262,29 @@ const selectTransformTool = () =>
}
};
state.clickcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
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;
@ -95,12 +294,27 @@ const selectTransformTool = () =>
iy += snap(evn.iy, true, 64);
}
state.dragging = {ix, iy};
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" && state.dragging) {
if (evn.target.id === "overlayCanvas") {
let x = evn.x;
let y = evn.y;
if (state.snapToGrid) {
@ -108,33 +322,154 @@ const selectTransformTool = () =>
y += snap(evn.y, true, 64);
}
state.selected = selectionBB(
state.dragging.ix,
state.dragging.iy,
x,
y
);
state.dragging = null;
if (state.scaling) {
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") {
if (state.dragging) state.dragging = null;
else state.selected = null;
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(evn);
state.reset();
}
};
// Keyboard callbacks
state.keydowncb = (evn) => {
console.debug(evn);
};
state.keydowncb = (evn) => {};
state.keyclickcb = (evn) => {
console.debug(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(
imgCanvas,
state.selected.x,
state.selected.y,
state.selected.w,
state.selected.h,
0,
0,
state.selected.w,
state.selected.h
);
// 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) => {
@ -146,8 +481,28 @@ const selectTransformTool = () =>
"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
}
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);
},
shortcut: "S",
}
);

269
js/ui/tool/stamp.js Normal file
View file

@ -0,0 +1,269 @@
const stampTool = () =>
toolbar.registerTool(
"res/icons/file-up.svg",
"Stamp Image",
(state, opt) => {
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
// Start Listeners
mouse.listen.canvas.onmousemove.on(state.movecb);
mouse.listen.canvas.left.onclick.on(state.drawcb);
// For calls from other tools to paste image
if (opt && opt.image) {
state.selected = state.addResource(
opt.name || "Clipboard",
opt.image,
opt.temporary === undefined ? true : opt.temporary
);
console.debug(state.selected);
state.ctxmenu.uploadButton.disabled = true;
state.back = opt.back || null;
} else if (opt) {
throw Error(
"Pasting from other tools must be in format {image, name?, temporary?, back?}"
);
} else {
state.ctxmenu.uploadButton.disabled = "";
}
},
(state, opt) => {
// Clear Listeners
mouse.listen.canvas.onmousemove.clear(state.movecb);
mouse.listen.canvas.left.onclick.clear(state.drawcb);
},
{
init: (state) => {
state.snapToGrid = true;
state.resources = [];
state.selected = null;
state.back = null;
const syncResources = () => {
state.resources.forEach((resource) => {
if (!document.getElementById(`resource-${resource.id}`)) {
const resourceWrapper = document.createElement("div");
resourceWrapper.id = `resource-${resource.id}`;
resourceWrapper.textContent = resource.name;
resourceWrapper.classList.add("resource");
resourceWrapper.addEventListener("click", () => {
state.selected = resource;
Array.from(state.ctxmenu.resourceList.children).forEach(
(child) => {
child.classList.remove("selected");
}
);
resourceWrapper.classList.add("selected");
});
resourceWrapper.addEventListener("mouseover", () => {
state.ctxmenu.previewPane.style.display = "block";
state.ctxmenu.previewPane.style.backgroundImage = `url(${resource.image.src})`;
});
resourceWrapper.addEventListener("mouseleave", () => {
state.ctxmenu.previewPane.style.display = "none";
});
state.ctxmenu.resourceList.appendChild(resourceWrapper);
}
});
const elements = Array.from(state.ctxmenu.resourceList.children);
if (elements.length > state.resources.length)
elements.forEach((element) => {
for (let resource in state.resources) {
if (element.id.endsWith(resource.id)) return;
}
state.ctxmenu.resourceList.removeChild(element);
});
};
state.addResource = (name, image, temporary = false) => {
const id = guid();
const resource = {
id,
name,
image,
temporary,
};
state.resources.push(resource);
syncResources();
return resource;
};
state.deleteResource = (id) => {
state.resources = state.resources.filter((v) => v.id !== id);
syncResources();
};
state.movecb = (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);
}
// Draw selected image
if (state.selected) {
ovCtx.drawImage(state.selected.image, x, y);
}
// 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.drawcb = (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);
}
const resource = state.selected;
if (resource) {
commands.runCommand("drawImage", "Image Stamp", {
image: resource.image,
x,
y,
});
if (resource.temporary) state.deleteResource(resource.id);
}
if (state.back) {
toolbar.unlock();
state.back({message: "Returning from stamp", pasted: true});
}
}
};
state.cancelcb = (evn) => {
if (evn.target.id === "overlayCanvas") {
if (state.back) {
toolbar.unlock();
state.back({message: "Returning from stamp", pasted: false});
}
}
};
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Snap To Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
"snapToGrid",
"Snap To Grid"
).label;
// Create resource list
const uploadButtonId = `upload-btn-${guid()}`;
const resourceManager = document.createElement("div");
resourceManager.classList.add("resource-manager");
const resourceList = document.createElement("div");
resourceList.classList.add("resource-list");
const previewPane = document.createElement("div");
previewPane.classList.add("preview-pane");
const uploadLabel = document.createElement("label");
uploadLabel.classList.add("upload-button");
uploadLabel.classList.add("button");
uploadLabel.classList.add("tool");
uploadLabel.textContent = "Upload Image";
uploadLabel.htmlFor = uploadButtonId;
const uploadButton = document.createElement("input");
uploadButton.id = uploadButtonId;
uploadButton.type = "file";
uploadButton.accept = "image/*";
uploadButton.multiple = true;
uploadButton.style.display = "none";
uploadButton.addEventListener("change", (evn) => {
[...uploadButton.files].forEach((file) => {
if (file.type.startsWith("image/")) {
console.info("Uploading Image " + file.name);
const url = window.URL || window.webkitURL;
const image = document.createElement("img");
image.src = url.createObjectURL(file);
state.selected = state.addResource(file.name, image, false);
}
});
uploadButton.value = null;
});
uploadLabel.appendChild(uploadButton);
resourceManager.appendChild(resourceList);
resourceManager.appendChild(uploadLabel);
resourceManager.appendChild(previewPane);
resourceManager.addEventListener(
"drop",
(evn) => {
evn.preventDefault();
resourceManager.classList.remove("dragging");
if (evn.dataTransfer.items) {
Array.from(evn.dataTransfer.items).forEach((item) => {
if (item.kind === "file" && item.type.startsWith("image/")) {
const file = item.getAsFile();
const url = window.URL || window.webkitURL;
const image = document.createElement("img");
image.src = url.createObjectURL(file);
state.addResource(file.name, image, false);
}
});
}
},
{passive: false}
);
resourceManager.addEventListener(
"dragover",
(evn) => {
evn.preventDefault();
},
{passive: false}
);
resourceManager.addEventListener("dragover", (evn) => {
resourceManager.classList.add("dragging");
});
resourceManager.addEventListener("dragover", (evn) => {
resourceManager.classList.remove("dragging");
});
state.ctxmenu.uploadButton = uploadButton;
state.ctxmenu.previewPane = previewPane;
state.ctxmenu.resourceManager = resourceManager;
state.ctxmenu.resourceList = resourceList;
}
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(state.ctxmenu.resourceManager);
},
shortcut: "U",
}
);

View file

@ -3,10 +3,21 @@
*/
const toolbar = {
_locked: false,
_toolbar: document.getElementById("ui-toolbar"),
_toolbar_lock_indicator: document.getElementById("toolbar-lock-indicator"),
tools: [],
lock() {
toolbar._locked = true;
toolbar._toolbar_lock_indicator.style.display = "block";
},
unlock() {
toolbar._locked = false;
toolbar._toolbar_lock_indicator.style.display = "none";
},
_makeToolbarEntry: (tool) => {
const toolTitle = document.createElement("img");
toolTitle.classList.add("tool-icon");
@ -78,6 +89,8 @@ const toolbar = {
state: {},
options,
enable: (opt = null) => {
if (toolbar._locked) return;
this.tools.filter((t) => t.enabled).forEach((t) => t.disable());
while (contextMenuEl.lastChild) {
@ -201,5 +214,6 @@ tools.maskbrush = maskBrushTool();
toolbar.addSeparator();
tools.selecttransform = selectTransformTool();
tools.stamp = stampTool();
toolbar.tools[0].enable();
toolbar.tools[toolbar.tools.length - 1].enable();

7
res/icons/file-up.svg Normal file
View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<path d="M12 12v6"></path>
<path d="m15 15-3-3-3 3"></path>
</svg>

After

Width:  |  Height:  |  Size: 391 B

5
res/icons/lock.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>

After

Width:  |  Height:  |  Size: 300 B