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:
Victor Seiji Hariki 2022-11-22 19:24:55 -03:00
parent ff66f5362d
commit 250c833895
20 changed files with 1091 additions and 652 deletions

19
.github/workflows/autoformat.yml vendored Normal file
View 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
View 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;
}

View file

@ -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
View 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
View 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
View 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);
}

View file

@ -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>

View file

@ -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);
}
);

View file

@ -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);
}

View file

@ -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
*/

View file

@ -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++) {

View file

@ -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();
});

View file

@ -1,6 +1,4 @@
(() => {
makeDraggable("historyContainer");
const historyView = document.getElementById("history");
const makeHistoryEntry = (index, id, title) => {

87
js/ui/tool/dream.js Normal file
View 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
View 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
View 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();

View file

@ -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
View 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
View 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
View 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