Merge pull request #37 from seijihariki/img2img
Add tool for img2img Former-commit-id: 4e6f088cd99e9274b7e0ca9413ba7e8c3503540e
This commit is contained in:
commit
42adf68610
21 changed files with 1437 additions and 676 deletions
16
.github.bkp/workflows/autoformat.yml
Normal file
16
.github.bkp/workflows/autoformat.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: Prettier Autoformatting
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, closed]
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Prettify
|
||||
uses: creyD/prettier_action@v4.2
|
||||
with:
|
||||
prettier_options: --write **/*.{js,html,css,md}
|
8
css/colors.css
Normal file
8
css/colors.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
:root {
|
||||
--c-primary: #2c3333;
|
||||
--c-hover: hsl(180, 7%, 30%);
|
||||
--c-active: hsl(180, 7%, 25%);
|
||||
--c-secondary: #395b64;
|
||||
--c-accent: #a5c9ca;
|
||||
--c-text: #e7f6f2;
|
||||
}
|
|
@ -14,44 +14,6 @@ body {
|
|||
background-color: #ccc;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
493
index.html
493
index.html
|
@ -1,188 +1,337 @@
|
|||
<!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="models">Model:</label>
|
||||
<select id="models" onchange="changeModel()"></select><br>
|
||||
<label for="samplerSelect">Sampler:</label>
|
||||
<select id="samplerSelect" onchange="changeSampler()"></select><br />
|
||||
<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="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 />
|
||||
<label for="upscalers">Choose upscaler</label>
|
||||
<select id="upscalers"></select>
|
||||
<button onclick="upscaleAndDownload()">Upscale (might take a sec)</button><br />
|
||||
<!-- 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="models">Model:</label>
|
||||
<select id="models" onchange="changeModel()"></select
|
||||
><br />
|
||||
<label for="samplerSelect">Sampler:</label>
|
||||
<select id="samplerSelect" onchange="changeSampler()"></select
|
||||
><br />
|
||||
<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="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 />
|
||||
<label for="upscalers">Choose upscaler</label>
|
||||
<select id="upscalers"></select>
|
||||
<button onclick="upscaleAndDownload()">
|
||||
Upscale (might take a sec)</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.9</span> <!-- 𝓃𝒾𝒸𝑒 -->
|
||||
<br />
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
onmouseover="document.activeElement.blur()">
|
||||
<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>
|
||||
|
|
|
@ -39,6 +39,12 @@ const commands = {
|
|||
Object.assign(copy, options);
|
||||
const state = {};
|
||||
|
||||
const entry = {
|
||||
id: guid(),
|
||||
title,
|
||||
state,
|
||||
};
|
||||
|
||||
// Attempt to run command
|
||||
try {
|
||||
run(title, copy, state);
|
||||
|
@ -53,6 +59,7 @@ const commands = {
|
|||
console.debug(`Undoing ${name}, currently ${commands.current}`);
|
||||
undo(title, state);
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
action: "undo",
|
||||
state,
|
||||
|
@ -63,6 +70,7 @@ const commands = {
|
|||
console.debug(`Redoing ${name}, currently ${commands.current}`);
|
||||
redo(title, copy, state);
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
action: "redo",
|
||||
state,
|
||||
|
@ -71,21 +79,29 @@ const commands = {
|
|||
};
|
||||
|
||||
// Add to history
|
||||
if (commands.history.length > commands.current + 1)
|
||||
commands.history.splice(commands.current + 1);
|
||||
if (commands.history.length > commands.current + 1) {
|
||||
commands.history.forEach((entry, index) => {
|
||||
if (index >= commands.current + 1)
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
action: "deleted",
|
||||
state,
|
||||
current: commands.current,
|
||||
});
|
||||
});
|
||||
|
||||
const entry = {
|
||||
id: guid(),
|
||||
title,
|
||||
undo: undoWrapper,
|
||||
redo: redoWrapper,
|
||||
state,
|
||||
};
|
||||
commands.history.splice(commands.current + 1);
|
||||
}
|
||||
|
||||
commands.history.push(entry);
|
||||
commands.current++;
|
||||
|
||||
entry.undo = undoWrapper;
|
||||
entry.redo = redoWrapper;
|
||||
|
||||
_commands_events.emit({
|
||||
id: entry.id,
|
||||
name,
|
||||
action: "run",
|
||||
state,
|
||||
|
@ -156,3 +172,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);
|
||||
}
|
||||
);
|
||||
|
|
444
js/index.js
444
js/index.js
|
@ -55,12 +55,31 @@ function sliderChangeHandlerFactory(
|
|||
textBoxId,
|
||||
dataKey,
|
||||
defaultV,
|
||||
save = true,
|
||||
setter = (k, v) => (stableDiffusionData[k] = v),
|
||||
getter = (k) => stableDiffusionData[k]
|
||||
) {
|
||||
const sliderEl = document.getElementById(sliderId);
|
||||
const textBoxEl = document.getElementById(textBoxId);
|
||||
const savedValue = localStorage.getItem(dataKey);
|
||||
return sliderChangeHandlerFactoryEl(
|
||||
document.getElementById(sliderId),
|
||||
document.getElementById(textBoxId),
|
||||
dataKey,
|
||||
defaultV,
|
||||
save,
|
||||
setter,
|
||||
getter
|
||||
);
|
||||
}
|
||||
|
||||
function sliderChangeHandlerFactoryEl(
|
||||
sliderEl,
|
||||
textBoxEl,
|
||||
dataKey,
|
||||
defaultV,
|
||||
save = true,
|
||||
setter = (k, v) => (stableDiffusionData[k] = v),
|
||||
getter = (k) => stableDiffusionData[k]
|
||||
) {
|
||||
const savedValue = save && localStorage.getItem(dataKey);
|
||||
|
||||
if (savedValue) setter(dataKey, savedValue || defaultV);
|
||||
|
||||
|
@ -70,12 +89,12 @@ function sliderChangeHandlerFactory(
|
|||
|
||||
if (value) setter(dataKey, value);
|
||||
|
||||
if (!eventSource || eventSource.id === textBoxId)
|
||||
if (!eventSource || eventSource === textBoxEl)
|
||||
sliderEl.value = getter(dataKey);
|
||||
setter(dataKey, Number(sliderEl.value));
|
||||
textBoxEl.value = getter(dataKey);
|
||||
|
||||
localStorage.setItem(dataKey, getter(dataKey));
|
||||
if (save) localStorage.setItem(dataKey, getter(dataKey));
|
||||
}
|
||||
|
||||
textBoxEl.onchange = changeHandler;
|
||||
|
@ -104,11 +123,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 +140,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 inProgress = false;
|
||||
var marchCoords = {};
|
||||
|
||||
|
@ -160,7 +176,6 @@ function startup() {
|
|||
getModels();
|
||||
drawBackground();
|
||||
changeScaleFactor();
|
||||
changePaintMode();
|
||||
changeSampler();
|
||||
changeSteps();
|
||||
changeCfgScale();
|
||||
|
@ -171,7 +186,6 @@ function startup() {
|
|||
changeSeed();
|
||||
changeOverMaskPx();
|
||||
changeHiResFix();
|
||||
changeEnableErasing();
|
||||
document.getElementById("overlayCanvas").onmousemove = mouseMove;
|
||||
document.getElementById("overlayCanvas").onmousedown = mouseDown;
|
||||
document.getElementById("overlayCanvas").onmouseup = mouseUp;
|
||||
|
@ -197,44 +211,67 @@ function writeArbitraryImage(img, x, y) {
|
|||
document.getElementById("preloadImage").files = null;
|
||||
}
|
||||
|
||||
function dream(x, y, prompt) {
|
||||
function dream(
|
||||
x,
|
||||
y,
|
||||
prompt,
|
||||
extra = {
|
||||
method: endpoint,
|
||||
stopMarching: () => {},
|
||||
bb: {x, y, w: prompt.width, h: prompt.height},
|
||||
}
|
||||
) {
|
||||
tmpImgXYWH.x = x;
|
||||
tmpImgXYWH.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) => {
|
||||
returnedImages = data.images;
|
||||
totalImagesReturned = data.images.length;
|
||||
blockNewImages = true;
|
||||
//console.log(data); // JSON data parsed by `data.json()` call
|
||||
imageAcceptReject(x, y, data);
|
||||
});
|
||||
checkProgress();
|
||||
console.info(`dreaming "${prompt.prompt}"`);
|
||||
console.debug(prompt);
|
||||
|
||||
// Start checking for progress
|
||||
const progressCheck = checkProgress(extra.bb);
|
||||
postData(prompt, extra)
|
||||
.then((data) => {
|
||||
returnedImages = data.images;
|
||||
totalImagesReturned = data.images.length;
|
||||
blockNewImages = true;
|
||||
//console.log(data); // JSON data parsed by `data.json()` call
|
||||
imageAcceptReject(x, y, data, extra);
|
||||
})
|
||||
.finally(() => clearInterval(progressCheck));
|
||||
}
|
||||
|
||||
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) {
|
||||
inProgress = false;
|
||||
document.getElementById("progressDiv").remove();
|
||||
const img = new Image();
|
||||
|
@ -261,7 +298,8 @@ function imageAcceptReject(x, y, data) {
|
|||
|
||||
function accept(evt) {
|
||||
// write image to imgcanvas
|
||||
marching = false;
|
||||
stopMarching && stopMarching();
|
||||
stopMarching = null;
|
||||
clearBackupMask();
|
||||
placeImage();
|
||||
removeChoiceButtons();
|
||||
|
@ -271,7 +309,8 @@ function accept(evt) {
|
|||
|
||||
function reject(evt) {
|
||||
// remove image entirely
|
||||
marching = false;
|
||||
stopMarching && stopMarching();
|
||||
stopMarching = null;
|
||||
restoreBackupMask();
|
||||
clearBackupMask();
|
||||
clearTargetMask();
|
||||
|
@ -375,59 +414,40 @@ 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 checkProgress() {
|
||||
function checkProgress(bb) {
|
||||
document.getElementById("progressDiv") &&
|
||||
document.getElementById("progressDiv").remove();
|
||||
endpoint = "progress?skip_current_image=false";
|
||||
// Skip image to stop using a ton of networking resources
|
||||
endpoint = "progress?skip_current_image=true";
|
||||
var div = document.createElement("div");
|
||||
div.id = "progressDiv";
|
||||
div.style.position = "absolute";
|
||||
div.style.width = "200px";
|
||||
div.style.height = "70px";
|
||||
div.style.left = parseInt(marchCoords.x + marchCoords.w - 100) + "px";
|
||||
div.style.top = parseInt(marchCoords.y + marchCoords.h) + "px";
|
||||
div.style.left = parseInt(bb.x + bb.w - 100) + "px";
|
||||
div.style.top = parseInt(bb.y + bb.h) + "px";
|
||||
div.innerHTML = '<span class="strokeText" id="estRemaining"></span>';
|
||||
document.getElementById("tempDiv").appendChild(div);
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
if (inProgress) {
|
||||
return setInterval(() => {
|
||||
fetch(host + url + endpoint)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
|
@ -439,8 +459,7 @@ function updateProgress() {
|
|||
|
||||
document.getElementById("estRemaining").innerText = estimate;
|
||||
});
|
||||
setTimeout(updateProgress, 500);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function mouseMove(evt) {
|
||||
|
@ -467,67 +486,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;
|
||||
|
@ -543,39 +504,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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -601,188 +529,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 = inProgress = true;
|
||||
var drawIt = {}; //why am i doing this????
|
||||
var target = drawTargets[drawTargets.length - 1]; //get the last one... why am i storing all of them?
|
||||
var oddOffset = 0;
|
||||
if (scaleFactor % 2 != 0) {
|
||||
oddOffset = basePixelCount / 2;
|
||||
}
|
||||
snapOffsetX = 0;
|
||||
snapOffsetY = 0;
|
||||
if (snapToGrid) {
|
||||
snapOffsetX = snap(target.x);
|
||||
snapOffsetY = snap(target.y);
|
||||
}
|
||||
finalX = snapOffsetX + target.x - oddOffset;
|
||||
finalY = snapOffsetY + target.y - oddOffset;
|
||||
|
||||
drawThis.x = marchCoords.x = finalX;
|
||||
drawThis.y = marchCoords.y = finalY;
|
||||
drawThis.w = marchCoords.w = target.w;
|
||||
drawThis.h = marchCoords.h = target.h;
|
||||
march(finalX, finalY, target.w, target.h);
|
||||
drawIt = drawThis; //TODO this is WRONG but also explicitly only draws the last image ... i think
|
||||
//check if there's image data already there
|
||||
// console.log(downX + ":" + downY + " :: " + this.isCanvasBlank(downX, downY));
|
||||
if (!isCanvasBlank(drawIt.x, drawIt.y, drawIt.w, drawIt.h, imgCanvas)) {
|
||||
// image exists, set up for img2img
|
||||
var mainCanvasCtx = document
|
||||
.getElementById("canvas")
|
||||
.getContext("2d");
|
||||
const imgChunk = mainCanvasCtx.getImageData(
|
||||
drawIt.x,
|
||||
drawIt.y,
|
||||
drawIt.w,
|
||||
drawIt.h
|
||||
); // imagedata object of the image being outpainted
|
||||
const imgChunkData = imgChunk.data; // imagedata.data object, a big inconvenient uint8clampedarray
|
||||
// these are the 3 mask monitors on the bottom of the page
|
||||
var initImgCanvas = document.getElementById("initImgCanvasMonitor");
|
||||
var overMaskCanvas = document.getElementById("overMaskCanvasMonitor");
|
||||
overMaskCanvas.width = initImgCanvas.width = target.w; //maskCanvas.width = target.w;
|
||||
overMaskCanvas.height = initImgCanvas.height = target.h; //maskCanvas.height = target.h;
|
||||
var initImgCanvasCtx = initImgCanvas.getContext("2d");
|
||||
var overMaskCanvasCtx = overMaskCanvas.getContext("2d");
|
||||
// get blank pixels to use as mask
|
||||
const initImgData = mainCanvasCtx.createImageData(drawIt.w, drawIt.h);
|
||||
let overMaskImgData = overMaskCanvasCtx.createImageData(
|
||||
drawIt.w,
|
||||
drawIt.h
|
||||
);
|
||||
// cover entire masks in black before adding masked areas
|
||||
|
||||
for (let i = 0; i < imgChunkData.length; i += 4) {
|
||||
// l->r, top->bottom, R G B A pixel values in a big ol array
|
||||
// make a simple mask
|
||||
if (imgChunkData[i + 3] == 0) {
|
||||
// rgba pixel values, 4th one is alpha, if it's 0 there's "nothing there" in the image display canvas and its time to outpaint
|
||||
overMaskImgData.data[i] = 255; // white mask gets painted over
|
||||
overMaskImgData.data[i + 1] = 255;
|
||||
overMaskImgData.data[i + 2] = 255;
|
||||
overMaskImgData.data[i + 3] = 255;
|
||||
|
||||
initImgData.data[i] = 0; // null area on initial image becomes opaque black pixels
|
||||
initImgData.data[i + 1] = 0;
|
||||
initImgData.data[i + 2] = 0;
|
||||
initImgData.data[i + 3] = 255;
|
||||
} else {
|
||||
// leave these pixels alone
|
||||
overMaskImgData.data[i] = 0; // black mask gets ignored for in/outpainting
|
||||
overMaskImgData.data[i + 1] = 0;
|
||||
overMaskImgData.data[i + 2] = 0;
|
||||
overMaskImgData.data[i + 3] = 255; // but it still needs an opaque alpha channel
|
||||
|
||||
initImgData.data[i] = imgChunkData[i]; // put the original picture back in the painted area
|
||||
initImgData.data[i + 1] = imgChunkData[i + 1];
|
||||
initImgData.data[i + 2] = imgChunkData[i + 2];
|
||||
initImgData.data[i + 3] = imgChunkData[i + 3]; //it's still RGBA so we can handily do this in nice chunks'o'4
|
||||
}
|
||||
}
|
||||
if (overMaskPx > 0) {
|
||||
// https://stackoverflow.com/a/30204783 ???? !!!!!!!!
|
||||
overMaskCanvasCtx.fillStyle = "black";
|
||||
overMaskCanvasCtx.fillRect(0, 0, drawIt.w, drawIt.h); // fill with black instead of null to start
|
||||
for (i = 0; i < overMaskImgData.data.length; i += 4) {
|
||||
if (overMaskImgData.data[i] == 255) {
|
||||
// white pixel?
|
||||
// just blotch all over the thing
|
||||
var rando = Math.floor(Math.random() * overMaskPx);
|
||||
overMaskCanvasCtx.beginPath();
|
||||
overMaskCanvasCtx.arc(
|
||||
(i / 4) % overMaskCanvas.width,
|
||||
Math.floor(i / 4 / overMaskCanvas.width),
|
||||
scaleFactor + rando, // was 4 * sf + rando, too big
|
||||
0,
|
||||
2 * Math.PI,
|
||||
true
|
||||
);
|
||||
overMaskCanvasCtx.fillStyle = "#FFFFFFFF";
|
||||
overMaskCanvasCtx.fill();
|
||||
}
|
||||
}
|
||||
overMaskImgData = overMaskCanvasCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
overMaskCanvas.width,
|
||||
overMaskCanvas.height
|
||||
);
|
||||
overMaskCanvasCtx.putImageData(overMaskImgData, 0, 0);
|
||||
}
|
||||
// also check for painted masks in region, add them as white pixels to mask canvas
|
||||
const maskChunk = maskPaintCtx.getImageData(
|
||||
drawIt.x,
|
||||
drawIt.y,
|
||||
drawIt.w,
|
||||
drawIt.h
|
||||
);
|
||||
const maskChunkData = maskChunk.data;
|
||||
for (let i = 0; i < maskChunkData.length; i += 4) {
|
||||
if (maskChunkData[i + 3] != 0) {
|
||||
overMaskImgData.data[i] = 255;
|
||||
overMaskImgData.data[i + 1] = 255;
|
||||
overMaskImgData.data[i + 2] = 255;
|
||||
overMaskImgData.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
// backup any painted masks ingested then them, replacable if user doesn't like resultant image
|
||||
var clearArea = maskPaintCtx.createImageData(drawIt.w, drawIt.h);
|
||||
backupMaskChunk = maskChunk;
|
||||
backupMaskX = drawIt.x;
|
||||
backupMaskY = drawIt.y;
|
||||
|
||||
var clearD = clearArea.data;
|
||||
for (let i = 0; i < clearD.length; i++) {
|
||||
clearD[i] = 0; // just null it all out
|
||||
}
|
||||
maskPaintCtx.putImageData(clearArea, drawIt.x, drawIt.y);
|
||||
// mask monitors
|
||||
overMaskCanvasCtx.putImageData(overMaskImgData, 0, 0); // :pray:
|
||||
var overMaskBase64 = overMaskCanvas.toDataURL();
|
||||
initImgCanvasCtx.putImageData(initImgData, 0, 0);
|
||||
var initImgBase64 = initImgCanvas.toDataURL();
|
||||
// anyway all that to say NOW let's run img2img
|
||||
endpoint = "img2img";
|
||||
stableDiffusionData.mask = overMaskBase64;
|
||||
stableDiffusionData.init_images = [initImgBase64];
|
||||
// slightly more involved than txt2img
|
||||
} else {
|
||||
// time to run txt2img
|
||||
endpoint = "txt2img";
|
||||
// easy enough
|
||||
}
|
||||
stableDiffusionData.prompt = document.getElementById("prompt").value;
|
||||
stableDiffusionData.negative_prompt =
|
||||
document.getElementById("negPrompt").value;
|
||||
stableDiffusionData.width = drawIt.w;
|
||||
stableDiffusionData.height = drawIt.h;
|
||||
stableDiffusionData.firstphase_height = drawIt.h / 2;
|
||||
stableDiffusionData.firstphase_width = drawIt.w / 2;
|
||||
dream(drawIt.x, drawIt.y, stableDiffusionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changePaintMode() {
|
||||
paintMode = document.getElementById("cbxPaint").checked;
|
||||
clearTargetMask();
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
}
|
||||
|
||||
function changeEnableErasing() {
|
||||
// yeah because this is for the image layer
|
||||
enableErasing = document.getElementById("cbxEnableErasing").checked;
|
||||
localStorage.setItem("enable_erase", enableErasing);
|
||||
}
|
||||
|
||||
function changeSampler() {
|
||||
if (!document.getElementById("samplerSelect").value == "") {
|
||||
// must be done, since before getSamplers is done, the options are empty
|
||||
|
@ -816,6 +566,7 @@ const changeScaleFactor = sliderChangeHandlerFactory(
|
|||
"scaleFactorTxt",
|
||||
"scaleFactor",
|
||||
8,
|
||||
true,
|
||||
(k, v) => (scaleFactor = v),
|
||||
(k) => scaleFactor
|
||||
);
|
||||
|
@ -1247,6 +998,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);
|
||||
}
|
||||
|
|
65
js/input.js
65
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
|
||||
*/
|
||||
|
@ -370,7 +362,18 @@ window.onkeydown = (evn) => {
|
|||
}, inputConfig.keyboardHoldTiming),
|
||||
};
|
||||
|
||||
// Process shortcuts
|
||||
// Process shortcuts if input target is not a text field
|
||||
switch (evn.target.tagName.toLowerCase()) {
|
||||
case "input":
|
||||
case "textarea":
|
||||
case "select":
|
||||
case "button":
|
||||
return; // If in an input field, do not process shortcuts
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
const callbacks = keyboard.shortcuts[evn.code];
|
||||
|
||||
if (callbacks)
|
||||
|
|
|
@ -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,14 @@ 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();
|
||||
});
|
||||
keyboard.onShortcut({key: "KeyI"}, () => {
|
||||
tools.img2img.enable();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
(() => {
|
||||
makeDraggable("historyContainer");
|
||||
|
||||
const historyView = document.getElementById("history");
|
||||
|
||||
const makeHistoryEntry = (index, id, title) => {
|
||||
|
@ -27,18 +25,26 @@
|
|||
};
|
||||
|
||||
_commands_events.on((message) => {
|
||||
if (message.action === "run") {
|
||||
Array.from(historyView.children).forEach((child) => {
|
||||
if (
|
||||
!commands.history.find((entry) => `hist-${entry.id}` === child.id)
|
||||
) {
|
||||
console.log("Removing " + child.id);
|
||||
historyView.removeChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
commands.history.forEach((entry, index) => {
|
||||
console.log("Inserting " + entry.id);
|
||||
if (!document.getElementById(`hist-${entry.id}`)) {
|
||||
console.log("Inserting " + entry.id);
|
||||
historyView.appendChild(
|
||||
makeHistoryEntry(index, `hist-${entry.id}`, entry.title)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
while (historyView.children.length > commands.history.length)
|
||||
historyView.removeChild(historyView.lastElementChild);
|
||||
|
||||
Array.from(historyView.children).forEach((child, index) => {
|
||||
if (index === commands.current) {
|
||||
child.classList.remove(["past"]);
|
||||
|
|
178
js/ui/tool/dream.js
Normal file
178
js/ui/tool/dream.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
const dream_generate_callback = (evn, state) => {
|
||||
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// Build request to the API
|
||||
const request = {};
|
||||
Object.assign(request, stableDiffusionData);
|
||||
|
||||
// Load prompt (maybe we should add some events so we don't have to do this)
|
||||
request.prompt = document.getElementById("prompt").value;
|
||||
request.negative_prompt = document.getElementById("negPrompt").value;
|
||||
|
||||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Setup marching ants
|
||||
stopMarching = march(bb);
|
||||
|
||||
// Setup some basic information for SD
|
||||
request.width = bb.w;
|
||||
request.height = bb.h;
|
||||
|
||||
request.firstphase_width = bb.w / 2;
|
||||
request.firstphase_height = bb.h / 2;
|
||||
|
||||
// Use txt2img if canvas is blank
|
||||
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) {
|
||||
// Dream
|
||||
dream(bb.x, bb.y, request, {method: "txt2img", stopMarching, bb});
|
||||
} else {
|
||||
// Use img2img if not
|
||||
|
||||
// Temporary canvas for init image and mask generation
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = request.width;
|
||||
auxCanvas.height = request.height;
|
||||
const auxCtx = auxCanvas.getContext("2d");
|
||||
|
||||
auxCtx.fillStyle = "#000F";
|
||||
|
||||
// Get init image
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
request.init_images = [auxCanvas.toDataURL()];
|
||||
|
||||
// Get mask image
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.globalCompositeOperation = "destination-in";
|
||||
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
auxCtx.globalCompositeOperation = "destination-out";
|
||||
auxCtx.drawImage(
|
||||
maskPaintCanvas,
|
||||
bb.x,
|
||||
bb.y,
|
||||
bb.w,
|
||||
bb.h,
|
||||
0,
|
||||
0,
|
||||
bb.w,
|
||||
bb.h
|
||||
);
|
||||
auxCtx.globalCompositeOperation = "destination-atop";
|
||||
auxCtx.fillStyle = "#FFFF";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
request.mask = auxCanvas.toDataURL();
|
||||
|
||||
// Dream
|
||||
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
|
||||
}
|
||||
}
|
||||
};
|
||||
const dream_erase_callback = (evn, state) => {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
commands.runCommand("eraseImage", "Erase Area", bb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Image to Image
|
||||
*/
|
||||
const dream_img2img_callback = (evn, state) => {
|
||||
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// Do nothing if no image exists
|
||||
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return;
|
||||
|
||||
// Build request to the API
|
||||
const request = {};
|
||||
Object.assign(request, stableDiffusionData);
|
||||
|
||||
request.denoising_strength = state.denoisingStrength;
|
||||
request.inpainting_fill = 1; // For img2img use original
|
||||
|
||||
// Load prompt (maybe we should add some events so we don't have to do this)
|
||||
request.prompt = document.getElementById("prompt").value;
|
||||
request.negative_prompt = document.getElementById("negPrompt").value;
|
||||
|
||||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Setup marching ants
|
||||
stopMarching = march(bb);
|
||||
|
||||
// Setup some basic information for SD
|
||||
request.width = bb.w;
|
||||
request.height = bb.h;
|
||||
|
||||
request.firstphase_width = bb.w / 2;
|
||||
request.firstphase_height = bb.h / 2;
|
||||
|
||||
// Use img2img
|
||||
|
||||
// Temporary canvas for init image and mask generation
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = request.width;
|
||||
auxCanvas.height = request.height;
|
||||
const auxCtx = auxCanvas.getContext("2d");
|
||||
|
||||
auxCtx.fillStyle = "#000F";
|
||||
|
||||
// Get init image
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
request.init_images = [auxCanvas.toDataURL()];
|
||||
|
||||
// Get mask image
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
auxCtx.globalCompositeOperation = "destination-out";
|
||||
auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
||||
|
||||
// Border Mask
|
||||
if (state.useBorderMask) {
|
||||
auxCtx.fillStyle = "#000F";
|
||||
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
|
||||
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
|
||||
auxCtx.fillRect(
|
||||
bb.w - state.borderMaskSize,
|
||||
0,
|
||||
state.borderMaskSize,
|
||||
bb.h
|
||||
);
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
bb.h - state.borderMaskSize,
|
||||
bb.w,
|
||||
state.borderMaskSize
|
||||
);
|
||||
}
|
||||
|
||||
auxCtx.globalCompositeOperation = "destination-atop";
|
||||
auxCtx.fillStyle = "#FFFF";
|
||||
auxCtx.fillRect(0, 0, bb.w, bb.h);
|
||||
request.mask = auxCanvas.toDataURL();
|
||||
|
||||
request.inpainting_mask_invert = true;
|
||||
|
||||
// Dream
|
||||
dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb});
|
||||
}
|
||||
};
|
27
js/ui/tool/maskbrush.js
Normal file
27
js/ui/tool/maskbrush.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const mask_brush_draw_callback = (evn, state) => {
|
||||
if (evn.initialTarget.id === "overlayCanvas") {
|
||||
maskPaintCtx.globalCompositeOperation = "source-over";
|
||||
maskPaintCtx.strokeStyle = "#FF6A6A";
|
||||
|
||||
maskPaintCtx.lineWidth = state.brushSize;
|
||||
maskPaintCtx.beginPath();
|
||||
maskPaintCtx.moveTo(evn.px, evn.py);
|
||||
maskPaintCtx.lineTo(evn.x, evn.y);
|
||||
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
||||
maskPaintCtx.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
const mask_brush_erase_callback = (evn, state) => {
|
||||
if (evn.initialTarget.id === "overlayCanvas") {
|
||||
maskPaintCtx.globalCompositeOperation = "destination-out";
|
||||
maskPaintCtx.strokeStyle = "#FFFFFFFF";
|
||||
|
||||
maskPaintCtx.lineWidth = state.brushSize;
|
||||
maskPaintCtx.beginPath();
|
||||
maskPaintCtx.moveTo(evn.px, evn.py);
|
||||
maskPaintCtx.lineTo(evn.x, evn.y);
|
||||
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
|
||||
maskPaintCtx.stroke();
|
||||
}
|
||||
};
|
456
js/ui/toolbar.js
Normal file
456
js/ui/toolbar.js
Normal file
|
@ -0,0 +1,456 @@
|
|||
/**
|
||||
* Toolbar
|
||||
*/
|
||||
|
||||
const toolbar = {
|
||||
_toolbar: document.getElementById("ui-toolbar"),
|
||||
|
||||
tools: [],
|
||||
|
||||
_makeToolbarEntry: (tool) => {
|
||||
const toolTitle = document.createElement("img");
|
||||
toolTitle.classList.add("tool-icon");
|
||||
toolTitle.src = tool.icon;
|
||||
|
||||
const toolEl = document.createElement("div");
|
||||
toolEl.id = `tool-${tool.id}`;
|
||||
toolEl.classList.add("tool");
|
||||
toolEl.title = tool.name;
|
||||
if (tool.options.shortcut) toolEl.title += ` (${tool.options.shortcut})`;
|
||||
toolEl.onclick = () => tool.enable();
|
||||
|
||||
toolEl.appendChild(toolTitle);
|
||||
|
||||
return toolEl;
|
||||
},
|
||||
|
||||
registerTool(
|
||||
icon,
|
||||
toolname,
|
||||
enable,
|
||||
disable,
|
||||
options = {
|
||||
/**
|
||||
* Runs on tool creation. It receives the tool state.
|
||||
*
|
||||
* Can be used to setup callback functions, for example.
|
||||
*/
|
||||
init: null,
|
||||
/**
|
||||
* Function to populate the state menu.
|
||||
*
|
||||
* It receives a div element (that is the menu) and the current tool state.
|
||||
*/
|
||||
populateContextMenu: null,
|
||||
/**
|
||||
* Help description of the tool; for now used for nothing
|
||||
*/
|
||||
description: "",
|
||||
/**
|
||||
* Shortcut; Text describing this tool's shortcut access
|
||||
*/
|
||||
shortcut: "",
|
||||
}
|
||||
) {
|
||||
// Set some defaults
|
||||
if (!options.init)
|
||||
options.init = (state) => console.debug(`Initialized tool '${toolname}'`);
|
||||
|
||||
if (!options.populateContextMenu)
|
||||
options.populateContextMenu = (menu, state) => {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = "Tool has no context menu";
|
||||
menu.appendChild(span);
|
||||
return;
|
||||
};
|
||||
|
||||
// Create tool
|
||||
const id = guid();
|
||||
|
||||
const contextMenuEl = document.getElementById("tool-context");
|
||||
|
||||
const tool = {
|
||||
id,
|
||||
icon,
|
||||
name: toolname,
|
||||
enabled: false,
|
||||
_element: null,
|
||||
state: {},
|
||||
options,
|
||||
enable: (opt = null) => {
|
||||
this.tools.filter((t) => t.enabled).forEach((t) => t.disable());
|
||||
|
||||
while (contextMenuEl.lastChild) {
|
||||
contextMenuEl.removeChild(contextMenuEl.lastChild);
|
||||
}
|
||||
options.populateContextMenu(contextMenuEl, tool.state);
|
||||
|
||||
tool._element && tool._element.classList.add("using");
|
||||
tool.enabled = true;
|
||||
enable(tool.state, opt);
|
||||
},
|
||||
disable: (opt = null) => {
|
||||
tool._element && tool._element.classList.remove("using");
|
||||
disable(tool.state, opt);
|
||||
tool.enabled = false;
|
||||
},
|
||||
};
|
||||
|
||||
// Initalize
|
||||
options.init && options.init(tool.state);
|
||||
|
||||
this.tools.push(tool);
|
||||
|
||||
// Add tool to toolbar
|
||||
tool._element = this._makeToolbarEntry(tool);
|
||||
this._toolbar.appendChild(tool._element);
|
||||
|
||||
return tool;
|
||||
},
|
||||
|
||||
addSeparator() {
|
||||
const separator = document.createElement("div");
|
||||
separator.classList.add("separator");
|
||||
this._toolbar.appendChild(separator);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Premade inputs for populating the context menus
|
||||
*/
|
||||
const _toolbar_input = {
|
||||
checkbox: (state, dataKey, text) => {
|
||||
if (state[dataKey] === undefined) state[dataKey] = false;
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = state[dataKey];
|
||||
checkbox.onchange = () => (state[dataKey] = checkbox.checked);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(new Text(text));
|
||||
|
||||
return {checkbox, label};
|
||||
},
|
||||
|
||||
slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => {
|
||||
const slider = document.createElement("input");
|
||||
slider.type = "range";
|
||||
slider.max = max;
|
||||
slider.step = step;
|
||||
slider.min = min;
|
||||
slider.value = state[dataKey];
|
||||
|
||||
const textEl = document.createElement("input");
|
||||
textEl.type = "number";
|
||||
textEl.value = state[dataKey];
|
||||
|
||||
console.log(state[dataKey]);
|
||||
|
||||
sliderChangeHandlerFactoryEl(
|
||||
slider,
|
||||
textEl,
|
||||
dataKey,
|
||||
state[dataKey],
|
||||
false,
|
||||
(k, v) => (state[dataKey] = v),
|
||||
(k) => state[dataKey]
|
||||
);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.appendChild(new Text(text));
|
||||
label.appendChild(textEl);
|
||||
label.appendChild(slider);
|
||||
|
||||
return {
|
||||
slider,
|
||||
text: textEl,
|
||||
label,
|
||||
setValue(v) {
|
||||
slider.value = v;
|
||||
textEl.value = slider.value;
|
||||
return parseInt(slider.value);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Dream and img2img tools
|
||||
*/
|
||||
const _reticle_draw = (evn, snapToGrid = true) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// draw targeting square reticle thingy cursor
|
||||
ovCtx.strokeStyle = "#FFF";
|
||||
ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
|
||||
}
|
||||
};
|
||||
|
||||
const tools = {};
|
||||
|
||||
/**
|
||||
* Dream tool
|
||||
*/
|
||||
tools.dream = toolbar.registerTool(
|
||||
"res/icons/image-plus.svg",
|
||||
"Dream",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.snapToGrid = true;
|
||||
state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid);
|
||||
state.dreamcb = (evn) => {
|
||||
dream_generate_callback(evn, state);
|
||||
};
|
||||
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"snapToGrid",
|
||||
"Snap To Grid"
|
||||
).label;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
},
|
||||
shortcut: "D",
|
||||
}
|
||||
);
|
||||
|
||||
tools.img2img = toolbar.registerTool(
|
||||
"res/icons/image.svg",
|
||||
"Img2Img",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.mousemovecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.on(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
|
||||
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
|
||||
mouse.listen.canvas.right.onclick.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.snapToGrid = true;
|
||||
state.denoisingStrength = 0.7;
|
||||
|
||||
state.useBorderMask = true;
|
||||
state.borderMaskSize = 64;
|
||||
|
||||
state.mousemovecb = (evn) => {
|
||||
_reticle_draw(evn, state.snapToGrid);
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
basePixelCount * scaleFactor,
|
||||
basePixelCount * scaleFactor,
|
||||
state.snapToGrid && basePixelCount
|
||||
);
|
||||
|
||||
// For displaying border mask
|
||||
const auxCanvas = document.createElement("canvas");
|
||||
auxCanvas.width = bb.w;
|
||||
auxCanvas.height = bb.h;
|
||||
const auxCtx = auxCanvas.getContext("2d");
|
||||
|
||||
if (state.useBorderMask) {
|
||||
auxCtx.fillStyle = "#FF6A6A50";
|
||||
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
|
||||
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
|
||||
auxCtx.fillRect(
|
||||
bb.w - state.borderMaskSize,
|
||||
0,
|
||||
state.borderMaskSize,
|
||||
bb.h
|
||||
);
|
||||
auxCtx.fillRect(
|
||||
0,
|
||||
bb.h - state.borderMaskSize,
|
||||
bb.w,
|
||||
state.borderMaskSize
|
||||
);
|
||||
}
|
||||
|
||||
const tmp = ovCtx.globalAlpha;
|
||||
ovCtx.globalAlpha = 0.4;
|
||||
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
|
||||
ovCtx.globalAlpha = tmp;
|
||||
};
|
||||
state.dreamcb = (evn) => {
|
||||
dream_img2img_callback(evn, state);
|
||||
};
|
||||
state.erasecb = (evn) => dream_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
// Snap To Grid Checkbox
|
||||
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"snapToGrid",
|
||||
"Snap To Grid"
|
||||
).label;
|
||||
|
||||
// Denoising Strength Slider
|
||||
state.ctxmenu.denoisingStrengthLabel = _toolbar_input.slider(
|
||||
state,
|
||||
"denoisingStrength",
|
||||
"Denoising Strength",
|
||||
0,
|
||||
1,
|
||||
0.05
|
||||
).label;
|
||||
|
||||
// Use Border Mask Checkbox
|
||||
state.ctxmenu.useBorderMaskLabel = _toolbar_input.checkbox(
|
||||
state,
|
||||
"useBorderMask",
|
||||
"Use Border Mask"
|
||||
).label;
|
||||
// Border Mask Size Slider
|
||||
state.ctxmenu.borderMaskSize = _toolbar_input.slider(
|
||||
state,
|
||||
"borderMaskSize",
|
||||
"Border Mask Size",
|
||||
0,
|
||||
128,
|
||||
1
|
||||
).label;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.snapToGridLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.denoisingStrengthLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.useBorderMaskLabel);
|
||||
menu.appendChild(document.createElement("br"));
|
||||
menu.appendChild(state.ctxmenu.borderMaskSize);
|
||||
},
|
||||
shortcut: "I",
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Mask Editing tools
|
||||
*/
|
||||
toolbar.addSeparator();
|
||||
|
||||
/**
|
||||
* Mask Brush tool
|
||||
*/
|
||||
tools.maskbrush = toolbar.registerTool(
|
||||
"res/icons/paintbrush.svg",
|
||||
"Mask Brush",
|
||||
(state, opt) => {
|
||||
// Draw new cursor immediately
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
|
||||
|
||||
// Start Listeners
|
||||
mouse.listen.canvas.onmousemove.on(state.movecb);
|
||||
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||
mouse.listen.canvas.left.onpaint.on(state.drawcb);
|
||||
mouse.listen.canvas.right.onpaint.on(state.erasecb);
|
||||
},
|
||||
(state, opt) => {
|
||||
// Clear Listeners
|
||||
mouse.listen.canvas.onmousemove.clear(state.movecb);
|
||||
mouse.listen.canvas.onwheel.on(state.wheelcb);
|
||||
mouse.listen.canvas.left.onpaint.clear(state.drawcb);
|
||||
mouse.listen.canvas.right.onpaint.clear(state.erasecb);
|
||||
},
|
||||
{
|
||||
init: (state) => {
|
||||
state.config = {
|
||||
brushScrollSpeed: 1 / 4,
|
||||
minBrushSize: 10,
|
||||
maxBrushSize: 500,
|
||||
};
|
||||
|
||||
state.brushSize = 64;
|
||||
state.setBrushSize = (size) => {
|
||||
state.brushSize = size;
|
||||
state.ctxmenu.brushSizeRange.value = size;
|
||||
state.ctxmenu.brushSizeText.value = size;
|
||||
};
|
||||
|
||||
state.movecb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
// draw big translucent red blob cursor
|
||||
ovCtx.beginPath();
|
||||
ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line???
|
||||
ovCtx.fillStyle = "#FF6A6A50";
|
||||
ovCtx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
state.wheelcb = (evn) => {
|
||||
if (evn.target.id === "overlayCanvas") {
|
||||
state.brushSize = state.setBrushSize(
|
||||
state.brushSize -
|
||||
Math.floor(state.config.brushScrollSpeed * evn.delta)
|
||||
);
|
||||
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
|
||||
state.movecb(evn);
|
||||
}
|
||||
};
|
||||
|
||||
state.drawcb = (evn) => mask_brush_draw_callback(evn, state);
|
||||
state.erasecb = (evn) => mask_brush_erase_callback(evn, state);
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
if (!state.ctxmenu) {
|
||||
state.ctxmenu = {};
|
||||
const brushSizeSlider = _toolbar_input.slider(
|
||||
state,
|
||||
"brushSize",
|
||||
"Brush Size",
|
||||
state.config.minBrushSize,
|
||||
state.config.maxBrushSize,
|
||||
1
|
||||
);
|
||||
state.ctxmenu.brushSizeLabel = brushSizeSlider.label;
|
||||
state.setBrushSize = brushSizeSlider.setValue;
|
||||
}
|
||||
|
||||
menu.appendChild(state.ctxmenu.brushSizeLabel);
|
||||
},
|
||||
shortcut: "M",
|
||||
}
|
||||
);
|
||||
|
||||
toolbar.tools[0].enable();
|
38
js/util.js
38
js/util.js
|
@ -41,3 +41,41 @@ const guid = (size = 3) => {
|
|||
id += s4();
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bounding box Calculation
|
||||
*/
|
||||
function snap(i, scaled = true, gridSize = 64) {
|
||||
// very cheap test proof of concept but it works surprisingly well
|
||||
var scaleOffset = 0;
|
||||
if (scaled) {
|
||||
if (scaleFactor % 2 != 0) {
|
||||
// odd number, snaps to center of cell, oops
|
||||
scaleOffset = gridSize / 2;
|
||||
}
|
||||
}
|
||||
var snapOffset = (i % gridSize) - scaleOffset;
|
||||
if (snapOffset == 0) {
|
||||
return snapOffset;
|
||||
}
|
||||
return -snapOffset;
|
||||
}
|
||||
|
||||
function getBoundingBox(cx, cy, w, h, gridSnap = null) {
|
||||
const offset = {x: 0, y: 0};
|
||||
const box = {x: 0, y: 0};
|
||||
|
||||
if (gridSnap) {
|
||||
offset.x = snap(cx, true, gridSnap);
|
||||
offset.y = snap(cy, true, gridSnap);
|
||||
}
|
||||
box.x = offset.x + cx;
|
||||
box.y = offset.y + cy;
|
||||
|
||||
return {
|
||||
x: Math.floor(box.x - w / 2),
|
||||
y: Math.floor(box.y - h / 2),
|
||||
w,
|
||||
h,
|
||||
};
|
||||
}
|
||||
|
|
7
res/icons/LICENSE
Normal file
7
res/icons/LICENSE
Normal file
|
@ -0,0 +1,7 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
8
res/icons/image-plus.svg
Normal file
8
res/icons/image-plus.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"></path>
|
||||
<line x1="16" y1="5" x2="22" y2="5"></line>
|
||||
<line x1="19" y1="2" x2="19" y2="8"></line>
|
||||
<circle cx="9" cy="9" r="2"></circle>
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 460 B |
6
res/icons/image.svg
Normal file
6
res/icons/image.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="9" cy="9" r="2"></circle>
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 356 B |
6
res/icons/paintbrush.svg
Normal file
6
res/icons/paintbrush.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z"></path>
|
||||
<path d="M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7"></path>
|
||||
<path d="M14.5 17.5 4.5 15"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 421 B |
Loading…
Reference in a new issue