Merge pull request #64 from zero01101/bleeding-edge
A fun tool for painting things
This commit is contained in:
commit
9b174d66c9
27 changed files with 1295 additions and 354 deletions
3
.github/workflows/autoformat.yml
vendored
3
.github/workflows/autoformat.yml
vendored
|
@ -13,8 +13,9 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: Prettify
|
||||
uses: creyD/prettier_action@v4.2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
prettier_options: --write **/*.{js,html,css,md}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
* {
|
||||
font-size: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
/* Body is stuck with no scroll */
|
||||
|
@ -136,7 +142,7 @@ body {
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
#models {
|
||||
.wideSelect {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -146,59 +152,83 @@ body {
|
|||
position: relative;
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.host-field-wrapper input {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% - 15px);
|
||||
|
||||
display: block;
|
||||
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
||||
position: absolute;
|
||||
left: calc(100% - 15px);
|
||||
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
|
||||
box-sizing: inherit;
|
||||
|
||||
border-radius: 50%;
|
||||
margin: 5px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
aspect-ratio: 1;
|
||||
transition-duration: 50ms;
|
||||
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status:active,
|
||||
.host-field-wrapper .connection-status:hover {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
width: fit-content;
|
||||
padding-left: 5px;
|
||||
padding-right: 6px;
|
||||
|
||||
margin: 3px;
|
||||
filter: brightness(110%);
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status:active {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
margin: 4px;
|
||||
.host-field-wrapper .connection-status > #connection-status-indicator-text {
|
||||
opacity: 0%;
|
||||
transition-duration: 20ms;
|
||||
}
|
||||
|
||||
.host-field-wrapper
|
||||
.connection-status:hover
|
||||
> #connection-status-indicator-text {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status.online {
|
||||
background-color: #49dd49;
|
||||
color: #1f3f1f;
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status.offline {
|
||||
background-color: #dd4949;
|
||||
color: #3f1f1f;
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status.cors-issue {
|
||||
background-color: #dddd49;
|
||||
color: #3f3f1f;
|
||||
}
|
||||
|
||||
.host-field-wrapper .connection-status.before {
|
||||
background-color: #777;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
input#host {
|
||||
|
@ -222,6 +252,19 @@ div.prompt-wrapper > textarea:focus {
|
|||
width: 700px;
|
||||
}
|
||||
|
||||
/* Style Field */
|
||||
select > .style-select-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select > .style-select-option:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
select > .style-select-option:active {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
/* Tool buttons */
|
||||
.button-array {
|
||||
display: flex;
|
||||
|
|
|
@ -90,8 +90,26 @@ div.slider-wrapper > input.text {
|
|||
appearance: textfield;
|
||||
border: 0px;
|
||||
|
||||
padding-top: 5px;
|
||||
height: 15px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Select Input */
|
||||
select > option:checked::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
|
||||
background-color: darkgreen;
|
||||
|
||||
-webkit-mask-image: url("/res/icons/check.svg");
|
||||
-webkit-mask-size: contain;
|
||||
mask-image: url("/res/icons/check.svg");
|
||||
mask-size: contain;
|
||||
}
|
||||
|
|
|
@ -38,14 +38,69 @@
|
|||
}
|
||||
|
||||
.resource-manager > .resource-list > * {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resource-manager > .resource-list > * > .resource-title {
|
||||
overflow: hidden;
|
||||
margin: 5px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.resource-manager > .resource-list > * > .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.resource-manager .actions > button {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
padding: 0;
|
||||
|
||||
width: 30px;
|
||||
aspect-ratio: 1;
|
||||
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.resource-manager .actions > button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.resource-manager .actions > button:active {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.resource-manager .actions > button > *:first-child {
|
||||
flex: 1;
|
||||
margin: 3px;
|
||||
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
background-color: var(--c-primary);
|
||||
}
|
||||
|
||||
.resource-manager .actions > .rename-btn > *:first-child {
|
||||
-webkit-mask-image: url("/res/icons/edit.svg");
|
||||
mask-image: url("/res/icons/edit.svg");
|
||||
}
|
||||
|
||||
.resource-manager .actions > .delete-btn > *:first-child {
|
||||
-webkit-mask-image: url("/res/icons/trash.svg");
|
||||
mask-image: url("/res/icons/trash.svg");
|
||||
}
|
||||
|
||||
.resource-manager > .resource-list > .selected:hover,
|
||||
.resource-manager > .resource-list > *:hover {
|
||||
background-color: #ffff;
|
||||
background-color: #fff8;
|
||||
}
|
||||
.resource-manager > .resource-list > .selected {
|
||||
background-color: #fff6;
|
||||
|
|
|
@ -43,8 +43,6 @@
|
|||
|
||||
padding: 0;
|
||||
|
||||
background-color: var(--c-text);
|
||||
|
||||
right: 2px;
|
||||
top: 10px;
|
||||
|
||||
|
@ -78,6 +76,8 @@
|
|||
border-radius: 5px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
#ui-toolbar .tool.using {
|
||||
|
@ -87,3 +87,8 @@
|
|||
#ui-toolbar .tool:hover {
|
||||
background-color: var(--c-hover);
|
||||
}
|
||||
|
||||
#ui-toolbar .tool:active {
|
||||
background-color: var(--c-hover);
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
|
37
index.html
37
index.html
|
@ -36,7 +36,9 @@
|
|||
<input id="host" value="http://127.0.0.1:7860" />
|
||||
<div
|
||||
id="connection-status-indicator"
|
||||
class="connection-status"></div>
|
||||
class="connection-status before">
|
||||
<span id="connection-status-indicator-text">Waiting</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<!-- Prompts section -->
|
||||
|
@ -51,6 +53,12 @@
|
|||
<div class="prompt-wrapper">
|
||||
<textarea id="negPrompt"></textarea>
|
||||
</div>
|
||||
<label for="styleSelect">Styles:</label>
|
||||
<select
|
||||
id="styleSelect"
|
||||
class="wideSelect"
|
||||
onchange="changeStyles()"
|
||||
multiple></select>
|
||||
<!-- <hr /> -->
|
||||
</div>
|
||||
<!-- SD section -->
|
||||
|
@ -59,7 +67,10 @@
|
|||
</button>
|
||||
<div class="content">
|
||||
<label for="models">Model:</label>
|
||||
<select id="models" onchange="changeModel()"></select>
|
||||
<select
|
||||
id="models"
|
||||
class="wideSelect"
|
||||
onchange="changeModel()"></select>
|
||||
<br />
|
||||
<label for="samplerSelect">Sampler:</label>
|
||||
<select id="samplerSelect" onchange="changeSampler()"></select>
|
||||
|
@ -75,16 +86,15 @@
|
|||
value="-1"
|
||||
step="1" />
|
||||
<br />
|
||||
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()" />
|
||||
<label for="cbxHRFix">Auto txt2img HRfix</label>
|
||||
<div id="resolution"></div>
|
||||
<div id="steps"></div>
|
||||
<div id="cfgScale"></div>
|
||||
<div id="batchSize"></div>
|
||||
<div id="batchCount"></div>
|
||||
</div>
|
||||
<!-- Unsectioned -->
|
||||
<div id="scaleFactor"></div>
|
||||
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()" />
|
||||
<label for="cbxHRFix">Auto txt2img HRfix</label>
|
||||
<br />
|
||||
<label for="maskBlur">Mask blur:</label>
|
||||
<span id="maskBlurText"></span>
|
||||
<br />
|
||||
|
@ -104,7 +114,7 @@
|
|||
<button onclick="downloadCanvas()">Save canvas</button>
|
||||
<br />
|
||||
<label for="upscalers">Choose upscaler</label>
|
||||
<select id="upscalers"></select>
|
||||
<select id="upscalers" class="wideSelect"></select>
|
||||
<button onclick="upscaleAndDownload()">
|
||||
Upscale (might take a sec)
|
||||
</button>
|
||||
|
@ -136,6 +146,10 @@
|
|||
<label for="heldButton">Mouse button:</label>
|
||||
<span id="heldButton"></span>
|
||||
<br />
|
||||
<button id="resetToDefaults" onclick="resetToDefaults()">
|
||||
Reset to defaults
|
||||
</button>
|
||||
<br />
|
||||
<span id="version">
|
||||
<a href="https://github.com/zero01101/openOutpaint" target="_blank">
|
||||
Alpha release v0.0.7.5
|
||||
|
@ -200,9 +214,8 @@
|
|||
<script src="js/lib/layers.js" type="text/javascript"></script>
|
||||
<script src="js/lib/commands.js" type="text/javascript"></script>
|
||||
|
||||
<script src="js/settingsbar.js" type="text/javascript"></script>
|
||||
|
||||
<script src="js/lib/toolbar.js" type="text/javascript"></script>
|
||||
<script src="js/lib/ui.js" type="text/javascript"></script>
|
||||
|
||||
<script
|
||||
src="js/initalize/layers.populate.js"
|
||||
|
@ -210,21 +223,25 @@
|
|||
|
||||
<!-- Content -->
|
||||
<script src="js/index.js" type="text/javascript"></script>
|
||||
<script src="js/shortcuts.js" type="text/javascript"></script>
|
||||
<script src="js/ui/floating/history.js" type="text/javascript"></script>
|
||||
|
||||
<!-- Load Tools -->
|
||||
<script src="js/ui/tool/dream.js" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/maskbrush.js" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/colorbrush.js" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/select.js" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/stamp.js" type="text/javascript"></script>
|
||||
|
||||
<!-- Initialize -->
|
||||
<script
|
||||
src="js/initalize/shortcuts.populate.js"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/initalize/toolbar.populate.js"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/initalize/debug.populate.js"
|
||||
type="text/javascript"></script>
|
||||
<script src="js/initalize/ui.populate.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
159
js/index.js
159
js/index.js
|
@ -24,6 +24,7 @@ var stableDiffusionData = {
|
|||
enable_hr: false,
|
||||
firstphase_width: 0,
|
||||
firstphase_height: 0,
|
||||
styles: [],
|
||||
// here's some more fields that might be useful
|
||||
|
||||
// ---txt2img specific:
|
||||
|
@ -48,6 +49,7 @@ var stableDiffusionData = {
|
|||
};
|
||||
|
||||
// stuff things use
|
||||
let debug = false;
|
||||
var returnedImages;
|
||||
var imageIndex = 0;
|
||||
var tmpImgXYWH = {};
|
||||
|
@ -58,7 +60,6 @@ var frameX = 512;
|
|||
var frameY = 512;
|
||||
var drawThis = {};
|
||||
const basePixelCount = 64; //64 px - ALWAYS 64 PX
|
||||
var scaleFactor = 8; //x64 px
|
||||
var snapToGrid = true;
|
||||
var backupMaskPaintCanvas; //???
|
||||
var backupMaskPaintCtx; //...? look i am bad at this
|
||||
|
@ -115,7 +116,6 @@ function startup() {
|
|||
changeSeed();
|
||||
changeOverMaskPx();
|
||||
changeHiResFix();
|
||||
document.getElementById("scaleFactor").value = scaleFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,6 +126,7 @@ function testHostConfiguration() {
|
|||
* Check host configuration
|
||||
*/
|
||||
const hostEl = document.getElementById("host");
|
||||
hostEl.value = localStorage.getItem("host");
|
||||
|
||||
const requestHost = (prompt, def = "http://127.0.0.1:7860") => {
|
||||
let value = window.prompt(prompt, def);
|
||||
|
@ -168,6 +169,10 @@ async function testHostConnection() {
|
|||
let firstTimeOnline = true;
|
||||
|
||||
const setConnectionStatus = (status) => {
|
||||
const connectionIndicatorText = document.getElementById(
|
||||
"connection-status-indicator-text"
|
||||
);
|
||||
|
||||
const statuses = {
|
||||
online: () => {
|
||||
connectionIndicator.classList.add("online");
|
||||
|
@ -177,7 +182,8 @@ async function testHostConnection() {
|
|||
"before",
|
||||
"server-error"
|
||||
);
|
||||
connectionIndicator.title = "Connected";
|
||||
connectionIndicatorText.textContent = connectionIndicator.title =
|
||||
"Connected";
|
||||
connectionStatus = true;
|
||||
},
|
||||
error: () => {
|
||||
|
@ -188,6 +194,7 @@ async function testHostConnection() {
|
|||
"before",
|
||||
"cors-issue"
|
||||
);
|
||||
connectionIndicatorText.textContent = "Error";
|
||||
connectionIndicator.title =
|
||||
"Server is online, but is returning an error response";
|
||||
connectionStatus = false;
|
||||
|
@ -200,6 +207,7 @@ async function testHostConnection() {
|
|||
"before",
|
||||
"server-error"
|
||||
);
|
||||
connectionIndicatorText.textContent = "CORS";
|
||||
connectionIndicator.title =
|
||||
"Server is online, but CORS is blocking our requests";
|
||||
connectionStatus = false;
|
||||
|
@ -212,6 +220,7 @@ async function testHostConnection() {
|
|||
"before",
|
||||
"server-error"
|
||||
);
|
||||
connectionIndicatorText.textContent = "Offline";
|
||||
connectionIndicator.title =
|
||||
"Server seems to be offline. Please check the console for more information.";
|
||||
connectionStatus = false;
|
||||
|
@ -224,6 +233,7 @@ async function testHostConnection() {
|
|||
"offline",
|
||||
"server-error"
|
||||
);
|
||||
connectionIndicatorText.textContent = "Waiting";
|
||||
connectionIndicator.title = "Waiting for check to complete.";
|
||||
connectionStatus = false;
|
||||
},
|
||||
|
@ -254,6 +264,7 @@ async function testHostConnection() {
|
|||
setConnectionStatus("online");
|
||||
// Load data as soon as connection is first stablished
|
||||
if (firstTimeOnline) {
|
||||
getStyles();
|
||||
getSamplers();
|
||||
getUpscalers();
|
||||
getModels();
|
||||
|
@ -272,10 +283,7 @@ async function testHostConnection() {
|
|||
await fetch(url, {mode: "no-cors"});
|
||||
|
||||
setConnectionStatus("corsissue");
|
||||
const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${document.URL.substring(
|
||||
0,
|
||||
document.URL.length - 1
|
||||
)}'`;
|
||||
const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${window.location.protocol}//${window.location.host}/'`;
|
||||
console.error(message);
|
||||
if (notify) alert(message);
|
||||
} catch (e) {
|
||||
|
@ -484,24 +492,46 @@ const makeSlider = (
|
|||
max,
|
||||
step,
|
||||
defaultValue,
|
||||
textStep = null,
|
||||
valuecb = null
|
||||
) => {
|
||||
const local = localStorage.getItem(lsKey);
|
||||
const local = lsKey && localStorage.getItem(lsKey);
|
||||
const def = parseFloat(local === null ? defaultValue : local);
|
||||
let cb = (v) => {
|
||||
stableDiffusionData[lsKey] = v;
|
||||
if (lsKey) localStorage.setItem(lsKey, v);
|
||||
};
|
||||
if (valuecb) {
|
||||
cb = (v) => {
|
||||
valuecb(v);
|
||||
localStorage.setItem(lsKey, v);
|
||||
};
|
||||
}
|
||||
return createSlider(label, el, {
|
||||
valuecb:
|
||||
valuecb ||
|
||||
((v) => {
|
||||
stableDiffusionData[lsKey] = v;
|
||||
localStorage.setItem(lsKey, v);
|
||||
}),
|
||||
valuecb: cb,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
defaultValue: def,
|
||||
textStep,
|
||||
});
|
||||
};
|
||||
|
||||
makeSlider(
|
||||
"Resolution",
|
||||
document.getElementById("resolution"),
|
||||
"resolution",
|
||||
64,
|
||||
1024,
|
||||
64,
|
||||
512,
|
||||
2,
|
||||
(v) => {
|
||||
stableDiffusionData.width = stableDiffusionData.height = v;
|
||||
stableDiffusionData.firstphase_width =
|
||||
stableDiffusionData.firstphase_height = v / 2;
|
||||
}
|
||||
);
|
||||
makeSlider(
|
||||
"CFG Scale",
|
||||
document.getElementById("cfgScale"),
|
||||
|
@ -509,7 +539,8 @@ makeSlider(
|
|||
-1,
|
||||
25,
|
||||
0.5,
|
||||
7.0
|
||||
7.0,
|
||||
0.1
|
||||
);
|
||||
makeSlider(
|
||||
"Batch Size",
|
||||
|
@ -529,27 +560,17 @@ makeSlider(
|
|||
1,
|
||||
2
|
||||
);
|
||||
makeSlider(
|
||||
"Scale Factor",
|
||||
document.getElementById("scaleFactor"),
|
||||
"scale_factor",
|
||||
1,
|
||||
16,
|
||||
1,
|
||||
8,
|
||||
(v) => {
|
||||
scaleFactor = v;
|
||||
}
|
||||
);
|
||||
|
||||
makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 1, 30);
|
||||
makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1);
|
||||
|
||||
function changeSnapMode() {
|
||||
snapToGrid = document.getElementById("cbxSnap").checked;
|
||||
}
|
||||
|
||||
function changeMaskBlur() {
|
||||
stableDiffusionData.mask_blur = document.getElementById("maskBlur").value;
|
||||
stableDiffusionData.mask_blur = parseInt(
|
||||
document.getElementById("maskBlur").value
|
||||
);
|
||||
localStorage.setItem("mask_blur", stableDiffusionData.mask_blur);
|
||||
}
|
||||
|
||||
|
@ -742,6 +763,78 @@ function changeModel() {
|
|||
});
|
||||
}
|
||||
|
||||
async function getStyles() {
|
||||
/** @type {HTMLSelectElement} */
|
||||
var styleSelect = document.getElementById("styleSelect");
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
/** @type {{name: string, prompt: string, negative_prompt: string}[]} */
|
||||
const data = await response.json();
|
||||
|
||||
/** @type {string[]} */
|
||||
let stored = null;
|
||||
try {
|
||||
stored = JSON.parse(localStorage.getItem("promptStyle"));
|
||||
// doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist?
|
||||
if (stored == null) stored = [];
|
||||
} catch (e) {
|
||||
stored = [];
|
||||
}
|
||||
|
||||
data.forEach((style) => {
|
||||
const option = document.createElement("option");
|
||||
option.classList.add("style-select-option");
|
||||
option.text = style.name;
|
||||
option.value = style.name;
|
||||
option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`;
|
||||
if (stored.length === 0) option.selected = style.name === "None";
|
||||
else
|
||||
option.selected = !!stored.find(
|
||||
(styleName) => style.name === styleName
|
||||
);
|
||||
|
||||
styleSelect.add(option);
|
||||
});
|
||||
|
||||
changeStyles();
|
||||
|
||||
stored.forEach((styleName, index) => {
|
||||
if (!data.findIndex((style) => style.name === styleName)) {
|
||||
stored.splice(index, 1);
|
||||
}
|
||||
});
|
||||
localStorage.setItem("promptStyle", JSON.stringify(stored));
|
||||
} catch (e) {
|
||||
console.warn("[index] Failed to fetch prompt styles");
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
function changeStyles() {
|
||||
/** @type {HTMLSelectElement} */
|
||||
const styleSelectEl = document.getElementById("styleSelect");
|
||||
const selected = Array.from(styleSelectEl.options).filter(
|
||||
(option) => option.selected
|
||||
);
|
||||
let selectedString = selected.map((option) => option.value);
|
||||
|
||||
if (selectedString.find((selected) => selected === "None")) {
|
||||
selectedString = [];
|
||||
Array.from(styleSelectEl.options).forEach((option) => {
|
||||
if (option.value !== "None") option.selected = false;
|
||||
});
|
||||
}
|
||||
|
||||
localStorage.setItem("promptStyle", JSON.stringify(selectedString));
|
||||
|
||||
// change the model
|
||||
if (selectedString.length > 0)
|
||||
console.log(`[index] Changing styles to ${selectedString.join(", ")}`);
|
||||
else console.log(`[index] Clearing styles`);
|
||||
stableDiffusionData.styles = selectedString;
|
||||
}
|
||||
|
||||
function getSamplers() {
|
||||
var samplerSelect = document.getElementById("samplerSelect");
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/samplers";
|
||||
|
@ -783,7 +876,7 @@ async function upscaleAndDownload() {
|
|||
var upscaler = document.getElementById("upscalers").value;
|
||||
var url =
|
||||
document.getElementById("host").value + "/sdapi/v1/extra-single-image/";
|
||||
var imgdata = croppedCanvas.toDataURL("image/png");
|
||||
var imgdata = croppedCanvas.canvas.toDataURL("image/png");
|
||||
var data = {
|
||||
"resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options
|
||||
upscaling_resize: upscale_factor,
|
||||
|
@ -880,3 +973,9 @@ imageCollection.element.addEventListener(
|
|||
},
|
||||
{passive: false}
|
||||
);
|
||||
|
||||
function resetToDefaults() {
|
||||
if (confirm("Are you sure you want to clear your settings?")) {
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,11 @@ mouse.listen.world.onmousemove.on((evn) => {
|
|||
*/
|
||||
const toggledebug = () => {
|
||||
const hidden = debugCanvas.style.display === "none";
|
||||
if (hidden) debugLayer.unhide();
|
||||
else debugLayer.hide();
|
||||
if (hidden) {
|
||||
debugLayer.unhide();
|
||||
debug = true;
|
||||
} else {
|
||||
debugLayer.hide();
|
||||
debug = false;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -54,7 +54,7 @@ mouse.registerContext(
|
|||
const target = evn.target;
|
||||
|
||||
// Get element bounding rect
|
||||
const bb = target.getBoundingClientRect();
|
||||
const bb = imageCollection.element.getBoundingClientRect();
|
||||
|
||||
// Get element width/height (css, cause I don't trust client sizes in chrome anymore)
|
||||
const w = imageCollection.size.w;
|
||||
|
@ -148,11 +148,13 @@ mouse.listen.window.onwheel.on((evn) => {
|
|||
|
||||
viewport.transform(imageCollection.element);
|
||||
|
||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||
debugCtx.fillStyle = "#F0F";
|
||||
debugCtx.beginPath();
|
||||
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
|
||||
debugCtx.fill();
|
||||
if (debug) {
|
||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||
debugCtx.fillStyle = "#F0F";
|
||||
debugCtx.beginPath();
|
||||
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
|
||||
debugCtx.fill();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -173,11 +175,13 @@ mouse.listen.window.btn.middle.onpaint.on((evn) => {
|
|||
}
|
||||
|
||||
viewport.transform(imageCollection.element);
|
||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||
debugCtx.fillStyle = "#F0F";
|
||||
debugCtx.beginPath();
|
||||
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
|
||||
debugCtx.fill();
|
||||
if (debug) {
|
||||
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
|
||||
debugCtx.fillStyle = "#F0F";
|
||||
debugCtx.beginPath();
|
||||
debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2);
|
||||
debugCtx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
mouse.listen.window.btn.middle.onpaintend.on((evn) => {
|
||||
|
|
|
@ -14,6 +14,9 @@ keyboard.onShortcut({key: "KeyD"}, () => {
|
|||
keyboard.onShortcut({key: "KeyM"}, () => {
|
||||
tools.maskbrush.enable();
|
||||
});
|
||||
keyboard.onShortcut({key: "KeyC"}, () => {
|
||||
tools.colorbrush.enable();
|
||||
});
|
||||
keyboard.onShortcut({key: "KeyI"}, () => {
|
||||
tools.img2img.enable();
|
||||
});
|
|
@ -15,6 +15,7 @@ toolbar.addSeparator();
|
|||
* Mask Brush tool
|
||||
*/
|
||||
tools.maskbrush = maskBrushTool();
|
||||
tools.colorbrush = colorBrushTool();
|
||||
|
||||
/**
|
||||
* Image Editing tools
|
||||
|
|
35
js/initalize/ui.populate.js
Normal file
35
js/initalize/ui.populate.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
document.querySelectorAll(".floating-window").forEach((w) => {
|
||||
makeDraggable(w);
|
||||
});
|
||||
|
||||
var coll = document.getElementsByClassName("collapsible");
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
let active = false;
|
||||
coll[i].addEventListener("click", function () {
|
||||
var content = this.nextElementSibling;
|
||||
|
||||
if (!active) {
|
||||
this.classList.add("active");
|
||||
content.classList.add("active");
|
||||
} else {
|
||||
this.classList.remove("active");
|
||||
content.classList.remove("active");
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (active) content.style.maxHeight = content.scrollHeight + "px";
|
||||
});
|
||||
|
||||
Array.from(content.children).forEach((child) => {
|
||||
observer.observe(child);
|
||||
});
|
||||
|
||||
if (active) {
|
||||
content.style.maxHeight = null;
|
||||
active = false;
|
||||
} else {
|
||||
content.style.maxHeight = content.scrollHeight + "px";
|
||||
active = true;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -278,7 +278,30 @@ commands.createCommand(
|
|||
}
|
||||
|
||||
// Apply command
|
||||
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
||||
const style = state.context.fillStyle;
|
||||
state.context.fillStyle = "black";
|
||||
|
||||
const op = state.context.globalCompositeOperation;
|
||||
state.context.globalCompositeOperation = "destination-out";
|
||||
|
||||
if (options.mask)
|
||||
state.context.drawImage(
|
||||
options.mask,
|
||||
state.box.x,
|
||||
state.box.y,
|
||||
state.box.w,
|
||||
state.box.h
|
||||
);
|
||||
else
|
||||
state.context.fillRect(
|
||||
state.box.x,
|
||||
state.box.y,
|
||||
state.box.w,
|
||||
state.box.h
|
||||
);
|
||||
|
||||
state.context.fillStyle = style;
|
||||
state.context.globalCompositeOperation = op;
|
||||
},
|
||||
(title, state) => {
|
||||
// Clear destination area
|
||||
|
|
|
@ -288,7 +288,7 @@ window.addEventListener(
|
|||
window.addEventListener(
|
||||
"mousemove",
|
||||
(evn) => {
|
||||
mouse._contexts.forEach((context) => {
|
||||
mouse._contexts.forEach(async (context) => {
|
||||
const target = context.target;
|
||||
const name = context.name;
|
||||
|
||||
|
|
|
@ -172,13 +172,30 @@ const layers = {
|
|||
* Moves this layer to another location
|
||||
*
|
||||
* @param {number} x X coordinate of the top left of the canvas
|
||||
* @param {number} y X coordinate of the top left of the canvas
|
||||
* @param {number} y Y coordinate of the top left of the canvas
|
||||
*/
|
||||
moveTo(x, y) {
|
||||
canvas.style.left = `${x}px`;
|
||||
canvas.style.top = `${y}px`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes layer in place
|
||||
*
|
||||
* @param {number} w New width
|
||||
* @param {number} h New height
|
||||
*/
|
||||
resize(w, h) {
|
||||
canvas.width = Math.round(
|
||||
options.resolution.w * (w / options.bb.w)
|
||||
);
|
||||
canvas.height = Math.round(
|
||||
options.resolution.h * (h / options.bb.h)
|
||||
);
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
},
|
||||
|
||||
// Hides this layer (don't draw)
|
||||
hide() {
|
||||
this.canvas.style.display = "none";
|
||||
|
|
|
@ -150,17 +150,20 @@ const _toolbar_input = {
|
|||
return {checkbox, label};
|
||||
},
|
||||
|
||||
slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => {
|
||||
slider: (state, dataKey, text, options = {}) => {
|
||||
defaultOpt(options, {min: 0, max: 1, step: 0.1, textStep: null, cb: null});
|
||||
const slider = document.createElement("div");
|
||||
|
||||
const value = createSlider(text, slider, {
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
min: options.min,
|
||||
max: options.max,
|
||||
step: options.step,
|
||||
valuecb: (v) => {
|
||||
state[dataKey] = v;
|
||||
options.cb && options.cb(v);
|
||||
},
|
||||
defaultValue: state[dataKey],
|
||||
textStep: options.textStep,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -172,21 +175,3 @@ const _toolbar_input = {
|
|||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Dream and img2img tools
|
||||
*/
|
||||
const _reticle_draw = (evn, snapToGrid = true) => {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// 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
|
||||
};
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
/**
|
||||
* This is a function that makes an HTMLElement draggable.
|
||||
*
|
||||
* The element must contain at least one child element with the class
|
||||
* 'draggable', which will make it the handle for dragging the element
|
||||
*
|
||||
* @param {HTMLElement} element Element to make Draggable
|
||||
*/
|
||||
function makeDraggable(element) {
|
||||
let dragging = false;
|
||||
let offset = {x: 0, y: 0};
|
||||
|
||||
const margin = 10;
|
||||
|
||||
// Keeps the draggable element inside the window
|
||||
const fixPos = () => {
|
||||
const dbb = element.getBoundingClientRect();
|
||||
if (dbb.left < margin) element.style.left = margin + "px";
|
||||
|
@ -17,6 +26,7 @@ function makeDraggable(element) {
|
|||
dbb.top + (window.innerHeight - margin - dbb.bottom) + "px";
|
||||
};
|
||||
|
||||
// Detects the start of the mouse dragging event
|
||||
mouse.listen.window.btn.left.onpaintstart.on((evn) => {
|
||||
if (
|
||||
element.contains(evn.target) &&
|
||||
|
@ -29,6 +39,7 @@ function makeDraggable(element) {
|
|||
}
|
||||
});
|
||||
|
||||
// Runs when mouse moves
|
||||
mouse.listen.window.btn.left.onpaint.on((evn) => {
|
||||
if (dragging) {
|
||||
element.style.right = null;
|
||||
|
@ -40,53 +51,30 @@ function makeDraggable(element) {
|
|||
}
|
||||
});
|
||||
|
||||
// Stops dragging the element
|
||||
mouse.listen.window.btn.left.onpaintend.on((evn) => {
|
||||
dragging = false;
|
||||
});
|
||||
|
||||
// Redraw after window resize
|
||||
window.addEventListener("resize", () => {
|
||||
fixPos();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".floating-window").forEach((w) => {
|
||||
makeDraggable(w);
|
||||
});
|
||||
|
||||
var coll = document.getElementsByClassName("collapsible");
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
let active = false;
|
||||
coll[i].addEventListener("click", function () {
|
||||
var content = this.nextElementSibling;
|
||||
|
||||
if (!active) {
|
||||
this.classList.add("active");
|
||||
content.classList.add("active");
|
||||
} else {
|
||||
this.classList.remove("active");
|
||||
content.classList.remove("active");
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (active) content.style.maxHeight = content.scrollHeight + "px";
|
||||
});
|
||||
|
||||
Array.from(content.children).forEach((child) => {
|
||||
observer.observe(child);
|
||||
});
|
||||
|
||||
if (active) {
|
||||
content.style.maxHeight = null;
|
||||
active = false;
|
||||
} else {
|
||||
content.style.maxHeight = content.scrollHeight + "px";
|
||||
active = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider Inputs
|
||||
* Creates a custom slider element from a given div element
|
||||
*
|
||||
* @param {string} name The display name of the sliders
|
||||
* @param {HTMLElement} wrapper The element to transform into a slider
|
||||
* @param {object} options Extra options
|
||||
* @param {number} options.min The minimum value of the slider
|
||||
* @param {number} options.max The maximum value of the slider
|
||||
* @param {number} options.step The step size for the slider
|
||||
* @param {number} option.defaultValue The default value of the slider
|
||||
* @param {number} [options.textStep=step] The step size for the slider text and setvalue \
|
||||
* (usually finer, and an integer divisor of step size)
|
||||
* @returns {{value: number}} A reference to the value of the slider
|
||||
*/
|
||||
function createSlider(name, wrapper, options = {}) {
|
||||
defaultOpt(options, {
|
||||
|
@ -95,6 +83,7 @@ function createSlider(name, wrapper, options = {}) {
|
|||
max: 1,
|
||||
step: 0.1,
|
||||
defaultValue: 0.7,
|
||||
textStep: null,
|
||||
});
|
||||
|
||||
let value = options.defaultValue;
|
||||
|
@ -106,6 +95,15 @@ function createSlider(name, wrapper, options = {}) {
|
|||
phantomRange.max = options.max;
|
||||
phantomRange.step = options.step;
|
||||
|
||||
let phantomTextRange = phantomRange;
|
||||
if (options.textStep) {
|
||||
phantomTextRange = document.createElement("input");
|
||||
phantomTextRange.type = "range";
|
||||
phantomTextRange.min = options.min;
|
||||
phantomTextRange.max = options.max;
|
||||
phantomTextRange.step = options.textStep;
|
||||
}
|
||||
|
||||
// Build slider element
|
||||
const underEl = document.createElement("div");
|
||||
underEl.classList.add("under");
|
||||
|
@ -128,8 +126,8 @@ function createSlider(name, wrapper, options = {}) {
|
|||
|
||||
// Set value
|
||||
const setValue = (val) => {
|
||||
phantomRange.value = val;
|
||||
value = parseFloat(phantomRange.value);
|
||||
phantomTextRange.value = val;
|
||||
value = parseFloat(phantomTextRange.value);
|
||||
bar.style.width = `${
|
||||
100 * ((value - options.min) / (options.max - options.min))
|
||||
}%`;
|
||||
|
@ -170,17 +168,15 @@ function createSlider(name, wrapper, options = {}) {
|
|||
|
||||
mouse.listen.window.btn.left.ondrag.on((evn) => {
|
||||
if (evn.initialTarget === overEl) {
|
||||
setValue(
|
||||
Math.max(
|
||||
options.min,
|
||||
Math.min(
|
||||
options.max,
|
||||
(evn.evn.layerX / wrapper.offsetWidth) *
|
||||
(options.max - options.min) +
|
||||
options.min
|
||||
)
|
||||
phantomRange.value = Math.max(
|
||||
options.min,
|
||||
Math.min(
|
||||
options.max,
|
||||
(evn.evn.layerX / wrapper.offsetWidth) * (options.max - options.min) +
|
||||
options.min
|
||||
)
|
||||
);
|
||||
setValue(parseFloat(phantomRange.value));
|
||||
}
|
||||
});
|
||||
|
113
js/lib/util.js
113
js/lib/util.js
|
@ -145,21 +145,14 @@ function makeWriteOnce(obj, name = "write-once object", exceptions = []) {
|
|||
* Snaps a single value to an infinite grid
|
||||
*
|
||||
* @param {number} i Original value to be snapped
|
||||
* @param {boolean} scaled If grid will change alignment for odd scaleFactor values (default: true)
|
||||
* @param {number} gridSize Size of the grid
|
||||
* @param {number} [offset=0] Value to offset the grid. Should be in the rande [0, gridSize[
|
||||
* @param {number} [gridSize=64] Size of the grid
|
||||
* @returns an offset, in which [i + offset = (a location snapped to the grid)]
|
||||
*/
|
||||
function snap(i, scaled = true, gridSize = 64) {
|
||||
// very cheap test proof of concept but it works surprisingly well
|
||||
var scaleOffset = 0;
|
||||
if (scaled) {
|
||||
if (scaleFactor % 2 != 0) {
|
||||
// odd number, snaps to center of cell, oops
|
||||
scaleOffset = gridSize / 2;
|
||||
}
|
||||
}
|
||||
const modulus = i % gridSize;
|
||||
var snapOffset = modulus - scaleOffset;
|
||||
function snap(i, offset = 0, gridSize = 64) {
|
||||
const modulus = (i - offset) % gridSize;
|
||||
var snapOffset = modulus;
|
||||
|
||||
if (modulus > gridSize / 2) snapOffset = modulus - gridSize;
|
||||
|
||||
if (snapOffset == 0) {
|
||||
|
@ -175,19 +168,20 @@ function snap(i, scaled = true, gridSize = 64) {
|
|||
* @param {number} cy - y-coordinate of the center of the box
|
||||
* @param {number} w - the width of the box
|
||||
* @param {height} h - the height of the box
|
||||
* @param {number | null} gridSnap - The size of the grid to snap to
|
||||
* @param {?number} gridSnap - The size of the grid to snap to
|
||||
* @param {number} [offset=0] - How much to offset the grid by
|
||||
* @returns {BoundingBox} - A bounding box object centered at (cx, cy)
|
||||
*/
|
||||
function getBoundingBox(cx, cy, w, h, gridSnap = null) {
|
||||
const offset = {x: 0, y: 0};
|
||||
function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) {
|
||||
const offs = {x: 0, y: 0};
|
||||
const box = {x: 0, y: 0};
|
||||
|
||||
if (gridSnap) {
|
||||
offset.x = snap(cx, true, gridSnap);
|
||||
offset.y = snap(cy, true, gridSnap);
|
||||
offs.x = snap(cx, offset, gridSnap);
|
||||
offs.y = snap(cy, offset, gridSnap);
|
||||
}
|
||||
box.x = offset.x + cx;
|
||||
box.y = offset.y + cy;
|
||||
box.x = offs.x + cx;
|
||||
box.y = offs.y + cy;
|
||||
|
||||
return {
|
||||
x: Math.floor(box.x - w / 2),
|
||||
|
@ -197,57 +191,58 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null) {
|
|||
};
|
||||
}
|
||||
|
||||
class NoContentError extends Error {}
|
||||
|
||||
/**
|
||||
* Crops a given canvas to content, returning a new canvas object with the content in it.
|
||||
*
|
||||
* @param {HTMLCanvasElement} sourceCanvas Canvas to get a content crop from
|
||||
* @returns {HTMLCanvasElement} A new canvas with the cropped part of the image
|
||||
* @param {object} options Extra options
|
||||
* @param {number} [options.border=0] Extra border around the content
|
||||
* @returns {{canvas: HTMLCanvasElement, bb: BoundingBox}} A new canvas with the cropped part of the image
|
||||
*/
|
||||
function cropCanvas(sourceCanvas) {
|
||||
var w = sourceCanvas.width;
|
||||
var h = sourceCanvas.height;
|
||||
var pix = {x: [], y: []};
|
||||
var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h);
|
||||
var x, y, index;
|
||||
function cropCanvas(sourceCanvas, options = {}) {
|
||||
defaultOpt(options, {border: 0});
|
||||
|
||||
for (y = 0; y < h; y++) {
|
||||
for (x = 0; x < w; x++) {
|
||||
const w = sourceCanvas.width;
|
||||
const h = sourceCanvas.height;
|
||||
var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h);
|
||||
/** @type {BoundingBox} */
|
||||
const bb = {x: 0, y: 0, w: 0, h: 0};
|
||||
|
||||
let minx = w;
|
||||
let maxx = -1;
|
||||
let miny = h;
|
||||
let maxy = -1;
|
||||
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
// lol i need to learn what this part does
|
||||
index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords
|
||||
const index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords
|
||||
//this part i get, this is checking that 4th RGBA byte for opacity
|
||||
if (imageData.data[index + 3] > 0) {
|
||||
pix.x.push(x);
|
||||
pix.y.push(y);
|
||||
minx = Math.min(minx, x);
|
||||
maxx = Math.max(maxx, x);
|
||||
miny = Math.min(miny, y);
|
||||
maxy = Math.max(maxy, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ...need to learn what this part does too :badpokerface:
|
||||
// is this just determining the boundaries of non-transparent pixel data?
|
||||
pix.x.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
pix.y.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
var n = pix.x.length - 1;
|
||||
w = pix.x[n] - pix.x[0] + 1;
|
||||
h = pix.y[n] - pix.y[0] + 1;
|
||||
// yup sure looks like it
|
||||
|
||||
try {
|
||||
var cut = sourceCanvas
|
||||
.getContext("2d")
|
||||
.getImageData(pix.x[0], pix.y[0], w, h);
|
||||
var cutCanvas = document.createElement("canvas");
|
||||
cutCanvas.width = w;
|
||||
cutCanvas.height = h;
|
||||
cutCanvas.getContext("2d").putImageData(cut, 0, 0);
|
||||
} catch (ex) {
|
||||
// probably empty image
|
||||
//TODO confirm edge cases?
|
||||
cutCanvas = null;
|
||||
}
|
||||
return cutCanvas;
|
||||
bb.x = minx - options.border;
|
||||
bb.y = miny - options.border;
|
||||
bb.w = maxx - minx + 2 * options.border;
|
||||
bb.h = maxy - miny + 2 * options.border;
|
||||
|
||||
if (maxx < 0) throw new NoContentError("Canvas has no content to crop");
|
||||
|
||||
var cutCanvas = document.createElement("canvas");
|
||||
cutCanvas.width = bb.w;
|
||||
cutCanvas.height = bb.h;
|
||||
cutCanvas
|
||||
.getContext("2d")
|
||||
.drawImage(sourceCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
return {canvas: cutCanvas, bb};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -276,7 +271,7 @@ function downloadCanvas(options = {}) {
|
|||
if (options.filename) link.download = options.filename;
|
||||
|
||||
var croppedCanvas = options.cropToContent
|
||||
? cropCanvas(options.canvas)
|
||||
? cropCanvas(options.canvas).canvas
|
||||
: options.canvas;
|
||||
if (croppedCanvas != null) {
|
||||
link.href = croppedCanvas.toDataURL("image/png");
|
||||
|
|
252
js/ui/tool/colorbrush.js
Normal file
252
js/ui/tool/colorbrush.js
Normal file
|
@ -0,0 +1,252 @@
|
|||
const _color_brush_draw_callback = (evn, state) => {
|
||||
const ctx = state.drawLayer.ctx;
|
||||
|
||||
ctx.strokeStyle = state.color;
|
||||
|
||||
ctx.filter = "blur(" + state.brushBlur + "px)";
|
||||
ctx.lineWidth = state.brushSize;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
evn.px === undefined ? evn.x : evn.px,
|
||||
evn.py === undefined ? evn.y : evn.py
|
||||
);
|
||||
ctx.lineTo(evn.x, evn.y);
|
||||
ctx.lineJoin = ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const _color_brush_erase_callback = (evn, state, ctx) => {
|
||||
ctx.strokeStyle = "black";
|
||||
|
||||
ctx.lineWidth = state.brushSize;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
evn.px === undefined ? evn.x : evn.px,
|
||||
evn.py === undefined ? evn.y : evn.py
|
||||
);
|
||||
ctx.lineTo(evn.x, evn.y);
|
||||
ctx.lineJoin = ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const colorBrushTool = () =>
|
||||
toolbar.registerTool(
|
||||
"res/icons/brush.svg",
|
||||
"Color Brush",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb({...mouse.coords.world.pos});
|
||||
state.drawLayer = imageCollection.registerLayer(null, {
|
||||
after: imgLayer,
|
||||
});
|
||||
state.eraseLayer = imageCollection.registerLayer(null, {
|
||||
after: imgLayer,
|
||||
});
|
||||
state.eraseLayer.canvas.style.display = "none";
|
||||
state.eraseBackup = imageCollection.registerLayer(null, {
|
||||
after: imgLayer,
|
||||
});
|
||||
state.eraseBackup.canvas.style.display = "none";
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.movecb);
|
||||
mouse.listen.world.onwheel.on(state.wheelcb);
|
||||
|
||||
mouse.listen.world.btn.left.onpaintstart.on(state.drawstartcb);
|
||||
mouse.listen.world.btn.left.onpaint.on(state.drawcb);
|
||||
mouse.listen.world.btn.left.onpaintend.on(state.drawendcb);
|
||||
|
||||
mouse.listen.world.btn.right.onpaintstart.on(state.erasestartcb);
|
||||
mouse.listen.world.btn.right.onpaint.on(state.erasecb);
|
||||
mouse.listen.world.btn.right.onpaintend.on(state.eraseendcb);
|
||||
|
||||
// Display Color
|
||||
setMask("none");
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.world.onmousemove.clear(state.movecb);
|
||||
mouse.listen.world.onwheel.clear(state.wheelcb);
|
||||
|
||||
mouse.listen.world.btn.left.onpaintstart.clear(state.drawstartcb);
|
||||
mouse.listen.world.btn.left.onpaint.clear(state.drawcb);
|
||||
mouse.listen.world.btn.left.onpaintend.clear(state.drawendcb);
|
||||
|
||||
mouse.listen.world.btn.right.onpaintstart.clear(state.erasestartcb);
|
||||
mouse.listen.world.btn.right.onpaint.clear(state.erasecb);
|
||||
mouse.listen.world.btn.right.onpaintend.clear(state.eraseendcb);
|
||||
|
||||
// Delete layer
|
||||
imageCollection.deleteLayer(state.drawLayer);
|
||||
imageCollection.deleteLayer(state.eraseBackup);
|
||||
imageCollection.deleteLayer(state.eraseLayer);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.config = {
|
||||
brushScrollSpeed: 1 / 5,
|
||||
minBrushSize: 2,
|
||||
maxBrushSize: 500,
|
||||
minBlur: 0,
|
||||
maxBlur: 30,
|
||||
};
|
||||
|
||||
state.color = "#FFFFFF";
|
||||
state.brushSize = 32;
|
||||
state.brushBlur = 0;
|
||||
state.affectMask = true;
|
||||
state.setBrushSize = (size) => {
|
||||
state.brushSize = size;
|
||||
state.ctxmenu.brushSizeRange.value = size;
|
||||
state.ctxmenu.brushSizeText.value = size;
|
||||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
// draw big translucent white blob cursor
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
||||
ovCtx.fillStyle = state.color + "50";
|
||||
ovCtx.fill();
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
if (!evn.evn.ctrlKey) {
|
||||
state.brushSize = state.setBrushSize(
|
||||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
||||
state.drawstartcb = (evn) => {
|
||||
if (state.affectMask) _mask_brush_draw_callback(evn, state);
|
||||
_color_brush_draw_callback(evn, state);
|
||||
};
|
||||
|
||||
state.drawcb = (evn) => {
|
||||
if (state.affectMask) _mask_brush_draw_callback(evn, state);
|
||||
_color_brush_draw_callback(evn, state);
|
||||
};
|
||||
|
||||
state.drawendcb = (evn) => {
|
||||
const canvas = state.drawLayer.canvas;
|
||||
const ctx = state.drawLayer.ctx;
|
||||
|
||||
const cropped = cropCanvas(canvas, {border: 10});
|
||||
const bb = cropped.bb;
|
||||
commands.runCommand("drawImage", "Color Brush Draw", {
|
||||
image: cropped.canvas,
|
||||
...bb,
|
||||
});
|
||||
|
||||
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
||||
};
|
||||
|
||||
state.erasestartcb = (evn) => {
|
||||
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);
|
||||
|
||||
imgCtx.globalCompositeOperation = "destination-out";
|
||||
_color_brush_erase_callback(evn, state, imgCtx);
|
||||
imgCtx.globalCompositeOperation = "source-over";
|
||||
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
|
||||
};
|
||||
|
||||
state.erasecb = (evn) => {
|
||||
if (state.affectMask) _mask_brush_erase_callback(evn, state);
|
||||
imgCtx.globalCompositeOperation = "destination-out";
|
||||
_color_brush_erase_callback(evn, state, imgCtx);
|
||||
imgCtx.globalCompositeOperation = "source-over";
|
||||
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
|
||||
};
|
||||
|
||||
state.eraseendcb = (evn) => {
|
||||
const canvas = state.eraseLayer.canvas;
|
||||
const ctx = state.eraseLayer.ctx;
|
||||
|
||||
const bkpcanvas = state.eraseBackup.canvas;
|
||||
|
||||
const cropped = cropCanvas(canvas, {border: 10});
|
||||
const bb = cropped.bb;
|
||||
|
||||
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
|
||||
imgCtx.drawImage(bkpcanvas, 0, 0);
|
||||
|
||||
commands.runCommand("eraseImage", "Color Brush Erase", {
|
||||
mask: cropped.canvas,
|
||||
...bb,
|
||||
});
|
||||
|
||||
ctx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
||||
};
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
|
||||
// Affects Mask Checkbox
|
||||
const affectMaskCheckbox = _toolbar_input.checkbox(
|
||||
state,
|
||||
"affectMask",
|
||||
"Affect Mask"
|
||||
).label;
|
||||
|
||||
state.ctxmenu.affectMaskCheckbox = affectMaskCheckbox;
|
||||
|
||||
// Brush size slider
|
||||
const brushSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushSize",
|
||||
"Brush Size",
|
||||
{
|
||||
min: state.config.minBrushSize,
|
||||
max: state.config.maxBrushSize,
|
||||
step: 5,
|
||||
textStep: 1,
|
||||
}
|
||||
);
|
||||
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
|
||||
// Brush size 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;
|
||||
|
||||
// Brush color
|
||||
const brushColorPicker = document.createElement("input");
|
||||
brushColorPicker.type = "color";
|
||||
brushColorPicker.style.width = "100%";
|
||||
brushColorPicker.value = state.color;
|
||||
brushColorPicker.addEventListener("input", (evn) => {
|
||||
state.color = evn.target.value;
|
||||
});
|
||||
|
||||
state.ctxmenu.brushColorPicker = brushColorPicker;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.affectMaskCheckbox);
|
||||
menu.appendChild(state.ctxmenu.brushSizeSlider);
|
||||
menu.appendChild(state.ctxmenu.brushBlurSlider);
|
||||
menu.appendChild(state.ctxmenu.brushColorPicker);
|
||||
},
|
||||
shortcut: "C",
|
||||
}
|
||||
);
|
|
@ -125,16 +125,40 @@ const _generate = async (endpoint, request, bb) => {
|
|||
image.src = "data:image/png;base64," + images[at];
|
||||
image.addEventListener("load", () => {
|
||||
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
|
||||
if (images[at]) layer.ctx.drawImage(image, bb.x, bb.y);
|
||||
if (images[at])
|
||||
layer.ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
image.width,
|
||||
image.height,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const stopMarchingAnts = march(bb);
|
||||
|
||||
// First Dream Run
|
||||
let stopProgress = _monitorProgress(bb);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
stopProgress();
|
||||
console.info(`[dream] Generating images for prompt '${request.prompt}'`);
|
||||
console.debug(request);
|
||||
|
||||
let stopProgress = null;
|
||||
try {
|
||||
stopProgress = _monitorProgress(bb);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
} catch (e) {
|
||||
alert(
|
||||
`Error generating images. Please try again or see consolde for more details`
|
||||
);
|
||||
console.warn(`[dream] Error generating images:`);
|
||||
console.warn(e);
|
||||
} finally {
|
||||
stopProgress();
|
||||
}
|
||||
|
||||
// Image navigation
|
||||
const prevImg = () => {
|
||||
|
@ -161,6 +185,8 @@ const _generate = async (endpoint, request, bb) => {
|
|||
commands.runCommand("drawImage", "Image Dream", {
|
||||
x: bb.x,
|
||||
y: bb.y,
|
||||
w: bb.w,
|
||||
h: bb.h,
|
||||
image: img,
|
||||
});
|
||||
clean(true);
|
||||
|
@ -168,11 +194,19 @@ const _generate = async (endpoint, request, bb) => {
|
|||
};
|
||||
|
||||
const makeMore = async () => {
|
||||
let stopProgress = _monitorProgress(bb);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
stopProgress();
|
||||
|
||||
imageindextxt.textContent = `${at + 1}/${images.length}`;
|
||||
try {
|
||||
stopProgress = _monitorProgress(bb);
|
||||
images.push(...(await _dream(endpoint, requestCopy)));
|
||||
imageindextxt.textContent = `${at + 1}/${images.length}`;
|
||||
} catch (e) {
|
||||
alert(
|
||||
`Error generating images. Please try again or see consolde for more details`
|
||||
);
|
||||
console.warn(`[dream] Error generating images:`);
|
||||
console.warn(e);
|
||||
} finally {
|
||||
stopProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const discardImg = async () => {
|
||||
|
@ -316,8 +350,8 @@ const dream_generate_callback = async (evn, state) => {
|
|||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.cursorSize,
|
||||
state.cursorSize,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
|
@ -332,13 +366,6 @@ const dream_generate_callback = async (evn, state) => {
|
|||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Setup some basic information for SD
|
||||
request.width = bb.w;
|
||||
request.height = bb.h;
|
||||
|
||||
request.firstphase_width = bb.w / 2;
|
||||
request.firstphase_height = bb.h / 2;
|
||||
|
||||
// Use txt2img if canvas is blank
|
||||
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) {
|
||||
// Dream
|
||||
|
@ -355,13 +382,23 @@ const dream_generate_callback = async (evn, state) => {
|
|||
auxCtx.fillStyle = "#000F";
|
||||
|
||||
// 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.fillRect(0, 0, request.width, request.height);
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
request.init_images = [auxCanvas.toDataURL()];
|
||||
|
||||
// Get mask image
|
||||
auxCtx.fillStyle = "#000F";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
if (state.invertMask) {
|
||||
// overmasking by definition is entirely pointless with an inverted mask outpaint
|
||||
// since it should explicitly avoid brushed masks too, we just won't even bother
|
||||
|
@ -374,22 +411,42 @@ const dream_generate_callback = async (evn, state) => {
|
|||
bb.h,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
|
||||
auxCtx.globalCompositeOperation = "destination-in";
|
||||
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
} else {
|
||||
auxCtx.globalCompositeOperation = "destination-in";
|
||||
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
// here's where to overmask to avoid including the brushed mask
|
||||
// 99% of my issues were from failing to set source-over for the overmask blotches
|
||||
if (state.overMaskPx > 0) {
|
||||
// transparent to white first
|
||||
auxCtx.globalCompositeOperation = "destination-atop";
|
||||
auxCtx.fillStyle = "#FFFF";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
applyOvermask(auxCanvas, auxCtx, state.overMaskPx);
|
||||
}
|
||||
|
||||
|
@ -402,13 +459,13 @@ const dream_generate_callback = async (evn, state) => {
|
|||
bb.h,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
}
|
||||
auxCtx.globalCompositeOperation = "destination-atop";
|
||||
auxCtx.fillStyle = "#FFFF";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
request.mask = auxCanvas.toDataURL();
|
||||
// Dream
|
||||
_generate("img2img", request, bb);
|
||||
|
@ -419,8 +476,8 @@ const dream_erase_callback = (evn, state) => {
|
|||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.cursorSize,
|
||||
state.cursorSize,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
commands.runCommand("eraseImage", "Erase Area", bb);
|
||||
|
@ -436,14 +493,25 @@ function applyOvermask(canvas, ctx, px) {
|
|||
if (ctxImgData.data[i] == 255) {
|
||||
// white pixel?
|
||||
// just blotch all over the thing
|
||||
var rando = Math.floor(Math.random() * px);
|
||||
/**
|
||||
* This should probably have a better randomness profile for the overmasking
|
||||
*
|
||||
* Essentially, we want to have much more smaller values for randomness than big ones,
|
||||
* because big values overshadow smaller circles and kinda ignores their randomness.
|
||||
*
|
||||
* And also, we want the profile to become more extreme the bigger the overmask size,
|
||||
* because bigger px values also make bigger circles ocuppy more horizontal space.
|
||||
*/
|
||||
let lowRandom =
|
||||
Math.atan(Math.random() * 10 - 10) / Math.abs(Math.atan(-10)) + 1;
|
||||
lowRandom = Math.pow(lowRandom, px / 8);
|
||||
|
||||
var rando = Math.floor(lowRandom * px);
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
(i / 4) % canvas.width,
|
||||
Math.floor(i / 4 / canvas.width),
|
||||
scaleFactor +
|
||||
rando +
|
||||
(rando > scaleFactor ? rando / scaleFactor : scaleFactor / rando), // was 4 * sf + rando, too big, but i think i want it more ... random
|
||||
rando, // was 4 * sf + rando, too big, but i think i want it more ... random
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
|
@ -462,8 +530,8 @@ const dream_img2img_callback = (evn, state) => {
|
|||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.cursorSize,
|
||||
state.cursorSize,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
|
@ -484,13 +552,6 @@ const dream_img2img_callback = (evn, state) => {
|
|||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Setup some basic information for SD
|
||||
request.width = bb.w;
|
||||
request.height = bb.h;
|
||||
|
||||
request.firstphase_width = bb.w / 2;
|
||||
request.firstphase_height = bb.h / 2;
|
||||
|
||||
// Use img2img
|
||||
|
||||
// Temporary canvas for init image and mask generation
|
||||
|
@ -502,36 +563,56 @@ const dream_img2img_callback = (evn, state) => {
|
|||
auxCtx.fillStyle = "#000F";
|
||||
|
||||
// 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.fillRect(0, 0, request.width, request.height);
|
||||
auxCtx.drawImage(
|
||||
imgCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
request.init_images = [auxCanvas.toDataURL()];
|
||||
|
||||
// Get mask image
|
||||
auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
auxCtx.globalCompositeOperation = "destination-out";
|
||||
auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(
|
||||
maskPaintCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height
|
||||
);
|
||||
|
||||
auxCtx.globalCompositeOperation = "destination-atop";
|
||||
auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.fillRect(0, 0, request.width, request.height);
|
||||
|
||||
// Border Mask
|
||||
if (state.keepBorderSize > 0) {
|
||||
auxCtx.globalCompositeOperation = "source-over";
|
||||
auxCtx.fillStyle = "#000F";
|
||||
auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h);
|
||||
auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize);
|
||||
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
|
||||
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
|
||||
auxCtx.fillRect(
|
||||
bb.w - state.keepBorderSize,
|
||||
request.width - state.keepBorderSize,
|
||||
0,
|
||||
state.keepBorderSize,
|
||||
bb.h
|
||||
request.height
|
||||
);
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
bb.h - state.keepBorderSize,
|
||||
bb.w,
|
||||
request.height - state.keepBorderSize,
|
||||
request.width,
|
||||
state.keepBorderSize
|
||||
);
|
||||
}
|
||||
|
@ -544,6 +625,42 @@ const dream_img2img_callback = (evn, state) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dream and img2img tools
|
||||
*/
|
||||
const _reticle_draw = (evn, state) => {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
state.cursorSize,
|
||||
state.cursorSize,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// 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
|
||||
|
||||
return () => {
|
||||
ovCtx.clearRect(bb.x - 10, bb.y - 10, bb.w + 20, bb.h + 20);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic wheel handler
|
||||
*/
|
||||
|
||||
const _dream_onwheel = (evn, state) => {
|
||||
if (!evn.evn.ctrlKey) {
|
||||
const v =
|
||||
state.cursorSize -
|
||||
Math.floor(state.config.cursorSizeScrollSpeed * evn.delta);
|
||||
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
|
||||
state.mousemovecb(evn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers Tools
|
||||
*/
|
||||
|
@ -560,6 +677,7 @@ const dreamTool = () =>
|
|||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.world.onwheel.on(state.wheelcb);
|
||||
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.world.btn.right.onclick.on(state.erasecb);
|
||||
|
||||
|
@ -569,6 +687,7 @@ const dreamTool = () =>
|
|||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.world.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.world.onwheel.clear(state.wheelcb);
|
||||
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
|
||||
|
||||
|
@ -577,12 +696,25 @@ const dreamTool = () =>
|
|||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.config = {
|
||||
cursorSizeScrollSpeed: 1,
|
||||
};
|
||||
|
||||
state.cursorSize = 512;
|
||||
|
||||
state.snapToGrid = true;
|
||||
state.invertMask = false;
|
||||
state.overMaskPx = 0;
|
||||
state.mousemovecb = (evn) => {
|
||||
|
||||
state.erasePrevReticle = () =>
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
_reticle_draw(evn, state.snapToGrid);
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
state.erasePrevReticle();
|
||||
state.erasePrevReticle = _reticle_draw(evn, state);
|
||||
};
|
||||
state.wheelcb = (evn) => {
|
||||
_dream_onwheel(evn, state);
|
||||
};
|
||||
state.dreamcb = (evn) => {
|
||||
dream_generate_callback(evn, state);
|
||||
|
@ -593,6 +725,22 @@ const dreamTool = () =>
|
|||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
|
||||
// Cursor Size Slider
|
||||
const cursorSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"cursorSize",
|
||||
"Cursor Size",
|
||||
{
|
||||
min: 0,
|
||||
max: 2048,
|
||||
step: 128,
|
||||
textStep: 2,
|
||||
}
|
||||
);
|
||||
|
||||
state.setCursorSize = cursorSizeSlider.setValue;
|
||||
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
|
||||
|
||||
// Snap to Grid Checkbox
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
|
@ -615,12 +763,16 @@ const dreamTool = () =>
|
|||
state,
|
||||
"overMaskPx",
|
||||
"Overmask px",
|
||||
0,
|
||||
128,
|
||||
1
|
||||
{
|
||||
min: 0,
|
||||
max: 64,
|
||||
step: 5,
|
||||
textStep: 1,
|
||||
}
|
||||
).slider;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.cursorSizeSlider);
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.invertMaskLabel);
|
||||
|
@ -644,6 +796,7 @@ const img2imgTool = () =>
|
|||
|
||||
// Start Listeners
|
||||
mouse.listen.world.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.world.onwheel.on(state.wheelcb);
|
||||
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.world.btn.right.onclick.on(state.erasecb);
|
||||
|
||||
|
@ -653,6 +806,7 @@ const img2imgTool = () =>
|
|||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.world.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.world.onwheel.clear(state.wheelcb);
|
||||
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
|
||||
|
||||
|
@ -661,6 +815,11 @@ const img2imgTool = () =>
|
|||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.config = {
|
||||
cursorSizeScrollSpeed: 1,
|
||||
};
|
||||
|
||||
state.cursorSize = 512;
|
||||
state.snapToGrid = true;
|
||||
state.invertMask = true;
|
||||
state.fullResolution = false;
|
||||
|
@ -669,45 +828,63 @@ const img2imgTool = () =>
|
|||
|
||||
state.keepBorderSize = 64;
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
state.erasePrevReticle = () =>
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
_reticle_draw(evn, state.snapToGrid);
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
state.erasePrevReticle();
|
||||
state.erasePrevReticle = _reticle_draw(evn, state);
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.cursorSize,
|
||||
state.cursorSize,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// Resolution
|
||||
const request = {
|
||||
width: stableDiffusionData.width,
|
||||
height: stableDiffusionData.height,
|
||||
};
|
||||
|
||||
// For displaying border mask
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = bb.w;
|
||||
auxCanvas.height = bb.h;
|
||||
auxCanvas.width = request.width;
|
||||
auxCanvas.height = request.height;
|
||||
const auxCtx = auxCanvas.getContext("2d");
|
||||
|
||||
if (state.keepBorderSize > 0) {
|
||||
auxCtx.fillStyle = "#6A6AFF7F";
|
||||
auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h);
|
||||
auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize);
|
||||
auxCtx.fillStyle = "#6A6AFF30";
|
||||
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
|
||||
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
|
||||
auxCtx.fillRect(
|
||||
bb.w - state.keepBorderSize,
|
||||
request.width - state.keepBorderSize,
|
||||
0,
|
||||
state.keepBorderSize,
|
||||
bb.h
|
||||
request.height
|
||||
);
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
bb.h - state.keepBorderSize,
|
||||
bb.w,
|
||||
request.height - state.keepBorderSize,
|
||||
request.width,
|
||||
state.keepBorderSize
|
||||
);
|
||||
ovCtx.drawImage(
|
||||
auxCanvas,
|
||||
0,
|
||||
0,
|
||||
request.width,
|
||||
request.height,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
}
|
||||
|
||||
const tmp = ovCtx.globalAlpha;
|
||||
ovCtx.globalAlpha = 0.4;
|
||||
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
|
||||
ovCtx.globalAlpha = tmp;
|
||||
};
|
||||
state.wheelcb = (evn) => {
|
||||
_dream_onwheel(evn, state);
|
||||
};
|
||||
state.dreamcb = (evn) => {
|
||||
dream_img2img_callback(evn, state);
|
||||
|
@ -717,6 +894,23 @@ const img2imgTool = () =>
|
|||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
|
||||
// Cursor Size Slider
|
||||
const cursorSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"cursorSize",
|
||||
"Cursor Size",
|
||||
{
|
||||
min: 0,
|
||||
max: 2048,
|
||||
step: 128,
|
||||
textStep: 2,
|
||||
}
|
||||
);
|
||||
|
||||
state.setCursorSize = cursorSizeSlider.setValue;
|
||||
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
|
||||
|
||||
// Snap To Grid Checkbox
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
|
@ -746,9 +940,12 @@ const img2imgTool = () =>
|
|||
state,
|
||||
"denoisingStrength",
|
||||
"Denoising Strength",
|
||||
0,
|
||||
1,
|
||||
0.05
|
||||
{
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textStep: 0.01,
|
||||
}
|
||||
).slider;
|
||||
|
||||
// Border Mask Size Slider
|
||||
|
@ -756,12 +953,16 @@ const img2imgTool = () =>
|
|||
state,
|
||||
"keepBorderSize",
|
||||
"Keep Border Size",
|
||||
0,
|
||||
128,
|
||||
1
|
||||
{
|
||||
min: 0,
|
||||
max: 128,
|
||||
step: 8,
|
||||
textStep: 1,
|
||||
}
|
||||
).slider;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.cursorSizeSlider);
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.invertMaskLabel);
|
||||
|
|
|
@ -52,11 +52,47 @@ const _mask_brush_erase_callback = (evn, state) => {
|
|||
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([]);
|
||||
}
|
||||
};
|
||||
|
||||
const maskBrushTool = () =>
|
||||
toolbar.registerTool(
|
||||
"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});
|
||||
|
@ -73,6 +109,10 @@ 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);
|
||||
|
@ -104,21 +144,22 @@ const maskBrushTool = () =>
|
|||
|
||||
state.preview = false;
|
||||
|
||||
state.movecb = (evn) => {
|
||||
// draw big translucent white blob cursor
|
||||
state.clearPrevCursor = () =>
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
||||
ovCtx.fillStyle = "#FFFFFF50";
|
||||
|
||||
ovCtx.fill();
|
||||
state.movecb = (evn) => {
|
||||
state.cursorLayer.moveTo(
|
||||
evn.x - state.brushSize / 2 - 10,
|
||||
evn.y - state.brushSize / 2 - 10
|
||||
);
|
||||
|
||||
if (state.preview) {
|
||||
ovCtx.strokeStyle = "#000F";
|
||||
ovCtx.setLineDash([4, 2]);
|
||||
ovCtx.stroke();
|
||||
ovCtx.setLineDash([]);
|
||||
}
|
||||
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.wheelcb = (evn) => {
|
||||
|
@ -127,7 +168,6 @@ const maskBrushTool = () =>
|
|||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
@ -142,9 +182,16 @@ const maskBrushTool = () =>
|
|||
state,
|
||||
"brushSize",
|
||||
"Brush Size",
|
||||
state.config.minBrushSize,
|
||||
state.config.maxBrushSize,
|
||||
1
|
||||
{
|
||||
min: state.config.minBrushSize,
|
||||
max: state.config.maxBrushSize,
|
||||
step: 5,
|
||||
textStep: 1,
|
||||
cb: (v) => {
|
||||
if (!state.cursorLayer) return;
|
||||
_paint_mb_cursor(state);
|
||||
},
|
||||
}
|
||||
);
|
||||
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
|
@ -174,9 +221,11 @@ const maskBrushTool = () =>
|
|||
if (previewMaskButton.classList.contains("active")) {
|
||||
maskPaintCanvas.classList.remove("opaque");
|
||||
state.preview = false;
|
||||
_paint_mb_cursor(state);
|
||||
} else {
|
||||
maskPaintCanvas.classList.add("opaque");
|
||||
state.preview = true;
|
||||
_paint_mb_cursor(state);
|
||||
}
|
||||
previewMaskButton.classList.toggle("active");
|
||||
};
|
||||
|
|
|
@ -54,7 +54,9 @@ const selectTransformTool = () =>
|
|||
|
||||
state.snapToGrid = true;
|
||||
state.keepAspectRatio = true;
|
||||
state.useClipboard = !!navigator.clipboard.write; // Use it by default if supported
|
||||
state.useClipboard = !!(
|
||||
navigator.clipboard && navigator.clipboard.write
|
||||
); // Use it by default if supported
|
||||
|
||||
state.original = null;
|
||||
state.dragging = null;
|
||||
|
@ -192,8 +194,8 @@ const selectTransformTool = () =>
|
|||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, true, 64);
|
||||
y += snap(evn.y, true, 64);
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
|
||||
// Update scale
|
||||
|
@ -337,8 +339,8 @@ const selectTransformTool = () =>
|
|||
let ix = evn.ix;
|
||||
let iy = evn.iy;
|
||||
if (state.snapToGrid) {
|
||||
ix += snap(evn.ix, true, 64);
|
||||
iy += snap(evn.iy, true, 64);
|
||||
ix += snap(evn.ix, 0, 64);
|
||||
iy += snap(evn.iy, 0, 64);
|
||||
}
|
||||
|
||||
// If is selected, check if drag is in handles/body and act accordingly
|
||||
|
@ -368,8 +370,8 @@ const selectTransformTool = () =>
|
|||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, true, 64);
|
||||
y += snap(evn.y, true, 64);
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
|
||||
// If we are scaling, stop scaling and do some handler magic
|
||||
|
@ -489,10 +491,11 @@ const selectTransformTool = () =>
|
|||
// Send to clipboard
|
||||
state.clipboard.copy.toBlob((blob) => {
|
||||
const item = new ClipboardItem({"image/png": blob});
|
||||
navigator.clipboard.write([item]).catch((e) => {
|
||||
console.warn("Error sending to clipboard");
|
||||
console.warn(e);
|
||||
});
|
||||
navigator.clipboard &&
|
||||
navigator.clipboard.write([item]).catch((e) => {
|
||||
console.warn("Error sending to clipboard");
|
||||
console.warn(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -501,25 +504,25 @@ const selectTransformTool = () =>
|
|||
state.ctrlvcb = (evn) => {
|
||||
if (state.useClipboard) {
|
||||
// If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required)
|
||||
navigator.clipboard.read().then((items) => {
|
||||
console.info(items[0]);
|
||||
for (const item of items) {
|
||||
for (const type of item.types) {
|
||||
if (type.startsWith("image/")) {
|
||||
item.getType(type).then((blob) => {
|
||||
// Converts blob to image
|
||||
const url = window.URL || window.webkitURL;
|
||||
const image = document.createElement("img");
|
||||
image.src = url.createObjectURL(file);
|
||||
tools.stamp.enable({
|
||||
image,
|
||||
back: tools.selecttransform.enable,
|
||||
navigator.clipboard &&
|
||||
navigator.clipboard.read().then((items) => {
|
||||
for (const item of items) {
|
||||
for (const type of item.types) {
|
||||
if (type.startsWith("image/")) {
|
||||
item.getType(type).then((blob) => {
|
||||
// Converts blob to image
|
||||
const url = window.URL || window.webkitURL;
|
||||
const image = document.createElement("img");
|
||||
image.src = url.createObjectURL(file);
|
||||
tools.stamp.enable({
|
||||
image,
|
||||
back: tools.selecttransform.enable,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (state.clipboard.copy) {
|
||||
// Use internal clipboard
|
||||
const image = document.createElement("img");
|
||||
|
@ -563,7 +566,7 @@ const selectTransformTool = () =>
|
|||
"Use clipboard"
|
||||
);
|
||||
state.ctxmenu.useClipboardLabel = clipboardCheckbox.label;
|
||||
if (!navigator.clipboard.write)
|
||||
if (!(navigator.clipboard && navigator.clipboard.write))
|
||||
clipboardCheckbox.checkbox.disabled = true; // Disable if not available
|
||||
|
||||
// Some useful actions to do with selection
|
||||
|
|
|
@ -19,7 +19,8 @@ const stampTool = () =>
|
|||
state.addResource(
|
||||
opt.name || "Clipboard",
|
||||
opt.image,
|
||||
opt.temporary === undefined ? true : opt.temporary
|
||||
opt.temporary === undefined ? true : opt.temporary,
|
||||
false
|
||||
);
|
||||
state.ctxmenu.uploadButton.disabled = true;
|
||||
state.back = opt.back || null;
|
||||
|
@ -56,8 +57,14 @@ const stampTool = () =>
|
|||
|
||||
state.lastMouseMove = {x: 0, y: 0};
|
||||
|
||||
state.selectResource = (resource) => {
|
||||
if (state.ctxmenu.uploadButton.disabled) return;
|
||||
state.selectResource = (resource, nolock = true) => {
|
||||
if (nolock && state.ctxmenu.uploadButton.disabled) return;
|
||||
|
||||
console.debug(
|
||||
`[stamp] Selecting Resource '${resource && resource.name}'[${
|
||||
resource && resource.id
|
||||
}]`
|
||||
);
|
||||
|
||||
const resourceWrapper = resource && resource.dom.wrapper;
|
||||
|
||||
|
@ -83,8 +90,38 @@ const stampTool = () =>
|
|||
if (state.loaded) state.movecb(state.lastMouseMove);
|
||||
};
|
||||
|
||||
// Synchronizes resources array with the DOM
|
||||
// Synchronizes resources array with the DOM and Local Storage
|
||||
const syncResources = () => {
|
||||
// Saves to local storage
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"tools.stamp.resources",
|
||||
JSON.stringify(
|
||||
state.resources
|
||||
.filter((resource) => !resource.temporary)
|
||||
.map((resource) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = resource.image.width;
|
||||
canvas.height = resource.image.height;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(resource.image, 0, 0);
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
src: canvas.toDataURL(),
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[stamp] Failed to synchronize resources with local storage"
|
||||
);
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
// Creates DOM elements when needed
|
||||
state.resources.forEach((resource) => {
|
||||
if (
|
||||
|
@ -93,12 +130,15 @@ const stampTool = () =>
|
|||
)
|
||||
) {
|
||||
console.debug(
|
||||
`Creating resource element 'resource-${resource.id}'`
|
||||
`[stamp] Creating Resource Element [resource-${resource.id}]`
|
||||
);
|
||||
const resourceWrapper = document.createElement("div");
|
||||
resourceWrapper.id = `resource-${resource.id}`;
|
||||
resourceWrapper.textContent = resource.name;
|
||||
resourceWrapper.classList.add("resource");
|
||||
const resourceTitle = document.createElement("span");
|
||||
resourceTitle.textContent = resource.name;
|
||||
resourceTitle.classList.add("resource-title");
|
||||
resourceWrapper.appendChild(resourceTitle);
|
||||
|
||||
resourceWrapper.addEventListener("click", () =>
|
||||
state.selectResource(resource)
|
||||
|
@ -112,6 +152,41 @@ const stampTool = () =>
|
|||
state.ctxmenu.previewPane.style.display = "none";
|
||||
});
|
||||
|
||||
// Add action buttons
|
||||
const actionArray = document.createElement("div");
|
||||
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;
|
||||
|
||||
syncResources();
|
||||
}
|
||||
});
|
||||
renameButton.title = "Rename Resource";
|
||||
renameButton.appendChild(document.createElement("div"));
|
||||
renameButton.classList.add("rename-btn");
|
||||
|
||||
const trashButton = document.createElement("button");
|
||||
trashButton.addEventListener(
|
||||
"click",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
state.ctxmenu.previewPane.style.display = "none";
|
||||
state.deleteResource(resource.id);
|
||||
},
|
||||
{passive: false}
|
||||
);
|
||||
trashButton.title = "Delete Resource";
|
||||
trashButton.appendChild(document.createElement("div"));
|
||||
trashButton.classList.add("delete-btn");
|
||||
|
||||
actionArray.appendChild(renameButton);
|
||||
actionArray.appendChild(trashButton);
|
||||
resourceWrapper.appendChild(actionArray);
|
||||
state.ctxmenu.resourceList.appendChild(resourceWrapper);
|
||||
resource.dom = {wrapper: resourceWrapper};
|
||||
}
|
||||
|
@ -124,15 +199,20 @@ const stampTool = () =>
|
|||
elements.forEach((element) => {
|
||||
let remove = true;
|
||||
state.resources.some((resource) => {
|
||||
if (element.id.endsWith(resource.id)) remove = false;
|
||||
if (element.id.endsWith(resource.id)) {
|
||||
remove = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (remove) state.ctxmenu.resourceList.removeChild(element);
|
||||
if (remove) {
|
||||
console.debug(`[stamp] Sync Removing Element [${element.id}]`);
|
||||
state.ctxmenu.resourceList.removeChild(element);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Adds a image resource (temporary allows only one draw, used for pasting)
|
||||
state.addResource = (name, image, temporary = false) => {
|
||||
state.addResource = (name, image, temporary = false, nolock = true) => {
|
||||
const id = guid();
|
||||
const resource = {
|
||||
id,
|
||||
|
@ -140,19 +220,28 @@ const stampTool = () =>
|
|||
image,
|
||||
temporary,
|
||||
};
|
||||
|
||||
console.info(`[stamp] Adding Resource '${name}'[${id}]`);
|
||||
|
||||
state.resources.push(resource);
|
||||
syncResources();
|
||||
|
||||
// Select this resource
|
||||
state.selectResource(resource);
|
||||
state.selectResource(resource, nolock);
|
||||
|
||||
return resource;
|
||||
};
|
||||
|
||||
// Deletes a resource (Yes, functionality is here, but we don't have an UI for this yet)
|
||||
// Used for temporary images too
|
||||
state.deleteResource = (id) => {
|
||||
state.resources = state.resources.filter((v) => v.id !== id);
|
||||
const resourceIndex = state.resources.findIndex((v) => v.id === id);
|
||||
const resource = state.resources[resourceIndex];
|
||||
if (state.selected === resource) state.selected = null;
|
||||
console.info(
|
||||
`[stamp] Deleting Resource '${resource.name}'[${resource.id}]`
|
||||
);
|
||||
|
||||
state.resources.splice(resourceIndex, 1);
|
||||
|
||||
syncResources();
|
||||
};
|
||||
|
@ -161,8 +250,8 @@ const stampTool = () =>
|
|||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, true, 64);
|
||||
y += snap(evn.y, true, 64);
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
|
||||
state.lastMouseMove = evn;
|
||||
|
@ -190,8 +279,8 @@ const stampTool = () =>
|
|||
let x = evn.x;
|
||||
let y = evn.y;
|
||||
if (state.snapToGrid) {
|
||||
x += snap(evn.x, true, 64);
|
||||
y += snap(evn.y, true, 64);
|
||||
x += snap(evn.x, 0, 64);
|
||||
y += snap(evn.y, 0, 64);
|
||||
}
|
||||
|
||||
const resource = state.selected;
|
||||
|
@ -203,7 +292,9 @@ const stampTool = () =>
|
|||
y,
|
||||
});
|
||||
|
||||
if (resource.temporary) state.deleteResource(resource.id);
|
||||
if (resource.temporary) {
|
||||
state.deleteResource(resource.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.back) {
|
||||
|
@ -268,7 +359,7 @@ const stampTool = () =>
|
|||
const image = document.createElement("img");
|
||||
image.src = url.createObjectURL(file);
|
||||
|
||||
state.addResource(file.name, image, false);
|
||||
image.onload = () => state.addResource(file.name, image, false);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -321,6 +412,29 @@ const stampTool = () =>
|
|||
state.ctxmenu.previewPane = previewPane;
|
||||
state.ctxmenu.resourceManager = resourceManager;
|
||||
state.ctxmenu.resourceList = resourceList;
|
||||
|
||||
// Performs resource fetch from local storage
|
||||
{
|
||||
const storageResources = localStorage.getItem(
|
||||
"tools.stamp.resources"
|
||||
);
|
||||
if (storageResources) {
|
||||
const parsed = JSON.parse(storageResources);
|
||||
state.resources.push(
|
||||
...parsed.map((resource) => {
|
||||
const image = document.createElement("img");
|
||||
image.src = resource.src;
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
image,
|
||||
};
|
||||
})
|
||||
);
|
||||
syncResources();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
|
|
5
res/icons/brush.svg
Normal file
5
res/icons/brush.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="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"></path>
|
||||
<path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 413 B |
4
res/icons/check.svg
Normal file
4
res/icons/check.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="20 6 9 17 4 12"></polyline>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 237 B |
5
res/icons/edit.svg
Normal file
5
res/icons/edit.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="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 344 B |
6
res/icons/trash.svg
Normal file
6
res/icons/trash.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<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="M3 6h18"></path>
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 330 B |
Loading…
Reference in a new issue