Merge pull request #37 from seijihariki/img2img
Add tool for img2img Former-commit-id: 4e6f088cd99e9274b7e0ca9413ba7e8c3503540e
This commit is contained in:
commit
42adf68610
21 changed files with 1437 additions and 676 deletions
16
.github.bkp/workflows/autoformat.yml
Normal file
16
.github.bkp/workflows/autoformat.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
name: Prettier Autoformatting
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
types: [opened, synchronize, closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Prettify
|
||||||
|
uses: creyD/prettier_action@v4.2
|
||||||
|
with:
|
||||||
|
prettier_options: --write **/*.{js,html,css,md}
|
8
css/colors.css
Normal file
8
css/colors.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
:root {
|
||||||
|
--c-primary: #2c3333;
|
||||||
|
--c-hover: hsl(180, 7%, 30%);
|
||||||
|
--c-active: hsl(180, 7%, 25%);
|
||||||
|
--c-secondary: #395b64;
|
||||||
|
--c-accent: #a5c9ca;
|
||||||
|
--c-text: #e7f6f2;
|
||||||
|
}
|
|
@ -14,44 +14,6 @@ body {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
#historyContainer > .info {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history.history {
|
|
||||||
height: 200px;
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history.history > .history-item {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
padding: 5px;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history.history > .history-item {
|
|
||||||
background-color: #0000;
|
|
||||||
}
|
|
||||||
#history.history > .history-item:hover {
|
|
||||||
background-color: #fff5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history.history > .history-item.current {
|
|
||||||
background-color: #66f5;
|
|
||||||
}
|
|
||||||
#history.history > .history-item.current:hover {
|
|
||||||
background-color: #66f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history.history > .history-item.future {
|
|
||||||
background-color: #4445;
|
|
||||||
}
|
|
||||||
#history.history > .history-item.future:hover {
|
|
||||||
background-color: #ddd5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainHSplit {
|
.mainHSplit {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
@ -68,40 +30,6 @@ body {
|
||||||
grid-row-gap: 5px;
|
grid-row-gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uiContainer {
|
|
||||||
position: fixed;
|
|
||||||
width: 250px;
|
|
||||||
height: auto;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uiTitleBar {
|
|
||||||
z-index: 999;
|
|
||||||
cursor: move;
|
|
||||||
background-color: rgba(104, 104, 104, 0.75);
|
|
||||||
z-index: 999;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
margin-bottom: auto;
|
|
||||||
font-size: 1.5em;
|
|
||||||
color: black;
|
|
||||||
text-align: center;
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
border: solid;
|
|
||||||
border-bottom: none;
|
|
||||||
border-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable {
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -158,7 +86,7 @@ button.tool:hover {
|
||||||
transition: max-height 0.2s ease-out;
|
transition: max-height 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.menu-container {
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
|
32
css/ui/generic.css
Normal file
32
css/ui/generic.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/* UI Floating Windows */
|
||||||
|
.floating-window {
|
||||||
|
position: fixed;
|
||||||
|
width: 250px;
|
||||||
|
height: auto;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window-title {
|
||||||
|
cursor: move;
|
||||||
|
background-color: rgba(104, 104, 104, 0.75);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-bottom: auto;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: black;
|
||||||
|
text-align: center;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
border: solid;
|
||||||
|
border-bottom: none;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
cursor: move;
|
||||||
|
}
|
38
css/ui/history.css
Normal file
38
css/ui/history.css
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#historyContainer > .info {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#history.history {
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#history.history > .history-item {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#history.history > .history-item {
|
||||||
|
background-color: #0000;
|
||||||
|
}
|
||||||
|
#history.history > .history-item:hover {
|
||||||
|
background-color: #fff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#history.history > .history-item.current {
|
||||||
|
background-color: #66f5;
|
||||||
|
}
|
||||||
|
#history.history > .history-item.current:hover {
|
||||||
|
background-color: #66f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#history.history > .history-item.future {
|
||||||
|
background-color: #4445;
|
||||||
|
}
|
||||||
|
#history.history > .history-item.future:hover {
|
||||||
|
background-color: #ddd5;
|
||||||
|
}
|
62
css/ui/toolbar.css
Normal file
62
css/ui/toolbar.css
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#ui-toolbar {
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
width: 60px;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
color: var(--c-text);
|
||||||
|
background-color: var(--c-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-toolbar .handle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-toolbar .handle > .line {
|
||||||
|
width: 80%;
|
||||||
|
border-top: 2px #777 dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-toolbar .tool .tool-icon {
|
||||||
|
filter: invert(60%);
|
||||||
|
}
|
||||||
|
#ui-toolbar .tool.using .tool-icon {
|
||||||
|
filter: invert(80%);
|
||||||
|
}
|
||||||
|
#ui-toolbar .tool:hover .tool-icon {
|
||||||
|
filter: invert(90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The separator */
|
||||||
|
#ui-toolbar .separator {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
align-self: center;
|
||||||
|
border-top: 1px var(--c-hover) solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for the tool buttons */
|
||||||
|
#ui-toolbar .tool {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-toolbar .tool.using {
|
||||||
|
background-color: var(--c-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-toolbar .tool:hover {
|
||||||
|
background-color: var(--c-hover);
|
||||||
|
}
|
301
index.html
301
index.html
|
@ -1,86 +1,159 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en-US">
|
<html lang="en-US">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>openOutpaint 🐠</title>
|
<title>openOutpaint 🐠</title>
|
||||||
|
<!-- CSS Variables -->
|
||||||
|
<link href="css/colors.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="css/index.css" rel="stylesheet" />
|
<link href="css/index.css" rel="stylesheet" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link href="css/ui/generic.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<link href="css/ui/history.css" rel="stylesheet" />
|
||||||
|
<link href="css/ui/toolbar.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Main Toolbar -->
|
<!-- Main Toolbar -->
|
||||||
<div id="infoContainer" class="uiContainer">
|
<div
|
||||||
<div id="infoTitleBar" class="draggable uiTitleBar">openOutpaint 🐠</div>
|
id="infoContainer"
|
||||||
<div id="info" class="info" style="min-width:200px;">
|
class="floating-window"
|
||||||
|
style="left: 10px; top: 10px">
|
||||||
|
<div id="infoTitleBar" class="draggable floating-window-title">
|
||||||
|
openOutpaint 🐠
|
||||||
|
</div>
|
||||||
|
<div id="info" class="menu-container" style="min-width: 200px">
|
||||||
<label for="host">Host</label>
|
<label for="host">Host</label>
|
||||||
<input id="host" value="http://127.0.0.1:7860"><br />
|
<input id="host" value="http://127.0.0.1:7860" /><br />
|
||||||
|
|
||||||
<!-- Prompts section -->
|
<!-- Prompts section -->
|
||||||
<button type="button" class="collapsible">Prompts</button>
|
<button type="button" class="collapsible">Prompts</button>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<label for="prompt">Prompt:</label> <br>
|
<label for="prompt">Prompt:</label> <br />
|
||||||
<textarea id="prompt">oceanographic study, underwater wildlife, award winning</textarea><br />
|
<textarea id="prompt">
|
||||||
<label for="negPrompt">Negative prompt:</label> <br>
|
oceanographic study, underwater wildlife, award winning</textarea
|
||||||
<textarea
|
><br />
|
||||||
id="negPrompt">people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry</textarea><br />
|
<label for="negPrompt">Negative prompt:</label> <br />
|
||||||
<hr>
|
<textarea id="negPrompt">
|
||||||
|
people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry</textarea
|
||||||
|
><br />
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<!-- SD section -->
|
<!-- SD section -->
|
||||||
<button type="button" class="collapsible">Stable Diffusion settings</button>
|
<button type="button" class="collapsible">
|
||||||
|
Stable Diffusion settings
|
||||||
|
</button>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<label for="models">Model:</label>
|
<label for="models">Model:</label>
|
||||||
<select id="models" onchange="changeModel()"></select><br>
|
<select id="models" onchange="changeModel()"></select
|
||||||
|
><br />
|
||||||
<label for="samplerSelect">Sampler:</label>
|
<label for="samplerSelect">Sampler:</label>
|
||||||
<select id="samplerSelect" onchange="changeSampler()"></select><br />
|
<select id="samplerSelect" onchange="changeSampler()"></select
|
||||||
<label for="seed">Seed (-1 for random):</label> <br>
|
><br />
|
||||||
<input type="number" id="seed" onchange="changeSeed()" min="1" max="9999999999" value="-1"
|
<label for="seed">Seed (-1 for random):</label> <br />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="seed"
|
||||||
|
onchange="changeSeed()"
|
||||||
|
min="1"
|
||||||
|
max="9999999999"
|
||||||
|
value="-1"
|
||||||
step="1" /><br />
|
step="1" /><br />
|
||||||
<label for="steps">Steps: <input type="number" id="stepsTxt"></label><br />
|
<label for="steps">Steps: <input type="number" id="stepsTxt" /></label
|
||||||
|
><br />
|
||||||
<input type="range" id="steps" name="steps" min="1" max="50" /><br />
|
<input type="range" id="steps" name="steps" min="1" max="50" /><br />
|
||||||
<label for="cfgScale">CFG scale: <input type="number" id="cfgScaleTxt"></label>
|
<label for="cfgScale"
|
||||||
|
>CFG scale: <input type="number" id="cfgScaleTxt"
|
||||||
|
/></label>
|
||||||
<br />
|
<br />
|
||||||
<input type="range" id="cfgScale" name="cfgScale" min="-1" max="25" step="0.5" /><br />
|
<input
|
||||||
<label for="batchSize">Batch size: <input type="number" id="batchSizeText"></label><br />
|
type="range"
|
||||||
<input type="range" id="batchSize" name="batchSize" min="1" max="8" step="1" /><br />
|
id="cfgScale"
|
||||||
<label for="batchCount">Batch count: <input type="number" id="batchCountText"></label><br />
|
name="cfgScale"
|
||||||
<input type="range" id="batchCount" name="batchCount" min="1" max="8" step="1" /><br />
|
min="-1"
|
||||||
<hr>
|
max="25"
|
||||||
|
step="0.5" /><br />
|
||||||
|
<label for="batchSize"
|
||||||
|
>Batch size: <input type="number" id="batchSizeText" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="batchSize"
|
||||||
|
name="batchSize"
|
||||||
|
min="1"
|
||||||
|
max="8"
|
||||||
|
step="1" /><br />
|
||||||
|
<label for="batchCount"
|
||||||
|
>Batch count: <input type="number" id="batchCountText" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="batchCount"
|
||||||
|
name="batchCount"
|
||||||
|
min="1"
|
||||||
|
max="8"
|
||||||
|
step="1" /><br />
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<!-- Unsectioned -->
|
<!-- Unsectioned -->
|
||||||
<label for="scaleFactor">Scale factor: <input type="number" id="scaleFactorTxt"></label><br />
|
<label for="scaleFactor"
|
||||||
<input type="range" id="scaleFactor" name="scaleFactor" min="1" max="16" /><br />
|
>Scale factor: <input type="number" id="scaleFactorTxt" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="scaleFactor"
|
||||||
|
name="scaleFactor"
|
||||||
|
min="1"
|
||||||
|
max="16" /><br />
|
||||||
<label for="cbxSnap">Snap to grid?</label>
|
<label for="cbxSnap">Snap to grid?</label>
|
||||||
<input type="checkbox" id="cbxSnap" onchange="changeSnapMode()" checked="checked"><br />
|
<input
|
||||||
<label for="cbxEnableErasing">Right-click erase?</label>
|
type="checkbox"
|
||||||
<input type="checkbox" id="cbxEnableErasing" onchange="changeEnableErasing()"><br />
|
id="cbxSnap"
|
||||||
<label for="cbxPaint">Mask mode?</label>
|
onchange="changeSnapMode()"
|
||||||
<input type="checkbox" id="cbxPaint" onchange="changePaintMode()"><br />
|
checked="checked" /><br />
|
||||||
|
|
||||||
<label for="cbxHRFix">Auto txt2img HRfix?</label>
|
<label for="cbxHRFix">Auto txt2img HRfix?</label>
|
||||||
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()"><br />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="cbxHRFix"
|
||||||
|
onchange="changeHiResFix()" /><br />
|
||||||
<label for="overMaskPx">Overmask px (0 to disable):</label>
|
<label for="overMaskPx">Overmask px (0 to disable):</label>
|
||||||
<input type="number" id="overMaskPx" onchange="changeOverMaskPx()" min="0" max="128" value="16"
|
<input
|
||||||
|
type="number"
|
||||||
|
id="overMaskPx"
|
||||||
|
onchange="changeOverMaskPx()"
|
||||||
|
min="0"
|
||||||
|
max="128"
|
||||||
|
value="16"
|
||||||
step="1" /><br />
|
step="1" /><br />
|
||||||
<label for="maskBlur">Mask blur:</label>
|
<label for="maskBlur">Mask blur:</label>
|
||||||
<span id="maskBlurText"></span><br />
|
<span id="maskBlurText"></span><br />
|
||||||
<input type="number" id="maskBlur" name="maskBlur" min="0" max="256" value="0" step="1"
|
<input
|
||||||
|
type="number"
|
||||||
|
id="maskBlur"
|
||||||
|
name="maskBlur"
|
||||||
|
min="0"
|
||||||
|
max="256"
|
||||||
|
value="0"
|
||||||
|
step="1"
|
||||||
onchange="changeMaskBlur()" /><br />
|
onchange="changeMaskBlur()" /><br />
|
||||||
<!-- Save/load image section -->
|
<!-- Save/load image section -->
|
||||||
<button type="button" class="collapsible">Save/Load/New image</button>
|
<button type="button" class="collapsible">Save/Load/New image</button>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<label for="preloadImage">Load image:</label>
|
<label for="preloadImage">Load image:</label>
|
||||||
<input type="file" id="preloadImage" onchange="preloadImage()" accept="image/*" /
|
<input type="file" id="preloadImage" onchange="preloadImage()"
|
||||||
style="width: 200px;"><br />
|
accept="image/*" / style="width: 200px;"><br />
|
||||||
<button onclick="downloadCanvas()">Save canvas</button><br />
|
<button onclick="downloadCanvas()">Save canvas</button><br />
|
||||||
<label for="upscalers">Choose upscaler</label>
|
<label for="upscalers">Choose upscaler</label>
|
||||||
<select id="upscalers"></select>
|
<select id="upscalers"></select>
|
||||||
<button onclick="upscaleAndDownload()">Upscale (might take a sec)</button><br />
|
<button onclick="upscaleAndDownload()">
|
||||||
|
Upscale (might take a sec)</button
|
||||||
|
><br />
|
||||||
|
|
||||||
<button onclick="newImage()">Clear canvas</button>
|
<button onclick="newImage()">Clear canvas</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Degub info -->
|
<!-- Debug info -->
|
||||||
<button type="button" class="collapsible">Debug info</button>
|
<button type="button" class="collapsible">Debug info</button>
|
||||||
<div id="coords" class="content">
|
<div id="coords" class="content">
|
||||||
<label for="mouseX">mouseX:</label>
|
<label for="mouseX">mouseX:</label>
|
||||||
|
@ -102,61 +175,123 @@
|
||||||
<span id="snapY"></span><br />
|
<span id="snapY"></span><br />
|
||||||
<label for="heldButton">Mouse button:</label>
|
<label for="heldButton">Mouse button:</label>
|
||||||
<span id="heldButton"></span><br />
|
<span id="heldButton"></span><br />
|
||||||
<span id="version">Alpha release v0.0.6.9</span> <!-- 𝓃𝒾𝒸𝑒 -->
|
<span id="version">Alpha release v0.0.6.6</span>
|
||||||
<br />
|
<br />
|
||||||
<hr>
|
<hr />
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
flex: 1;
|
||||||
|
border-top: 1px black solid;
|
||||||
|
margin-right: 10px;
|
||||||
|
"></div>
|
||||||
|
Context Menu
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
flex: 1;
|
||||||
|
border-top: 1px black solid;
|
||||||
|
margin-left: 10px;
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tool-context" class="context-menu"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- History -->
|
||||||
|
<div id="ui-history" class="floating-window" style="right: 10px; top: 10px">
|
||||||
<!-- History Toolbar -->
|
<div class="draggable floating-window-title">History</div>
|
||||||
<div id="historyContainer" class="uiContainer" style="right: 0;">
|
<div class="menu-container" style="min-width: 200px">
|
||||||
<div id="historyTitleBar" class="draggable uiTitleBar">History</div>
|
|
||||||
<div class="info" style="min-width:200px;">
|
|
||||||
<div id="history" class="history"></div>
|
<div id="history" class="history"></div>
|
||||||
<div class="toolbar" style="padding: 10px;">
|
<div class="toolbar" style="padding: 10px">
|
||||||
<button type="button" onclick="commands.undo()" class="tool">undo</button>
|
<button type="button" onclick="commands.undo()" class="tool">
|
||||||
<button type="button" onclick="commands.redo()" class="tool">redo</button>
|
undo
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="commands.redo()" class="tool">
|
||||||
|
redo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div
|
||||||
|
id="ui-toolbar"
|
||||||
|
class="floating-window"
|
||||||
|
style="right: 10px; top: 350px">
|
||||||
|
<div class="draggable handle">
|
||||||
|
<span class="line"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="mainHSplit" class="mainHSplit">
|
<div class="toolbar-section"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Canvases -->
|
||||||
|
<div
|
||||||
|
id="mainHSplit"
|
||||||
|
class="mainHSplit"
|
||||||
|
onmouseover="document.activeElement.blur()">
|
||||||
<div id="uiWrapper" class="uiWrapper">
|
<div id="uiWrapper" class="uiWrapper">
|
||||||
<div id="canvasHolder" class="canvasHolder" oncontextmenu="return false;">
|
<div
|
||||||
<canvas id="backgroundCanvas" class="mainCanvases backgroundCanvas" width="2560" height="1440"
|
id="canvasHolder"
|
||||||
style="z-index: 0;">
|
class="canvasHolder"
|
||||||
|
oncontextmenu="return false;">
|
||||||
|
<canvas
|
||||||
|
id="backgroundCanvas"
|
||||||
|
class="mainCanvases backgroundCanvas"
|
||||||
|
width="2560"
|
||||||
|
height="1440"
|
||||||
|
style="z-index: 0">
|
||||||
<!-- gray grid bg canvas -->
|
<!-- gray grid bg canvas -->
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas>
|
</canvas>
|
||||||
<canvas id="canvas" class="mainCanvases canvas" width="2560" height="1440" style="z-index: 1;">
|
<canvas
|
||||||
|
id="canvas"
|
||||||
|
class="mainCanvases canvas"
|
||||||
|
width="2560"
|
||||||
|
height="1440"
|
||||||
|
style="z-index: 1">
|
||||||
<!-- normal canvas on which images are drawn -->
|
<!-- normal canvas on which images are drawn -->
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas>
|
</canvas>
|
||||||
<canvas id="tempCanvas" class="mainCanvases tempCanvas" width="2560" height="1440" style="z-index: 2;">
|
<canvas
|
||||||
|
id="tempCanvas"
|
||||||
|
class="mainCanvases tempCanvas"
|
||||||
|
width="2560"
|
||||||
|
height="1440"
|
||||||
|
style="z-index: 2">
|
||||||
<!-- temporary canvas on which images being selected/rejected or imported arbitrary images are superimposed -->
|
<!-- temporary canvas on which images being selected/rejected or imported arbitrary images are superimposed -->
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas>
|
</canvas>
|
||||||
<canvas id="targetCanvas" class="mainCanvases targetCanvas" width="2560" height="1440"
|
<canvas
|
||||||
style="z-index: 3;">
|
id="targetCanvas"
|
||||||
|
class="mainCanvases targetCanvas"
|
||||||
|
width="2560"
|
||||||
|
height="1440"
|
||||||
|
style="z-index: 3">
|
||||||
<!-- canvas on which "targeting" squares are drawn -->
|
<!-- canvas on which "targeting" squares are drawn -->
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas>
|
</canvas>
|
||||||
<canvas id="maskPaintCanvas" class="mainCanvases maskPaintCanvas" width="2560" height="1440"
|
<canvas
|
||||||
style="z-index: 4;">
|
id="maskPaintCanvas"
|
||||||
|
class="mainCanvases maskPaintCanvas"
|
||||||
|
width="2560"
|
||||||
|
height="1440"
|
||||||
|
style="z-index: 4">
|
||||||
<!-- canvas on which masking brush is "painted" -->
|
<!-- canvas on which masking brush is "painted" -->
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas>
|
</canvas>
|
||||||
<canvas id="overlayCanvas" class="mainCanvases overlayCanvas" width="2560" height="1440"
|
<canvas
|
||||||
style="z-index: 5;">
|
id="overlayCanvas"
|
||||||
|
class="mainCanvases overlayCanvas"
|
||||||
|
width="2560"
|
||||||
|
height="1440"
|
||||||
|
style="z-index: 5">
|
||||||
<!-- canvas on which "cursor" reticle or arc is drawn -->
|
<!-- canvas on which "cursor" reticle or arc is drawn -->
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas>
|
</canvas>
|
||||||
<div id="tempDiv" style="position: relative; z-index: 6;">
|
<div id="tempDiv" style="position: relative; z-index: 6">
|
||||||
<!-- where popup buttons go -->
|
<!-- where popup buttons go -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="masks" class="masks">
|
<div id="masks" class="masks">
|
||||||
|
@ -164,25 +299,39 @@
|
||||||
<!-- <canvas id="maskCanvasMonitor" class="maskCanvasMonitor" width="512" height="512">
|
<!-- <canvas id="maskCanvasMonitor" class="maskCanvasMonitor" width="512" height="512">
|
||||||
<p>lol ur browser sucks</p>
|
<p>lol ur browser sucks</p>
|
||||||
</canvas><br /> -->
|
</canvas><br /> -->
|
||||||
<canvas id="overMaskCanvasMonitor" class="overMaskCanvasMonitor" width="512" height="512">
|
<canvas
|
||||||
<p>lol ur browser sucks</p>
|
id="overMaskCanvasMonitor"
|
||||||
</canvas><br />
|
class="overMaskCanvasMonitor"
|
||||||
<canvas id="initImgCanvasMonitor" class="initImgCanvasMonitor" width="512" height="512">
|
width="512"
|
||||||
<p>lol ur browser sucks</p>
|
height="512">
|
||||||
</canvas><br />
|
<p>lol ur browser sucks</p> </canvas
|
||||||
|
><br />
|
||||||
|
<canvas
|
||||||
|
id="initImgCanvasMonitor"
|
||||||
|
class="initImgCanvasMonitor"
|
||||||
|
width="512"
|
||||||
|
height="512">
|
||||||
|
<p>lol ur browser sucks</p> </canvas
|
||||||
|
><br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Base Libs -->
|
||||||
<script src="js/util.js" type="text/javascript"></script>
|
<script src="js/util.js" type="text/javascript"></script>
|
||||||
<script src="js/input.js" type="text/javascript"></script>
|
<script src="js/input.js" type="text/javascript"></script>
|
||||||
<script src="js/commands.js" type="text/javascript"></script>
|
<script src="js/commands.js" type="text/javascript"></script>
|
||||||
<script src="js/index.js" type="text/javascript"></script>
|
<script src="js/ui/history.js" type="text/javascript"></script>
|
||||||
<script src="js/settingsbar.js" type="text/javascript"></script>
|
<script src="js/settingsbar.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<script src="js/index.js" type="text/javascript"></script>
|
||||||
<script src="js/shortcuts.js" type="text/javascript"></script>
|
<script src="js/shortcuts.js" type="text/javascript"></script>
|
||||||
|
|
||||||
<script src="js/ui/history.js" type="text/javascript"></script>
|
<!-- Load Tools -->
|
||||||
</body>
|
<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/toolbar.js" type="text/javascript"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -39,6 +39,12 @@ const commands = {
|
||||||
Object.assign(copy, options);
|
Object.assign(copy, options);
|
||||||
const state = {};
|
const state = {};
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
id: guid(),
|
||||||
|
title,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
// Attempt to run command
|
// Attempt to run command
|
||||||
try {
|
try {
|
||||||
run(title, copy, state);
|
run(title, copy, state);
|
||||||
|
@ -53,6 +59,7 @@ const commands = {
|
||||||
console.debug(`Undoing ${name}, currently ${commands.current}`);
|
console.debug(`Undoing ${name}, currently ${commands.current}`);
|
||||||
undo(title, state);
|
undo(title, state);
|
||||||
_commands_events.emit({
|
_commands_events.emit({
|
||||||
|
id: entry.id,
|
||||||
name,
|
name,
|
||||||
action: "undo",
|
action: "undo",
|
||||||
state,
|
state,
|
||||||
|
@ -63,6 +70,7 @@ const commands = {
|
||||||
console.debug(`Redoing ${name}, currently ${commands.current}`);
|
console.debug(`Redoing ${name}, currently ${commands.current}`);
|
||||||
redo(title, copy, state);
|
redo(title, copy, state);
|
||||||
_commands_events.emit({
|
_commands_events.emit({
|
||||||
|
id: entry.id,
|
||||||
name,
|
name,
|
||||||
action: "redo",
|
action: "redo",
|
||||||
state,
|
state,
|
||||||
|
@ -71,21 +79,29 @@ const commands = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to history
|
// Add to history
|
||||||
if (commands.history.length > commands.current + 1)
|
if (commands.history.length > commands.current + 1) {
|
||||||
commands.history.splice(commands.current + 1);
|
commands.history.forEach((entry, index) => {
|
||||||
|
if (index >= commands.current + 1)
|
||||||
const entry = {
|
_commands_events.emit({
|
||||||
id: guid(),
|
id: entry.id,
|
||||||
title,
|
name,
|
||||||
undo: undoWrapper,
|
action: "deleted",
|
||||||
redo: redoWrapper,
|
|
||||||
state,
|
state,
|
||||||
};
|
current: commands.current,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.history.splice(commands.current + 1);
|
||||||
|
}
|
||||||
|
|
||||||
commands.history.push(entry);
|
commands.history.push(entry);
|
||||||
commands.current++;
|
commands.current++;
|
||||||
|
|
||||||
|
entry.undo = undoWrapper;
|
||||||
|
entry.redo = redoWrapper;
|
||||||
|
|
||||||
_commands_events.emit({
|
_commands_events.emit({
|
||||||
|
id: entry.id,
|
||||||
name,
|
name,
|
||||||
action: "run",
|
action: "run",
|
||||||
state,
|
state,
|
||||||
|
@ -156,3 +172,53 @@ commands.createCommand(
|
||||||
state.context.drawImage(state.original, state.box.x, state.box.y);
|
state.context.drawImage(state.original, state.box.x, state.box.y);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
commands.createCommand(
|
||||||
|
"eraseImage",
|
||||||
|
(title, options, state) => {
|
||||||
|
if (
|
||||||
|
!options ||
|
||||||
|
options.x === undefined ||
|
||||||
|
options.y === undefined ||
|
||||||
|
options.w === undefined ||
|
||||||
|
options.h === undefined
|
||||||
|
)
|
||||||
|
throw "Command eraseImage requires options in the format: {x, y, w, h, ctx?}";
|
||||||
|
|
||||||
|
// Check if we have state
|
||||||
|
if (!state.context) {
|
||||||
|
const context = options.ctx || imgCtx;
|
||||||
|
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,
|
||||||
|
w: options.w,
|
||||||
|
h: options.h,
|
||||||
|
};
|
||||||
|
// Create Image
|
||||||
|
const cutout = document.createElement("canvas");
|
||||||
|
cutout.width = state.box.w;
|
||||||
|
cutout.height = state.box.h;
|
||||||
|
cutout.getContext("2d").putImageData(imgData, 0, 0);
|
||||||
|
state.original = new Image();
|
||||||
|
state.original.src = cutout.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply command
|
||||||
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
||||||
|
},
|
||||||
|
(title, state) => {
|
||||||
|
// Clear destination area
|
||||||
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
||||||
|
// Undo
|
||||||
|
state.context.drawImage(state.original, state.box.x, state.box.y);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
414
js/index.js
414
js/index.js
|
@ -55,12 +55,31 @@ function sliderChangeHandlerFactory(
|
||||||
textBoxId,
|
textBoxId,
|
||||||
dataKey,
|
dataKey,
|
||||||
defaultV,
|
defaultV,
|
||||||
|
save = true,
|
||||||
setter = (k, v) => (stableDiffusionData[k] = v),
|
setter = (k, v) => (stableDiffusionData[k] = v),
|
||||||
getter = (k) => stableDiffusionData[k]
|
getter = (k) => stableDiffusionData[k]
|
||||||
) {
|
) {
|
||||||
const sliderEl = document.getElementById(sliderId);
|
return sliderChangeHandlerFactoryEl(
|
||||||
const textBoxEl = document.getElementById(textBoxId);
|
document.getElementById(sliderId),
|
||||||
const savedValue = localStorage.getItem(dataKey);
|
document.getElementById(textBoxId),
|
||||||
|
dataKey,
|
||||||
|
defaultV,
|
||||||
|
save,
|
||||||
|
setter,
|
||||||
|
getter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliderChangeHandlerFactoryEl(
|
||||||
|
sliderEl,
|
||||||
|
textBoxEl,
|
||||||
|
dataKey,
|
||||||
|
defaultV,
|
||||||
|
save = true,
|
||||||
|
setter = (k, v) => (stableDiffusionData[k] = v),
|
||||||
|
getter = (k) => stableDiffusionData[k]
|
||||||
|
) {
|
||||||
|
const savedValue = save && localStorage.getItem(dataKey);
|
||||||
|
|
||||||
if (savedValue) setter(dataKey, savedValue || defaultV);
|
if (savedValue) setter(dataKey, savedValue || defaultV);
|
||||||
|
|
||||||
|
@ -70,12 +89,12 @@ function sliderChangeHandlerFactory(
|
||||||
|
|
||||||
if (value) setter(dataKey, value);
|
if (value) setter(dataKey, value);
|
||||||
|
|
||||||
if (!eventSource || eventSource.id === textBoxId)
|
if (!eventSource || eventSource === textBoxEl)
|
||||||
sliderEl.value = getter(dataKey);
|
sliderEl.value = getter(dataKey);
|
||||||
setter(dataKey, Number(sliderEl.value));
|
setter(dataKey, Number(sliderEl.value));
|
||||||
textBoxEl.value = getter(dataKey);
|
textBoxEl.value = getter(dataKey);
|
||||||
|
|
||||||
localStorage.setItem(dataKey, getter(dataKey));
|
if (save) localStorage.setItem(dataKey, getter(dataKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
textBoxEl.onchange = changeHandler;
|
textBoxEl.onchange = changeHandler;
|
||||||
|
@ -104,11 +123,9 @@ var heldButton = 0;
|
||||||
var snapX = 0;
|
var snapX = 0;
|
||||||
var snapY = 0;
|
var snapY = 0;
|
||||||
var drawThis = {};
|
var drawThis = {};
|
||||||
var clicked = false;
|
|
||||||
const basePixelCount = 64; //64 px - ALWAYS 64 PX
|
const basePixelCount = 64; //64 px - ALWAYS 64 PX
|
||||||
var scaleFactor = 8; //x64 px
|
var scaleFactor = 8; //x64 px
|
||||||
var snapToGrid = true;
|
var snapToGrid = true;
|
||||||
var paintMode = false;
|
|
||||||
var backupMaskPaintCanvas; //???
|
var backupMaskPaintCanvas; //???
|
||||||
var backupMaskPaintCtx; //...? look i am bad at this
|
var backupMaskPaintCtx; //...? look i am bad at this
|
||||||
var backupMaskChunk = null;
|
var backupMaskChunk = null;
|
||||||
|
@ -123,9 +140,8 @@ var arbitraryImageData;
|
||||||
var arbitraryImageBitmap;
|
var arbitraryImageBitmap;
|
||||||
var arbitraryImageBase64; // seriously js cmon work with me here
|
var arbitraryImageBase64; // seriously js cmon work with me here
|
||||||
var placingArbitraryImage = false; // for when the user has loaded an existing image from their computer
|
var placingArbitraryImage = false; // for when the user has loaded an existing image from their computer
|
||||||
var enableErasing = false; // accidental right-click erase if the user isn't trying to erase is a bad thing
|
|
||||||
var marchOffset = 0;
|
var marchOffset = 0;
|
||||||
var marching = false;
|
var stopMarching = null;
|
||||||
var inProgress = false;
|
var inProgress = false;
|
||||||
var marchCoords = {};
|
var marchCoords = {};
|
||||||
|
|
||||||
|
@ -160,7 +176,6 @@ function startup() {
|
||||||
getModels();
|
getModels();
|
||||||
drawBackground();
|
drawBackground();
|
||||||
changeScaleFactor();
|
changeScaleFactor();
|
||||||
changePaintMode();
|
|
||||||
changeSampler();
|
changeSampler();
|
||||||
changeSteps();
|
changeSteps();
|
||||||
changeCfgScale();
|
changeCfgScale();
|
||||||
|
@ -171,7 +186,6 @@ function startup() {
|
||||||
changeSeed();
|
changeSeed();
|
||||||
changeOverMaskPx();
|
changeOverMaskPx();
|
||||||
changeHiResFix();
|
changeHiResFix();
|
||||||
changeEnableErasing();
|
|
||||||
document.getElementById("overlayCanvas").onmousemove = mouseMove;
|
document.getElementById("overlayCanvas").onmousemove = mouseMove;
|
||||||
document.getElementById("overlayCanvas").onmousedown = mouseDown;
|
document.getElementById("overlayCanvas").onmousedown = mouseDown;
|
||||||
document.getElementById("overlayCanvas").onmouseup = mouseUp;
|
document.getElementById("overlayCanvas").onmouseup = mouseUp;
|
||||||
|
@ -197,28 +211,50 @@ function writeArbitraryImage(img, x, y) {
|
||||||
document.getElementById("preloadImage").files = null;
|
document.getElementById("preloadImage").files = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dream(x, y, prompt) {
|
function dream(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
prompt,
|
||||||
|
extra = {
|
||||||
|
method: endpoint,
|
||||||
|
stopMarching: () => {},
|
||||||
|
bb: {x, y, w: prompt.width, h: prompt.height},
|
||||||
|
}
|
||||||
|
) {
|
||||||
tmpImgXYWH.x = x;
|
tmpImgXYWH.x = x;
|
||||||
tmpImgXYWH.y = y;
|
tmpImgXYWH.y = y;
|
||||||
tmpImgXYWH.w = prompt.width;
|
tmpImgXYWH.w = prompt.width;
|
||||||
tmpImgXYWH.h = prompt.height;
|
tmpImgXYWH.h = prompt.height;
|
||||||
console.log(
|
console.log(
|
||||||
"dreaming to " + host + url + endpoint + ":\r\n" + JSON.stringify(prompt)
|
"dreaming to " +
|
||||||
|
host +
|
||||||
|
url +
|
||||||
|
(extra.method || endpoint) +
|
||||||
|
":\r\n" +
|
||||||
|
JSON.stringify(prompt)
|
||||||
);
|
);
|
||||||
postData(prompt).then((data) => {
|
console.info(`dreaming "${prompt.prompt}"`);
|
||||||
|
console.debug(prompt);
|
||||||
|
|
||||||
|
// Start checking for progress
|
||||||
|
const progressCheck = checkProgress(extra.bb);
|
||||||
|
postData(prompt, extra)
|
||||||
|
.then((data) => {
|
||||||
returnedImages = data.images;
|
returnedImages = data.images;
|
||||||
totalImagesReturned = data.images.length;
|
totalImagesReturned = data.images.length;
|
||||||
blockNewImages = true;
|
blockNewImages = true;
|
||||||
//console.log(data); // JSON data parsed by `data.json()` call
|
//console.log(data); // JSON data parsed by `data.json()` call
|
||||||
imageAcceptReject(x, y, data);
|
imageAcceptReject(x, y, data, extra);
|
||||||
});
|
})
|
||||||
checkProgress();
|
.finally(() => clearInterval(progressCheck));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postData(promptData) {
|
async function postData(promptData, extra = null) {
|
||||||
this.host = document.getElementById("host").value;
|
this.host = document.getElementById("host").value;
|
||||||
// Default options are marked with *
|
// Default options are marked with *
|
||||||
const response = await fetch(this.host + this.url + this.endpoint, {
|
const response = await fetch(
|
||||||
|
this.host + this.url + extra.method || endpoint,
|
||||||
|
{
|
||||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||||
mode: "cors", // no-cors, *cors, same-origin
|
mode: "cors", // no-cors, *cors, same-origin
|
||||||
cache: "default", // *default, no-cache, reload, force-cache, only-if-cached
|
cache: "default", // *default, no-cache, reload, force-cache, only-if-cached
|
||||||
|
@ -230,11 +266,12 @@ async function postData(promptData) {
|
||||||
redirect: "follow", // manual, *follow, error
|
redirect: "follow", // manual, *follow, error
|
||||||
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
|
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
|
||||||
body: JSON.stringify(promptData), // body data type must match "Content-Type" header
|
body: JSON.stringify(promptData), // body data type must match "Content-Type" header
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return response.json(); // parses JSON response into native JavaScript objects
|
return response.json(); // parses JSON response into native JavaScript objects
|
||||||
}
|
}
|
||||||
|
|
||||||
function imageAcceptReject(x, y, data) {
|
function imageAcceptReject(x, y, data, extra = null) {
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
document.getElementById("progressDiv").remove();
|
document.getElementById("progressDiv").remove();
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
@ -261,7 +298,8 @@ function imageAcceptReject(x, y, data) {
|
||||||
|
|
||||||
function accept(evt) {
|
function accept(evt) {
|
||||||
// write image to imgcanvas
|
// write image to imgcanvas
|
||||||
marching = false;
|
stopMarching && stopMarching();
|
||||||
|
stopMarching = null;
|
||||||
clearBackupMask();
|
clearBackupMask();
|
||||||
placeImage();
|
placeImage();
|
||||||
removeChoiceButtons();
|
removeChoiceButtons();
|
||||||
|
@ -271,7 +309,8 @@ function accept(evt) {
|
||||||
|
|
||||||
function reject(evt) {
|
function reject(evt) {
|
||||||
// remove image entirely
|
// remove image entirely
|
||||||
marching = false;
|
stopMarching && stopMarching();
|
||||||
|
stopMarching = null;
|
||||||
restoreBackupMask();
|
restoreBackupMask();
|
||||||
clearBackupMask();
|
clearBackupMask();
|
||||||
clearTargetMask();
|
clearTargetMask();
|
||||||
|
@ -375,59 +414,40 @@ function sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function snap(i, scaled = true) {
|
function march(bb) {
|
||||||
// very cheap test proof of concept but it works surprisingly well
|
let offset = 0;
|
||||||
var scaleOffset = 0;
|
|
||||||
if (scaled) {
|
const interval = setInterval(() => {
|
||||||
if (scaleFactor % 2 != 0) {
|
drawMarchingAnts(bb, offset++);
|
||||||
// odd number, snaps to center of cell, oops
|
offset %= 16;
|
||||||
scaleOffset = basePixelCount / 2;
|
}, 20);
|
||||||
}
|
|
||||||
}
|
return () => clearInterval(interval);
|
||||||
var snapOffset = (i % basePixelCount) - scaleOffset;
|
|
||||||
if (snapOffset == 0) {
|
|
||||||
return snapOffset;
|
|
||||||
}
|
|
||||||
return -snapOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function march() {
|
function drawMarchingAnts(bb, offset) {
|
||||||
if (marching) {
|
|
||||||
marchOffset++;
|
|
||||||
if (marchOffset > 16) {
|
|
||||||
marchOffset = 0;
|
|
||||||
}
|
|
||||||
drawMarchingAnts();
|
|
||||||
setTimeout(march, 20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawMarchingAnts() {
|
|
||||||
clearTargetMask();
|
clearTargetMask();
|
||||||
tgtCtx.strokeStyle = "#FFFFFFFF"; //"#55000077";
|
tgtCtx.strokeStyle = "#FFFFFFFF"; //"#55000077";
|
||||||
tgtCtx.setLineDash([4, 2]);
|
tgtCtx.setLineDash([4, 2]);
|
||||||
tgtCtx.lineDashOffset = -marchOffset;
|
tgtCtx.lineDashOffset = -offset;
|
||||||
tgtCtx.strokeRect(marchCoords.x, marchCoords.y, marchCoords.w, marchCoords.h);
|
tgtCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkProgress() {
|
function checkProgress(bb) {
|
||||||
document.getElementById("progressDiv") &&
|
document.getElementById("progressDiv") &&
|
||||||
document.getElementById("progressDiv").remove();
|
document.getElementById("progressDiv").remove();
|
||||||
endpoint = "progress?skip_current_image=false";
|
// Skip image to stop using a ton of networking resources
|
||||||
|
endpoint = "progress?skip_current_image=true";
|
||||||
var div = document.createElement("div");
|
var div = document.createElement("div");
|
||||||
div.id = "progressDiv";
|
div.id = "progressDiv";
|
||||||
div.style.position = "absolute";
|
div.style.position = "absolute";
|
||||||
div.style.width = "200px";
|
div.style.width = "200px";
|
||||||
div.style.height = "70px";
|
div.style.height = "70px";
|
||||||
div.style.left = parseInt(marchCoords.x + marchCoords.w - 100) + "px";
|
div.style.left = parseInt(bb.x + bb.w - 100) + "px";
|
||||||
div.style.top = parseInt(marchCoords.y + marchCoords.h) + "px";
|
div.style.top = parseInt(bb.y + bb.h) + "px";
|
||||||
div.innerHTML = '<span class="strokeText" id="estRemaining"></span>';
|
div.innerHTML = '<span class="strokeText" id="estRemaining"></span>';
|
||||||
document.getElementById("tempDiv").appendChild(div);
|
document.getElementById("tempDiv").appendChild(div);
|
||||||
updateProgress();
|
return setInterval(() => {
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress() {
|
|
||||||
if (inProgress) {
|
|
||||||
fetch(host + url + endpoint)
|
fetch(host + url + endpoint)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -439,8 +459,7 @@ function updateProgress() {
|
||||||
|
|
||||||
document.getElementById("estRemaining").innerText = estimate;
|
document.getElementById("estRemaining").innerText = estimate;
|
||||||
});
|
});
|
||||||
setTimeout(updateProgress, 500);
|
}, 1500);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseMove(evt) {
|
function mouseMove(evt) {
|
||||||
|
@ -467,67 +486,9 @@ function mouseMove(evt) {
|
||||||
finalX = snapOffsetX + canvasX;
|
finalX = snapOffsetX + canvasX;
|
||||||
finalY = snapOffsetY + canvasY;
|
finalY = snapOffsetY + canvasY;
|
||||||
ovCtx.drawImage(arbitraryImage, finalX, finalY);
|
ovCtx.drawImage(arbitraryImage, finalX, finalY);
|
||||||
} else if (!paintMode) {
|
|
||||||
// draw targeting square reticle thingy cursor
|
|
||||||
ovCtx.strokeStyle = "#FFFFFF";
|
|
||||||
snapOffsetX = 0;
|
|
||||||
snapOffsetY = 0;
|
|
||||||
if (snapToGrid) {
|
|
||||||
snapOffsetX = snap(canvasX);
|
|
||||||
snapOffsetY = snap(canvasY);
|
|
||||||
}
|
|
||||||
finalX = snapOffsetX + canvasX;
|
|
||||||
finalY = snapOffsetY + canvasY;
|
|
||||||
ovCtx.strokeRect(
|
|
||||||
parseInt(finalX - (basePixelCount * scaleFactor) / 2),
|
|
||||||
parseInt(finalY - (basePixelCount * scaleFactor) / 2),
|
|
||||||
basePixelCount * scaleFactor,
|
|
||||||
basePixelCount * scaleFactor
|
|
||||||
); //origin is middle of the frame
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mask implementation
|
|
||||||
*/
|
|
||||||
mouse.listen.canvas.onmousemove.on((evn) => {
|
|
||||||
if (paintMode && evn.target.id === "overlayCanvas") {
|
|
||||||
// draw big translucent red blob cursor
|
|
||||||
ovCtx.beginPath();
|
|
||||||
ovCtx.arc(evn.x, evn.y, 4 * scaleFactor, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
|
||||||
ovCtx.fillStyle = "#FF6A6A50";
|
|
||||||
ovCtx.fill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mouse.listen.canvas.left.onpaint.on((evn) => {
|
|
||||||
if (paintMode && evn.initialTarget.id === "overlayCanvas") {
|
|
||||||
maskPaintCtx.globalCompositeOperation = "source-over";
|
|
||||||
maskPaintCtx.strokeStyle = "#FF6A6A";
|
|
||||||
|
|
||||||
maskPaintCtx.lineWidth = 8 * scaleFactor;
|
|
||||||
maskPaintCtx.beginPath();
|
|
||||||
maskPaintCtx.moveTo(evn.px, evn.py);
|
|
||||||
maskPaintCtx.lineTo(evn.x, evn.y);
|
|
||||||
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
|
||||||
maskPaintCtx.stroke();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mouse.listen.canvas.right.onpaint.on((evn) => {
|
|
||||||
if (paintMode && evn.initialTarget.id === "overlayCanvas") {
|
|
||||||
maskPaintCtx.globalCompositeOperation = "destination-out";
|
|
||||||
maskPaintCtx.strokeStyle = "#FFFFFFFF";
|
|
||||||
|
|
||||||
maskPaintCtx.lineWidth = 8 * scaleFactor;
|
|
||||||
maskPaintCtx.beginPath();
|
|
||||||
maskPaintCtx.moveTo(evn.px, evn.py);
|
|
||||||
maskPaintCtx.lineTo(evn.x, evn.y);
|
|
||||||
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
|
||||||
maskPaintCtx.stroke();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function mouseDown(evt) {
|
function mouseDown(evt) {
|
||||||
const rect = ovCanvas.getBoundingClientRect();
|
const rect = ovCanvas.getBoundingClientRect();
|
||||||
var oddOffset = 0;
|
var oddOffset = 0;
|
||||||
|
@ -543,39 +504,6 @@ function mouseDown(evt) {
|
||||||
nextBox.w = arbitraryImageData.width;
|
nextBox.w = arbitraryImageData.width;
|
||||||
nextBox.h = arbitraryImageData.height;
|
nextBox.h = arbitraryImageData.height;
|
||||||
dropTargets.push(nextBox);
|
dropTargets.push(nextBox);
|
||||||
} else if (!paintMode) {
|
|
||||||
//const rect = ovCanvas.getBoundingClientRect()
|
|
||||||
var nextBox = {};
|
|
||||||
nextBox.x =
|
|
||||||
evt.clientX -
|
|
||||||
(basePixelCount * scaleFactor) / 2 -
|
|
||||||
rect.left +
|
|
||||||
oddOffset; //origin is middle of the frame
|
|
||||||
nextBox.y =
|
|
||||||
evt.clientY - (basePixelCount * scaleFactor) / 2 - rect.top + oddOffset; //TODO make a way to set the origin to numpad dirs?
|
|
||||||
nextBox.w = basePixelCount * scaleFactor;
|
|
||||||
nextBox.h = basePixelCount * scaleFactor;
|
|
||||||
drawTargets.push(nextBox);
|
|
||||||
}
|
|
||||||
} else if (evt.button == 2) {
|
|
||||||
if (enableErasing && !paintMode) {
|
|
||||||
// right click, also gotta make sure mask blob isn't being used as it's visually inconsistent with behavior of erased region
|
|
||||||
ctx = imgCanvas.getContext("2d");
|
|
||||||
if (snapToGrid) {
|
|
||||||
ctx.clearRect(
|
|
||||||
canvasX + snap(canvasX) - (basePixelCount * scaleFactor) / 2,
|
|
||||||
canvasY + snap(canvasY) - (basePixelCount * scaleFactor) / 2,
|
|
||||||
basePixelCount * scaleFactor,
|
|
||||||
basePixelCount * scaleFactor
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.clearRect(
|
|
||||||
canvasX - (basePixelCount * scaleFactor) / 2,
|
|
||||||
canvasY - (basePixelCount * scaleFactor) / 2,
|
|
||||||
basePixelCount * scaleFactor,
|
|
||||||
basePixelCount * scaleFactor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -601,186 +529,8 @@ function mouseUp(evt) {
|
||||||
drawThis.h = target.h;
|
drawThis.h = target.h;
|
||||||
drawIt = drawThis; // i still think this is really stupid and redundant and unnecessary and redundant
|
drawIt = drawThis; // i still think this is really stupid and redundant and unnecessary and redundant
|
||||||
drop(drawIt);
|
drop(drawIt);
|
||||||
} else if (paintMode) {
|
|
||||||
clicked = false;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (!blockNewImages) {
|
|
||||||
//TODO seriously, refactor this
|
|
||||||
blockNewImages = true;
|
|
||||||
marching = inProgress = true;
|
|
||||||
var drawIt = {}; //why am i doing this????
|
|
||||||
var target = drawTargets[drawTargets.length - 1]; //get the last one... why am i storing all of them?
|
|
||||||
var oddOffset = 0;
|
|
||||||
if (scaleFactor % 2 != 0) {
|
|
||||||
oddOffset = basePixelCount / 2;
|
|
||||||
}
|
|
||||||
snapOffsetX = 0;
|
|
||||||
snapOffsetY = 0;
|
|
||||||
if (snapToGrid) {
|
|
||||||
snapOffsetX = snap(target.x);
|
|
||||||
snapOffsetY = snap(target.y);
|
|
||||||
}
|
|
||||||
finalX = snapOffsetX + target.x - oddOffset;
|
|
||||||
finalY = snapOffsetY + target.y - oddOffset;
|
|
||||||
|
|
||||||
drawThis.x = marchCoords.x = finalX;
|
|
||||||
drawThis.y = marchCoords.y = finalY;
|
|
||||||
drawThis.w = marchCoords.w = target.w;
|
|
||||||
drawThis.h = marchCoords.h = target.h;
|
|
||||||
march(finalX, finalY, target.w, target.h);
|
|
||||||
drawIt = drawThis; //TODO this is WRONG but also explicitly only draws the last image ... i think
|
|
||||||
//check if there's image data already there
|
|
||||||
// console.log(downX + ":" + downY + " :: " + this.isCanvasBlank(downX, downY));
|
|
||||||
if (!isCanvasBlank(drawIt.x, drawIt.y, drawIt.w, drawIt.h, imgCanvas)) {
|
|
||||||
// image exists, set up for img2img
|
|
||||||
var mainCanvasCtx = document
|
|
||||||
.getElementById("canvas")
|
|
||||||
.getContext("2d");
|
|
||||||
const imgChunk = mainCanvasCtx.getImageData(
|
|
||||||
drawIt.x,
|
|
||||||
drawIt.y,
|
|
||||||
drawIt.w,
|
|
||||||
drawIt.h
|
|
||||||
); // imagedata object of the image being outpainted
|
|
||||||
const imgChunkData = imgChunk.data; // imagedata.data object, a big inconvenient uint8clampedarray
|
|
||||||
// these are the 3 mask monitors on the bottom of the page
|
|
||||||
var initImgCanvas = document.getElementById("initImgCanvasMonitor");
|
|
||||||
var overMaskCanvas = document.getElementById("overMaskCanvasMonitor");
|
|
||||||
overMaskCanvas.width = initImgCanvas.width = target.w; //maskCanvas.width = target.w;
|
|
||||||
overMaskCanvas.height = initImgCanvas.height = target.h; //maskCanvas.height = target.h;
|
|
||||||
var initImgCanvasCtx = initImgCanvas.getContext("2d");
|
|
||||||
var overMaskCanvasCtx = overMaskCanvas.getContext("2d");
|
|
||||||
// get blank pixels to use as mask
|
|
||||||
const initImgData = mainCanvasCtx.createImageData(drawIt.w, drawIt.h);
|
|
||||||
let overMaskImgData = overMaskCanvasCtx.createImageData(
|
|
||||||
drawIt.w,
|
|
||||||
drawIt.h
|
|
||||||
);
|
|
||||||
// cover entire masks in black before adding masked areas
|
|
||||||
|
|
||||||
for (let i = 0; i < imgChunkData.length; i += 4) {
|
|
||||||
// l->r, top->bottom, R G B A pixel values in a big ol array
|
|
||||||
// make a simple mask
|
|
||||||
if (imgChunkData[i + 3] == 0) {
|
|
||||||
// rgba pixel values, 4th one is alpha, if it's 0 there's "nothing there" in the image display canvas and its time to outpaint
|
|
||||||
overMaskImgData.data[i] = 255; // white mask gets painted over
|
|
||||||
overMaskImgData.data[i + 1] = 255;
|
|
||||||
overMaskImgData.data[i + 2] = 255;
|
|
||||||
overMaskImgData.data[i + 3] = 255;
|
|
||||||
|
|
||||||
initImgData.data[i] = 0; // null area on initial image becomes opaque black pixels
|
|
||||||
initImgData.data[i + 1] = 0;
|
|
||||||
initImgData.data[i + 2] = 0;
|
|
||||||
initImgData.data[i + 3] = 255;
|
|
||||||
} else {
|
|
||||||
// leave these pixels alone
|
|
||||||
overMaskImgData.data[i] = 0; // black mask gets ignored for in/outpainting
|
|
||||||
overMaskImgData.data[i + 1] = 0;
|
|
||||||
overMaskImgData.data[i + 2] = 0;
|
|
||||||
overMaskImgData.data[i + 3] = 255; // but it still needs an opaque alpha channel
|
|
||||||
|
|
||||||
initImgData.data[i] = imgChunkData[i]; // put the original picture back in the painted area
|
|
||||||
initImgData.data[i + 1] = imgChunkData[i + 1];
|
|
||||||
initImgData.data[i + 2] = imgChunkData[i + 2];
|
|
||||||
initImgData.data[i + 3] = imgChunkData[i + 3]; //it's still RGBA so we can handily do this in nice chunks'o'4
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (overMaskPx > 0) {
|
|
||||||
// https://stackoverflow.com/a/30204783 ???? !!!!!!!!
|
|
||||||
overMaskCanvasCtx.fillStyle = "black";
|
|
||||||
overMaskCanvasCtx.fillRect(0, 0, drawIt.w, drawIt.h); // fill with black instead of null to start
|
|
||||||
for (i = 0; i < overMaskImgData.data.length; i += 4) {
|
|
||||||
if (overMaskImgData.data[i] == 255) {
|
|
||||||
// white pixel?
|
|
||||||
// just blotch all over the thing
|
|
||||||
var rando = Math.floor(Math.random() * overMaskPx);
|
|
||||||
overMaskCanvasCtx.beginPath();
|
|
||||||
overMaskCanvasCtx.arc(
|
|
||||||
(i / 4) % overMaskCanvas.width,
|
|
||||||
Math.floor(i / 4 / overMaskCanvas.width),
|
|
||||||
scaleFactor + rando, // was 4 * sf + rando, too big
|
|
||||||
0,
|
|
||||||
2 * Math.PI,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
overMaskCanvasCtx.fillStyle = "#FFFFFFFF";
|
|
||||||
overMaskCanvasCtx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
overMaskImgData = overMaskCanvasCtx.getImageData(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
overMaskCanvas.width,
|
|
||||||
overMaskCanvas.height
|
|
||||||
);
|
|
||||||
overMaskCanvasCtx.putImageData(overMaskImgData, 0, 0);
|
|
||||||
}
|
|
||||||
// also check for painted masks in region, add them as white pixels to mask canvas
|
|
||||||
const maskChunk = maskPaintCtx.getImageData(
|
|
||||||
drawIt.x,
|
|
||||||
drawIt.y,
|
|
||||||
drawIt.w,
|
|
||||||
drawIt.h
|
|
||||||
);
|
|
||||||
const maskChunkData = maskChunk.data;
|
|
||||||
for (let i = 0; i < maskChunkData.length; i += 4) {
|
|
||||||
if (maskChunkData[i + 3] != 0) {
|
|
||||||
overMaskImgData.data[i] = 255;
|
|
||||||
overMaskImgData.data[i + 1] = 255;
|
|
||||||
overMaskImgData.data[i + 2] = 255;
|
|
||||||
overMaskImgData.data[i + 3] = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// backup any painted masks ingested then them, replacable if user doesn't like resultant image
|
|
||||||
var clearArea = maskPaintCtx.createImageData(drawIt.w, drawIt.h);
|
|
||||||
backupMaskChunk = maskChunk;
|
|
||||||
backupMaskX = drawIt.x;
|
|
||||||
backupMaskY = drawIt.y;
|
|
||||||
|
|
||||||
var clearD = clearArea.data;
|
|
||||||
for (let i = 0; i < clearD.length; i++) {
|
|
||||||
clearD[i] = 0; // just null it all out
|
|
||||||
}
|
|
||||||
maskPaintCtx.putImageData(clearArea, drawIt.x, drawIt.y);
|
|
||||||
// mask monitors
|
|
||||||
overMaskCanvasCtx.putImageData(overMaskImgData, 0, 0); // :pray:
|
|
||||||
var overMaskBase64 = overMaskCanvas.toDataURL();
|
|
||||||
initImgCanvasCtx.putImageData(initImgData, 0, 0);
|
|
||||||
var initImgBase64 = initImgCanvas.toDataURL();
|
|
||||||
// anyway all that to say NOW let's run img2img
|
|
||||||
endpoint = "img2img";
|
|
||||||
stableDiffusionData.mask = overMaskBase64;
|
|
||||||
stableDiffusionData.init_images = [initImgBase64];
|
|
||||||
// slightly more involved than txt2img
|
|
||||||
} else {
|
|
||||||
// time to run txt2img
|
|
||||||
endpoint = "txt2img";
|
|
||||||
// easy enough
|
|
||||||
}
|
|
||||||
stableDiffusionData.prompt = document.getElementById("prompt").value;
|
|
||||||
stableDiffusionData.negative_prompt =
|
|
||||||
document.getElementById("negPrompt").value;
|
|
||||||
stableDiffusionData.width = drawIt.w;
|
|
||||||
stableDiffusionData.height = drawIt.h;
|
|
||||||
stableDiffusionData.firstphase_height = drawIt.h / 2;
|
|
||||||
stableDiffusionData.firstphase_width = drawIt.w / 2;
|
|
||||||
dream(drawIt.x, drawIt.y, stableDiffusionData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function changePaintMode() {
|
|
||||||
paintMode = document.getElementById("cbxPaint").checked;
|
|
||||||
clearTargetMask();
|
|
||||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeEnableErasing() {
|
|
||||||
// yeah because this is for the image layer
|
|
||||||
enableErasing = document.getElementById("cbxEnableErasing").checked;
|
|
||||||
localStorage.setItem("enable_erase", enableErasing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSampler() {
|
function changeSampler() {
|
||||||
|
@ -816,6 +566,7 @@ const changeScaleFactor = sliderChangeHandlerFactory(
|
||||||
"scaleFactorTxt",
|
"scaleFactorTxt",
|
||||||
"scaleFactor",
|
"scaleFactor",
|
||||||
8,
|
8,
|
||||||
|
true,
|
||||||
(k, v) => (scaleFactor = v),
|
(k, v) => (scaleFactor = v),
|
||||||
(k) => scaleFactor
|
(k) => scaleFactor
|
||||||
);
|
);
|
||||||
|
@ -1247,6 +998,5 @@ function loadSettings() {
|
||||||
document.getElementById("maskBlur").value = Number(_mask_blur);
|
document.getElementById("maskBlur").value = Number(_mask_blur);
|
||||||
document.getElementById("seed").value = Number(_seed);
|
document.getElementById("seed").value = Number(_seed);
|
||||||
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
||||||
document.getElementById("cbxEnableErasing").checked = Boolean(_enable_erase);
|
|
||||||
document.getElementById("overMaskPx").value = Number(_overmask_px);
|
document.getElementById("overMaskPx").value = Number(_overmask_px);
|
||||||
}
|
}
|
||||||
|
|
65
js/input.js
65
js/input.js
|
@ -31,9 +31,9 @@ function _context_coords() {
|
||||||
}
|
}
|
||||||
function _mouse_observers() {
|
function _mouse_observers() {
|
||||||
return {
|
return {
|
||||||
// Simple click handlers
|
// Simple click handler
|
||||||
onclick: new Observer(),
|
onclick: new Observer(),
|
||||||
// Double click handlers (will still trigger simple click handler as well)
|
// Double click handler (will still trigger simple click handler as well)
|
||||||
ondclick: new Observer(),
|
ondclick: new Observer(),
|
||||||
// Drag handler
|
// Drag handler
|
||||||
ondragstart: new Observer(),
|
ondragstart: new Observer(),
|
||||||
|
@ -48,6 +48,7 @@ function _mouse_observers() {
|
||||||
|
|
||||||
function _context_observers() {
|
function _context_observers() {
|
||||||
return {
|
return {
|
||||||
|
onwheel: new Observer(),
|
||||||
onmousemove: new Observer(),
|
onmousemove: new Observer(),
|
||||||
left: _mouse_observers(),
|
left: _mouse_observers(),
|
||||||
middle: _mouse_observers(),
|
middle: _mouse_observers(),
|
||||||
|
@ -270,36 +271,27 @@ window.onmousemove = (evn) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/** MOUSE DEBUG */
|
|
||||||
/*
|
|
||||||
mouse.listen.window.right.onclick.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.onclick')
|
|
||||||
);
|
|
||||||
|
|
||||||
mouse.listen.window.right.ondclick.on(() =>
|
window.addEventListener(
|
||||||
console.debug('mouse.listen.window.right.ondclick')
|
"wheel",
|
||||||
|
(evn) => {
|
||||||
|
evn.preventDefault();
|
||||||
|
["window", "canvas", "world"].forEach((ctx) => {
|
||||||
|
mouse.listen[ctx].onwheel.emit({
|
||||||
|
target: evn.target,
|
||||||
|
delta: evn.deltaY,
|
||||||
|
deltaX: evn.deltaX,
|
||||||
|
deltaY: evn.deltaY,
|
||||||
|
deltaZ: evn.deltaZ,
|
||||||
|
mode: evn.deltaMode,
|
||||||
|
x: mouse[ctx].pos.x,
|
||||||
|
y: mouse[ctx].pos.y,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{passive: false}
|
||||||
);
|
);
|
||||||
mouse.listen.window.right.ondragstart.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.ondragstart')
|
|
||||||
);
|
|
||||||
mouse.listen.window.right.ondrag.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.ondrag')
|
|
||||||
);
|
|
||||||
mouse.listen.window.right.ondragend.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.ondragend')
|
|
||||||
);
|
|
||||||
|
|
||||||
mouse.listen.window.right.onpaintstart.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.onpaintstart')
|
|
||||||
);
|
|
||||||
mouse.listen.window.right.onpaint.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.onpaint')
|
|
||||||
);
|
|
||||||
mouse.listen.window.right.onpaintend.on(() =>
|
|
||||||
console.debug('mouse.listen.window.right.onpaintend')
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keyboard input processing
|
* Keyboard input processing
|
||||||
*/
|
*/
|
||||||
|
@ -370,7 +362,18 @@ window.onkeydown = (evn) => {
|
||||||
}, inputConfig.keyboardHoldTiming),
|
}, inputConfig.keyboardHoldTiming),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process shortcuts
|
// Process shortcuts if input target is not a text field
|
||||||
|
switch (evn.target.tagName.toLowerCase()) {
|
||||||
|
case "input":
|
||||||
|
case "textarea":
|
||||||
|
case "select":
|
||||||
|
case "button":
|
||||||
|
return; // If in an input field, do not process shortcuts
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const callbacks = keyboard.shortcuts[evn.code];
|
const callbacks = keyboard.shortcuts[evn.code];
|
||||||
|
|
||||||
if (callbacks)
|
if (callbacks)
|
||||||
|
|
|
@ -1,40 +1,4 @@
|
||||||
//dragElement(document.getElementById("infoContainer"));
|
function makeDraggable(element) {
|
||||||
//dragElement(document.getElementById("historyContainer"));
|
|
||||||
|
|
||||||
function dragElement(elmnt) {
|
|
||||||
var p3 = 0,
|
|
||||||
p4 = 0;
|
|
||||||
var draggableElements = elmnt.getElementsByClassName("draggable");
|
|
||||||
for (var i = 0; i < draggableElements.length; i++) {
|
|
||||||
draggableElements[i].onmousedown = dragMouseDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragMouseDown(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
p3 = e.clientX;
|
|
||||||
p4 = e.clientY;
|
|
||||||
document.onmouseup = closeDragElement;
|
|
||||||
document.onmousemove = elementDrag;
|
|
||||||
}
|
|
||||||
|
|
||||||
function elementDrag(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
elmnt.style.bottom = null;
|
|
||||||
elmnt.style.right = null;
|
|
||||||
elmnt.style.top = elmnt.offsetTop - (p4 - e.clientY) + "px";
|
|
||||||
elmnt.style.left = elmnt.offsetLeft - (p3 - e.clientX) + "px";
|
|
||||||
p3 = e.clientX;
|
|
||||||
p4 = e.clientY;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDragElement() {
|
|
||||||
document.onmouseup = null;
|
|
||||||
document.onmousemove = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDraggable(id) {
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
const startbb = element.getBoundingClientRect();
|
const startbb = element.getBoundingClientRect();
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
let offset = {x: 0, y: 0};
|
let offset = {x: 0, y: 0};
|
||||||
|
@ -66,7 +30,9 @@ function makeDraggable(id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeDraggable("infoContainer");
|
document.querySelectorAll(".floating-window").forEach((w) => {
|
||||||
|
makeDraggable(w);
|
||||||
|
});
|
||||||
|
|
||||||
var coll = document.getElementsByClassName("collapsible");
|
var coll = document.getElementsByClassName("collapsible");
|
||||||
for (var i = 0; i < coll.length; i++) {
|
for (var i = 0; i < coll.length; i++) {
|
||||||
|
|
|
@ -6,3 +6,14 @@ keyboard.onShortcut({ctrl: true, key: "KeyZ"}, () => {
|
||||||
keyboard.onShortcut({ctrl: true, key: "KeyY"}, () => {
|
keyboard.onShortcut({ctrl: true, key: "KeyY"}, () => {
|
||||||
commands.redo();
|
commands.redo();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tool shortcuts
|
||||||
|
keyboard.onShortcut({key: "KeyD"}, () => {
|
||||||
|
tools.dream.enable();
|
||||||
|
});
|
||||||
|
keyboard.onShortcut({key: "KeyM"}, () => {
|
||||||
|
tools.maskbrush.enable();
|
||||||
|
});
|
||||||
|
keyboard.onShortcut({key: "KeyI"}, () => {
|
||||||
|
tools.img2img.enable();
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
(() => {
|
(() => {
|
||||||
makeDraggable("historyContainer");
|
|
||||||
|
|
||||||
const historyView = document.getElementById("history");
|
const historyView = document.getElementById("history");
|
||||||
|
|
||||||
const makeHistoryEntry = (index, id, title) => {
|
const makeHistoryEntry = (index, id, title) => {
|
||||||
|
@ -27,18 +25,26 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
_commands_events.on((message) => {
|
_commands_events.on((message) => {
|
||||||
|
if (message.action === "run") {
|
||||||
|
Array.from(historyView.children).forEach((child) => {
|
||||||
|
if (
|
||||||
|
!commands.history.find((entry) => `hist-${entry.id}` === child.id)
|
||||||
|
) {
|
||||||
|
console.log("Removing " + child.id);
|
||||||
|
historyView.removeChild(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
commands.history.forEach((entry, index) => {
|
commands.history.forEach((entry, index) => {
|
||||||
console.log("Inserting " + entry.id);
|
|
||||||
if (!document.getElementById(`hist-${entry.id}`)) {
|
if (!document.getElementById(`hist-${entry.id}`)) {
|
||||||
|
console.log("Inserting " + entry.id);
|
||||||
historyView.appendChild(
|
historyView.appendChild(
|
||||||
makeHistoryEntry(index, `hist-${entry.id}`, entry.title)
|
makeHistoryEntry(index, `hist-${entry.id}`, entry.title)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
while (historyView.children.length > commands.history.length)
|
|
||||||
historyView.removeChild(historyView.lastElementChild);
|
|
||||||
|
|
||||||
Array.from(historyView.children).forEach((child, index) => {
|
Array.from(historyView.children).forEach((child, index) => {
|
||||||
if (index === commands.current) {
|
if (index === commands.current) {
|
||||||
child.classList.remove(["past"]);
|
child.classList.remove(["past"]);
|
||||||
|
|
178
js/ui/tool/dream.js
Normal file
178
js/ui/tool/dream.js
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
const dream_generate_callback = (evn, state) => {
|
||||||
|
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
|
||||||
|
const bb = getBoundingBox(
|
||||||
|
evn.x,
|
||||||
|
evn.y,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
state.snapToGrid && basePixelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build request to the API
|
||||||
|
const request = {};
|
||||||
|
Object.assign(request, stableDiffusionData);
|
||||||
|
|
||||||
|
// Load prompt (maybe we should add some events so we don't have to do this)
|
||||||
|
request.prompt = document.getElementById("prompt").value;
|
||||||
|
request.negative_prompt = document.getElementById("negPrompt").value;
|
||||||
|
|
||||||
|
// Don't allow another image until is finished
|
||||||
|
blockNewImages = true;
|
||||||
|
|
||||||
|
// Setup marching ants
|
||||||
|
stopMarching = march(bb);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
dream(bb.x, bb.y, request, {method: "txt2img", stopMarching, bb});
|
||||||
|
} else {
|
||||||
|
// Use img2img if not
|
||||||
|
|
||||||
|
// Temporary canvas for init image and mask generation
|
||||||
|
const auxCanvas = document.createElement("canvas");
|
||||||
|
auxCanvas.width = request.width;
|
||||||
|
auxCanvas.height = request.height;
|
||||||
|
const auxCtx = auxCanvas.getContext("2d");
|
||||||
|
|
||||||
|
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);
|
||||||
|
request.init_images = [auxCanvas.toDataURL()];
|
||||||
|
|
||||||
|
// Get mask image
|
||||||
|
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||||
|
auxCtx.globalCompositeOperation = "destination-in";
|
||||||
|
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||||
|
auxCtx.globalCompositeOperation = "destination-out";
|
||||||
|
auxCtx.drawImage(
|
||||||
|
maskPaintCanvas,
|
||||||
|
bb.x,
|
||||||
|
bb.y,
|
||||||
|
bb.w,
|
||||||
|
bb.h,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bb.w,
|
||||||
|
bb.h
|
||||||
|
);
|
||||||
|
auxCtx.globalCompositeOperation = "destination-atop";
|
||||||
|
auxCtx.fillStyle = "#FFFF";
|
||||||
|
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||||
|
request.mask = auxCanvas.toDataURL();
|
||||||
|
|
||||||
|
// Dream
|
||||||
|
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dream_erase_callback = (evn, state) => {
|
||||||
|
const bb = getBoundingBox(
|
||||||
|
evn.x,
|
||||||
|
evn.y,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
state.snapToGrid && basePixelCount
|
||||||
|
);
|
||||||
|
commands.runCommand("eraseImage", "Erase Area", bb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image to Image
|
||||||
|
*/
|
||||||
|
const dream_img2img_callback = (evn, state) => {
|
||||||
|
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
|
||||||
|
const bb = getBoundingBox(
|
||||||
|
evn.x,
|
||||||
|
evn.y,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
state.snapToGrid && basePixelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do nothing if no image exists
|
||||||
|
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return;
|
||||||
|
|
||||||
|
// Build request to the API
|
||||||
|
const request = {};
|
||||||
|
Object.assign(request, stableDiffusionData);
|
||||||
|
|
||||||
|
request.denoising_strength = state.denoisingStrength;
|
||||||
|
request.inpainting_fill = 1; // For img2img use original
|
||||||
|
|
||||||
|
// Load prompt (maybe we should add some events so we don't have to do this)
|
||||||
|
request.prompt = document.getElementById("prompt").value;
|
||||||
|
request.negative_prompt = document.getElementById("negPrompt").value;
|
||||||
|
|
||||||
|
// Don't allow another image until is finished
|
||||||
|
blockNewImages = true;
|
||||||
|
|
||||||
|
// Setup marching ants
|
||||||
|
stopMarching = march(bb);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const auxCanvas = document.createElement("canvas");
|
||||||
|
auxCanvas.width = request.width;
|
||||||
|
auxCanvas.height = request.height;
|
||||||
|
const auxCtx = auxCanvas.getContext("2d");
|
||||||
|
|
||||||
|
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);
|
||||||
|
request.init_images = [auxCanvas.toDataURL()];
|
||||||
|
|
||||||
|
// Get mask image
|
||||||
|
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||||
|
auxCtx.globalCompositeOperation = "destination-out";
|
||||||
|
auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||||
|
|
||||||
|
// Border Mask
|
||||||
|
if (state.useBorderMask) {
|
||||||
|
auxCtx.fillStyle = "#000F";
|
||||||
|
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
|
||||||
|
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
|
||||||
|
auxCtx.fillRect(
|
||||||
|
bb.w - state.borderMaskSize,
|
||||||
|
0,
|
||||||
|
state.borderMaskSize,
|
||||||
|
bb.h
|
||||||
|
);
|
||||||
|
auxCtx.fillRect(
|
||||||
|
0,
|
||||||
|
bb.h - state.borderMaskSize,
|
||||||
|
bb.w,
|
||||||
|
state.borderMaskSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
auxCtx.globalCompositeOperation = "destination-atop";
|
||||||
|
auxCtx.fillStyle = "#FFFF";
|
||||||
|
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||||
|
request.mask = auxCanvas.toDataURL();
|
||||||
|
|
||||||
|
request.inpainting_mask_invert = true;
|
||||||
|
|
||||||
|
// Dream
|
||||||
|
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
|
||||||
|
}
|
||||||
|
};
|
27
js/ui/tool/maskbrush.js
Normal file
27
js/ui/tool/maskbrush.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const mask_brush_draw_callback = (evn, state) => {
|
||||||
|
if (evn.initialTarget.id === "overlayCanvas") {
|
||||||
|
maskPaintCtx.globalCompositeOperation = "source-over";
|
||||||
|
maskPaintCtx.strokeStyle = "#FF6A6A";
|
||||||
|
|
||||||
|
maskPaintCtx.lineWidth = state.brushSize;
|
||||||
|
maskPaintCtx.beginPath();
|
||||||
|
maskPaintCtx.moveTo(evn.px, evn.py);
|
||||||
|
maskPaintCtx.lineTo(evn.x, evn.y);
|
||||||
|
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
||||||
|
maskPaintCtx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mask_brush_erase_callback = (evn, state) => {
|
||||||
|
if (evn.initialTarget.id === "overlayCanvas") {
|
||||||
|
maskPaintCtx.globalCompositeOperation = "destination-out";
|
||||||
|
maskPaintCtx.strokeStyle = "#FFFFFFFF";
|
||||||
|
|
||||||
|
maskPaintCtx.lineWidth = state.brushSize;
|
||||||
|
maskPaintCtx.beginPath();
|
||||||
|
maskPaintCtx.moveTo(evn.px, evn.py);
|
||||||
|
maskPaintCtx.lineTo(evn.x, evn.y);
|
||||||
|
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
||||||
|
maskPaintCtx.stroke();
|
||||||
|
}
|
||||||
|
};
|
456
js/ui/toolbar.js
Normal file
456
js/ui/toolbar.js
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
/**
|
||||||
|
* Toolbar
|
||||||
|
*/
|
||||||
|
|
||||||
|
const toolbar = {
|
||||||
|
_toolbar: document.getElementById("ui-toolbar"),
|
||||||
|
|
||||||
|
tools: [],
|
||||||
|
|
||||||
|
_makeToolbarEntry: (tool) => {
|
||||||
|
const toolTitle = document.createElement("img");
|
||||||
|
toolTitle.classList.add("tool-icon");
|
||||||
|
toolTitle.src = tool.icon;
|
||||||
|
|
||||||
|
const toolEl = document.createElement("div");
|
||||||
|
toolEl.id = `tool-${tool.id}`;
|
||||||
|
toolEl.classList.add("tool");
|
||||||
|
toolEl.title = tool.name;
|
||||||
|
if (tool.options.shortcut) toolEl.title += ` (${tool.options.shortcut})`;
|
||||||
|
toolEl.onclick = () => tool.enable();
|
||||||
|
|
||||||
|
toolEl.appendChild(toolTitle);
|
||||||
|
|
||||||
|
return toolEl;
|
||||||
|
},
|
||||||
|
|
||||||
|
registerTool(
|
||||||
|
icon,
|
||||||
|
toolname,
|
||||||
|
enable,
|
||||||
|
disable,
|
||||||
|
options = {
|
||||||
|
/**
|
||||||
|
* Runs on tool creation. It receives the tool state.
|
||||||
|
*
|
||||||
|
* Can be used to setup callback functions, for example.
|
||||||
|
*/
|
||||||
|
init: null,
|
||||||
|
/**
|
||||||
|
* Function to populate the state menu.
|
||||||
|
*
|
||||||
|
* It receives a div element (that is the menu) and the current tool state.
|
||||||
|
*/
|
||||||
|
populateContextMenu: null,
|
||||||
|
/**
|
||||||
|
* Help description of the tool; for now used for nothing
|
||||||
|
*/
|
||||||
|
description: "",
|
||||||
|
/**
|
||||||
|
* Shortcut; Text describing this tool's shortcut access
|
||||||
|
*/
|
||||||
|
shortcut: "",
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Set some defaults
|
||||||
|
if (!options.init)
|
||||||
|
options.init = (state) => console.debug(`Initialized tool '${toolname}'`);
|
||||||
|
|
||||||
|
if (!options.populateContextMenu)
|
||||||
|
options.populateContextMenu = (menu, state) => {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = "Tool has no context menu";
|
||||||
|
menu.appendChild(span);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create tool
|
||||||
|
const id = guid();
|
||||||
|
|
||||||
|
const contextMenuEl = document.getElementById("tool-context");
|
||||||
|
|
||||||
|
const tool = {
|
||||||
|
id,
|
||||||
|
icon,
|
||||||
|
name: toolname,
|
||||||
|
enabled: false,
|
||||||
|
_element: null,
|
||||||
|
state: {},
|
||||||
|
options,
|
||||||
|
enable: (opt = null) => {
|
||||||
|
this.tools.filter((t) => t.enabled).forEach((t) => t.disable());
|
||||||
|
|
||||||
|
while (contextMenuEl.lastChild) {
|
||||||
|
contextMenuEl.removeChild(contextMenuEl.lastChild);
|
||||||
|
}
|
||||||
|
options.populateContextMenu(contextMenuEl, tool.state);
|
||||||
|
|
||||||
|
tool._element && tool._element.classList.add("using");
|
||||||
|
tool.enabled = true;
|
||||||
|
enable(tool.state, opt);
|
||||||
|
},
|
||||||
|
disable: (opt = null) => {
|
||||||
|
tool._element && tool._element.classList.remove("using");
|
||||||
|
disable(tool.state, opt);
|
||||||
|
tool.enabled = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initalize
|
||||||
|
options.init && options.init(tool.state);
|
||||||
|
|
||||||
|
this.tools.push(tool);
|
||||||
|
|
||||||
|
// Add tool to toolbar
|
||||||
|
tool._element = this._makeToolbarEntry(tool);
|
||||||
|
this._toolbar.appendChild(tool._element);
|
||||||
|
|
||||||
|
return tool;
|
||||||
|
},
|
||||||
|
|
||||||
|
addSeparator() {
|
||||||
|
const separator = document.createElement("div");
|
||||||
|
separator.classList.add("separator");
|
||||||
|
this._toolbar.appendChild(separator);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Premade inputs for populating the context menus
|
||||||
|
*/
|
||||||
|
const _toolbar_input = {
|
||||||
|
checkbox: (state, dataKey, text) => {
|
||||||
|
if (state[dataKey] === undefined) state[dataKey] = false;
|
||||||
|
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.checked = state[dataKey];
|
||||||
|
checkbox.onchange = () => (state[dataKey] = checkbox.checked);
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(new Text(text));
|
||||||
|
|
||||||
|
return {checkbox, label};
|
||||||
|
},
|
||||||
|
|
||||||
|
slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => {
|
||||||
|
const slider = document.createElement("input");
|
||||||
|
slider.type = "range";
|
||||||
|
slider.max = max;
|
||||||
|
slider.step = step;
|
||||||
|
slider.min = min;
|
||||||
|
slider.value = state[dataKey];
|
||||||
|
|
||||||
|
const textEl = document.createElement("input");
|
||||||
|
textEl.type = "number";
|
||||||
|
textEl.value = state[dataKey];
|
||||||
|
|
||||||
|
console.log(state[dataKey]);
|
||||||
|
|
||||||
|
sliderChangeHandlerFactoryEl(
|
||||||
|
slider,
|
||||||
|
textEl,
|
||||||
|
dataKey,
|
||||||
|
state[dataKey],
|
||||||
|
false,
|
||||||
|
(k, v) => (state[dataKey] = v),
|
||||||
|
(k) => state[dataKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.appendChild(new Text(text));
|
||||||
|
label.appendChild(textEl);
|
||||||
|
label.appendChild(slider);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slider,
|
||||||
|
text: textEl,
|
||||||
|
label,
|
||||||
|
setValue(v) {
|
||||||
|
slider.value = v;
|
||||||
|
textEl.value = slider.value;
|
||||||
|
return parseInt(slider.value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dream and img2img tools
|
||||||
|
*/
|
||||||
|
const _reticle_draw = (evn, snapToGrid = true) => {
|
||||||
|
if (evn.target.id === "overlayCanvas") {
|
||||||
|
const bb = getBoundingBox(
|
||||||
|
evn.x,
|
||||||
|
evn.y,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
snapToGrid && basePixelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw targeting square reticle thingy cursor
|
||||||
|
ovCtx.strokeStyle = "#FFF";
|
||||||
|
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dream tool
|
||||||
|
*/
|
||||||
|
tools.dream = toolbar.registerTool(
|
||||||
|
"res/icons/image-plus.svg",
|
||||||
|
"Dream",
|
||||||
|
(state, opt) => {
|
||||||
|
// Draw new cursor immediately
|
||||||
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||||
|
state.mousemovecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
||||||
|
|
||||||
|
// Start Listeners
|
||||||
|
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||||
|
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||||
|
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||||
|
},
|
||||||
|
(state, opt) => {
|
||||||
|
// Clear Listeners
|
||||||
|
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||||
|
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||||
|
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
init: (state) => {
|
||||||
|
state.snapToGrid = true;
|
||||||
|
state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid);
|
||||||
|
state.dreamcb = (evn) => {
|
||||||
|
dream_generate_callback(evn, state);
|
||||||
|
};
|
||||||
|
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||||
|
},
|
||||||
|
populateContextMenu: (menu, state) => {
|
||||||
|
if (!state.ctxmenu) {
|
||||||
|
state.ctxmenu = {};
|
||||||
|
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||||
|
state,
|
||||||
|
"snapToGrid",
|
||||||
|
"Snap To Grid"
|
||||||
|
).label;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||||
|
},
|
||||||
|
shortcut: "D",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
tools.img2img = toolbar.registerTool(
|
||||||
|
"res/icons/image.svg",
|
||||||
|
"Img2Img",
|
||||||
|
(state, opt) => {
|
||||||
|
// Draw new cursor immediately
|
||||||
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||||
|
state.mousemovecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
||||||
|
|
||||||
|
// Start Listeners
|
||||||
|
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||||
|
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||||
|
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||||
|
},
|
||||||
|
(state, opt) => {
|
||||||
|
// Clear Listeners
|
||||||
|
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||||
|
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||||
|
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
init: (state) => {
|
||||||
|
state.snapToGrid = true;
|
||||||
|
state.denoisingStrength = 0.7;
|
||||||
|
|
||||||
|
state.useBorderMask = true;
|
||||||
|
state.borderMaskSize = 64;
|
||||||
|
|
||||||
|
state.mousemovecb = (evn) => {
|
||||||
|
_reticle_draw(evn, state.snapToGrid);
|
||||||
|
const bb = getBoundingBox(
|
||||||
|
evn.x,
|
||||||
|
evn.y,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
basePixelCount * scaleFactor,
|
||||||
|
state.snapToGrid && basePixelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// For displaying border mask
|
||||||
|
const auxCanvas = document.createElement("canvas");
|
||||||
|
auxCanvas.width = bb.w;
|
||||||
|
auxCanvas.height = bb.h;
|
||||||
|
const auxCtx = auxCanvas.getContext("2d");
|
||||||
|
|
||||||
|
if (state.useBorderMask) {
|
||||||
|
auxCtx.fillStyle = "#FF6A6A50";
|
||||||
|
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
|
||||||
|
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
|
||||||
|
auxCtx.fillRect(
|
||||||
|
bb.w - state.borderMaskSize,
|
||||||
|
0,
|
||||||
|
state.borderMaskSize,
|
||||||
|
bb.h
|
||||||
|
);
|
||||||
|
auxCtx.fillRect(
|
||||||
|
0,
|
||||||
|
bb.h - state.borderMaskSize,
|
||||||
|
bb.w,
|
||||||
|
state.borderMaskSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmp = ovCtx.globalAlpha;
|
||||||
|
ovCtx.globalAlpha = 0.4;
|
||||||
|
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
|
||||||
|
ovCtx.globalAlpha = tmp;
|
||||||
|
};
|
||||||
|
state.dreamcb = (evn) => {
|
||||||
|
dream_img2img_callback(evn, state);
|
||||||
|
};
|
||||||
|
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||||
|
},
|
||||||
|
populateContextMenu: (menu, state) => {
|
||||||
|
if (!state.ctxmenu) {
|
||||||
|
state.ctxmenu = {};
|
||||||
|
// Snap To Grid Checkbox
|
||||||
|
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||||
|
state,
|
||||||
|
"snapToGrid",
|
||||||
|
"Snap To Grid"
|
||||||
|
).label;
|
||||||
|
|
||||||
|
// Denoising Strength Slider
|
||||||
|
state.ctxmenu.denoisingStrengthLabel = _toolbar_input.slider(
|
||||||
|
state,
|
||||||
|
"denoisingStrength",
|
||||||
|
"Denoising Strength",
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0.05
|
||||||
|
).label;
|
||||||
|
|
||||||
|
// Use Border Mask Checkbox
|
||||||
|
state.ctxmenu.useBorderMaskLabel = _toolbar_input.checkbox(
|
||||||
|
state,
|
||||||
|
"useBorderMask",
|
||||||
|
"Use Border Mask"
|
||||||
|
).label;
|
||||||
|
// Border Mask Size Slider
|
||||||
|
state.ctxmenu.borderMaskSize = _toolbar_input.slider(
|
||||||
|
state,
|
||||||
|
"borderMaskSize",
|
||||||
|
"Border Mask Size",
|
||||||
|
0,
|
||||||
|
128,
|
||||||
|
1
|
||||||
|
).label;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||||
|
menu.appendChild(document.createElement("br"));
|
||||||
|
menu.appendChild(state.ctxmenu.denoisingStrengthLabel);
|
||||||
|
menu.appendChild(document.createElement("br"));
|
||||||
|
menu.appendChild(state.ctxmenu.useBorderMaskLabel);
|
||||||
|
menu.appendChild(document.createElement("br"));
|
||||||
|
menu.appendChild(state.ctxmenu.borderMaskSize);
|
||||||
|
},
|
||||||
|
shortcut: "I",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mask Editing tools
|
||||||
|
*/
|
||||||
|
toolbar.addSeparator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mask Brush tool
|
||||||
|
*/
|
||||||
|
tools.maskbrush = toolbar.registerTool(
|
||||||
|
"res/icons/paintbrush.svg",
|
||||||
|
"Mask Brush",
|
||||||
|
(state, opt) => {
|
||||||
|
// Draw new cursor immediately
|
||||||
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||||
|
state.movecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
||||||
|
|
||||||
|
// Start Listeners
|
||||||
|
mouse.listen.canvas.onmousemove.on(state.movecb);
|
||||||
|
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||||
|
mouse.listen.canvas.left.onpaint.on(state.drawcb);
|
||||||
|
mouse.listen.canvas.right.onpaint.on(state.erasecb);
|
||||||
|
},
|
||||||
|
(state, opt) => {
|
||||||
|
// Clear Listeners
|
||||||
|
mouse.listen.canvas.onmousemove.clear(state.movecb);
|
||||||
|
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||||
|
mouse.listen.canvas.left.onpaint.clear(state.drawcb);
|
||||||
|
mouse.listen.canvas.right.onpaint.clear(state.erasecb);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
init: (state) => {
|
||||||
|
state.config = {
|
||||||
|
brushScrollSpeed: 1 / 4,
|
||||||
|
minBrushSize: 10,
|
||||||
|
maxBrushSize: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.brushSize = 64;
|
||||||
|
state.setBrushSize = (size) => {
|
||||||
|
state.brushSize = size;
|
||||||
|
state.ctxmenu.brushSizeRange.value = size;
|
||||||
|
state.ctxmenu.brushSizeText.value = size;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.movecb = (evn) => {
|
||||||
|
if (evn.target.id === "overlayCanvas") {
|
||||||
|
// draw big translucent red blob cursor
|
||||||
|
ovCtx.beginPath();
|
||||||
|
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
||||||
|
ovCtx.fillStyle = "#FF6A6A50";
|
||||||
|
ovCtx.fill();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.wheelcb = (evn) => {
|
||||||
|
if (evn.target.id === "overlayCanvas") {
|
||||||
|
state.brushSize = state.setBrushSize(
|
||||||
|
state.brushSize -
|
||||||
|
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||||
|
);
|
||||||
|
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||||
|
state.movecb(evn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.drawcb = (evn) => mask_brush_draw_callback(evn, state);
|
||||||
|
state.erasecb = (evn) => mask_brush_erase_callback(evn, state);
|
||||||
|
},
|
||||||
|
populateContextMenu: (menu, state) => {
|
||||||
|
if (!state.ctxmenu) {
|
||||||
|
state.ctxmenu = {};
|
||||||
|
const brushSizeSlider = _toolbar_input.slider(
|
||||||
|
state,
|
||||||
|
"brushSize",
|
||||||
|
"Brush Size",
|
||||||
|
state.config.minBrushSize,
|
||||||
|
state.config.maxBrushSize,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
state.ctxmenu.brushSizeLabel = brushSizeSlider.label;
|
||||||
|
state.setBrushSize = brushSizeSlider.setValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.appendChild(state.ctxmenu.brushSizeLabel);
|
||||||
|
},
|
||||||
|
shortcut: "M",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
toolbar.tools[0].enable();
|
38
js/util.js
38
js/util.js
|
@ -41,3 +41,41 @@ const guid = (size = 3) => {
|
||||||
id += s4();
|
id += s4();
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bounding box Calculation
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var snapOffset = (i % gridSize) - scaleOffset;
|
||||||
|
if (snapOffset == 0) {
|
||||||
|
return snapOffset;
|
||||||
|
}
|
||||||
|
return -snapOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBoundingBox(cx, cy, w, h, gridSnap = null) {
|
||||||
|
const offset = {x: 0, y: 0};
|
||||||
|
const box = {x: 0, y: 0};
|
||||||
|
|
||||||
|
if (gridSnap) {
|
||||||
|
offset.x = snap(cx, true, gridSnap);
|
||||||
|
offset.y = snap(cy, true, gridSnap);
|
||||||
|
}
|
||||||
|
box.x = offset.x + cx;
|
||||||
|
box.y = offset.y + cy;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.floor(box.x - w / 2),
|
||||||
|
y: Math.floor(box.y - h / 2),
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
7
res/icons/LICENSE
Normal file
7
res/icons/LICENSE
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
8
res/icons/image-plus.svg
Normal file
8
res/icons/image-plus.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<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="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"></path>
|
||||||
|
<line x1="16" y1="5" x2="22" y2="5"></line>
|
||||||
|
<line x1="19" y1="2" x2="19" y2="8"></line>
|
||||||
|
<circle cx="9" cy="9" r="2"></circle>
|
||||||
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 460 B |
6
res/icons/image.svg
Normal file
6
res/icons/image.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">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<circle cx="9" cy="9" r="2"></circle>
|
||||||
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 356 B |
6
res/icons/paintbrush.svg
Normal file
6
res/icons/paintbrush.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="M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z"></path>
|
||||||
|
<path d="M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7"></path>
|
||||||
|
<path d="M14.5 17.5 4.5 15"></path>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 421 B |
Loading…
Reference in a new issue