Added tool menu and migrated image generation ot new input
Also updates a lot of other things (brush size now independent from scale factor, split some files, and a lot other things; removed erase safeguard as now erase is supported by undo/redo; tried adding github prettier autoformatting to pull requests; may have some other things as well Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com> Former-commit-id: 0ba21f23c69f9dca2c3189a838b945900b01f81d
This commit is contained in:
parent
ff66f5362d
commit
250c833895
20 changed files with 1091 additions and 652 deletions
19
.github/workflows/autoformat.yml
vendored
Normal file
19
.github/workflows/autoformat.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
name: Prettier Autoformatting
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
fetch-depth: 0
|
||||
- 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;
|
||||
}
|
||||
|
||||
#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 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
@ -68,40 +30,6 @@ body {
|
|||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -158,7 +86,7 @@ button.tool:hover {
|
|||
transition: max-height 0.2s ease-out;
|
||||
}
|
||||
|
||||
.info {
|
||||
.menu-container {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
padding-left: 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);
|
||||
}
|
510
index.html
510
index.html
|
@ -1,199 +1,341 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>openOutpaint 🐠</title>
|
||||
<!-- CSS Variables -->
|
||||
<link href="css/colors.css" rel="stylesheet" />
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>openOutpaint 🐠</title>
|
||||
<link href="css/index.css" rel="stylesheet" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<link href="css/index.css" rel="stylesheet" />
|
||||
<link href="css/ui/generic.css" rel="stylesheet" />
|
||||
|
||||
<body>
|
||||
<!-- Main Toolbar -->
|
||||
<div id="infoContainer" class="uiContainer">
|
||||
<div id="infoTitleBar" class="draggable uiTitleBar">openOutpaint 🐠</div>
|
||||
<div id="info" class="info" style="min-width:200px;">
|
||||
<link href="css/ui/history.css" rel="stylesheet" />
|
||||
<link href="css/ui/toolbar.css" rel="stylesheet" />
|
||||
|
||||
<label for="host">Host</label>
|
||||
<input id="host" value="http://127.0.0.1:7860"><br />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
|
||||
<!-- Prompts section -->
|
||||
<button type="button" class="collapsible">Prompts</button>
|
||||
<div class="content">
|
||||
<label for="prompt">Prompt:</label> <br>
|
||||
<textarea id="prompt">oceanographic study, underwater wildlife, award winning</textarea><br />
|
||||
<label for="negPrompt">Negative prompt:</label> <br>
|
||||
<textarea
|
||||
id="negPrompt">people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry</textarea><br />
|
||||
<hr>
|
||||
</div>
|
||||
<!-- SD section -->
|
||||
<button type="button" class="collapsible">Stable Diffusion settings</button>
|
||||
<div class="content">
|
||||
<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 />
|
||||
<label for="samplerSelect">Sampler:</label>
|
||||
<select id="samplerSelect" onchange="changeSampler()">
|
||||
<option value="DDIM">DDIM</option>
|
||||
<option value="Euler a">Euler A</option>
|
||||
<option value="Euler">Euler</option>
|
||||
<option value="LMS">LMS</option>
|
||||
<option value="Heun">Heun</option>
|
||||
<option value="DPM2">DPM2</option>
|
||||
<option value="DPM2 a">DPM2a</option>
|
||||
<option value="DPM++ 2S a">DPM++2Sa</option>
|
||||
<option value="DPM++ 2m">DPM++2m</option>
|
||||
<option value="DPM fast">DPM fast</option>
|
||||
<option value="DPM adaptive">DPM adaptive</option>
|
||||
<option value="LMS Karras">LMS Karras</option>
|
||||
<option value="DPM2 Karras">DPM2 Karras</option>
|
||||
<option value="DPM2 a Karras">DPM2a Karras</option>
|
||||
<option value="DPM++ 2S a Karras">DPM++2Sa Karras</option>
|
||||
<option value="DPM++ 2M Karras">DPM++2M Karras</option>
|
||||
</select><br />
|
||||
<label for="steps">Steps: <input type="number" id="stepsTxt"></label><br />
|
||||
<input type="range" id="steps" name="steps" min="1" max="50" /><br />
|
||||
<label for="cfgScale">CFG scale: <input type="number" id="cfgScaleTxt"></label>
|
||||
<br />
|
||||
<input type="range" id="cfgScale" name="cfgScale" min="-1" 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>
|
||||
<!-- Unsectioned -->
|
||||
<label for="scaleFactor">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>
|
||||
<input type="checkbox" id="cbxSnap" onchange="changeSnapMode()" checked="checked"><br />
|
||||
<label for="cbxEnableErasing">Right-click erase?</label>
|
||||
<input type="checkbox" id="cbxEnableErasing" onchange="changeEnableErasing()"><br />
|
||||
<label for="cbxPaint">Mask mode?</label>
|
||||
<input type="checkbox" id="cbxPaint" onchange="changePaintMode()"><br />
|
||||
<body>
|
||||
<!-- Main Toolbar -->
|
||||
<div
|
||||
id="infoContainer"
|
||||
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>
|
||||
<input id="host" value="http://127.0.0.1:7860" /><br />
|
||||
|
||||
<label for="cbxHRFix">Auto txt2img HRfix?</label>
|
||||
<input type="checkbox" id="cbxHRFix" onchange="changeHiResFix()"><br />
|
||||
<label for="overMaskPx">Overmask px (0 to disable):</label>
|
||||
<input type="number" id="overMaskPx" onchange="changeOverMaskPx()" min="0" max="128" value="16"
|
||||
step="1" /><br />
|
||||
<label for="maskBlur">Mask blur:</label>
|
||||
<span id="maskBlurText"></span><br />
|
||||
<input type="number" id="maskBlur" name="maskBlur" min="0" max="256" value="0" step="1"
|
||||
onchange="changeMaskBlur()" /><br />
|
||||
<!-- Save/load image section -->
|
||||
<button type="button" class="collapsible">Save/Load/New image</button>
|
||||
<div class="content">
|
||||
<label for="preloadImage">Load image:</label>
|
||||
<input type="file" id="preloadImage" onchange="preloadImage()" accept="image/*" /
|
||||
style="width: 200px;"><br />
|
||||
<button onclick="downloadCanvas()">Save canvas</button><br />
|
||||
<button onclick="newImage()">Clear canvas</button>
|
||||
</div>
|
||||
<!-- Degub info -->
|
||||
<button type="button" class="collapsible">Debug info</button>
|
||||
<div id="coords" class="content">
|
||||
<label for="mouseX">mouseX:</label>
|
||||
<span id="mouseX"></span>
|
||||
<br />
|
||||
<label for="mouseY">mouseY:</label>
|
||||
<span id="mouseY"></span>
|
||||
<br />
|
||||
<label for="canvasX">canvasX:</label>
|
||||
<span id="canvasX"></span>
|
||||
<br />
|
||||
<label for="canvasY">canvasY:</label>
|
||||
<span id="canvasY"></span>
|
||||
<br />
|
||||
<label for="snapX">snapX:</label>
|
||||
<span id="snapX"></span>
|
||||
<br />
|
||||
<label for="snapY">snapY:</label>
|
||||
<span id="snapY"></span><br />
|
||||
<label for="heldButton">Mouse button:</label>
|
||||
<span id="heldButton"></span><br />
|
||||
<span id="version">Alpha release v0.0.6.6</span>
|
||||
<br />
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Prompts section -->
|
||||
<button type="button" class="collapsible">Prompts</button>
|
||||
<div class="content">
|
||||
<label for="prompt">Prompt:</label> <br />
|
||||
<textarea id="prompt">
|
||||
oceanographic study, underwater wildlife, award winning</textarea
|
||||
><br />
|
||||
<label for="negPrompt">Negative prompt:</label> <br />
|
||||
<textarea id="negPrompt">
|
||||
people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry</textarea
|
||||
><br />
|
||||
<hr />
|
||||
</div>
|
||||
<!-- SD section -->
|
||||
<button type="button" class="collapsible">
|
||||
Stable Diffusion settings
|
||||
</button>
|
||||
<div class="content">
|
||||
<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 />
|
||||
<label for="samplerSelect">Sampler:</label>
|
||||
<select id="samplerSelect" onchange="changeSampler()">
|
||||
<option value="DDIM">DDIM</option>
|
||||
<option value="Euler a">Euler A</option>
|
||||
<option value="Euler">Euler</option>
|
||||
<option value="LMS">LMS</option>
|
||||
<option value="Heun">Heun</option>
|
||||
<option value="DPM2">DPM2</option>
|
||||
<option value="DPM2 a">DPM2a</option>
|
||||
<option value="DPM++ 2S a">DPM++2Sa</option>
|
||||
<option value="DPM++ 2m">DPM++2m</option>
|
||||
<option value="DPM fast">DPM fast</option>
|
||||
<option value="DPM adaptive">DPM adaptive</option>
|
||||
<option value="LMS Karras">LMS Karras</option>
|
||||
<option value="DPM2 Karras">DPM2 Karras</option>
|
||||
<option value="DPM2 a Karras">DPM2a Karras</option>
|
||||
<option value="DPM++ 2S a Karras">DPM++2Sa Karras</option>
|
||||
<option value="DPM++ 2M Karras">DPM++2M Karras</option></select
|
||||
><br />
|
||||
<label for="steps">Steps: <input type="number" id="stepsTxt" /></label
|
||||
><br />
|
||||
<input type="range" id="steps" name="steps" min="1" max="50" /><br />
|
||||
<label for="cfgScale"
|
||||
>CFG scale: <input type="number" id="cfgScaleTxt"
|
||||
/></label>
|
||||
<br />
|
||||
<input
|
||||
type="range"
|
||||
id="cfgScale"
|
||||
name="cfgScale"
|
||||
min="-1"
|
||||
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>
|
||||
<!-- Unsectioned -->
|
||||
<label for="scaleFactor"
|
||||
>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>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cbxSnap"
|
||||
onchange="changeSnapMode()"
|
||||
checked="checked" /><br />
|
||||
<label for="cbxHRFix">Auto txt2img HRfix?</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="cbxHRFix"
|
||||
onchange="changeHiResFix()" /><br />
|
||||
<label for="overMaskPx">Overmask px (0 to disable):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="overMaskPx"
|
||||
onchange="changeOverMaskPx()"
|
||||
min="0"
|
||||
max="128"
|
||||
value="16"
|
||||
step="1" /><br />
|
||||
<label for="maskBlur">Mask blur:</label>
|
||||
<span id="maskBlurText"></span><br />
|
||||
<input
|
||||
type="number"
|
||||
id="maskBlur"
|
||||
name="maskBlur"
|
||||
min="0"
|
||||
max="256"
|
||||
value="0"
|
||||
step="1"
|
||||
onchange="changeMaskBlur()" /><br />
|
||||
<!-- Save/load image section -->
|
||||
<button type="button" class="collapsible">Save/Load/New image</button>
|
||||
<div class="content">
|
||||
<label for="preloadImage">Load image:</label>
|
||||
<input type="file" id="preloadImage" onchange="preloadImage()"
|
||||
accept="image/*" / style="width: 200px;"><br />
|
||||
<button onclick="downloadCanvas()">Save canvas</button><br />
|
||||
<button onclick="newImage()">Clear canvas</button>
|
||||
</div>
|
||||
<!-- Debug info -->
|
||||
<button type="button" class="collapsible">Debug info</button>
|
||||
<div id="coords" class="content">
|
||||
<label for="mouseX">mouseX:</label>
|
||||
<span id="mouseX"></span>
|
||||
<br />
|
||||
<label for="mouseY">mouseY:</label>
|
||||
<span id="mouseY"></span>
|
||||
<br />
|
||||
<label for="canvasX">canvasX:</label>
|
||||
<span id="canvasX"></span>
|
||||
<br />
|
||||
<label for="canvasY">canvasY:</label>
|
||||
<span id="canvasY"></span>
|
||||
<br />
|
||||
<label for="snapX">snapX:</label>
|
||||
<span id="snapX"></span>
|
||||
<br />
|
||||
<label for="snapY">snapY:</label>
|
||||
<span id="snapY"></span><br />
|
||||
<label for="heldButton">Mouse button:</label>
|
||||
<span id="heldButton"></span><br />
|
||||
<span id="version">Alpha release v0.0.6.6</span>
|
||||
<br />
|
||||
<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>
|
||||
<!-- History -->
|
||||
<div id="ui-history" class="floating-window" style="right: 10px; top: 10px">
|
||||
<div class="draggable floating-window-title">History</div>
|
||||
<div class="menu-container" style="min-width: 200px">
|
||||
<div id="history" class="history"></div>
|
||||
<div class="toolbar" style="padding: 10px">
|
||||
<button type="button" onclick="commands.undo()" class="tool">
|
||||
undo
|
||||
</button>
|
||||
<button type="button" onclick="commands.redo()" class="tool">
|
||||
redo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Toolbar -->
|
||||
<div id="historyContainer" class="uiContainer" style="right: 0;">
|
||||
<div id="historyTitleBar" class="draggable uiTitleBar">History</div>
|
||||
<div class="info" style="min-width:200px;">
|
||||
<div id="history" class="history"></div>
|
||||
<div class="toolbar" style="padding: 10px;">
|
||||
<button type="button" onclick="commands.undo()" class="tool">undo</button>
|
||||
<button type="button" onclick="commands.redo()" class="tool">redo</button>
|
||||
</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 class="toolbar-section"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="mainHSplit" class="mainHSplit">
|
||||
<div id="uiWrapper" class="uiWrapper">
|
||||
<div id="canvasHolder" class="canvasHolder" oncontextmenu="return false;">
|
||||
<canvas id="backgroundCanvas" class="mainCanvases backgroundCanvas" width="2560" height="1440"
|
||||
style="z-index: 0;">
|
||||
<!-- gray grid bg canvas -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas id="canvas" class="mainCanvases canvas" width="2560" height="1440" style="z-index: 1;">
|
||||
<!-- normal canvas on which images are drawn -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<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 -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas id="targetCanvas" class="mainCanvases targetCanvas" width="2560" height="1440"
|
||||
style="z-index: 3;">
|
||||
<!-- canvas on which "targeting" squares are drawn -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas id="maskPaintCanvas" class="mainCanvases maskPaintCanvas" width="2560" height="1440"
|
||||
style="z-index: 4;">
|
||||
<!-- canvas on which masking brush is "painted" -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas id="overlayCanvas" class="mainCanvases overlayCanvas" width="2560" height="1440"
|
||||
style="z-index: 5;">
|
||||
<!-- canvas on which "cursor" reticle or arc is drawn -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<div id="tempDiv" style="position: relative; z-index: 6;">
|
||||
<!-- where popup buttons go -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="masks" class="masks">
|
||||
<div>
|
||||
<!-- <canvas id="maskCanvasMonitor" class="maskCanvasMonitor" width="512" height="512">
|
||||
<!-- Canvases -->
|
||||
<div id="mainHSplit" class="mainHSplit">
|
||||
<div id="uiWrapper" class="uiWrapper">
|
||||
<div
|
||||
id="canvasHolder"
|
||||
class="canvasHolder"
|
||||
oncontextmenu="return false;">
|
||||
<canvas
|
||||
id="backgroundCanvas"
|
||||
class="mainCanvases backgroundCanvas"
|
||||
width="2560"
|
||||
height="1440"
|
||||
style="z-index: 0">
|
||||
<!-- gray grid bg canvas -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas
|
||||
id="canvas"
|
||||
class="mainCanvases canvas"
|
||||
width="2560"
|
||||
height="1440"
|
||||
style="z-index: 1">
|
||||
<!-- normal canvas on which images are drawn -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<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 -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas
|
||||
id="targetCanvas"
|
||||
class="mainCanvases targetCanvas"
|
||||
width="2560"
|
||||
height="1440"
|
||||
style="z-index: 3">
|
||||
<!-- canvas on which "targeting" squares are drawn -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas
|
||||
id="maskPaintCanvas"
|
||||
class="mainCanvases maskPaintCanvas"
|
||||
width="2560"
|
||||
height="1440"
|
||||
style="z-index: 4">
|
||||
<!-- canvas on which masking brush is "painted" -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<canvas
|
||||
id="overlayCanvas"
|
||||
class="mainCanvases overlayCanvas"
|
||||
width="2560"
|
||||
height="1440"
|
||||
style="z-index: 5">
|
||||
<!-- canvas on which "cursor" reticle or arc is drawn -->
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas>
|
||||
<div id="tempDiv" style="position: relative; z-index: 6">
|
||||
<!-- where popup buttons go -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="masks" class="masks">
|
||||
<div>
|
||||
<!-- <canvas id="maskCanvasMonitor" class="maskCanvasMonitor" width="512" height="512">
|
||||
<p>lol ur browser sucks</p>
|
||||
</canvas><br /> -->
|
||||
<canvas id="overMaskCanvasMonitor" class="overMaskCanvasMonitor" width="512" height="512">
|
||||
<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>
|
||||
<canvas
|
||||
id="overMaskCanvasMonitor"
|
||||
class="overMaskCanvasMonitor"
|
||||
width="512"
|
||||
height="512">
|
||||
<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>
|
||||
|
||||
<!-- Base Libs -->
|
||||
<script src="js/util.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/ui/history.js" type="text/javascript"></script>
|
||||
<script src="js/settingsbar.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/commands.js" type="text/javascript"></script>
|
||||
<script src="js/index.js" type="text/javascript"></script>
|
||||
<script src="js/settingsbar.js" type="text/javascript"></script>
|
||||
<script src="js/shortcuts.js" type="text/javascript"></script>
|
||||
|
||||
<script src="js/ui/history.js" type="text/javascript"></script>
|
||||
</body>
|
||||
<!-- Content -->
|
||||
<script src="js/index.js" type="text/javascript"></script>
|
||||
<script src="js/shortcuts.js" type="text/javascript"></script>
|
||||
|
||||
</html>
|
||||
<!-- Load Tools -->
|
||||
<script src="js/ui/tool/dream.js" type="text/javascript"></script>
|
||||
<script src="js/ui/tool/maskbrush.js" type="text/javascript"></script>
|
||||
|
||||
<script src="js/ui/toolbar.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -156,3 +156,53 @@ commands.createCommand(
|
|||
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);
|
||||
}
|
||||
);
|
||||
|
|
374
js/index.js
374
js/index.js
|
@ -104,11 +104,9 @@ var heldButton = 0;
|
|||
var snapX = 0;
|
||||
var snapY = 0;
|
||||
var drawThis = {};
|
||||
var clicked = false;
|
||||
const basePixelCount = 64; //64 px - ALWAYS 64 PX
|
||||
var scaleFactor = 8; //x64 px
|
||||
var snapToGrid = true;
|
||||
var paintMode = false;
|
||||
var backupMaskPaintCanvas; //???
|
||||
var backupMaskPaintCtx; //...? look i am bad at this
|
||||
var backupMaskChunk = null;
|
||||
|
@ -123,9 +121,8 @@ var arbitraryImageData;
|
|||
var arbitraryImageBitmap;
|
||||
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 enableErasing = false; // accidental right-click erase if the user isn't trying to erase is a bad thing
|
||||
var marchOffset = 0;
|
||||
var marching = false;
|
||||
var stopMarching = null;
|
||||
var marchCoords = {};
|
||||
|
||||
// info div, sometimes hidden
|
||||
|
@ -156,7 +153,6 @@ function startup() {
|
|||
loadSettings();
|
||||
drawBackground();
|
||||
changeScaleFactor();
|
||||
changePaintMode();
|
||||
changeSampler();
|
||||
changeSteps();
|
||||
changeCfgScale();
|
||||
|
@ -167,7 +163,6 @@ function startup() {
|
|||
changeSeed();
|
||||
changeOverMaskPx();
|
||||
changeHiResFix();
|
||||
changeEnableErasing();
|
||||
document.getElementById("overlayCanvas").onmousemove = mouseMove;
|
||||
document.getElementById("overlayCanvas").onmousedown = mouseDown;
|
||||
document.getElementById("overlayCanvas").onmouseup = mouseUp;
|
||||
|
@ -193,43 +188,56 @@ function writeArbitraryImage(img, x, y) {
|
|||
document.getElementById("preloadImage").files = null;
|
||||
}
|
||||
|
||||
function dream(x, y, prompt) {
|
||||
function dream(
|
||||
x,
|
||||
y,
|
||||
prompt,
|
||||
extra = {method: endpoint, stopMarching: () => {}}
|
||||
) {
|
||||
tmpImgXYWH.x = x;
|
||||
tmpImgXYWH.y = y;
|
||||
tmpImgXYWH.w = prompt.width;
|
||||
tmpImgXYWH.h = prompt.height;
|
||||
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) => {
|
||||
postData(prompt, extra).then((data) => {
|
||||
returnedImages = data.images;
|
||||
totalImagesReturned = data.images.length;
|
||||
blockNewImages = true;
|
||||
//console.log(data); // JSON data parsed by `data.json()` call
|
||||
imageAcceptReject(x, y, data);
|
||||
imageAcceptReject(x, y, data, extra);
|
||||
});
|
||||
}
|
||||
|
||||
async function postData(promptData) {
|
||||
async function postData(promptData, extra = null) {
|
||||
this.host = document.getElementById("host").value;
|
||||
// Default options are marked with *
|
||||
const response = await fetch(this.host + this.url + this.endpoint, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
mode: "cors", // no-cors, *cors, same-origin
|
||||
cache: "default", // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
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
|
||||
body: JSON.stringify(promptData), // body data type must match "Content-Type" header
|
||||
});
|
||||
const response = await fetch(
|
||||
this.host + this.url + extra.method || endpoint,
|
||||
{
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
mode: "cors", // no-cors, *cors, same-origin
|
||||
cache: "default", // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
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
|
||||
body: JSON.stringify(promptData), // body data type must match "Content-Type" header
|
||||
}
|
||||
);
|
||||
return response.json(); // parses JSON response into native JavaScript objects
|
||||
}
|
||||
|
||||
function imageAcceptReject(x, y, data) {
|
||||
function imageAcceptReject(x, y, data, extra = null) {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
tempCtx.drawImage(img, x, y); //imgCtx for actual image, tmp for... holding?
|
||||
|
@ -254,7 +262,8 @@ function imageAcceptReject(x, y, data) {
|
|||
|
||||
function accept(evt) {
|
||||
// write image to imgcanvas
|
||||
marching = false;
|
||||
stopMarching && stopMarching();
|
||||
stopMarching = null;
|
||||
clearBackupMask();
|
||||
placeImage();
|
||||
removeChoiceButtons();
|
||||
|
@ -264,7 +273,8 @@ function accept(evt) {
|
|||
|
||||
function reject(evt) {
|
||||
// remove image entirely
|
||||
marching = false;
|
||||
stopMarching && stopMarching();
|
||||
stopMarching = null;
|
||||
restoreBackupMask();
|
||||
clearBackupMask();
|
||||
clearTargetMask();
|
||||
|
@ -368,39 +378,23 @@ function sleep(ms) {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function snap(i, scaled = true) {
|
||||
// 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 = basePixelCount / 2;
|
||||
}
|
||||
}
|
||||
var snapOffset = (i % basePixelCount) - scaleOffset;
|
||||
if (snapOffset == 0) {
|
||||
return snapOffset;
|
||||
}
|
||||
return -snapOffset;
|
||||
function march(bb) {
|
||||
let offset = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
drawMarchingAnts(bb, offset++);
|
||||
offset %= 16;
|
||||
}, 20);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
|
||||
function march() {
|
||||
if (marching) {
|
||||
marchOffset++;
|
||||
if (marchOffset > 16) {
|
||||
marchOffset = 0;
|
||||
}
|
||||
drawMarchingAnts();
|
||||
setTimeout(march, 20);
|
||||
}
|
||||
}
|
||||
|
||||
function drawMarchingAnts() {
|
||||
function drawMarchingAnts(bb, offset) {
|
||||
clearTargetMask();
|
||||
tgtCtx.strokeStyle = "#FFFFFFFF"; //"#55000077";
|
||||
tgtCtx.setLineDash([4, 2]);
|
||||
tgtCtx.lineDashOffset = -marchOffset;
|
||||
tgtCtx.strokeRect(marchCoords.x, marchCoords.y, marchCoords.w, marchCoords.h);
|
||||
tgtCtx.lineDashOffset = -offset;
|
||||
tgtCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
|
||||
}
|
||||
|
||||
function mouseMove(evt) {
|
||||
|
@ -427,67 +421,9 @@ function mouseMove(evt) {
|
|||
finalX = snapOffsetX + canvasX;
|
||||
finalY = snapOffsetY + canvasY;
|
||||
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) {
|
||||
const rect = ovCanvas.getBoundingClientRect();
|
||||
var oddOffset = 0;
|
||||
|
@ -503,39 +439,6 @@ function mouseDown(evt) {
|
|||
nextBox.w = arbitraryImageData.width;
|
||||
nextBox.h = arbitraryImageData.height;
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -561,188 +464,10 @@ function mouseUp(evt) {
|
|||
drawThis.h = target.h;
|
||||
drawIt = drawThis; // i still think this is really stupid and redundant and unnecessary and redundant
|
||||
drop(drawIt);
|
||||
} else if (paintMode) {
|
||||
clicked = false;
|
||||
return;
|
||||
} else {
|
||||
if (!blockNewImages) {
|
||||
//TODO seriously, refactor this
|
||||
blockNewImages = true;
|
||||
marching = 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() {
|
||||
stableDiffusionData.sampler_index =
|
||||
document.getElementById("samplerSelect").value;
|
||||
|
@ -972,6 +697,5 @@ function loadSettings() {
|
|||
document.getElementById("maskBlur").value = Number(_mask_blur);
|
||||
document.getElementById("seed").value = Number(_seed);
|
||||
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
||||
document.getElementById("cbxEnableErasing").checked = Boolean(_enable_erase);
|
||||
document.getElementById("overMaskPx").value = Number(_overmask_px);
|
||||
}
|
||||
|
|
52
js/input.js
52
js/input.js
|
@ -31,9 +31,9 @@ function _context_coords() {
|
|||
}
|
||||
function _mouse_observers() {
|
||||
return {
|
||||
// Simple click handlers
|
||||
// Simple click handler
|
||||
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(),
|
||||
// Drag handler
|
||||
ondragstart: new Observer(),
|
||||
|
@ -48,6 +48,7 @@ function _mouse_observers() {
|
|||
|
||||
function _context_observers() {
|
||||
return {
|
||||
onwheel: new Observer(),
|
||||
onmousemove: new Observer(),
|
||||
left: _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(() =>
|
||||
console.debug('mouse.listen.window.right.ondclick')
|
||||
window.addEventListener(
|
||||
"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
|
||||
*/
|
||||
|
|
|
@ -1,40 +1,4 @@
|
|||
//dragElement(document.getElementById("infoContainer"));
|
||||
//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);
|
||||
function makeDraggable(element) {
|
||||
const startbb = element.getBoundingClientRect();
|
||||
let dragging = false;
|
||||
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");
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
|
|
|
@ -6,3 +6,11 @@ keyboard.onShortcut({ctrl: true, key: "KeyZ"}, () => {
|
|||
keyboard.onShortcut({ctrl: true, key: "KeyY"}, () => {
|
||||
commands.redo();
|
||||
});
|
||||
|
||||
// Tool shortcuts
|
||||
keyboard.onShortcut({key: "KeyD"}, () => {
|
||||
tools.dream.enable();
|
||||
});
|
||||
keyboard.onShortcut({key: "KeyM"}, () => {
|
||||
tools.maskbrush.enable();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
(() => {
|
||||
makeDraggable("historyContainer");
|
||||
|
||||
const historyView = document.getElementById("history");
|
||||
|
||||
const makeHistoryEntry = (index, id, title) => {
|
||||
|
|
87
js/ui/tool/dream.js
Normal file
87
js/ui/tool/dream.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
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"});
|
||||
} 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"});
|
||||
}
|
||||
}
|
||||
};
|
||||
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);
|
||||
};
|
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();
|
||||
}
|
||||
};
|
300
js/ui/toolbar.js
Normal file
300
js/ui/toolbar.js
Normal file
|
@ -0,0 +1,300 @@
|
|||
/**
|
||||
* 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);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
_reticle_draw({...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 = {};
|
||||
// Snap To Grid Checkbox
|
||||
const snapToGridCheckbox = document.createElement("input");
|
||||
snapToGridCheckbox.type = "checkbox";
|
||||
snapToGridCheckbox.checked = state.snapToGrid;
|
||||
snapToGridCheckbox.onchange = () =>
|
||||
(state.snapToGrid = snapToGridCheckbox.checked);
|
||||
state.ctxmenu.snapToGridCheckbox = snapToGridCheckbox;
|
||||
|
||||
const snapToGridLabel = document.createElement("label");
|
||||
snapToGridLabel.appendChild(snapToGridCheckbox);
|
||||
snapToGridLabel.appendChild(new Text("Snap to Grid"));
|
||||
state.ctxmenu.snapToGridLabel = snapToGridLabel;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
},
|
||||
shortcut: "D",
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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.setBrushSize(
|
||||
Math.max(
|
||||
state.config.minBrushSize,
|
||||
Math.min(
|
||||
state.config.maxBrushSize,
|
||||
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 = {};
|
||||
// Brush Size slider
|
||||
const brushSizeRange = document.createElement("input");
|
||||
brushSizeRange.type = "range";
|
||||
brushSizeRange.value = state.brushSize;
|
||||
brushSizeRange.max = state.config.maxBrushSize;
|
||||
brushSizeRange.step = 8;
|
||||
brushSizeRange.min = state.config.minBrushSize;
|
||||
brushSizeRange.oninput = () =>
|
||||
(state.brushSize = parseInt(brushSizeRange.value));
|
||||
state.ctxmenu.brushSizeRange = brushSizeRange;
|
||||
const brushSizeText = document.createElement("input");
|
||||
brushSizeText.type = "number";
|
||||
brushSizeText.value = state.brushSize;
|
||||
brushSizeText.oninput = () =>
|
||||
(state.brushSize = parseInt(brushSizeText.value));
|
||||
state.ctxmenu.brushSizeText = brushSizeText;
|
||||
|
||||
const brushSizeLabel = document.createElement("label");
|
||||
brushSizeLabel.appendChild(new Text("Brush Size"));
|
||||
brushSizeLabel.appendChild(brushSizeText);
|
||||
brushSizeLabel.appendChild(brushSizeRange);
|
||||
state.ctxmenu.brushSizeLabel = brushSizeLabel;
|
||||
}
|
||||
|
||||
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();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
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