Merge branch 'pr/49' into main
Former-commit-id: d4295067d1ed96215cbe3d5bcc274e7b73ab388e
This commit is contained in:
commit
317c921852
19 changed files with 1285 additions and 348 deletions
|
@ -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
77
css/ui/tool/stamp.css
Normal 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;
|
||||
}
|
|
@ -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%;
|
||||
|
|
20
index.html
20
index.html
|
@ -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>
|
||||
|
||||
|
@ -92,12 +95,8 @@ people, person, humans, human, divers, diver, glitch, error, text, watermark, ba
|
|||
onchange="changeMaskBlur()" />
|
||||
<br />
|
||||
<!-- Save/load image section -->
|
||||
<button type="button" class="collapsible">Save/Load/New image</button>
|
||||
<button type="button" class="collapsible">Save/Upscaling</button>
|
||||
<div class="content">
|
||||
<label for="preloadImage">Load image:</label>
|
||||
<input type="file" id="preloadImage" onchange="preloadImage()"
|
||||
accept="image/*" / style="width: 200px;">
|
||||
<br />
|
||||
<button onclick="downloadCanvas()">Save canvas</button>
|
||||
<br />
|
||||
<label for="upscalers">Choose upscaler</label>
|
||||
|
@ -165,11 +164,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 +178,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>
|
||||
|
||||
|
@ -295,6 +295,8 @@ people, person, humans, human, divers, diver, glitch, error, text, watermark, ba
|
|||
<!-- Load Tools -->
|
||||
<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>
|
||||
|
|
|
@ -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
|
||||
|
|
56
js/index.js
56
js/index.js
|
@ -130,25 +130,6 @@ function startup() {
|
|||
document.getElementById("scaleFactor").value = scaleFactor;
|
||||
}
|
||||
|
||||
function drop(imageParams) {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
writeArbitraryImage(img, imageParams.x, imageParams.y);
|
||||
};
|
||||
img.src = arbitraryImageBase64;
|
||||
}
|
||||
|
||||
function writeArbitraryImage(img, x, y) {
|
||||
commands.runCommand("drawImage", "Image Stamp", {
|
||||
x,
|
||||
y,
|
||||
image: img,
|
||||
});
|
||||
blockNewImages = false;
|
||||
placingArbitraryImage = false;
|
||||
document.getElementById("preloadImage").files = null;
|
||||
}
|
||||
|
||||
function dream(
|
||||
x,
|
||||
y,
|
||||
|
@ -611,43 +592,6 @@ function drawBackground() {
|
|||
}
|
||||
}
|
||||
|
||||
function preloadImage() {
|
||||
// possible firefox-only bug?
|
||||
// attempt to prevent requesting a dream if double-clicking a selected image
|
||||
document.getElementById("overlayCanvas").onmousemove = null;
|
||||
document.getElementById("overlayCanvas").onmousedown = null;
|
||||
document.getElementById("overlayCanvas").onmouseup = null;
|
||||
|
||||
var file = document.getElementById("preloadImage").files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (evt) {
|
||||
var imgCanvas = document.createElement("canvas");
|
||||
var imgCtx = imgCanvas.getContext("2d");
|
||||
arbitraryImage = new Image();
|
||||
arbitraryImage.onload = function () {
|
||||
blockNewImages = true;
|
||||
// now put it into imagedata for canvas fun
|
||||
imgCanvas.width = arbitraryImage.width;
|
||||
imgCanvas.height = arbitraryImage.height;
|
||||
imgCtx.drawImage(arbitraryImage, 0, 0);
|
||||
arbitraryImageData = imgCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
arbitraryImage.width,
|
||||
arbitraryImage.height
|
||||
); // can't use that to drawImage on a canvas...
|
||||
arbitraryImageBitmap = createImageBitmap(arbitraryImageData); // apparently that either... maybe just the raw image?
|
||||
arbitraryImageBase64 = imgCanvas.toDataURL();
|
||||
placingArbitraryImage = true;
|
||||
document.getElementById("overlayCanvas").onmousemove = mouseMove;
|
||||
document.getElementById("overlayCanvas").onmousedown = mouseDown;
|
||||
document.getElementById("overlayCanvas").onmouseup = mouseUp;
|
||||
};
|
||||
arbitraryImage.src = evt.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function downloadCanvas() {
|
||||
var link = document.createElement("a");
|
||||
link.download =
|
||||
|
|
22
js/input.js
22
js/input.js
|
@ -201,6 +201,8 @@ window.onmouseup = (evn) => {
|
|||
target: evn.target,
|
||||
initialTarget: mouse.coords[name].dragging[key].target,
|
||||
buttonId: evn.button,
|
||||
ix: mouse.coords[name].dragging[key].x,
|
||||
iy: mouse.coords[name].dragging[key].y,
|
||||
x: mouse.coords[name].pos.x,
|
||||
y: mouse.coords[name].pos.y,
|
||||
evn,
|
||||
|
@ -213,6 +215,8 @@ window.onmouseup = (evn) => {
|
|||
target: evn.target,
|
||||
initialTarget: mouse.coords[name].dragging[key].target,
|
||||
buttonId: evn.button,
|
||||
ix: mouse.coords[name].dragging[key].x,
|
||||
iy: mouse.coords[name].dragging[key].y,
|
||||
x: mouse.coords[name].pos.x,
|
||||
y: mouse.coords[name].pos.y,
|
||||
evn,
|
||||
|
@ -264,6 +268,8 @@ window.onmousemove = (evn) => {
|
|||
mouse.listen[name][key].ondragstart.emit({
|
||||
target: evn.target,
|
||||
buttonId: evn.button,
|
||||
ix: mouse.coords[name].dragging[key].x,
|
||||
iy: mouse.coords[name].dragging[key].y,
|
||||
x: mouse.coords[name].pos.x,
|
||||
y: mouse.coords[name].pos.y,
|
||||
evn,
|
||||
|
@ -283,6 +289,8 @@ window.onmousemove = (evn) => {
|
|||
target: evn.target,
|
||||
initialTarget: mouse.coords[name].dragging[key].target,
|
||||
button: index,
|
||||
ix: mouse.coords[name].dragging[key].x,
|
||||
iy: mouse.coords[name].dragging[key].y,
|
||||
px: mouse.coords[name].prev.x,
|
||||
py: mouse.coords[name].prev.y,
|
||||
x: mouse.coords[name].pos.x,
|
||||
|
@ -297,6 +305,8 @@ window.onmousemove = (evn) => {
|
|||
target: evn.target,
|
||||
initialTarget: mouse.coords[name].dragging[key].target,
|
||||
button: index,
|
||||
ix: mouse.coords[name].dragging[key].x,
|
||||
iy: mouse.coords[name].dragging[key].y,
|
||||
px: mouse.coords[name].prev.x,
|
||||
py: mouse.coords[name].prev.y,
|
||||
x: mouse.coords[name].pos.x,
|
||||
|
@ -382,9 +392,17 @@ 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);
|
||||
this.shortcuts[key] = this.shortcuts[key].filter(
|
||||
(v) => v.id !== id && v.callback !== id
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -107,6 +107,7 @@ function createSlider(name, wrapper, options = {}) {
|
|||
textEl.value = `${name}: ${value}`;
|
||||
});
|
||||
textEl.addEventListener("focus", () => {
|
||||
overEl.style.pointerEvents = "none";
|
||||
textEl.value = value;
|
||||
});
|
||||
|
||||
|
@ -125,7 +126,6 @@ function createSlider(name, wrapper, options = {}) {
|
|||
|
||||
mouse.listen.window.left.onclick.on((evn) => {
|
||||
if (evn.target === overEl) {
|
||||
overEl.style.pointerEvents = "none";
|
||||
textEl.select();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -212,3 +212,179 @@ const dream_img2img_callback = (evn, state) => {
|
|||
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers Tools
|
||||
*/
|
||||
const dreamTool = () =>
|
||||
toolbar.registerTool(
|
||||
"res/icons/image-plus.svg",
|
||||
"Dream",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({
|
||||
...mouse.coords.canvas.pos,
|
||||
target: {id: "overlayCanvas"},
|
||||
});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.snapToGrid = true;
|
||||
state.overMaskPx = 0;
|
||||
state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid);
|
||||
state.dreamcb = (evn) => {
|
||||
dream_generate_callback(evn, state);
|
||||
};
|
||||
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"snapToGrid",
|
||||
"Snap To Grid"
|
||||
).label;
|
||||
state.ctxmenu.overMaskPxLabel = _toolbar_input.slider(
|
||||
state,
|
||||
"overMaskPx",
|
||||
"Overmask px",
|
||||
0,
|
||||
128,
|
||||
1
|
||||
).slider;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.overMaskPxLabel);
|
||||
},
|
||||
shortcut: "D",
|
||||
}
|
||||
);
|
||||
|
||||
const img2imgTool = () =>
|
||||
toolbar.registerTool(
|
||||
"res/icons/image.svg",
|
||||
"Img2Img",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({
|
||||
...mouse.coords.canvas.pos,
|
||||
target: {id: "overlayCanvas"},
|
||||
});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.snapToGrid = true;
|
||||
state.denoisingStrength = 0.7;
|
||||
|
||||
state.borderMaskSize = 64;
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
_reticle_draw(evn, state.snapToGrid);
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// For displaying border mask
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = bb.w;
|
||||
auxCanvas.height = bb.h;
|
||||
const auxCtx = auxCanvas.getContext("2d");
|
||||
|
||||
if (state.borderMaskSize > 0) {
|
||||
auxCtx.fillStyle = "#FF6A6A50";
|
||||
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
|
||||
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
|
||||
auxCtx.fillRect(
|
||||
bb.w - state.borderMaskSize,
|
||||
0,
|
||||
state.borderMaskSize,
|
||||
bb.h
|
||||
);
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
bb.h - state.borderMaskSize,
|
||||
bb.w,
|
||||
state.borderMaskSize
|
||||
);
|
||||
}
|
||||
|
||||
const tmp = ovCtx.globalAlpha;
|
||||
ovCtx.globalAlpha = 0.4;
|
||||
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
|
||||
ovCtx.globalAlpha = tmp;
|
||||
};
|
||||
state.dreamcb = (evn) => {
|
||||
dream_img2img_callback(evn, state);
|
||||
};
|
||||
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
// Snap To Grid Checkbox
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"snapToGrid",
|
||||
"Snap To Grid"
|
||||
).label;
|
||||
|
||||
// Denoising Strength Slider
|
||||
state.ctxmenu.denoisingStrengthSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"denoisingStrength",
|
||||
"Denoising Strength",
|
||||
0,
|
||||
1,
|
||||
0.05
|
||||
).slider;
|
||||
|
||||
// Border Mask Size Slider
|
||||
state.ctxmenu.borderMaskSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"borderMaskSize",
|
||||
"Border Mask Size",
|
||||
0,
|
||||
128,
|
||||
1
|
||||
).slider;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.denoisingStrengthSlider);
|
||||
menu.appendChild(state.ctxmenu.borderMaskSlider);
|
||||
},
|
||||
shortcut: "I",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const mask_brush_draw_callback = (evn, state) => {
|
||||
const _mask_brush_draw_callback = (evn, state) => {
|
||||
if (evn.initialTarget.id === "overlayCanvas") {
|
||||
maskPaintCtx.globalCompositeOperation = "source-over";
|
||||
maskPaintCtx.strokeStyle = "#FF6A6A";
|
||||
|
@ -12,7 +12,7 @@ const mask_brush_draw_callback = (evn, state) => {
|
|||
}
|
||||
};
|
||||
|
||||
const mask_brush_erase_callback = (evn, state) => {
|
||||
const _mask_brush_erase_callback = (evn, state) => {
|
||||
if (evn.initialTarget.id === "overlayCanvas") {
|
||||
maskPaintCtx.globalCompositeOperation = "destination-out";
|
||||
maskPaintCtx.strokeStyle = "#FFFFFFFF";
|
||||
|
@ -25,3 +25,85 @@ const mask_brush_erase_callback = (evn, state) => {
|
|||
maskPaintCtx.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
const maskBrushTool = () =>
|
||||
toolbar.registerTool(
|
||||
"res/icons/paintbrush.svg",
|
||||
"Mask Brush",
|
||||
(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.onwheel.on(state.wheelcb);
|
||||
mouse.listen.canvas.left.onpaint.on(state.drawcb);
|
||||
mouse.listen.canvas.right.onpaint.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.movecb);
|
||||
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||
mouse.listen.canvas.left.onpaint.clear(state.drawcb);
|
||||
mouse.listen.canvas.right.onpaint.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.config = {
|
||||
brushScrollSpeed: 1 / 4,
|
||||
minBrushSize: 10,
|
||||
maxBrushSize: 500,
|
||||
};
|
||||
|
||||
state.brushSize = 64;
|
||||
state.setBrushSize = (size) => {
|
||||
state.brushSize = size;
|
||||
state.ctxmenu.brushSizeRange.value = size;
|
||||
state.ctxmenu.brushSizeText.value = size;
|
||||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
// draw big translucent red blob cursor
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
||||
ovCtx.fillStyle = "#FF6A6A50";
|
||||
ovCtx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
state.brushSize = state.setBrushSize(
|
||||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
||||
state.drawcb = (evn) => _mask_brush_draw_callback(evn, state);
|
||||
state.erasecb = (evn) => _mask_brush_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
const brushSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushSize",
|
||||
"Brush Size",
|
||||
state.config.minBrushSize,
|
||||
state.config.maxBrushSize,
|
||||
1
|
||||
);
|
||||
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.brushSizeSlider);
|
||||
},
|
||||
shortcut: "M",
|
||||
}
|
||||
);
|
||||
|
|
0
js/ui/tool/populate.js
Normal file
0
js/ui/tool/populate.js
Normal file
515
js/ui/tool/select.js
Normal file
515
js/ui/tool/select.js
Normal file
|
@ -0,0 +1,515 @@
|
|||
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, 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.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
|
||||
}
|
||||
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",
|
||||
}
|
||||
);
|
281
js/ui/tool/stamp.js
Normal file
281
js/ui/tool/stamp.js
Normal file
|
@ -0,0 +1,281 @@
|
|||
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) {
|
||||
const resource = state.addResource(
|
||||
opt.name || "Clipboard",
|
||||
opt.image,
|
||||
opt.temporary === undefined ? true : opt.temporary
|
||||
);
|
||||
state.selected = resource;
|
||||
document.getElementById(`resource-${resource.id}`).click();
|
||||
state.ctxmenu.uploadButton.disabled = true;
|
||||
state.back = opt.back || null;
|
||||
toolbar.lock();
|
||||
} 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);
|
||||
|
||||
// Deselect
|
||||
state.selected = null;
|
||||
Array.from(state.ctxmenu.resourceList.children).forEach((child) => {
|
||||
child.classList.remove("selected");
|
||||
});
|
||||
},
|
||||
{
|
||||
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", () => {
|
||||
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("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) => {
|
||||
let remove = true;
|
||||
state.resources.some((resource) => {
|
||||
console.debug(element.id, resource.id);
|
||||
if (element.id.endsWith(resource.id)) remove = false;
|
||||
});
|
||||
|
||||
if (remove) 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",
|
||||
}
|
||||
);
|
287
js/ui/toolbar.js
287
js/ui/toolbar.js
|
@ -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) {
|
||||
|
@ -134,15 +147,7 @@ const _toolbar_input = {
|
|||
return {checkbox, label};
|
||||
},
|
||||
|
||||
slider: (
|
||||
state,
|
||||
dataKey,
|
||||
text,
|
||||
min = 0,
|
||||
max = 1,
|
||||
step = 0.1,
|
||||
defaultValue = 0.3
|
||||
) => {
|
||||
slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => {
|
||||
const slider = document.createElement("div");
|
||||
|
||||
const value = createSlider(text, slider, {
|
||||
|
@ -152,7 +157,7 @@ const _toolbar_input = {
|
|||
valuecb: (v) => {
|
||||
state[dataKey] = v;
|
||||
},
|
||||
defaultValue,
|
||||
defaultValue: state[dataKey],
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -179,6 +184,7 @@ const _reticle_draw = (evn, snapToGrid = true) => {
|
|||
);
|
||||
|
||||
// draw targeting square reticle thingy cursor
|
||||
ovCtx.lineWidth = 1;
|
||||
ovCtx.strokeStyle = "#FFF";
|
||||
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
|
||||
}
|
||||
|
@ -189,179 +195,8 @@ const tools = {};
|
|||
/**
|
||||
* Dream tool
|
||||
*/
|
||||
tools.dream = toolbar.registerTool(
|
||||
"res/icons/image-plus.svg",
|
||||
"Dream",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({
|
||||
...mouse.coords.canvas.pos,
|
||||
target: {id: "overlayCanvas"},
|
||||
});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.snapToGrid = true;
|
||||
state.overMaskPx = 0;
|
||||
state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid);
|
||||
state.dreamcb = (evn) => {
|
||||
dream_generate_callback(evn, state);
|
||||
};
|
||||
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"snapToGrid",
|
||||
"Snap To Grid"
|
||||
).label;
|
||||
state.ctxmenu.overMaskPxLabel = _toolbar_input.slider(
|
||||
state,
|
||||
"overMaskPx",
|
||||
"Overmask px",
|
||||
0,
|
||||
128,
|
||||
1,
|
||||
64
|
||||
).slider;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.overMaskPxLabel);
|
||||
},
|
||||
shortcut: "D",
|
||||
}
|
||||
);
|
||||
|
||||
tools.img2img = toolbar.registerTool(
|
||||
"res/icons/image.svg",
|
||||
"Img2Img",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({
|
||||
...mouse.coords.canvas.pos,
|
||||
target: {id: "overlayCanvas"},
|
||||
});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.snapToGrid = true;
|
||||
state.denoisingStrength = 0.7;
|
||||
|
||||
state.borderMaskSize = 64;
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
_reticle_draw(evn, state.snapToGrid);
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// For displaying border mask
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = bb.w;
|
||||
auxCanvas.height = bb.h;
|
||||
const auxCtx = auxCanvas.getContext("2d");
|
||||
|
||||
if (state.borderMaskSize > 0) {
|
||||
auxCtx.fillStyle = "#FF6A6A50";
|
||||
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
|
||||
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
|
||||
auxCtx.fillRect(
|
||||
bb.w - state.borderMaskSize,
|
||||
0,
|
||||
state.borderMaskSize,
|
||||
bb.h
|
||||
);
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
bb.h - state.borderMaskSize,
|
||||
bb.w,
|
||||
state.borderMaskSize
|
||||
);
|
||||
}
|
||||
|
||||
const tmp = ovCtx.globalAlpha;
|
||||
ovCtx.globalAlpha = 0.4;
|
||||
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
|
||||
ovCtx.globalAlpha = tmp;
|
||||
};
|
||||
state.dreamcb = (evn) => {
|
||||
dream_img2img_callback(evn, state);
|
||||
};
|
||||
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
// Snap To Grid Checkbox
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"snapToGrid",
|
||||
"Snap To Grid"
|
||||
).label;
|
||||
|
||||
// Denoising Strength Slider
|
||||
state.ctxmenu.denoisingStrengthSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"denoisingStrength",
|
||||
"Denoising Strength",
|
||||
0,
|
||||
1,
|
||||
0.05,
|
||||
0.7
|
||||
).slider;
|
||||
|
||||
// Border Mask Size Slider
|
||||
state.ctxmenu.borderMaskSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"borderMaskSize",
|
||||
"Border Mask Size",
|
||||
0,
|
||||
128,
|
||||
1,
|
||||
64
|
||||
).slider;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.denoisingStrengthSlider);
|
||||
menu.appendChild(state.ctxmenu.borderMaskSlider);
|
||||
},
|
||||
shortcut: "I",
|
||||
}
|
||||
);
|
||||
tools.dream = dreamTool();
|
||||
tools.img2img = img2imgTool();
|
||||
|
||||
/**
|
||||
* Mask Editing tools
|
||||
|
@ -371,86 +206,14 @@ toolbar.addSeparator();
|
|||
/**
|
||||
* Mask Brush tool
|
||||
*/
|
||||
tools.maskbrush = toolbar.registerTool(
|
||||
"res/icons/paintbrush.svg",
|
||||
"Mask Brush",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb({...mouse.coords.canvas.pos, target: {id: "overlayCanvas"}});
|
||||
tools.maskbrush = maskBrushTool();
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.movecb);
|
||||
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||
mouse.listen.canvas.left.onpaint.on(state.drawcb);
|
||||
mouse.listen.canvas.right.onpaint.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.movecb);
|
||||
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||
mouse.listen.canvas.left.onpaint.clear(state.drawcb);
|
||||
mouse.listen.canvas.right.onpaint.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.config = {
|
||||
brushScrollSpeed: 1 / 4,
|
||||
minBrushSize: 10,
|
||||
maxBrushSize: 500,
|
||||
};
|
||||
/**
|
||||
* Image Editing tools
|
||||
*/
|
||||
toolbar.addSeparator();
|
||||
|
||||
state.brushSize = 64;
|
||||
state.setBrushSize = (size) => {
|
||||
state.brushSize = size;
|
||||
state.ctxmenu.brushSizeRange.value = size;
|
||||
state.ctxmenu.brushSizeText.value = size;
|
||||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
// draw big translucent red blob cursor
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
||||
ovCtx.fillStyle = "#FF6A6A50";
|
||||
ovCtx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
state.brushSize = state.setBrushSize(
|
||||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
||||
state.drawcb = (evn) => mask_brush_draw_callback(evn, state);
|
||||
state.erasecb = (evn) => mask_brush_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
const brushSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushSize",
|
||||
"Brush Size",
|
||||
state.config.minBrushSize,
|
||||
state.config.maxBrushSize,
|
||||
1,
|
||||
64
|
||||
);
|
||||
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.brushSizeSlider);
|
||||
},
|
||||
shortcut: "M",
|
||||
}
|
||||
);
|
||||
tools.selecttransform = selectTransformTool();
|
||||
tools.stamp = stampTool();
|
||||
|
||||
toolbar.tools[0].enable();
|
||||
|
|
|
@ -64,7 +64,10 @@ function snap(i, scaled = true, gridSize = 64) {
|
|||
scaleOffset = gridSize / 2;
|
||||
}
|
||||
}
|
||||
var snapOffset = (i % gridSize) - scaleOffset;
|
||||
const modulus = i % gridSize;
|
||||
var snapOffset = modulus - scaleOffset;
|
||||
if (modulus > gridSize / 2) snapOffset = modulus - gridSize;
|
||||
|
||||
if (snapOffset == 0) {
|
||||
return snapOffset;
|
||||
}
|
||||
|
|
15
res/icons/box-select.svg
Normal file
15
res/icons/box-select.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<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="M5 3a2 2 0 0 0-2 2"></path>
|
||||
<path d="M19 3a2 2 0 0 1 2 2"></path>
|
||||
<path d="M21 19a2 2 0 0 1-2 2"></path>
|
||||
<path d="M5 21a2 2 0 0 1-2-2"></path>
|
||||
<path d="M9 3h1"></path>
|
||||
<path d="M9 21h1"></path>
|
||||
<path d="M14 3h1"></path>
|
||||
<path d="M14 21h1"></path>
|
||||
<path d="M3 9v1"></path>
|
||||
<path d="M21 9v1"></path>
|
||||
<path d="M3 14v1"></path>
|
||||
<path d="M21 14v1"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 573 B |
7
res/icons/file-up.svg
Normal file
7
res/icons/file-up.svg
Normal 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
5
res/icons/lock.svg
Normal 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 |
Loading…
Reference in a new issue