Merge pull request #67 from zero01101/bleeding-edge
Prototype (though pretty stable) of a layer management system
39
css/icons.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
.ui.icon > .icon-eye-off {
|
||||
-webkit-mask-image: url("/res/icons/eye-off.svg");
|
||||
mask-image: url("/res/icons/eye-off.svg");
|
||||
}
|
||||
|
||||
.ui.icon > .icon-eye {
|
||||
-webkit-mask-image: url("/res/icons/eye.svg");
|
||||
mask-image: url("/res/icons/eye.svg");
|
||||
}
|
||||
|
||||
.ui.icon > .icon-file-plus {
|
||||
-webkit-mask-image: url("/res/icons/file-plus.svg");
|
||||
mask-image: url("/res/icons/file-plus.svg");
|
||||
}
|
||||
|
||||
.ui.icon > .icon-file-x {
|
||||
-webkit-mask-image: url("/res/icons/file-x.svg");
|
||||
mask-image: url("/res/icons/file-x.svg");
|
||||
}
|
||||
|
||||
.ui.icon > .icon-chevron-down {
|
||||
-webkit-mask-image: url("/res/icons/chevron-down.svg");
|
||||
mask-image: url("/res/icons/chevron-down.svg");
|
||||
}
|
||||
|
||||
.ui.icon > .icon-chevron-up {
|
||||
-webkit-mask-image: url("/res/icons/chevron-up.svg");
|
||||
mask-image: url("/res/icons/chevron-up.svg");
|
||||
}
|
||||
.ui.icon > .icon-chevron-first {
|
||||
-webkit-mask-image: url("/res/icons/chevron-first.svg");
|
||||
mask-image: url("/res/icons/chevron-first.svg");
|
||||
}
|
||||
|
||||
.ui.icon > .icon-chevron-flat-down {
|
||||
-webkit-mask-image: url("/res/icons/chevron-first.svg");
|
||||
mask-image: url("/res/icons/chevron-first.svg");
|
||||
transform: rotate(-90deg);
|
||||
}
|
|
@ -20,30 +20,6 @@ body {
|
|||
overflow: clip;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backgroundCanvas {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.mainHSplit {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
grid-column-gap: 5px;
|
||||
grid-row-gap: 5px;
|
||||
}
|
||||
|
||||
.uiWrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 15fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-column-gap: 5px;
|
||||
grid-row-gap: 5px;
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
background-color: rgb(0, 0, 0);
|
||||
color: rgb(255, 255, 255);
|
||||
|
@ -88,28 +64,6 @@ body {
|
|||
cursor: auto;
|
||||
}
|
||||
|
||||
.canvasHolder {
|
||||
position: relative;
|
||||
width: 2560px;
|
||||
height: 1440px;
|
||||
}
|
||||
|
||||
.mainCanvases {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 2560px;
|
||||
height: 1440px;
|
||||
}
|
||||
|
||||
.masks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: 1fr;
|
||||
grid-column-gap: 0px;
|
||||
grid-row-gap: 0px;
|
||||
}
|
||||
|
||||
/* Mask colors for mask inversion */
|
||||
/* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */
|
||||
.mask-canvas {
|
||||
|
@ -135,13 +89,6 @@ body {
|
|||
brightness(103%) contrast(108%);
|
||||
}
|
||||
|
||||
.strokeText {
|
||||
-webkit-text-stroke: 1px #000;
|
||||
font-size: 150%;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wideSelect {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -44,3 +44,22 @@
|
|||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#layer-overlay {
|
||||
position: fixed;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
#layer-render.pixelated canvas {
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
|
|
@ -113,3 +113,58 @@ select > option:checked::after {
|
|||
mask-image: url("/res/icons/check.svg");
|
||||
mask-size: contain;
|
||||
}
|
||||
/*************/
|
||||
/* UI styles */
|
||||
/*************/
|
||||
|
||||
/* The separator */
|
||||
.ui.separator {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
align-self: center;
|
||||
border-top: 1px var(--c-hover) solid;
|
||||
}
|
||||
|
||||
/* Icon button */
|
||||
.ui.square {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.ui.button {
|
||||
cursor: pointer;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
color: var(--c-text);
|
||||
background-color: var(--c-primary);
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.ui.button:hover {
|
||||
background-color: var(--c-hover);
|
||||
}
|
||||
|
||||
.ui.button:active {
|
||||
background-color: var(--c-hover);
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.ui.button.icon {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ui.button.icon > *:first-child {
|
||||
flex: 1;
|
||||
margin: 3px;
|
||||
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: var(--c-text);
|
||||
}
|
||||
|
|
134
css/ui/layers.css
Normal file
|
@ -0,0 +1,134 @@
|
|||
.layer-manager {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--c-primary);
|
||||
}
|
||||
|
||||
#layer-list {
|
||||
height: 200px;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
background-color: var(--c-primary);
|
||||
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
#layer-list > *:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
#layer-list .ui-layer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 25px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--c-text);
|
||||
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
#layer-list .ui-layer.active {
|
||||
background-color: var(--c-active);
|
||||
}
|
||||
#layer-list .ui-layer.active:hover,
|
||||
#layer-list .ui-layer:hover {
|
||||
background-color: var(--c-hover);
|
||||
}
|
||||
#layer-list .ui-layer.active:active,
|
||||
#layer-list .ui-layer:active {
|
||||
background-color: var(--c-hover);
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
#layer-list .ui-layer > .title {
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
border: 0;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
#layer-list .ui-layer > .actions {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
#layer-list .actions > button {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
padding: 0;
|
||||
|
||||
width: 25px;
|
||||
aspect-ratio: 1;
|
||||
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#layer-list .ui-layer > .actions > *:hover > * {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#layer-list .actions > button > *:first-child {
|
||||
flex: 1;
|
||||
margin: 3px;
|
||||
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
background-color: var(--c-text);
|
||||
}
|
||||
|
||||
#layer-list .actions > .rename-btn > *:first-child {
|
||||
-webkit-mask-image: url("/res/icons/edit.svg");
|
||||
mask-image: url("/res/icons/edit.svg");
|
||||
}
|
||||
|
||||
#layer-list .actions > .delete-btn > *:first-child {
|
||||
-webkit-mask-image: url("/res/icons/trash.svg");
|
||||
mask-image: url("/res/icons/trash.svg");
|
||||
}
|
||||
|
||||
#layer-list .actions > .hide-btn > *:first-child {
|
||||
-webkit-mask-image: url("/res/icons/eye.svg");
|
||||
mask-image: url("/res/icons/eye.svg");
|
||||
}
|
||||
#layer-list .hidden .actions > .hide-btn > *:first-child {
|
||||
-webkit-mask-image: url("/res/icons/eye-off.svg");
|
||||
mask-image: url("/res/icons/eye-off.svg");
|
||||
}
|
||||
|
||||
.layer-manager > .separator {
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.layer-manager > .layer-list-actions {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.layer-manager > .layer-list-actions > * {
|
||||
flex: 1;
|
||||
height: 25px;
|
||||
}
|
3
css/ui/tool/dream.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.dream-interrupt-btn {
|
||||
width: 100px;
|
||||
}
|
70
index.html
|
@ -5,6 +5,7 @@
|
|||
<title>openOutpaint 🐠</title>
|
||||
<!-- CSS Variables -->
|
||||
<link href="css/colors.css" rel="stylesheet" />
|
||||
<link href="css/icons.css" rel="stylesheet" />
|
||||
|
||||
<link href="css/index.css" rel="stylesheet" />
|
||||
<link href="css/layers.css" rel="stylesheet" />
|
||||
|
@ -12,9 +13,11 @@
|
|||
<link href="css/ui/generic.css" rel="stylesheet" />
|
||||
|
||||
<link href="css/ui/history.css" rel="stylesheet" />
|
||||
<link href="css/ui/layers.css" rel="stylesheet" />
|
||||
<link href="css/ui/toolbar.css" rel="stylesheet" />
|
||||
|
||||
<!-- Tool Specific CSS -->
|
||||
<link href="css/ui/tool/dream.css" rel="stylesheet" />
|
||||
<link href="css/ui/tool/stamp.css" rel="stylesheet" />
|
||||
<link href="css/ui/tool/colorbrush.css" rel="stylesheet" />
|
||||
|
||||
|
@ -109,6 +112,12 @@
|
|||
step="1"
|
||||
onchange="changeMaskBlur()" />
|
||||
<br />
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cbxSmooth"
|
||||
checked
|
||||
onchange="changeSmoothRendering()" />
|
||||
<label for="cbxSmooth">Smooth Rendering</label>
|
||||
<!-- Save/load image section -->
|
||||
<button type="button" class="collapsible">Save/Upscaling</button>
|
||||
<div class="content">
|
||||
|
@ -194,11 +203,66 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layers -->
|
||||
<div
|
||||
id="ui-layers"
|
||||
class="floating-window"
|
||||
style="right: 10px; bottom: 10px">
|
||||
<div class="draggable floating-window-title">Layers</div>
|
||||
<div class="menu-container" style="min-width: 200px">
|
||||
<div class="layer-manager">
|
||||
<div id="layer-list" class="layer-list"></div>
|
||||
<div class="ui separator"></div>
|
||||
<div class="layer-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
title="Add Layer"
|
||||
onclick="commands.runCommand('addLayer', 'Added Layer')"
|
||||
class="ui icon button">
|
||||
<div class="icon-file-plus"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Move Layer Up"
|
||||
onclick="commands.runCommand('moveLayer', 'Moved Layer Up',{delta: 1})"
|
||||
class="ui icon button">
|
||||
<div class="icon-chevron-up"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Move Layer Down"
|
||||
onclick="commands.runCommand('moveLayer', 'Moved Layer Down', {delta: -1})"
|
||||
class="ui icon button">
|
||||
<div class="icon-chevron-down"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Merge Layer Down"
|
||||
onclick="commands.runCommand('mergeLayer', 'Merged Layer Down')"
|
||||
class="ui icon button">
|
||||
<div class="icon-chevron-flat-down"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Move Layer Down"
|
||||
onclick="commands.runCommand('deleteLayer', 'Deleted Layer')"
|
||||
class="ui icon button">
|
||||
<div class="icon-file-x"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div
|
||||
id="ui-toolbar"
|
||||
class="floating-window toolbar"
|
||||
style="right: 10px; top: 350px">
|
||||
style="right: 270px; top: 10px">
|
||||
<div class="draggable handle">
|
||||
<span class="line"></span>
|
||||
</div>
|
||||
|
@ -209,6 +273,9 @@
|
|||
<!-- Canvases -->
|
||||
<div id="layer-render" class="layer-render-target"></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<canvas id="layer-overlay" class="layer-overlay"></canvas>
|
||||
|
||||
<!-- Base Libs -->
|
||||
<script src="js/lib/util.js" type="text/javascript"></script>
|
||||
<script src="js/lib/input.js" type="text/javascript"></script>
|
||||
|
@ -225,6 +292,7 @@
|
|||
<!-- Content -->
|
||||
<script src="js/index.js" type="text/javascript"></script>
|
||||
<script src="js/ui/floating/history.js" type="text/javascript"></script>
|
||||
<script src="js/ui/floating/layers.js" type="text/javascript"></script>
|
||||
|
||||
<!-- Load Tools -->
|
||||
<script src="js/ui/tool/dream.js" type="text/javascript"></script>
|
||||
|
|
170
js/index.js
|
@ -113,8 +113,8 @@ function startup() {
|
|||
drawBackground();
|
||||
changeSampler();
|
||||
changeMaskBlur();
|
||||
changeSmoothRendering();
|
||||
changeSeed();
|
||||
changeOverMaskPx();
|
||||
changeHiResFix();
|
||||
}
|
||||
|
||||
|
@ -331,116 +331,28 @@ async function testHostConnection() {
|
|||
|
||||
function newImage(evt) {
|
||||
clearPaintedMask();
|
||||
clearBackupMask();
|
||||
commands.runCommand("eraseImage", "Clear Canvas", {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: imgCanvas.width,
|
||||
h: imgCanvas.height,
|
||||
uil.layers.forEach(({layer}) => {
|
||||
commands.runCommand("eraseImage", "Clear Canvas", {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: layer.canvas.width,
|
||||
h: layer.canvas.height,
|
||||
ctx: layer.ctx,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function prevImg(evt) {
|
||||
if (imageIndex == 0) {
|
||||
imageIndex = totalImagesReturned;
|
||||
}
|
||||
changeImg(false);
|
||||
}
|
||||
|
||||
function nextImg(evt) {
|
||||
if (imageIndex == totalImagesReturned - 1) {
|
||||
imageIndex = -1;
|
||||
}
|
||||
changeImg(true);
|
||||
}
|
||||
|
||||
function changeImg(forward) {
|
||||
const img = new Image();
|
||||
tempCtx.clearRect(0, 0, tempCtx.width, tempCtx.height);
|
||||
img.onload = function () {
|
||||
tempCtx.drawImage(img, tmpImgXYWH.x, tmpImgXYWH.y); //imgCtx for actual image, tmp for... holding?
|
||||
};
|
||||
var tmpIndex = document.getElementById("currentImgIndex");
|
||||
if (forward) {
|
||||
imageIndex++;
|
||||
} else {
|
||||
imageIndex--;
|
||||
}
|
||||
tmpIndex.innerText = imageIndex + 1;
|
||||
// load the image data after defining the closure
|
||||
img.src = "data:image/png;base64," + returnedImages[imageIndex]; //TODO need way to dream batches and select from results
|
||||
}
|
||||
|
||||
function removeChoiceButtons(evt) {
|
||||
const element = document.getElementById("veryTempDiv");
|
||||
element.remove();
|
||||
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
||||
}
|
||||
|
||||
function backupAndClearMask(x, y, w, h) {
|
||||
var clearArea = maskPaintCtx.createImageData(w, h);
|
||||
backupMaskChunk = maskPaintCtx.getImageData(x, y, w, h);
|
||||
backupMaskX = x;
|
||||
backupMaskY = y;
|
||||
var clearD = clearArea.data;
|
||||
for (i = 0; i < clearD.length; i += 4) {
|
||||
clearD[i] = 0;
|
||||
clearD[i + 1] = 0;
|
||||
clearD[i + 2] = 0;
|
||||
clearD[i + 3] = 0;
|
||||
}
|
||||
maskPaintCtx.putImageData(clearArea, x, y);
|
||||
}
|
||||
|
||||
function restoreBackupMask() {
|
||||
// reapply mask if exists
|
||||
if (backupMaskChunk != null && backupMaskX != null && backupMaskY != null) {
|
||||
// backup mask data exists
|
||||
var iData = new ImageData(
|
||||
backupMaskChunk.data,
|
||||
backupMaskChunk.height,
|
||||
backupMaskChunk.width
|
||||
);
|
||||
maskPaintCtx.putImageData(iData, backupMaskX, backupMaskY);
|
||||
}
|
||||
}
|
||||
|
||||
function clearBackupMask() {
|
||||
// clear backupmask
|
||||
backupMaskChunk = null;
|
||||
backupMaskX = null;
|
||||
backupMaskY = null;
|
||||
}
|
||||
|
||||
function clearImgMask() {
|
||||
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
|
||||
}
|
||||
|
||||
function clearPaintedMask() {
|
||||
maskPaintCtx.clearRect(0, 0, maskPaintCanvas.width, maskPaintCanvas.height);
|
||||
}
|
||||
|
||||
function placeImage() {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
commands.runCommand("drawImage", "Image Dream", {
|
||||
x: tmpImgXYWH.x,
|
||||
y: tmpImgXYWH.y,
|
||||
image: img,
|
||||
});
|
||||
tmpImgXYWH = {};
|
||||
returnedImages = null;
|
||||
};
|
||||
// load the image data after defining the closure
|
||||
img.src = "data:image/png;base64," + returnedImages[imageIndex];
|
||||
}
|
||||
function march(bb, options = {}) {
|
||||
defaultOpt(options, {
|
||||
style: "#FFFF",
|
||||
width: "2px",
|
||||
filter: null,
|
||||
});
|
||||
|
||||
function sleep(ms) {
|
||||
// what was this even for, anyway?
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function march(bb) {
|
||||
const expanded = {...bb};
|
||||
expanded.x--;
|
||||
expanded.y--;
|
||||
|
@ -455,7 +367,7 @@ function march(bb) {
|
|||
let offset = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
drawMarchingAnts(layer.ctx, bb, offset++);
|
||||
drawMarchingAnts(layer.ctx, bb, offset++, options);
|
||||
offset %= 12;
|
||||
}, 20);
|
||||
|
||||
|
@ -465,13 +377,18 @@ function march(bb) {
|
|||
};
|
||||
}
|
||||
|
||||
function drawMarchingAnts(ctx, bb, offset) {
|
||||
function drawMarchingAnts(ctx, bb, offset, options) {
|
||||
ctx.save();
|
||||
|
||||
ctx.clearRect(0, 0, bb.w + 2, bb.h + 2);
|
||||
ctx.strokeStyle = "#FFFFFFFF"; //"#55000077";
|
||||
ctx.strokeWidth = "2px";
|
||||
ctx.strokeStyle = options.style;
|
||||
ctx.strokeWidth = options.width;
|
||||
ctx.filter = options.filter;
|
||||
ctx.setLineDash([4, 2]);
|
||||
ctx.lineDashOffset = -offset;
|
||||
ctx.strokeRect(1, 1, bb.w, bb.h);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function changeSampler() {
|
||||
|
@ -563,10 +480,6 @@ makeSlider(
|
|||
|
||||
makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1);
|
||||
|
||||
function changeSnapMode() {
|
||||
snapToGrid = document.getElementById("cbxSnap").checked;
|
||||
}
|
||||
|
||||
function changeMaskBlur() {
|
||||
stableDiffusionData.mask_blur = parseInt(
|
||||
document.getElementById("maskBlur").value
|
||||
|
@ -579,20 +492,22 @@ function changeSeed() {
|
|||
localStorage.setItem("seed", stableDiffusionData.seed);
|
||||
}
|
||||
|
||||
function changeOverMaskPx() {
|
||||
// overMaskPx = document.getElementById("overMaskPx").value;
|
||||
// localStorage.setItem("overmask_px", overMaskPx);
|
||||
}
|
||||
|
||||
function changeHiResFix() {
|
||||
stableDiffusionData.enable_hr = Boolean(
|
||||
document.getElementById("cbxHRFix").checked
|
||||
);
|
||||
localStorage.setItem("enable_hr", stableDiffusionData.enable_hr);
|
||||
}
|
||||
function changeSmoothRendering() {
|
||||
const layers = document.getElementById("layer-render");
|
||||
if (document.getElementById("cbxSmooth").checked) {
|
||||
layers.classList.remove("pixelated");
|
||||
} else {
|
||||
layers.classList.add("pixelated");
|
||||
}
|
||||
}
|
||||
|
||||
function isCanvasBlank(x, y, w, h, specifiedCanvas) {
|
||||
var canvas = document.getElementById(specifiedCanvas.id);
|
||||
function isCanvasBlank(x, y, w, h, canvas) {
|
||||
return !canvas
|
||||
.getContext("2d")
|
||||
.getImageData(x, y, w, h)
|
||||
|
@ -871,7 +786,14 @@ async function upscaleAndDownload() {
|
|||
// get cropped canvas, send it to upscaler, download result
|
||||
var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something
|
||||
var upscaler = document.getElementById("upscalers").value;
|
||||
var croppedCanvas = cropCanvas(imgCanvas);
|
||||
var croppedCanvas = cropCanvas(
|
||||
uil.getVisible({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: imageCollection.size.w,
|
||||
h: imageCollection.size.h,
|
||||
})
|
||||
);
|
||||
if (croppedCanvas != null) {
|
||||
var upscaler = document.getElementById("upscalers").value;
|
||||
var url =
|
||||
|
@ -936,15 +858,6 @@ function loadSettings() {
|
|||
? false
|
||||
: localStorage.getItem("enable_hr")
|
||||
);
|
||||
var _enable_erase = Boolean(
|
||||
localStorage.getItem("enable_erase") == (null || "false")
|
||||
? false
|
||||
: localStorage.getItem("enable_erase")
|
||||
);
|
||||
var _overmask_px =
|
||||
localStorage.getItem("overmask_px") == null
|
||||
? 0
|
||||
: localStorage.getItem("overmask_px");
|
||||
|
||||
// set the values into the UI
|
||||
document.getElementById("prompt").value = String(_prompt);
|
||||
|
@ -955,7 +868,6 @@ function loadSettings() {
|
|||
document.getElementById("maskBlur").value = Number(_mask_blur);
|
||||
document.getElementById("seed").value = Number(_seed);
|
||||
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
||||
// document.getElementById("overMaskPx").value = Number(_overmask_px);
|
||||
}
|
||||
|
||||
imageCollection.element.addEventListener(
|
||||
|
|
|
@ -12,9 +12,11 @@ const bgLayer = imageCollection.registerLayer("bg", {
|
|||
});
|
||||
const imgLayer = imageCollection.registerLayer("image", {
|
||||
name: "Image",
|
||||
ctxOptions: {desynchronized: true},
|
||||
});
|
||||
const maskPaintLayer = imageCollection.registerLayer("mask", {
|
||||
name: "Mask Paint",
|
||||
ctxOptions: {desynchronized: true},
|
||||
});
|
||||
const ovLayer = imageCollection.registerLayer("overlay", {
|
||||
name: "Overlay",
|
||||
|
@ -37,25 +39,12 @@ const ovCtx = ovLayer.ctx;
|
|||
const debugCanvas = debugLayer.canvas; // where mouse cursor renders
|
||||
const debugCtx = debugLayer.ctx;
|
||||
|
||||
/**
|
||||
* Function that returns a canvas with full visible information of a certain bounding box.
|
||||
*
|
||||
* For now, only the img is used.
|
||||
*
|
||||
* @param {BoundingBox} bb The bouding box to get visible data from
|
||||
* @returns {HTMLCanvasElement} The canvas element containing visible image data
|
||||
*/
|
||||
const getVisible = (bb) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = bb.w;
|
||||
canvas.height = bb.h;
|
||||
ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
ctx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
|
||||
return canvas;
|
||||
};
|
||||
/* WIP: Most cursors shouldn't need a zoomable canvas */
|
||||
/** @type {HTMLCanvasElement} */
|
||||
const uiCanvas = document.getElementById("layer-overlay"); // where mouse cursor renders
|
||||
uiCanvas.width = uiCanvas.clientWidth;
|
||||
uiCanvas.height = uiCanvas.clientHeight;
|
||||
const uiCtx = uiCanvas.getContext("2d", {desynchronized: true});
|
||||
|
||||
debugLayer.hide(); // Hidden by default
|
||||
|
||||
|
@ -131,6 +120,15 @@ const viewport = {
|
|||
get h() {
|
||||
return (window.innerHeight * 1) / this.zoom;
|
||||
},
|
||||
viewToCanvas(x, y) {
|
||||
return {x, y};
|
||||
},
|
||||
canvasToView(x, y) {
|
||||
return {
|
||||
x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2,
|
||||
y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Apply transformation
|
||||
*
|
||||
|
@ -155,6 +153,7 @@ viewport.transform(imageCollection.element);
|
|||
mouse.listen.window.onwheel.on((evn) => {
|
||||
if (evn.evn.ctrlKey) {
|
||||
evn.evn.preventDefault();
|
||||
|
||||
const pcx = viewport.cx;
|
||||
const pcy = viewport.cy;
|
||||
if (evn.delta < 0) {
|
||||
|
@ -168,6 +167,8 @@ mouse.listen.window.onwheel.on((evn) => {
|
|||
|
||||
viewport.transform(imageCollection.element);
|
||||
|
||||
toolbar.currentTool.redraw();
|
||||
|
||||
if (debug) {
|
||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||
debugCtx.fillStyle = "#F0F";
|
||||
|
@ -210,4 +211,6 @@ mouse.listen.window.btn.middle.onpaintend.on((evn) => {
|
|||
|
||||
window.addEventListener("resize", () => {
|
||||
viewport.transform(imageCollection.element);
|
||||
uiCanvas.width = uiCanvas.clientWidth;
|
||||
uiCanvas.height = uiCanvas.clientHeight;
|
||||
});
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
|
||||
const _commands_events = new Observer();
|
||||
|
||||
/** CommandNonExistentError */
|
||||
class CommandNonExistentError extends Error {}
|
||||
|
||||
/** Global Commands Object */
|
||||
const commands = makeReadOnly(
|
||||
{
|
||||
|
@ -32,7 +29,14 @@ const commands = makeReadOnly(
|
|||
*/
|
||||
async undo(n = 1) {
|
||||
for (var i = 0; i < n && this.current > -1; i++) {
|
||||
await this._history[this._current--].undo();
|
||||
try {
|
||||
await this._history[this._current--].undo();
|
||||
} catch (e) {
|
||||
console.warn("[commands] Failed to undo command");
|
||||
console.warn(e);
|
||||
this._current++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
@ -42,7 +46,14 @@ const commands = makeReadOnly(
|
|||
*/
|
||||
async redo(n = 1) {
|
||||
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) {
|
||||
await this._history[++this._current].redo();
|
||||
try {
|
||||
await this._history[++this._current].redo();
|
||||
} catch {
|
||||
console.warn("[commands] Failed to redo command");
|
||||
console.warn(e);
|
||||
this._current--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -67,7 +78,7 @@ const commands = makeReadOnly(
|
|||
* @returns {Command}
|
||||
*/
|
||||
createCommand(name, run, undo, redo = run) {
|
||||
const command = async function runWrapper(title, options) {
|
||||
const command = async function runWrapper(title, options, extra) {
|
||||
// Create copy of options and state object
|
||||
const copy = {};
|
||||
Object.assign(copy, options);
|
||||
|
@ -93,11 +104,11 @@ const commands = makeReadOnly(
|
|||
return;
|
||||
}
|
||||
|
||||
const undoWrapper = () => {
|
||||
const undoWrapper = async () => {
|
||||
console.debug(
|
||||
`[commands] Undoing '${title}'[${name}], currently ${this._current}`
|
||||
);
|
||||
undo(title, state);
|
||||
await undo(title, state);
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
|
@ -106,11 +117,11 @@ const commands = makeReadOnly(
|
|||
current: this._current,
|
||||
});
|
||||
};
|
||||
const redoWrapper = () => {
|
||||
const redoWrapper = async () => {
|
||||
console.debug(
|
||||
`[commands] Redoing '${title}'[${name}], currently ${this._current}`
|
||||
);
|
||||
redo(title, copy, state);
|
||||
await redo(title, copy, state);
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
|
@ -120,6 +131,11 @@ const commands = makeReadOnly(
|
|||
});
|
||||
};
|
||||
|
||||
entry.undo = undoWrapper;
|
||||
entry.redo = redoWrapper;
|
||||
|
||||
if (!extra.recordHistory) return entry;
|
||||
|
||||
// Add to history
|
||||
if (commands._history.length > commands._current + 1) {
|
||||
commands._history.forEach((entry, index) => {
|
||||
|
@ -139,9 +155,6 @@ const commands = makeReadOnly(
|
|||
commands._history.push(entry);
|
||||
commands._current++;
|
||||
|
||||
entry.undo = undoWrapper;
|
||||
entry.redo = redoWrapper;
|
||||
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
|
@ -163,13 +176,16 @@ const commands = makeReadOnly(
|
|||
* @param {string} name The name of the command to run
|
||||
* @param {string} title The display name of the command on the history panel view
|
||||
* @param {any} options The options to be sent to the command to be run
|
||||
* @return {Promise<{undo: () => void, redo: () => void}>} The command's return value
|
||||
*/
|
||||
runCommand(name, title, options = null) {
|
||||
async runCommand(name, title, options = null, extra = {}) {
|
||||
defaultOpt(extra, {
|
||||
recordHistory: true,
|
||||
});
|
||||
if (!this._types[name])
|
||||
throw new CommandNonExistentError(
|
||||
`[commands] Command '${name}' does not exist`
|
||||
);
|
||||
this._types[name](title, options);
|
||||
throw new ReferenceError(`[commands] Command '${name}' does not exist`);
|
||||
|
||||
return this._types[name](title, options, extra);
|
||||
},
|
||||
},
|
||||
"commands",
|
||||
|
@ -192,7 +208,7 @@ commands.createCommand(
|
|||
|
||||
// Check if we have state
|
||||
if (!state.context) {
|
||||
const context = options.ctx || imgCtx;
|
||||
const context = options.ctx || uil.ctx;
|
||||
state.context = context;
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
|
@ -252,16 +268,10 @@ commands.createCommand(
|
|||
|
||||
// Check if we have state
|
||||
if (!state.context) {
|
||||
const context = options.ctx || imgCtx;
|
||||
const context = options.ctx || uil.ctx;
|
||||
state.context = context;
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
const imgData = context.getImageData(
|
||||
options.x,
|
||||
options.y,
|
||||
options.w,
|
||||
options.h
|
||||
);
|
||||
state.box = {
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
|
@ -272,7 +282,19 @@ commands.createCommand(
|
|||
const cutout = document.createElement("canvas");
|
||||
cutout.width = state.box.w;
|
||||
cutout.height = state.box.h;
|
||||
cutout.getContext("2d").putImageData(imgData, 0, 0);
|
||||
cutout
|
||||
.getContext("2d")
|
||||
.drawImage(
|
||||
context.canvas,
|
||||
options.x,
|
||||
options.y,
|
||||
options.w,
|
||||
options.h,
|
||||
0,
|
||||
0,
|
||||
options.w,
|
||||
options.h
|
||||
);
|
||||
state.original = new Image();
|
||||
state.original.src = cutout.toDataURL();
|
||||
}
|
||||
|
|
|
@ -77,8 +77,6 @@ const layers = {
|
|||
size,
|
||||
resolution: options.resolution,
|
||||
|
||||
active: null,
|
||||
|
||||
/**
|
||||
* Registers a new layer
|
||||
*
|
||||
|
@ -87,7 +85,9 @@ const layers = {
|
|||
* @param {string} options.name
|
||||
* @param {?BoundingBox} options.bb
|
||||
* @param {{w: number, h: number}} options.resolution
|
||||
* @param {?string} options.group
|
||||
* @param {object} options.after
|
||||
* @param {object} options.ctxOptions
|
||||
* @returns
|
||||
*/
|
||||
registerLayer: (key = null, options = {}) => {
|
||||
|
@ -101,11 +101,17 @@ const layers = {
|
|||
// Bounding box for layer
|
||||
bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h},
|
||||
|
||||
// Bounding box for layer
|
||||
// Resolution for layer
|
||||
resolution: null,
|
||||
|
||||
// Group for the layer ("group/subgroup/subsubgroup")
|
||||
group: null,
|
||||
|
||||
// If set, will insert the layer after the given one
|
||||
after: null,
|
||||
|
||||
// Context creation options
|
||||
ctxOptions: {},
|
||||
});
|
||||
|
||||
// Calculate resolution
|
||||
|
@ -135,7 +141,7 @@ const layers = {
|
|||
options.after.canvas.after(canvas);
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctx = canvas.getContext("2d", options.ctxOptions);
|
||||
|
||||
// Path used for logging purposes
|
||||
const _layerlogpath = key
|
||||
|
@ -168,6 +174,24 @@ const layers = {
|
|||
canvas,
|
||||
ctx,
|
||||
|
||||
/**
|
||||
* Moves this layer to another level (after given layer)
|
||||
*
|
||||
* @param {Layer} layer Will move layer to after this one
|
||||
*/
|
||||
moveAfter(layer) {
|
||||
layer.canvas.after(this.canvas);
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves this layer to another level (before given layer)
|
||||
*
|
||||
* @param {Layer} layer Will move layer to before this one
|
||||
*/
|
||||
moveBefore(layer) {
|
||||
layer.canvas.before(this.canvas);
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves this layer to another location
|
||||
*
|
||||
|
@ -204,14 +228,8 @@ const layers = {
|
|||
unhide() {
|
||||
this.canvas.style.display = "block";
|
||||
},
|
||||
|
||||
// Activates this layer
|
||||
activate() {
|
||||
collection.active = this;
|
||||
},
|
||||
},
|
||||
_layerlogpath,
|
||||
["active"]
|
||||
_layerlogpath
|
||||
);
|
||||
|
||||
// Add to indexers
|
||||
|
@ -260,8 +278,7 @@ const layers = {
|
|||
else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`);
|
||||
},
|
||||
},
|
||||
_logpath,
|
||||
["active"]
|
||||
_logpath
|
||||
);
|
||||
|
||||
layers._collections.push(collection);
|
||||
|
@ -271,11 +288,6 @@ const layers = {
|
|||
`[layers] Collection '${options.name}' at ${_logpath} registered`
|
||||
);
|
||||
|
||||
// We must create a layer to select
|
||||
collection
|
||||
.registerLayer(options.initLayer.key, options.initLayer.options)
|
||||
.activate();
|
||||
|
||||
return collection;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,6 +8,10 @@ const toolbar = {
|
|||
_toolbar_lock_indicator: document.getElementById("toolbar-lock-indicator"),
|
||||
|
||||
tools: [],
|
||||
_current_tool: null,
|
||||
get currentTool() {
|
||||
return this._current_tool;
|
||||
},
|
||||
|
||||
lock() {
|
||||
toolbar._locked = true;
|
||||
|
@ -88,6 +92,12 @@ const toolbar = {
|
|||
_element: null,
|
||||
state: {},
|
||||
options,
|
||||
/**
|
||||
* If the tool has a redraw() function in its state, then run it
|
||||
*/
|
||||
redraw: () => {
|
||||
tool.state.redraw && tool.state.redraw();
|
||||
},
|
||||
enable: (opt = null) => {
|
||||
if (toolbar._locked) return;
|
||||
|
||||
|
@ -100,12 +110,15 @@ const toolbar = {
|
|||
|
||||
tool._element && tool._element.classList.add("using");
|
||||
tool.enabled = true;
|
||||
|
||||
this._current_tool = tool;
|
||||
enable(tool.state, opt);
|
||||
},
|
||||
disable: (opt = null) => {
|
||||
tool._element && tool._element.classList.remove("using");
|
||||
disable(tool.state, opt);
|
||||
this._current_tool = null;
|
||||
tool.enabled = false;
|
||||
disable(tool.state, opt);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -250,14 +250,19 @@ function cropCanvas(sourceCanvas, options = {}) {
|
|||
*
|
||||
* @param {Object} options - Optional Information
|
||||
* @param {boolean} [options.cropToContent] - If we wish to crop to content first (default: true)
|
||||
* @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: imgCanvas)
|
||||
* @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: visible)
|
||||
* @param {string} [options.filename] - The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\
|
||||
* If null, opens image in new tab.
|
||||
*/
|
||||
function downloadCanvas(options = {}) {
|
||||
defaultOpt(options, {
|
||||
cropToContent: true,
|
||||
canvas: imgCanvas,
|
||||
canvas: uil.getVisible({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: imageCollection.size.w,
|
||||
h: imageCollection.size.h,
|
||||
}),
|
||||
filename:
|
||||
new Date()
|
||||
.toISOString()
|
||||
|
|
537
js/ui/floating/layers.js
Normal file
|
@ -0,0 +1,537 @@
|
|||
/**
|
||||
* The layering UI window
|
||||
*/
|
||||
|
||||
const uil = {
|
||||
_ui_layer_list: document.getElementById("layer-list"),
|
||||
layers: [],
|
||||
_active: null,
|
||||
set active(v) {
|
||||
Array.from(this._ui_layer_list.children).forEach((child) => {
|
||||
child.classList.remove("active");
|
||||
});
|
||||
|
||||
v.entry.classList.add("active");
|
||||
|
||||
this._active = v;
|
||||
},
|
||||
get active() {
|
||||
return this._active;
|
||||
},
|
||||
|
||||
get layer() {
|
||||
return this.active && this.active.layer;
|
||||
},
|
||||
|
||||
get canvas() {
|
||||
return this.layer && this.active.layer.canvas;
|
||||
},
|
||||
|
||||
get ctx() {
|
||||
return this.layer && this.active.layer.ctx;
|
||||
},
|
||||
|
||||
get w() {
|
||||
return imageCollection.size.w;
|
||||
},
|
||||
get h() {
|
||||
return imageCollection.size.h;
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronizes layer array to DOM
|
||||
*/
|
||||
_syncLayers() {
|
||||
const layersEl = document.getElementById("layer-list");
|
||||
|
||||
const copy = this.layers.map((i) => i);
|
||||
copy.reverse();
|
||||
|
||||
copy.forEach((uiLayer, index) => {
|
||||
// If we have the correct layer here, then do nothing
|
||||
if (
|
||||
layersEl.children[index] &&
|
||||
layersEl.children[index].id === `ui-layer-${uiLayer.id}`
|
||||
)
|
||||
return;
|
||||
|
||||
// If the layer we are processing does not exist, then create it and add before current element
|
||||
if (!uiLayer.entry) {
|
||||
uiLayer.entry = document.createElement("div");
|
||||
uiLayer.entry.id = `ui-layer-${uiLayer.id}`;
|
||||
uiLayer.entry.classList.add("ui-layer");
|
||||
uiLayer.entry.addEventListener("click", () => {
|
||||
this.active = uiLayer;
|
||||
});
|
||||
|
||||
// Title Element
|
||||
const titleEl = document.createElement("input");
|
||||
titleEl.classList.add("title");
|
||||
titleEl.value = uiLayer.name;
|
||||
titleEl.style.pointerEvents = "none";
|
||||
|
||||
const deselect = () => {
|
||||
titleEl.style.pointerEvents = "none";
|
||||
titleEl.setSelectionRange(0, 0);
|
||||
};
|
||||
|
||||
titleEl.addEventListener("blur", deselect);
|
||||
uiLayer.entry.appendChild(titleEl);
|
||||
|
||||
uiLayer.entry.addEventListener("change", () => {
|
||||
const name = titleEl.value.trim();
|
||||
titleEl.value = name;
|
||||
uiLayer.entry.title = name;
|
||||
|
||||
uiLayer.name = name;
|
||||
|
||||
this._syncLayers();
|
||||
|
||||
titleEl.blur();
|
||||
});
|
||||
uiLayer.entry.addEventListener("dblclick", () => {
|
||||
titleEl.style.pointerEvents = "auto";
|
||||
titleEl.focus();
|
||||
titleEl.select();
|
||||
});
|
||||
|
||||
// Add action buttons
|
||||
const actionArray = document.createElement("div");
|
||||
actionArray.classList.add("actions");
|
||||
|
||||
if (uiLayer.deletable) {
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.addEventListener(
|
||||
"click",
|
||||
(evn) => {
|
||||
commands.runCommand("deleteLayer", "Deleted Layer", {
|
||||
layer: uiLayer,
|
||||
});
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
|
||||
deleteButton.addEventListener(
|
||||
"dblclick",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
deleteButton.title = "Delete Layer";
|
||||
deleteButton.appendChild(document.createElement("div"));
|
||||
deleteButton.classList.add("delete-btn");
|
||||
|
||||
actionArray.appendChild(deleteButton);
|
||||
}
|
||||
|
||||
const hideButton = document.createElement("button");
|
||||
hideButton.addEventListener(
|
||||
"click",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
uiLayer.hidden = !uiLayer.hidden;
|
||||
if (uiLayer.hidden) {
|
||||
uiLayer.entry.classList.add("hidden");
|
||||
} else uiLayer.entry.classList.remove("hidden");
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
hideButton.addEventListener(
|
||||
"dblclick",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
hideButton.title = "Hide/Unhide Layer";
|
||||
hideButton.appendChild(document.createElement("div"));
|
||||
hideButton.classList.add("hide-btn");
|
||||
|
||||
actionArray.appendChild(hideButton);
|
||||
uiLayer.entry.appendChild(actionArray);
|
||||
|
||||
if (layersEl.children[index])
|
||||
layersEl.children[index].before(uiLayer.entry);
|
||||
else layersEl.appendChild(uiLayer.entry);
|
||||
} else if (!layersEl.querySelector(`#ui-layer-${uiLayer.id}`)) {
|
||||
// If layer exists but is not on the DOM, add it back
|
||||
if (index === 0) layersEl.children[0].before(uiLayer.entry);
|
||||
else layersEl.children[index - 1].after(uiLayer.entry);
|
||||
} else {
|
||||
// If the layer already exists, just move it here
|
||||
layersEl.children[index].before(uiLayer.entry);
|
||||
}
|
||||
});
|
||||
|
||||
// Deletes layer if not in array
|
||||
for (var i = 0; i < layersEl.children.length; i++) {
|
||||
if (!copy.find((l) => layersEl.children[i].id === `ui-layer-${l.id}`)) {
|
||||
layersEl.children[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronizes with the layer lib
|
||||
this.layers.forEach((uiLayer, index) => {
|
||||
if (index === 0) uiLayer.layer.moveAfter(bgLayer);
|
||||
else uiLayer.layer.moveAfter(copy[index - 1].layer);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a user-manageable layer for image editing.
|
||||
*
|
||||
* Should not be called directly. Use the command instead.
|
||||
*
|
||||
* @param {string} group The group the layer belongs to. [does nothing for now]
|
||||
* @param {string} name The name of the new layer.
|
||||
* @returns
|
||||
*/
|
||||
_addLayer(group, name) {
|
||||
const layer = imageCollection.registerLayer(null, {
|
||||
name,
|
||||
after:
|
||||
(this.layers.length > 0 && this.layers[this.layers.length - 1].layer) ||
|
||||
bgLayer,
|
||||
});
|
||||
|
||||
const uiLayer = {
|
||||
id: layer.id,
|
||||
group,
|
||||
name,
|
||||
_hidden: false,
|
||||
set hidden(v) {
|
||||
if (v) {
|
||||
this._hidden = true;
|
||||
this.layer.hide(v);
|
||||
} else {
|
||||
this._hidden = false;
|
||||
this.layer.unhide(v);
|
||||
}
|
||||
},
|
||||
get hidden() {
|
||||
return this._hidden;
|
||||
},
|
||||
entry: null,
|
||||
layer,
|
||||
};
|
||||
this.layers.push(uiLayer);
|
||||
|
||||
this._syncLayers();
|
||||
|
||||
this.active = uiLayer;
|
||||
|
||||
return uiLayer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves a layer to a specified position.
|
||||
*
|
||||
* Should not be called directly. Use the command instead.
|
||||
*
|
||||
* @param {UserLayer} layer Layer to move
|
||||
* @param {number} position Position to move the layer to
|
||||
*/
|
||||
_moveLayerTo(layer, position) {
|
||||
if (position < 0 || position >= this.layers.length)
|
||||
throw new RangeError("Position out of bounds");
|
||||
|
||||
const index = this.layers.indexOf(layer);
|
||||
if (index !== -1) {
|
||||
if (this.layers.length < 2) return; // Do nothing if moving a layer doesn't make sense
|
||||
|
||||
this.layers.splice(index, 1);
|
||||
this.layers.splice(position, 0, layer);
|
||||
|
||||
this._syncLayers();
|
||||
|
||||
return;
|
||||
}
|
||||
throw new ReferenceError("Layer could not be found");
|
||||
},
|
||||
/**
|
||||
* Moves a layer up a single position.
|
||||
*
|
||||
* Should not be called directly. Use the command instead.
|
||||
*
|
||||
* @param {UserLayer} [layer=uil.active] Layer to move
|
||||
*/
|
||||
_moveLayerUp(layer = uil.active) {
|
||||
const index = this.layers.indexOf(layer);
|
||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||
try {
|
||||
this._moveLayerTo(layer, index + 1);
|
||||
} catch (e) {}
|
||||
},
|
||||
/**
|
||||
* Moves a layer down a single position.
|
||||
*
|
||||
* Should not be called directly. Use the command instead.
|
||||
*
|
||||
* @param {UserLayer} [layer=uil.active] Layer to move
|
||||
*/
|
||||
_moveLayerDown(layer = uil.active) {
|
||||
const index = this.layers.indexOf(layer);
|
||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||
try {
|
||||
this._moveLayerTo(layer, index - 1);
|
||||
} catch (e) {}
|
||||
},
|
||||
/**
|
||||
* Function that returns a canvas with full visible information of a certain bounding box.
|
||||
*
|
||||
* For now, only the img is used.
|
||||
*
|
||||
* @param {BoundingBox} bb The bouding box to get visible data from
|
||||
* @param {object} [options] Options
|
||||
* @param {boolean} [options.includeBg=false] Whether to include the background
|
||||
* @returns {HTMLCanvasElement} The canvas element containing visible image data
|
||||
*/
|
||||
getVisible(bb, options = {}) {
|
||||
defaultOpt(options, {
|
||||
includeBg: false,
|
||||
});
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = bb.w;
|
||||
canvas.height = bb.h;
|
||||
if (options.includeBg)
|
||||
ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
this.layers.forEach((layer) => {
|
||||
if (!layer.hidden)
|
||||
ctx.drawImage(
|
||||
layer.layer.canvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
});
|
||||
|
||||
return canvas;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Command for creating a new layer
|
||||
*/
|
||||
commands.createCommand(
|
||||
"addLayer",
|
||||
(title, opt, state) => {
|
||||
const options = Object.assign({}, opt) || {};
|
||||
defaultOpt(options, {
|
||||
group: null,
|
||||
name: "New Layer",
|
||||
deletable: true,
|
||||
});
|
||||
|
||||
if (!state.layer) {
|
||||
const {group, name} = options;
|
||||
|
||||
const layer = imageCollection.registerLayer(null, {
|
||||
name,
|
||||
after:
|
||||
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
|
||||
bgLayer,
|
||||
});
|
||||
|
||||
state.layer = {
|
||||
id: layer.id,
|
||||
group,
|
||||
name,
|
||||
deletable: options.deletable,
|
||||
_hidden: false,
|
||||
set hidden(v) {
|
||||
if (v) {
|
||||
this._hidden = true;
|
||||
this.layer.hide(v);
|
||||
} else {
|
||||
this._hidden = false;
|
||||
this.layer.unhide(v);
|
||||
}
|
||||
},
|
||||
get hidden() {
|
||||
return this._hidden;
|
||||
},
|
||||
entry: null,
|
||||
layer,
|
||||
};
|
||||
}
|
||||
uil.layers.push(state.layer);
|
||||
|
||||
uil._syncLayers();
|
||||
|
||||
uil.active = state.layer;
|
||||
},
|
||||
(title, state) => {
|
||||
const index = uil.layers.findIndex((v) => v === state.layer);
|
||||
|
||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||
|
||||
if (uil.active === state.layer)
|
||||
uil.active = uil.layers[index + 1] || uil.layers[index - 1];
|
||||
uil.layers.splice(index, 1);
|
||||
uil._syncLayers();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Command for moving a layer to a position
|
||||
*/
|
||||
commands.createCommand(
|
||||
"moveLayer",
|
||||
(title, opt, state) => {
|
||||
const options = opt || {};
|
||||
defaultOpt(options, {
|
||||
layer: null,
|
||||
to: null,
|
||||
delta: null,
|
||||
});
|
||||
|
||||
if (!state.layer) {
|
||||
if (options.to === null && options.delta === null)
|
||||
throw new Error(
|
||||
"[layers.moveLayer] Options must contain one of {to?, delta?}"
|
||||
);
|
||||
|
||||
const layer = options.layer || uil.active;
|
||||
|
||||
const index = uil.layers.indexOf(layer);
|
||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||
|
||||
let position = options.to;
|
||||
|
||||
if (position === null) position = index + options.delta;
|
||||
|
||||
state.layer = layer;
|
||||
state.oldposition = index;
|
||||
state.position = position;
|
||||
}
|
||||
|
||||
uil._moveLayerTo(state.layer, state.position);
|
||||
},
|
||||
(title, state) => {
|
||||
uil._moveLayerTo(state.layer, state.oldposition);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Command for deleting a layer
|
||||
*/
|
||||
commands.createCommand(
|
||||
"deleteLayer",
|
||||
(title, opt, state) => {
|
||||
const options = opt || {};
|
||||
defaultOpt(options, {
|
||||
layer: null,
|
||||
});
|
||||
|
||||
if (!state.layer) {
|
||||
const layer = options.layer || uil.active;
|
||||
|
||||
if (!layer.deletable)
|
||||
throw new TypeError("[layer.deleteLayer] Layer is not deletable");
|
||||
|
||||
const index = uil.layers.indexOf(layer);
|
||||
if (index === -1)
|
||||
throw new ReferenceError(
|
||||
"[layer.deleteLayer] Layer could not be found"
|
||||
);
|
||||
|
||||
state.layer = layer;
|
||||
state.position = index;
|
||||
}
|
||||
|
||||
uil.layers.splice(state.position, 1);
|
||||
uil.active = uil.layers[state.position - 1] || uil.layers[state.position];
|
||||
|
||||
uil._syncLayers();
|
||||
|
||||
state.layer.layer.hide();
|
||||
},
|
||||
(title, state) => {
|
||||
uil.layers.splice(state.position, 0, state.layer);
|
||||
uil.active = state.layer;
|
||||
|
||||
uil._syncLayers();
|
||||
|
||||
state.layer.layer.unhide();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Command for merging a layer into the layer below it
|
||||
*/
|
||||
commands.createCommand(
|
||||
"mergeLayer",
|
||||
async (title, opt, state) => {
|
||||
const options = opt || {};
|
||||
defaultOpt(options, {
|
||||
layerS: null,
|
||||
layerD: null,
|
||||
});
|
||||
|
||||
const layerS = options.layer || uil.active;
|
||||
|
||||
if (!layerS.deletable)
|
||||
throw new TypeError(
|
||||
"[layer.mergeLayer] Layer is a root layer and cannot be merged"
|
||||
);
|
||||
|
||||
const index = uil.layers.indexOf(layerS);
|
||||
if (index === -1)
|
||||
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
||||
|
||||
if (index === 0 && !options.layerD)
|
||||
throw new ReferenceError(
|
||||
"[layer.mergeLayer] No layer below source layer exists"
|
||||
);
|
||||
|
||||
// Use layer under source layer to merge into if not given
|
||||
const layerD = options.layerD || uil.layers[index - 1];
|
||||
|
||||
state.layerS = layerS;
|
||||
state.layerD = layerD;
|
||||
|
||||
// REFERENCE: This is a great reference for metacommands (commands that use other commands)
|
||||
// These commands should NOT record history as we are already executing a command
|
||||
state.drawCommand = await commands.runCommand(
|
||||
"drawImage",
|
||||
"Merge Layer Draw",
|
||||
{
|
||||
image: state.layerS.layer.canvas,
|
||||
x: 0,
|
||||
y: 0,
|
||||
ctx: state.layerD.layer.ctx,
|
||||
},
|
||||
{recordHistory: false}
|
||||
);
|
||||
state.delCommand = await commands.runCommand(
|
||||
"deleteLayer",
|
||||
"Merge Layer Delete",
|
||||
{layer: state.layerS},
|
||||
{recordHistory: false}
|
||||
);
|
||||
},
|
||||
(title, state) => {
|
||||
state.drawCommand.undo();
|
||||
state.delCommand.undo();
|
||||
},
|
||||
(title, options, state) => {
|
||||
state.drawCommand.redo();
|
||||
state.delCommand.redo();
|
||||
}
|
||||
);
|
||||
|
||||
commands.runCommand(
|
||||
"addLayer",
|
||||
"Initial Layer Creation",
|
||||
{name: "Default Image Layer", deletable: false},
|
||||
{recordHistory: false}
|
||||
);
|
|
@ -3,7 +3,12 @@ const _color_brush_draw_callback = (evn, state) => {
|
|||
|
||||
ctx.strokeStyle = state.color;
|
||||
|
||||
ctx.filter = "blur(" + state.brushBlur + "px)";
|
||||
ctx.filter =
|
||||
"blur(" +
|
||||
state.brushBlur +
|
||||
"px) opacity(" +
|
||||
state.brushOpacity * 100 +
|
||||
"%)";
|
||||
ctx.lineWidth = state.brushSize;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
|
@ -13,6 +18,7 @@ const _color_brush_draw_callback = (evn, state) => {
|
|||
ctx.lineTo(evn.x, evn.y);
|
||||
ctx.lineJoin = ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
ctx.filter = null;
|
||||
};
|
||||
|
||||
const _color_brush_erase_callback = (evn, state, ctx) => {
|
||||
|
@ -35,8 +41,14 @@ const colorBrushTool = () =>
|
|||
"Color Brush",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb({...mouse.coords.world.pos});
|
||||
uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb({
|
||||
...mouse.coords.world.pos,
|
||||
evn: {
|
||||
clientX: mouse.coords.window.pos.x,
|
||||
clientY: mouse.coords.window.pos.y,
|
||||
},
|
||||
});
|
||||
|
||||
// Layer for eyedropper magnifiying glass
|
||||
state.glassLayer = imageCollection.registerLayer(null, {
|
||||
|
@ -44,21 +56,24 @@ const colorBrushTool = () =>
|
|||
resolution: {w: 7, h: 7},
|
||||
after: maskPaintLayer,
|
||||
});
|
||||
state.glassLayer.canvas.style.display = "none";
|
||||
state.glassLayer.hide();
|
||||
state.glassLayer.canvas.style.imageRendering = "pixelated";
|
||||
state.glassLayer.canvas.style.borderRadius = "50%";
|
||||
|
||||
state.drawLayer = imageCollection.registerLayer(null, {
|
||||
after: imgLayer,
|
||||
ctxOptions: {willReadFrequently: true},
|
||||
});
|
||||
state.eraseLayer = imageCollection.registerLayer(null, {
|
||||
after: imgLayer,
|
||||
ctxOptions: {willReadFrequently: true},
|
||||
});
|
||||
state.eraseLayer.canvas.style.display = "none";
|
||||
state.eraseLayer.hide();
|
||||
state.eraseBackup = imageCollection.registerLayer(null, {
|
||||
after: imgLayer,
|
||||
});
|
||||
state.eraseBackup.canvas.style.display = "none";
|
||||
state.eraseBackup.hide();
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.movecb);
|
||||
|
@ -105,6 +120,8 @@ const colorBrushTool = () =>
|
|||
// Cancel any eyedropping
|
||||
state.drawing = false;
|
||||
state.disableDropper();
|
||||
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -119,6 +136,7 @@ const colorBrushTool = () =>
|
|||
state.color = "#FFFFFF";
|
||||
state.brushSize = 32;
|
||||
state.brushBlur = 0;
|
||||
state.brushOpacity = 1;
|
||||
state.affectMask = true;
|
||||
state.setBrushSize = (size) => {
|
||||
state.brushSize = size;
|
||||
|
@ -131,13 +149,13 @@ const colorBrushTool = () =>
|
|||
state.enableDropper = () => {
|
||||
state.eyedropper = true;
|
||||
state.movecb(lastMouseMoveEvn);
|
||||
state.glassLayer.canvas.style.display = "block";
|
||||
state.glassLayer.unhide();
|
||||
};
|
||||
|
||||
state.disableDropper = () => {
|
||||
state.eyedropper = false;
|
||||
state.movecb(lastMouseMoveEvn);
|
||||
state.glassLayer.canvas.style.display = "none";
|
||||
state.glassLayer.hide();
|
||||
};
|
||||
|
||||
let lastMouseMoveEvn = {x: 0, y: 0};
|
||||
|
@ -145,26 +163,43 @@ const colorBrushTool = () =>
|
|||
state.movecb = (evn) => {
|
||||
lastMouseMoveEvn = evn;
|
||||
|
||||
// draw drawing cursor
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
const vcp = {x: evn.evn.clientX, y: evn.evn.clientY};
|
||||
|
||||
// draw drawing cursor
|
||||
uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
|
||||
uiCtx.beginPath();
|
||||
uiCtx.arc(
|
||||
vcp.x,
|
||||
vcp.y,
|
||||
(state.eyedropper ? 50 : state.brushSize / 2) * viewport.zoom,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
);
|
||||
uiCtx.strokeStyle = "black";
|
||||
uiCtx.stroke();
|
||||
|
||||
// Draw eyedropper cursor and magnifiying glass
|
||||
if (state.eyedropper) {
|
||||
const bb = getBoundingBox(evn.x, evn.y, 7, 7, false);
|
||||
|
||||
const canvas = getVisible(bb);
|
||||
const canvas = uil.getVisible(bb, {includeBg: true});
|
||||
state.glassLayer.ctx.clearRect(0, 0, 7, 7);
|
||||
state.glassLayer.ctx.drawImage(canvas, 0, 0);
|
||||
state.glassLayer.moveTo(evn.x - 50, evn.y - 50);
|
||||
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, 50, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 7x on a line???
|
||||
ovCtx.strokeStyle = "black";
|
||||
ovCtx.stroke();
|
||||
} else {
|
||||
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 7x on a line???
|
||||
ovCtx.fillStyle = state.color + "50";
|
||||
ovCtx.fill();
|
||||
uiCtx.beginPath();
|
||||
uiCtx.arc(
|
||||
vcp.x,
|
||||
vcp.y,
|
||||
(state.brushSize / 2) * viewport.zoom,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
);
|
||||
uiCtx.fillStyle = state.color + "50";
|
||||
uiCtx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -174,7 +209,7 @@ const colorBrushTool = () =>
|
|||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
@ -211,13 +246,20 @@ const colorBrushTool = () =>
|
|||
state.leftclickcb = (evn) => {
|
||||
if (evn.target === imageCollection.inputElement && state.eyedropper) {
|
||||
const bb = getBoundingBox(evn.x, evn.y, 1, 1, false);
|
||||
const visibleCanvas = getVisible(bb);
|
||||
const visibleCanvas = uil.getVisible(bb);
|
||||
const dat = visibleCanvas
|
||||
.getContext("2d")
|
||||
.getImageData(0, 0, 1, 1).data;
|
||||
state.setColor(
|
||||
"#" + ((dat[0] << 16) | (dat[1] << 8) | dat[2]).toString(16)
|
||||
);
|
||||
state.disableDropper();
|
||||
}
|
||||
};
|
||||
|
||||
state.rightclickcb = (evn) => {
|
||||
if (evn.target === imageCollection.inputElement && state.eyedropper) {
|
||||
state.disableDropper();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -255,29 +297,35 @@ const colorBrushTool = () =>
|
|||
};
|
||||
|
||||
state.erasestartcb = (evn) => {
|
||||
if (state.eyedropper) return;
|
||||
state.erasing = true;
|
||||
if (state.affectMask) _mask_brush_erase_callback(evn, state);
|
||||
|
||||
// Make a backup of the current image to apply erase later
|
||||
const bkpcanvas = state.eraseBackup.canvas;
|
||||
const bkpctx = state.eraseBackup.ctx;
|
||||
bkpctx.clearRect(0, 0, bkpcanvas.width, bkpcanvas.height);
|
||||
bkpctx.drawImage(imgCanvas, 0, 0);
|
||||
bkpctx.drawImage(uil.canvas, 0, 0);
|
||||
|
||||
imgCtx.globalCompositeOperation = "destination-out";
|
||||
_color_brush_erase_callback(evn, state, imgCtx);
|
||||
imgCtx.globalCompositeOperation = "source-over";
|
||||
uil.ctx.globalCompositeOperation = "destination-out";
|
||||
_color_brush_erase_callback(evn, state, uil.ctx);
|
||||
uil.ctx.globalCompositeOperation = "source-over";
|
||||
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
|
||||
};
|
||||
|
||||
state.erasecb = (evn) => {
|
||||
if (state.eyedropper || !state.erasing) return;
|
||||
if (state.affectMask) _mask_brush_erase_callback(evn, state);
|
||||
imgCtx.globalCompositeOperation = "destination-out";
|
||||
_color_brush_erase_callback(evn, state, imgCtx);
|
||||
imgCtx.globalCompositeOperation = "source-over";
|
||||
uil.ctx.globalCompositeOperation = "destination-out";
|
||||
_color_brush_erase_callback(evn, state, uil.ctx);
|
||||
uil.ctx.globalCompositeOperation = "source-over";
|
||||
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
|
||||
};
|
||||
|
||||
state.eraseendcb = (evn) => {
|
||||
if (!state.erasing) return;
|
||||
state.erasing = false;
|
||||
|
||||
const canvas = state.eraseLayer.canvas;
|
||||
const ctx = state.eraseLayer.ctx;
|
||||
|
||||
|
@ -286,8 +334,9 @@ const colorBrushTool = () =>
|
|||
const cropped = cropCanvas(canvas, {border: 10});
|
||||
const bb = cropped.bb;
|
||||
|
||||
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
|
||||
imgCtx.drawImage(bkpcanvas, 0, 0);
|
||||
uil.ctx.filter = null;
|
||||
uil.ctx.clearRect(0, 0, uil.canvas.width, uil.canvas.height);
|
||||
uil.ctx.drawImage(bkpcanvas, 0, 0);
|
||||
|
||||
commands.runCommand("eraseImage", "Color Brush Erase", {
|
||||
mask: cropped.canvas,
|
||||
|
@ -325,7 +374,21 @@ const colorBrushTool = () =>
|
|||
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
|
||||
// Brush size slider
|
||||
// Brush opacity slider
|
||||
const brushOpacitySlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushOpacity",
|
||||
"Brush Opacity",
|
||||
{
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textStep: 0.001,
|
||||
}
|
||||
);
|
||||
state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider;
|
||||
|
||||
// Brush blur slider
|
||||
const brushBlurSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushBlur",
|
||||
|
@ -364,7 +427,8 @@ const colorBrushTool = () =>
|
|||
"eyedropper"
|
||||
);
|
||||
brushColorEyeDropper.addEventListener("click", () => {
|
||||
state.enableDropper();
|
||||
if (state.eyedropper) state.disableDropper();
|
||||
else state.enableDropper();
|
||||
});
|
||||
|
||||
brushColorPickerWrapper.appendChild(brushColorPicker);
|
||||
|
@ -375,6 +439,7 @@ const colorBrushTool = () =>
|
|||
|
||||
menu.appendChild(state.ctxmenu.affectMaskCheckbox);
|
||||
menu.appendChild(state.ctxmenu.brushSizeSlider);
|
||||
menu.appendChild(state.ctxmenu.brushOpacitySlider);
|
||||
menu.appendChild(state.ctxmenu.brushBlurSlider);
|
||||
menu.appendChild(state.ctxmenu.brushColorPicker);
|
||||
},
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
let blockNewImages = false;
|
||||
let generating = false;
|
||||
|
||||
/**
|
||||
* Starts progress monitoring bar
|
||||
*
|
||||
* @param {BoundingBox} bb Bouding Box to draw progress to
|
||||
* @param {(data: object) => void} [oncheck] Callback function for when a progress check returns
|
||||
* @returns {() => void}
|
||||
*/
|
||||
const _monitorProgress = (bb) => {
|
||||
const _monitorProgress = (bb, oncheck = null) => {
|
||||
const minDelay = 1000;
|
||||
|
||||
const apiURL = `${host}${url}progress?skip_current_image=true`;
|
||||
|
@ -33,6 +35,8 @@ const _monitorProgress = (bb) => {
|
|||
/** @type {StableDiffusionProgressResponse} */
|
||||
const data = await response.json();
|
||||
|
||||
oncheck && oncheck(data);
|
||||
|
||||
// Draw Progress Bar
|
||||
layer.ctx.fillStyle = "#5F5";
|
||||
layer.ctx.fillRect(1, 1, bb.w * data.progress, 10);
|
||||
|
@ -81,6 +85,7 @@ const _dream = async (endpoint, request) => {
|
|||
/** @type {StableDiffusionResponse} */
|
||||
let data = null;
|
||||
try {
|
||||
generating = true;
|
||||
const response = await fetch(apiURL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
@ -92,6 +97,7 @@ const _dream = async (endpoint, request) => {
|
|||
|
||||
data = await response.json();
|
||||
} finally {
|
||||
generating = false;
|
||||
}
|
||||
|
||||
return data.images;
|
||||
|
@ -103,9 +109,15 @@ const _dream = async (endpoint, request) => {
|
|||
* @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to
|
||||
* @param {StableDiffusionRequest} request Stable diffusion request
|
||||
* @param {BoundingBox} bb Generated image placement location
|
||||
* @param {number} [drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration)
|
||||
* @returns {Promise<HTMLImageElement | null>}
|
||||
*/
|
||||
const _generate = async (endpoint, request, bb) => {
|
||||
const _generate = async (
|
||||
endpoint,
|
||||
request,
|
||||
bb,
|
||||
drawEvery = 0.2 / request.n_iter
|
||||
) => {
|
||||
const requestCopy = {...request};
|
||||
|
||||
// Images to select through
|
||||
|
@ -120,26 +132,47 @@ const _generate = async (endpoint, request, bb) => {
|
|||
after: maskPaintLayer,
|
||||
});
|
||||
|
||||
const redraw = () => {
|
||||
const makeElement = (type, x, y) => {
|
||||
const el = document.createElement(type);
|
||||
el.style.position = "absolute";
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = `${y}px`;
|
||||
|
||||
// We can use the input element to add interactible html elements in the world
|
||||
imageCollection.inputElement.appendChild(el);
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
const redraw = (url = images[at]) => {
|
||||
if (!url) return;
|
||||
|
||||
const image = new Image();
|
||||
image.src = "data:image/png;base64," + images[at];
|
||||
image.src = "data:image/png;base64," + url;
|
||||
image.addEventListener("load", () => {
|
||||
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
|
||||
if (images[at])
|
||||
layer.ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
image.width,
|
||||
image.height,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
layer.ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
image.width,
|
||||
image.height,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Add Interrupt Button
|
||||
const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h);
|
||||
interruptButton.classList.add("dream-interrupt-btn");
|
||||
interruptButton.textContent = "Interrupt";
|
||||
interruptButton.addEventListener("click", () => {
|
||||
fetch(`${host}${url}interrupt`, {method: "POST"});
|
||||
interruptButton.disabled = true;
|
||||
});
|
||||
const stopMarchingAnts = march(bb);
|
||||
|
||||
// First Dream Run
|
||||
|
@ -148,8 +181,28 @@ const _generate = async (endpoint, request, bb) => {
|
|||
|
||||
let stopProgress = null;
|
||||
try {
|
||||
stopProgress = _monitorProgress(bb);
|
||||
let stopDrawingStatus = false;
|
||||
let lastProgress = 0;
|
||||
let nextCP = drawEvery;
|
||||
stopProgress = _monitorProgress(bb, (data) => {
|
||||
if (stopDrawingStatus) return;
|
||||
|
||||
if (lastProgress < nextCP && data.progress >= nextCP) {
|
||||
nextCP += drawEvery;
|
||||
fetch(`${host}${url}progress?skip_current_image=false`).then(
|
||||
async (response) => {
|
||||
if (stopDrawingStatus) return;
|
||||
const imagedata = await response.json();
|
||||
redraw(imagedata.current_image);
|
||||
}
|
||||
);
|
||||
}
|
||||
lastProgress = data.progress;
|
||||
});
|
||||
|
||||
imageCollection.inputElement.appendChild(interruptButton);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
stopDrawingStatus = true;
|
||||
} catch (e) {
|
||||
alert(
|
||||
`Error generating images. Please try again or see consolde for more details`
|
||||
|
@ -158,6 +211,7 @@ const _generate = async (endpoint, request, bb) => {
|
|||
console.warn(e);
|
||||
} finally {
|
||||
stopProgress();
|
||||
imageCollection.inputElement.removeChild(interruptButton);
|
||||
}
|
||||
|
||||
// Image navigation
|
||||
|
@ -196,6 +250,8 @@ const _generate = async (endpoint, request, bb) => {
|
|||
const makeMore = async () => {
|
||||
try {
|
||||
stopProgress = _monitorProgress(bb);
|
||||
interruptButton.disabled = false;
|
||||
imageCollection.inputElement.appendChild(interruptButton);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
imageindextxt.textContent = `${at + 1}/${images.length}`;
|
||||
} catch (e) {
|
||||
|
@ -206,6 +262,7 @@ const _generate = async (endpoint, request, bb) => {
|
|||
console.warn(e);
|
||||
} finally {
|
||||
stopProgress();
|
||||
imageCollection.inputElement.removeChild(interruptButton);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -265,18 +322,6 @@ const _generate = async (endpoint, request, bb) => {
|
|||
keyboard.listen.onkeyclick.clear(onarrow);
|
||||
};
|
||||
|
||||
const makeElement = (type, x, y) => {
|
||||
const el = document.createElement(type);
|
||||
el.style.position = "absolute";
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = `${y}px`;
|
||||
|
||||
// We can use the input element to add interactible html elements in the world
|
||||
imageCollection.inputElement.appendChild(el);
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
redraw();
|
||||
|
||||
imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h);
|
||||
|
@ -366,8 +411,11 @@ const dream_generate_callback = async (evn, state) => {
|
|||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Get visible pixels
|
||||
const visibleCanvas = uil.getVisible(bb);
|
||||
|
||||
// Use txt2img if canvas is blank
|
||||
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) {
|
||||
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) {
|
||||
// Dream
|
||||
_generate("txt2img", request, bb);
|
||||
} else {
|
||||
|
@ -384,9 +432,9 @@ const dream_generate_callback = async (evn, state) => {
|
|||
// Get init image
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
visibleCanvas,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
|
@ -417,9 +465,9 @@ const dream_generate_callback = async (evn, state) => {
|
|||
|
||||
auxCtx.globalCompositeOperation = "destination-in";
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
visibleCanvas,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
|
@ -430,9 +478,9 @@ const dream_generate_callback = async (evn, state) => {
|
|||
} else {
|
||||
auxCtx.globalCompositeOperation = "destination-in";
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
visibleCanvas,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
|
@ -535,8 +583,11 @@ const dream_img2img_callback = (evn, state) => {
|
|||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// Get visible pixels
|
||||
const visibleCanvas = uil.getVisible(bb);
|
||||
|
||||
// Do nothing if no image exists
|
||||
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return;
|
||||
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return;
|
||||
|
||||
// Build request to the API
|
||||
const request = {};
|
||||
|
@ -565,9 +616,9 @@ const dream_img2img_callback = (evn, state) => {
|
|||
// Get init image
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
visibleCanvas,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
|
@ -601,14 +652,48 @@ const dream_img2img_callback = (evn, state) => {
|
|||
if (state.keepBorderSize > 0) {
|
||||
auxCtx.globalCompositeOperation = "source-over";
|
||||
auxCtx.fillStyle = "#000F";
|
||||
if (state.gradient) {
|
||||
const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0);
|
||||
lg.addColorStop(0, "#000F");
|
||||
lg.addColorStop(1, "#0000");
|
||||
auxCtx.fillStyle = lg;
|
||||
}
|
||||
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
|
||||
if (state.gradient) {
|
||||
const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize);
|
||||
tg.addColorStop(0, "#000F");
|
||||
tg.addColorStop(1, "#0000");
|
||||
auxCtx.fillStyle = tg;
|
||||
}
|
||||
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
|
||||
if (state.gradient) {
|
||||
const rg = auxCtx.createLinearGradient(
|
||||
request.width,
|
||||
0,
|
||||
request.width - state.keepBorderSize,
|
||||
0
|
||||
);
|
||||
rg.addColorStop(0, "#000F");
|
||||
rg.addColorStop(1, "#0000");
|
||||
auxCtx.fillStyle = rg;
|
||||
}
|
||||
auxCtx.fillRect(
|
||||
request.width - state.keepBorderSize,
|
||||
0,
|
||||
state.keepBorderSize,
|
||||
request.height
|
||||
);
|
||||
if (state.gradient) {
|
||||
const bg = auxCtx.createLinearGradient(
|
||||
0,
|
||||
request.height,
|
||||
0,
|
||||
request.height - state.keepBorderSize
|
||||
);
|
||||
bg.addColorStop(0, "#000F");
|
||||
bg.addColorStop(1, "#0000");
|
||||
auxCtx.fillStyle = bg;
|
||||
}
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
request.height - state.keepBorderSize,
|
||||
|
@ -628,7 +713,14 @@ const dream_img2img_callback = (evn, state) => {
|
|||
/**
|
||||
* Dream and img2img tools
|
||||
*/
|
||||
const _reticle_draw = (evn, state) => {
|
||||
const _reticle_draw = (evn, state, tool, style = {}) => {
|
||||
defaultOpt(style, {
|
||||
sizeTextStyle: "#FFF5",
|
||||
toolTextStyle: "#FFF5",
|
||||
reticleWidth: 1,
|
||||
reticleStyle: "#FFF",
|
||||
});
|
||||
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
|
@ -636,14 +728,70 @@ const _reticle_draw = (evn, state) => {
|
|||
state.cursorSize,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
|
||||
uiCtx.save();
|
||||
|
||||
// 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
|
||||
uiCtx.lineWidth = style.reticleWidth;
|
||||
uiCtx.strokeStyle = style.reticleStyle;
|
||||
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame
|
||||
|
||||
uiCtx.font = `bold 20px Open Sans`;
|
||||
|
||||
// Draw Tool Name
|
||||
{
|
||||
const xshrink = Math.min(1, (bbvp.w - 20) / uiCtx.measureText(tool).width);
|
||||
|
||||
uiCtx.font = `bold ${20 * xshrink}px Open Sans`;
|
||||
|
||||
uiCtx.textAlign = "left";
|
||||
uiCtx.fillStyle = style.toolTextStyle;
|
||||
uiCtx.fillText(tool, bbvp.x + 10, bbvp.y + 10 + 20 * xshrink);
|
||||
}
|
||||
|
||||
// Draw width and height
|
||||
{
|
||||
uiCtx.textAlign = "center";
|
||||
uiCtx.fillStyle = style.sizeTextStyle;
|
||||
uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2);
|
||||
const xshrink = Math.min(
|
||||
1,
|
||||
(bbvp.w - 30) / uiCtx.measureText(`${state.cursorSize}px`).width
|
||||
);
|
||||
const yshrink = Math.min(
|
||||
1,
|
||||
(bbvp.h - 30) / uiCtx.measureText(`${state.cursorSize}px`).width
|
||||
);
|
||||
uiCtx.font = `bold ${20 * xshrink}px Open Sans`;
|
||||
uiCtx.fillText(
|
||||
`${state.cursorSize}px`,
|
||||
0,
|
||||
bbvp.h / 2 - 10 * xshrink,
|
||||
state.cursorSize
|
||||
);
|
||||
uiCtx.rotate(-Math.PI / 2);
|
||||
uiCtx.font = `bold ${20 * yshrink}px Open Sans`;
|
||||
uiCtx.fillText(
|
||||
`${state.cursorSize}px`,
|
||||
0,
|
||||
bbvp.h / 2 - 10 * yshrink,
|
||||
state.cursorSize
|
||||
);
|
||||
|
||||
uiCtx.restore();
|
||||
}
|
||||
|
||||
return () => {
|
||||
ovCtx.clearRect(bb.x - 10, bb.y - 10, bb.w + 20, bb.h + 20);
|
||||
uiCtx.save();
|
||||
|
||||
uiCtx.clearRect(bbvp.x - 10, bbvp.y - 10, bbvp.w + 20, bbvp.h + 20);
|
||||
|
||||
uiCtx.restore();
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -670,10 +818,11 @@ const dreamTool = () =>
|
|||
"Dream",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
state.lastMouseMove = {
|
||||
...mouse.coords.world.pos,
|
||||
});
|
||||
};
|
||||
state.redraw();
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.mousemovecb);
|
||||
|
@ -693,6 +842,8 @@ const dreamTool = () =>
|
|||
|
||||
// Hide Mask
|
||||
setMask("none");
|
||||
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -707,12 +858,30 @@ const dreamTool = () =>
|
|||
state.overMaskPx = 0;
|
||||
|
||||
state.erasePrevReticle = () =>
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
|
||||
state.lastMouseMove = {
|
||||
...mouse.coords.world.pos,
|
||||
};
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
state.lastMouseMove = evn;
|
||||
state.erasePrevReticle();
|
||||
state.erasePrevReticle = _reticle_draw(evn, state);
|
||||
const style =
|
||||
state.cursorSize > stableDiffusionData.width
|
||||
? "#FBB5"
|
||||
: state.cursorSize < stableDiffusionData.width
|
||||
? "#BFB5"
|
||||
: "#FFF5";
|
||||
state.erasePrevReticle = _reticle_draw(evn, state, "Dream", {
|
||||
sizeTextStyle: style,
|
||||
});
|
||||
};
|
||||
|
||||
state.redraw = () => {
|
||||
state.mousemovecb(state.lastMouseMove);
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
_dream_onwheel(evn, state);
|
||||
};
|
||||
|
@ -789,10 +958,10 @@ const img2imgTool = () =>
|
|||
"Img2Img",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({
|
||||
state.lastMouseMove = {
|
||||
...mouse.coords.world.pos,
|
||||
});
|
||||
};
|
||||
state.redraw();
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.mousemovecb);
|
||||
|
@ -812,6 +981,7 @@ const img2imgTool = () =>
|
|||
|
||||
// Hide mask
|
||||
setMask("none");
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -827,13 +997,28 @@ const img2imgTool = () =>
|
|||
state.denoisingStrength = 0.7;
|
||||
|
||||
state.keepBorderSize = 64;
|
||||
state.gradient = true;
|
||||
|
||||
state.erasePrevReticle = () =>
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
|
||||
state.lastMouseMove = {
|
||||
...mouse.coords.world.pos,
|
||||
};
|
||||
state.mousemovecb = (evn) => {
|
||||
state.lastMouseMove = evn;
|
||||
state.erasePrevReticle();
|
||||
state.erasePrevReticle = _reticle_draw(evn, state);
|
||||
|
||||
const style =
|
||||
state.cursorSize > stableDiffusionData.width
|
||||
? "#FBB5"
|
||||
: state.cursorSize < stableDiffusionData.width
|
||||
? "#BFB5"
|
||||
: "#FFF5";
|
||||
state.erasePrevReticle = _reticle_draw(evn, state, "Img2Img", {
|
||||
sizeTextStyle: style,
|
||||
});
|
||||
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
|
@ -848,6 +1033,12 @@ const img2imgTool = () =>
|
|||
height: stableDiffusionData.height,
|
||||
};
|
||||
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
|
||||
// For displaying border mask
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = request.width;
|
||||
|
@ -856,33 +1047,82 @@ const img2imgTool = () =>
|
|||
|
||||
if (state.keepBorderSize > 0) {
|
||||
auxCtx.fillStyle = "#6A6AFF30";
|
||||
if (state.gradient) {
|
||||
const lg = auxCtx.createLinearGradient(
|
||||
0,
|
||||
0,
|
||||
state.keepBorderSize,
|
||||
0
|
||||
);
|
||||
lg.addColorStop(0, "#6A6AFF30");
|
||||
lg.addColorStop(1, "#0000");
|
||||
auxCtx.fillStyle = lg;
|
||||
}
|
||||
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
|
||||
if (state.gradient) {
|
||||
const tg = auxCtx.createLinearGradient(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
state.keepBorderSize
|
||||
);
|
||||
tg.addColorStop(0, "#6A6AFF30");
|
||||
tg.addColorStop(1, "#6A6AFF00");
|
||||
auxCtx.fillStyle = tg;
|
||||
}
|
||||
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
|
||||
if (state.gradient) {
|
||||
const rg = auxCtx.createLinearGradient(
|
||||
request.width,
|
||||
0,
|
||||
request.width - state.keepBorderSize,
|
||||
0
|
||||
);
|
||||
rg.addColorStop(0, "#6A6AFF30");
|
||||
rg.addColorStop(1, "#6A6AFF00");
|
||||
auxCtx.fillStyle = rg;
|
||||
}
|
||||
auxCtx.fillRect(
|
||||
request.width - state.keepBorderSize,
|
||||
0,
|
||||
state.keepBorderSize,
|
||||
request.height
|
||||
);
|
||||
if (state.gradient) {
|
||||
const bg = auxCtx.createLinearGradient(
|
||||
0,
|
||||
request.height,
|
||||
0,
|
||||
request.height - state.keepBorderSize
|
||||
);
|
||||
bg.addColorStop(0, "#6A6AFF30");
|
||||
bg.addColorStop(1, "#6A6AFF00");
|
||||
auxCtx.fillStyle = bg;
|
||||
}
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
request.height - state.keepBorderSize,
|
||||
request.width,
|
||||
state.keepBorderSize
|
||||
);
|
||||
ovCtx.drawImage(
|
||||
uiCtx.drawImage(
|
||||
auxCanvas,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h
|
||||
bbvp.x,
|
||||
bbvp.y,
|
||||
bbvp.w,
|
||||
bbvp.h
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
state.redraw = () => {
|
||||
state.mousemovecb(state.lastMouseMove);
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
_dream_onwheel(evn, state);
|
||||
};
|
||||
|
@ -948,6 +1188,13 @@ const img2imgTool = () =>
|
|||
}
|
||||
).slider;
|
||||
|
||||
// Border Mask Gradient Checkbox
|
||||
state.ctxmenu.borderMaskGradientCheckbox = _toolbar_input.checkbox(
|
||||
state,
|
||||
"gradient",
|
||||
"Border Mask Gradient"
|
||||
).label;
|
||||
|
||||
// Border Mask Size Slider
|
||||
state.ctxmenu.borderMaskSlider = _toolbar_input.slider(
|
||||
state,
|
||||
|
@ -970,8 +1217,14 @@ const img2imgTool = () =>
|
|||
menu.appendChild(state.ctxmenu.fullResolutionLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.denoisingStrengthSlider);
|
||||
menu.appendChild(state.ctxmenu.borderMaskGradientCheckbox);
|
||||
menu.appendChild(state.ctxmenu.borderMaskSlider);
|
||||
},
|
||||
shortcut: "I",
|
||||
}
|
||||
);
|
||||
|
||||
window.onbeforeunload = async () => {
|
||||
// Stop current generation on page close
|
||||
if (generating) await fetch(`${host}${url}interrupt`, {method: "POST"});
|
||||
};
|
||||
|
|
|
@ -22,6 +22,8 @@ const interrogateTool = () =>
|
|||
|
||||
// Hide Mask
|
||||
setMask("none");
|
||||
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -40,11 +42,16 @@ const interrogateTool = () =>
|
|||
|
||||
state.mousemovecb = (evn) => {
|
||||
state.erasePrevReticle();
|
||||
state.erasePrevReticle = _reticle_draw(evn, state);
|
||||
state.erasePrevReticle = _reticle_draw(evn, state, "Interrogate", {
|
||||
toolTextStyle: "#AFA5",
|
||||
sizeTextStyle: "#AFA5",
|
||||
reticleStyle: "#AFAF",
|
||||
});
|
||||
};
|
||||
state.wheelcb = (evn) => {
|
||||
_interrogate_onwheel(evn, state);
|
||||
};
|
||||
|
||||
state.interrogatecb = (evn) => {
|
||||
interrogate_callback(evn, state);
|
||||
};
|
||||
|
@ -98,7 +105,7 @@ const _interrogate_onwheel = (evn, state) => {
|
|||
}
|
||||
};
|
||||
|
||||
const interrogate_callback = (evn, state) => {
|
||||
const interrogate_callback = async (evn, state) => {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
|
@ -107,7 +114,9 @@ const interrogate_callback = (evn, state) => {
|
|||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
// Do nothing if no image exists
|
||||
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return;
|
||||
const sectionCanvas = uil.getVisible({x: bb.x, y: bb.y, w: bb.w, h: bb.h});
|
||||
|
||||
if (isCanvasBlank(0, 0, bb.w, bb.h, sectionCanvas)) return;
|
||||
|
||||
// Build request to the API
|
||||
const request = {};
|
||||
|
@ -122,16 +131,25 @@ const interrogate_callback = (evn, state) => {
|
|||
|
||||
// Get init image
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(sectionCanvas, 0, 0);
|
||||
request.image = auxCanvas.toDataURL();
|
||||
|
||||
request.model = "clip"; //TODO maybe make a selectable option once A1111 supports the new openclip thingy?
|
||||
const interrogation = _interrogate(request).then(function (result) {
|
||||
if (confirm(result + "\n\nDo you want to replace your prompt with this?")) {
|
||||
document.getElementById("prompt").value = result;
|
||||
const stopMarching = march(bb, {style: "#AFAF"});
|
||||
try {
|
||||
const result = await _interrogate(request);
|
||||
const text = prompt(
|
||||
result +
|
||||
"\n\nDo you want to replace your prompt with this? You can change it down below:",
|
||||
result
|
||||
);
|
||||
if (text) {
|
||||
document.getElementById("prompt").value = text;
|
||||
tools.dream.enable();
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
stopMarching();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,15 +17,17 @@ const setMask = (state) => {
|
|||
canvas.classList.remove("display", "hold", "clear");
|
||||
break;
|
||||
default:
|
||||
console.debug(`Invalid mask type: ${state}`);
|
||||
console.debug(`[maskbrush.setMask] Invalid mask type: ${state}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const _mask_brush_draw_callback = (evn, state) => {
|
||||
const _mask_brush_draw_callback = (evn, state, opacity = 100) => {
|
||||
maskPaintCtx.globalCompositeOperation = "source-over";
|
||||
maskPaintCtx.strokeStyle = "black";
|
||||
|
||||
maskPaintCtx.filter =
|
||||
"blur(" + state.brushBlur + "px) opacity(" + opacity + "%)";
|
||||
maskPaintCtx.lineWidth = state.brushSize;
|
||||
maskPaintCtx.beginPath();
|
||||
maskPaintCtx.moveTo(
|
||||
|
@ -35,12 +37,16 @@ const _mask_brush_draw_callback = (evn, state) => {
|
|||
maskPaintCtx.lineTo(evn.x, evn.y);
|
||||
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
||||
maskPaintCtx.stroke();
|
||||
maskPaintCtx.filter = null;
|
||||
};
|
||||
|
||||
const _mask_brush_erase_callback = (evn, state) => {
|
||||
const _mask_brush_erase_callback = (evn, state, opacity = 100) => {
|
||||
maskPaintCtx.globalCompositeOperation = "destination-out";
|
||||
maskPaintCtx.strokeStyle = "black";
|
||||
|
||||
maskPaintCtx.filter = "blur(" + state.brushBlur + "px)";
|
||||
maskPaintCtx.filter =
|
||||
"blur(" + state.brushBlur + "px) opacity(" + opacity + "%)";
|
||||
maskPaintCtx.lineWidth = state.brushSize;
|
||||
maskPaintCtx.beginPath();
|
||||
maskPaintCtx.moveTo(
|
||||
|
@ -50,34 +56,7 @@ const _mask_brush_erase_callback = (evn, state) => {
|
|||
maskPaintCtx.lineTo(evn.x, evn.y);
|
||||
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
||||
maskPaintCtx.stroke();
|
||||
};
|
||||
|
||||
const _paint_mb_cursor = (state) => {
|
||||
const v = state.brushSize;
|
||||
state.cursorLayer.resize(v + 20, v + 20);
|
||||
|
||||
const ctx = state.cursorLayer.ctx;
|
||||
|
||||
ctx.clearRect(0, 0, v + 20, v + 20);
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
(v + 20) / 2,
|
||||
(v + 20) / 2,
|
||||
state.brushSize / 2,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
);
|
||||
ctx.fillStyle = "#FFFFFF50";
|
||||
|
||||
ctx.fill();
|
||||
|
||||
if (state.preview) {
|
||||
ctx.strokeStyle = "#000F";
|
||||
ctx.setLineDash([4, 2]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
maskPaintCtx.filter = null;
|
||||
};
|
||||
|
||||
const maskBrushTool = () =>
|
||||
|
@ -85,17 +64,9 @@ const maskBrushTool = () =>
|
|||
"res/icons/paintbrush.svg",
|
||||
"Mask Brush",
|
||||
(state, opt) => {
|
||||
// New layer for the cursor
|
||||
state.cursorLayer = imageCollection.registerLayer(null, {
|
||||
after: maskPaintLayer,
|
||||
bb: {x: 0, y: 0, w: state.brushSize + 20, h: state.brushSize + 20},
|
||||
});
|
||||
|
||||
_paint_mb_cursor(state);
|
||||
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb({...mouse.coords.world.pos});
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
state.redraw();
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.movecb);
|
||||
|
@ -109,10 +80,6 @@ const maskBrushTool = () =>
|
|||
setMask("neutral");
|
||||
},
|
||||
(state, opt) => {
|
||||
// Don't want to keep hogging resources
|
||||
imageCollection.deleteLayer(state.cursorLayer);
|
||||
state.cursorLayer = null;
|
||||
|
||||
// Clear Listeners
|
||||
mouse.listen.world.onmousemove.clear(state.movecb);
|
||||
mouse.listen.world.onwheel.clear(state.wheelcb);
|
||||
|
@ -126,6 +93,8 @@ const maskBrushTool = () =>
|
|||
state.ctxmenu.previewMaskButton.classList.remove("active");
|
||||
maskPaintCanvas.classList.remove("opaque");
|
||||
state.preview = false;
|
||||
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -133,9 +102,13 @@ const maskBrushTool = () =>
|
|||
brushScrollSpeed: 1 / 4,
|
||||
minBrushSize: 10,
|
||||
maxBrushSize: 500,
|
||||
minBlur: 0,
|
||||
maxBlur: 30,
|
||||
};
|
||||
|
||||
state.brushSize = 64;
|
||||
state.brushBlur = 0;
|
||||
state.brushOpacity = 1;
|
||||
state.setBrushSize = (size) => {
|
||||
state.brushSize = size;
|
||||
state.ctxmenu.brushSizeRange.value = size;
|
||||
|
@ -145,21 +118,41 @@ const maskBrushTool = () =>
|
|||
state.preview = false;
|
||||
|
||||
state.clearPrevCursor = () =>
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
|
||||
state.redraw = () => {
|
||||
state.movecb({
|
||||
...mouse.coords.world.pos,
|
||||
evn: {
|
||||
clientX: mouse.coords.window.pos.x,
|
||||
clientY: mouse.coords.window.pos.y,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
state.cursorLayer.moveTo(
|
||||
evn.x - state.brushSize / 2 - 10,
|
||||
evn.y - state.brushSize / 2 - 10
|
||||
);
|
||||
const vcp = {x: evn.evn.clientX, y: evn.evn.clientY};
|
||||
const scp = state.brushSize * viewport.zoom;
|
||||
|
||||
state.clearPrevCursor = () =>
|
||||
ovCtx.clearRect(
|
||||
evn.x - state.brushSize / 2 - 10,
|
||||
evn.y - state.brushSize / 2 - 10,
|
||||
evn.x + state.brushSize / 2 + 10,
|
||||
evn.y + state.brushSize / 2 + 10
|
||||
state.clearPrevCursor();
|
||||
state;
|
||||
clearPrevCursor = () =>
|
||||
uiCtx.clearRect(
|
||||
vcp.x - scp / 2 - 10,
|
||||
vcp.y - scp / 2 - 10,
|
||||
vcp.x + scp / 2 + 10,
|
||||
vcp.y + scp / 2 + 10
|
||||
);
|
||||
|
||||
uiCtx.beginPath();
|
||||
uiCtx.arc(vcp.x, vcp.y, scp / 2, 0, 2 * Math.PI, true);
|
||||
uiCtx.strokeStyle = "black";
|
||||
uiCtx.stroke();
|
||||
|
||||
uiCtx.beginPath();
|
||||
uiCtx.arc(vcp.x, vcp.y, scp / 2, 0, 2 * Math.PI, true);
|
||||
uiCtx.fillStyle = "#FFFFFF50";
|
||||
uiCtx.fill();
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
|
@ -168,16 +161,19 @@ const maskBrushTool = () =>
|
|||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
||||
state.drawcb = (evn) => _mask_brush_draw_callback(evn, state);
|
||||
state.erasecb = (evn) => _mask_brush_erase_callback(evn, state);
|
||||
state.drawcb = (evn) =>
|
||||
_mask_brush_draw_callback(evn, state, state.brushOpacity * 100);
|
||||
state.erasecb = (evn) =>
|
||||
_mask_brush_erase_callback(evn, state, state.brushOpacity * 100);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
|
||||
// Brush size slider
|
||||
const brushSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushSize",
|
||||
|
@ -189,13 +185,41 @@ const maskBrushTool = () =>
|
|||
textStep: 1,
|
||||
cb: (v) => {
|
||||
if (!state.cursorLayer) return;
|
||||
_paint_mb_cursor(state);
|
||||
|
||||
state.redraw();
|
||||
},
|
||||
}
|
||||
);
|
||||
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
|
||||
// Brush opacity slider
|
||||
const brushOpacitySlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushOpacity",
|
||||
"Brush Opacity",
|
||||
{
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textStep: 0.001,
|
||||
}
|
||||
);
|
||||
state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider;
|
||||
|
||||
// Brush blur slider
|
||||
const brushBlurSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushBlur",
|
||||
"Brush Blur",
|
||||
{
|
||||
min: state.config.minBlur,
|
||||
max: state.config.maxBlur,
|
||||
step: 1,
|
||||
}
|
||||
);
|
||||
state.ctxmenu.brushBlurSlider = brushBlurSlider.slider;
|
||||
|
||||
// Some mask-related action buttons
|
||||
const actionArray = document.createElement("div");
|
||||
actionArray.classList.add("button-array");
|
||||
|
@ -221,11 +245,12 @@ const maskBrushTool = () =>
|
|||
if (previewMaskButton.classList.contains("active")) {
|
||||
maskPaintCanvas.classList.remove("opaque");
|
||||
state.preview = false;
|
||||
_paint_mb_cursor(state);
|
||||
|
||||
state.redraw();
|
||||
} else {
|
||||
maskPaintCanvas.classList.add("opaque");
|
||||
state.preview = true;
|
||||
_paint_mb_cursor(state);
|
||||
state.redraw();
|
||||
}
|
||||
previewMaskButton.classList.toggle("active");
|
||||
};
|
||||
|
@ -239,6 +264,8 @@ const maskBrushTool = () =>
|
|||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.brushSizeSlider);
|
||||
menu.appendChild(state.ctxmenu.brushOpacitySlider);
|
||||
menu.appendChild(state.ctxmenu.brushBlurSlider);
|
||||
menu.appendChild(state.ctxmenu.actionArray);
|
||||
},
|
||||
shortcut: "M",
|
||||
|
|
|
@ -46,6 +46,9 @@ const selectTransformTool = () =>
|
|||
state.reset();
|
||||
|
||||
// Resets cursor
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
|
||||
// Clears overlay
|
||||
imageCollection.inputElement.style.cursor = "auto";
|
||||
},
|
||||
{
|
||||
|
@ -76,7 +79,7 @@ const selectTransformTool = () =>
|
|||
state.lastMouseTarget = null;
|
||||
state.lastMouseMove = null;
|
||||
|
||||
const redraw = () => {
|
||||
state.redraw = () => {
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(state.lastMouseMove);
|
||||
};
|
||||
|
@ -84,7 +87,7 @@ const selectTransformTool = () =>
|
|||
// Clears selection and make things right
|
||||
state.reset = () => {
|
||||
if (state.selected)
|
||||
imgCtx.drawImage(
|
||||
uil.ctx.drawImage(
|
||||
state.original.image,
|
||||
state.original.x,
|
||||
state.original.y
|
||||
|
@ -93,7 +96,7 @@ const selectTransformTool = () =>
|
|||
if (state.dragging) state.dragging = null;
|
||||
else state.selected = null;
|
||||
|
||||
redraw();
|
||||
state.redraw();
|
||||
};
|
||||
|
||||
// Selection bounding box object. Has some witchery to deal with handles.
|
||||
|
@ -188,6 +191,7 @@ const selectTransformTool = () =>
|
|||
// Mouse move handler. As always, also renders cursor
|
||||
state.movecb = (evn) => {
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
||||
imageCollection.inputElement.style.cursor = "auto";
|
||||
state.lastMouseTarget = evn.target;
|
||||
state.lastMouseMove = evn;
|
||||
|
@ -198,6 +202,10 @@ const selectTransformTool = () =>
|
|||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
|
||||
const vpc = viewport.canvasToView(x, y);
|
||||
|
||||
uiCtx.save();
|
||||
|
||||
// Update scale
|
||||
if (state.scaling) {
|
||||
state.scaling.scaleTo(x, y, state.keepAspectRatio);
|
||||
|
@ -212,17 +220,23 @@ const selectTransformTool = () =>
|
|||
|
||||
// Draw dragging box
|
||||
if (state.dragging) {
|
||||
ovCtx.setLineDash([2, 2]);
|
||||
ovCtx.lineWidth = 1;
|
||||
ovCtx.strokeStyle = "#FFF";
|
||||
uiCtx.setLineDash([2, 2]);
|
||||
uiCtx.lineWidth = 1;
|
||||
uiCtx.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([]);
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
|
||||
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
|
||||
uiCtx.setLineDash([]);
|
||||
}
|
||||
|
||||
if (state.selected) {
|
||||
|
@ -236,6 +250,12 @@ const selectTransformTool = () =>
|
|||
h: state.selected.h,
|
||||
};
|
||||
|
||||
const bbvp = {
|
||||
...viewport.canvasToView(bb.x, bb.y),
|
||||
w: viewport.zoom * bb.w,
|
||||
h: viewport.zoom * bb.h,
|
||||
};
|
||||
|
||||
// Draw Image
|
||||
ovCtx.drawImage(
|
||||
state.selected.image,
|
||||
|
@ -250,34 +270,40 @@ const selectTransformTool = () =>
|
|||
);
|
||||
|
||||
// Draw selection box
|
||||
ovCtx.setLineDash([4, 2]);
|
||||
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
|
||||
ovCtx.setLineDash([]);
|
||||
uiCtx.strokeStyle = "#FFF";
|
||||
uiCtx.setLineDash([4, 2]);
|
||||
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
|
||||
uiCtx.setLineDash([]);
|
||||
|
||||
// Draw Scaling/Rotation Origin
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(
|
||||
state.selected.x + state.selected.w / 2,
|
||||
state.selected.y + state.selected.h / 2,
|
||||
uiCtx.beginPath();
|
||||
uiCtx.arc(
|
||||
bbvp.x + bbvp.w / 2,
|
||||
bbvp.y + bbvp.h / 2,
|
||||
5,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ovCtx.stroke();
|
||||
uiCtx.stroke();
|
||||
|
||||
// Draw Scaling Handles
|
||||
let cursorInHandle = false;
|
||||
state.selected.handles().forEach((handle) => {
|
||||
const bbvph = {
|
||||
...viewport.canvasToView(handle.x, handle.y),
|
||||
w: viewport.zoom * handle.w,
|
||||
h: viewport.zoom * handle.h,
|
||||
};
|
||||
if (handle.contains(evn.x, evn.y)) {
|
||||
cursorInHandle = true;
|
||||
ovCtx.strokeRect(
|
||||
handle.x - 1,
|
||||
handle.y - 1,
|
||||
handle.w + 2,
|
||||
handle.h + 2
|
||||
uiCtx.strokeRect(
|
||||
bbvph.x - 1,
|
||||
bbvph.y - 1,
|
||||
bbvph.w + 2,
|
||||
bbvph.h + 2
|
||||
);
|
||||
} else {
|
||||
ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h);
|
||||
uiCtx.strokeRect(bbvph.x, bbvph.y, bbvph.w, bbvph.h);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -287,15 +313,17 @@ const selectTransformTool = () =>
|
|||
}
|
||||
|
||||
// Draw current cursor location
|
||||
ovCtx.lineWidth = 3;
|
||||
ovCtx.strokeStyle = "#FFF";
|
||||
uiCtx.lineWidth = 3;
|
||||
uiCtx.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();
|
||||
uiCtx.beginPath();
|
||||
uiCtx.moveTo(vpc.x, vpc.y + 10);
|
||||
uiCtx.lineTo(vpc.x, vpc.y - 10);
|
||||
uiCtx.moveTo(vpc.x + 10, vpc.y);
|
||||
uiCtx.lineTo(vpc.x - 10, vpc.y);
|
||||
uiCtx.stroke();
|
||||
|
||||
uiCtx.restore();
|
||||
};
|
||||
|
||||
// Handles left mouse clicks
|
||||
|
@ -312,7 +340,7 @@ const selectTransformTool = () =>
|
|||
|
||||
// If something is selected, commit changes to the canvas
|
||||
if (state.selected) {
|
||||
imgCtx.drawImage(
|
||||
uil.ctx.drawImage(
|
||||
state.selected.image,
|
||||
state.original.x,
|
||||
state.original.y
|
||||
|
@ -330,7 +358,7 @@ const selectTransformTool = () =>
|
|||
state.original = null;
|
||||
state.selected = null;
|
||||
|
||||
redraw();
|
||||
state.redraw();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -406,7 +434,7 @@ const selectTransformTool = () =>
|
|||
const ctx = cvs.getContext("2d");
|
||||
|
||||
ctx.drawImage(
|
||||
imgCanvas,
|
||||
uil.canvas,
|
||||
state.selected.x,
|
||||
state.selected.y,
|
||||
state.selected.w,
|
||||
|
@ -417,7 +445,7 @@ const selectTransformTool = () =>
|
|||
state.selected.h
|
||||
);
|
||||
|
||||
imgCtx.clearRect(
|
||||
uil.ctx.clearRect(
|
||||
state.selected.x,
|
||||
state.selected.y,
|
||||
state.selected.w,
|
||||
|
@ -431,7 +459,7 @@ const selectTransformTool = () =>
|
|||
|
||||
state.dragging = null;
|
||||
}
|
||||
redraw();
|
||||
state.redraw();
|
||||
};
|
||||
|
||||
// Handler for right clicks. Basically resets everything
|
||||
|
@ -449,7 +477,7 @@ const selectTransformTool = () =>
|
|||
state.selected &&
|
||||
commands.runCommand("eraseImage", "Erase Area", state.selected);
|
||||
state.selected = null;
|
||||
redraw();
|
||||
state.redraw();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -593,8 +621,10 @@ const selectTransformTool = () =>
|
|||
createResourceButton.onclick = () => {
|
||||
const image = document.createElement("img");
|
||||
image.src = state.selected.image.toDataURL();
|
||||
tools.stamp.state.addResource("Selection Resource", image);
|
||||
tools.stamp.enable();
|
||||
image.onload = () => {
|
||||
tools.stamp.state.addResource("Selection Resource", image);
|
||||
tools.stamp.enable();
|
||||
};
|
||||
};
|
||||
|
||||
actionArray.appendChild(saveSelectionButton);
|
||||
|
|
|
@ -46,6 +46,8 @@ const stampTool = () =>
|
|||
Array.from(state.ctxmenu.resourceList.children).forEach((child) => {
|
||||
child.classList.remove("selected");
|
||||
});
|
||||
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
|
@ -157,15 +159,20 @@ const stampTool = () =>
|
|||
actionArray.classList.add("actions");
|
||||
|
||||
const renameButton = document.createElement("button");
|
||||
renameButton.addEventListener("click", () => {
|
||||
const name = prompt("Rename your resource:", resource.name);
|
||||
if (name) {
|
||||
resource.name = name;
|
||||
resourceTitle.textContent = name;
|
||||
renameButton.addEventListener(
|
||||
"click",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
const name = prompt("Rename your resource:", resource.name);
|
||||
if (name) {
|
||||
resource.name = name;
|
||||
resourceTitle.textContent = name;
|
||||
|
||||
syncResources();
|
||||
}
|
||||
});
|
||||
syncResources();
|
||||
}
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
renameButton.title = "Rename Resource";
|
||||
renameButton.appendChild(document.createElement("div"));
|
||||
renameButton.classList.add("rename-btn");
|
||||
|
|
5
res/icons/chevron-down.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">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 239 B |
5
res/icons/chevron-first.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">
|
||||
<polyline points="17 18 11 12 17 6"></polyline>
|
||||
<path d="M7 6v12"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 267 B |
4
res/icons/chevron-up.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<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">
|
||||
<polyline points="18 15 12 9 6 15"></polyline>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 238 B |
7
res/icons/eye-off.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="M9.88 9.88a3 3 0 1 0 4.24 4.24"></path>
|
||||
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"></path>
|
||||
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"></path>
|
||||
<line x1="2" y1="2" x2="22" y2="22"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 474 B |
5
res/icons/eye.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">
|
||||
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 296 B |
7
res/icons/file-plus.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>
|
||||
<line x1="12" y1="18" x2="12" y2="12"></line>
|
||||
<line x1="9" y1="15" x2="15" y2="15"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 422 B |
7
res/icons/file-x.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>
|
||||
<line x1="9.5" y1="12.5" x2="14.5" y2="17.5"></line>
|
||||
<line x1="14.5" y1="12.5" x2="9.5" y2="17.5"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 437 B |